注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

iOS安全之三攻三防

互联网世界每分钟都在上演黑客攻击,由此导致的财产损失不计其数。金融行业在安全方面的重视不断加深,而传统互联网行业在安全方面并没有足够重视,这样导致开发的APP在逆向开发人员面前等同于裸奔,甚至有些小厂前后台在账号密码处理上采取明文传送,本地存储,这等同于将账号...
继续阅读 »

互联网世界每分钟都在上演黑客攻击,由此导致的财产损失不计其数。金融行业在安全方面的重视不断加深,而传统互联网行业在安全方面并没有足够重视,这样导致开发的APP在逆向开发人员面前等同于裸奔,甚至有些小厂前后台在账号密码处理上采取明文传送,本地存储,这等同于将账号密码直接暴露无疑。当然即使采用加密传送,逆向APP后依然可以获取到账号密码,让你在神不知鬼不觉的情况下将账号密码发送到了黑客邮箱,所以攻防终究是一个相互博弈的过程。本文主要分析常见的几种攻击和防护手段,通过攻击你可以看到你的APP是如何被一步一步被攻破的。有了攻击,我们针对相应的攻击就是见招拆招了。

一、攻击原理

从APPStore下载正式版本版本应用,进行一键砸壳,绝大部分应用均可以脱壳成功。
使用脚本或第三方工具MonkeyDev对应用实现重签名。
利用动态调试(LLDB,Cycript,Reveal)和静态分析(反汇编),找到关键函数进行破解。
Theos编写插件,让使用更加方便。

二、攻守第一回合

1. 第一攻武器:代码注入+method_exchangeImplementations

在shell脚本实现iOS包重签名及代码注入的最后,我们成功使用method_exchange截获微信点击按钮,代码如下:

+(void)load
{
Method oldMethod = class_getInstanceMethod(objc_getClass("WCAccountLoginControlLogic"), @selector(onFirstViewRegester));

Method newMethod = class_getInstanceMethod(self, @selector(test));

method_exchangeImplementations(oldMethod, newMethod);
}

-(void)test{
NSLog(@"----截获到微信注册按钮点击------");
}

2. 第一防护盾:framwork+fishHook

关于为什么使用framwork而不是直接在代码中创建一个类,并在类的load方法中编写防护代码,原因是自己创建framwork的加载要早于代码注入的framwork,代码注入的framwork的执行要早于自己类load的加载,具体原理请看dyld加载应用启动原理详解。防护代码如下:


注意:当我们检查到hook代码时,比较好的处理方式是将该手机的UDID,账号等信息发送给后台服务器,让后台服务器进行封号禁设备处理,而不是直接exit(0)让程序强制退出,因为这样的好处是让黑客很难定位。

三、攻守第二回合

1. 第二攻武器:MonkeyDev

MonkeyDev可以帮助我们更加方便的实现代码重签名和hook,底层是使用了方法交换的SET和GET方法进行hook,关于MoneyDev的使用在逆向iOS系统桌面实现一键清空徽标有讲。同样以截获微信注册按钮为例,hook代码示例如下:

%hook WCAccountLoginControlLogic
- (void)onFirstViewRegester:(id)arg{
NSLog(@"---hook-----");
}

%end

2. 第二防护盾:依然framwork+fishHook

+(void)load{
//setIMP
struct rebinding gt;
gt.name = "method_getImplementation";
gt.replacement = my_getIMP;
gt.replaced = (void *)&getIMP;
//getIMP
struct rebinding st;
st.name = "method_setImplementation";
st.replacement = my_setIMP;
st.replaced = (void *)&setIMP;

struct rebinding rebs[2] = {gt,st};
rebind_symbols(rebs, 2);

}

//保存原来的交换函数
IMP (*getIMP)(Method _Nonnull m);
IMP (*setIMP)(Method _Nonnull m, IMP _Nonnull imp);


IMP my_getIMP(Method _Nonnull m){
NSLog(@"🍺----检查到了HOOk-----🍺");
return nil;
}
IMP my_setIMP(Method _Nonnull m, IMP _Nonnull imp){

NSLog(@"🍺----检查到了HOOk-----🍺");
return nil;
}

三、攻守第三回合

上面的两次攻击都是通过代码注入来实现hook目的,我们能不能防止第三方代码进行注入呢?答案当然是可以,接下来我们来防止第三方代码注入。

1. 第三防护盾:在编译设置阶段增加字段"-Wl,-sectcreate,__RESTRICT,__restrict,/dev/null",如下图:


1.1 增加该字段后在MachO文件就会增加_RESTRICT,__restrict段,如下图:


1.2 为什么增加这个字段就可以了呢?这里我们就要回归到dyld的源码了,在dyld加载过程中有一个函数hasRestrictedSegment就是用来判断是否存在__RESTRICT,__RESTRICT中是否是__restrict名称,如果是,则会禁止加载第三方注入的库文件,源码如下:

#if __MAC_OS_X_VERSION_MIN_REQUIRED
static bool hasRestrictedSegment(const macho_header* mh)
{
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

//dyld::log("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

return false;
}
#endif

2. 第三攻击武器:直接修改MachO二进制文件

通过Synalyze It!工具更改MachO二进制文件字段,然后重新签名打包即可破坏该防护过程:

3. 第三防护2级护盾:代码过滤,增加白名单。

3.1 既然禁止第三方注入代码都很容易被攻破,接下来我们就从代码入手,过滤第三方库注入库,增加白名单,代码如下: 

@implementation ViewController
+(void)load
{

const struct mach_header_64 * header = _dyld_get_image_header(0);
if (hasRestrictedSegment(header)) {
NSLog(@"---- 防止状态 ------");

//如果__RESTRICT字段被绕过,开始开名单检测
CheckWhitelist()

}else{
NSLog(@"--- 防护字段被修改了 -----");
}


}

static bool hasRestrictedSegment(const struct macho_header* mh)
{
const uint32_t cmd_count = mh->ncmds;
const struct load_command* const cmds = (struct load_command*)(((char*)mh)+sizeof(struct macho_header));
const struct load_command* cmd = cmds;
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_SEGMENT_COMMAND:
{
const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;

printf("seg name: %s\n", seg->segname);
if (strcmp(seg->segname, "__RESTRICT") == 0) {
const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
const struct macho_section* const sectionsEnd = §ionsStart[seg->nsects];
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
if (strcmp(sect->sectname, "__restrict") == 0)
return true;
}
}
}
break;
}
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}

return false;
}

#pragma mark -- 白名单监测
bool CheckWhitelist(){

int count = _dyld_image_count();//加载了多少数量

for (int i = 0; i < count; i++) {
//遍历拿到库名称!
const char * imageName = _dyld_get_image_name(i);
if (!strstr(libraries, imageName)&&!strstr(imageName, "/var/mobile/Containers/Bundle/Application")) {
printf("该库非白名单之内!!\n%s",imageName);
return NO;
}

return YES;
}

3.2 原理就是使用系统的函数帮我们检测自己设定的__RESTRICT是否被更改,如果被更改说明我们被Hook了,接下来在被hook的字段中增加自己的处理逻辑即可。

总结:对最后一个防护代码也很容易进行攻击,比如找到hasRestrictedSegment函数,让其直接返回YES。所以建议将该函数进行封装,尽量不要使用Bool作为返回值。综上: 攻和守本来就是一个博弈的过程,没有绝对安全的城墙。
最后附上过滤白名单源码下载,直接拖入工程即可使用,达到较好的代码防护目的。如果帮助到你请给一个Star。
我是Qinz,希望我的文章对你有帮助。

链接:https://www.jianshu.com/p/655c91b61f8a

收起阅读 »

iOS逆向(6)-从fishhook看runtime,hook系统C函数

在上篇文章不知MachO怎敢说自己懂DYLD中已经详细介绍了MachO,并且由MachO引出了dyld,再由dyld讲述了App的启动流程,而在App的启动流程中又说到了一些关键的名称如:LC_LOAD_DYLINKER、LC_LOAD_DYLIB以及objc...
继续阅读 »

在上篇文章不知MachO怎敢说自己懂DYLD中已经详细介绍了MachO,并且由MachO引出了dyld,再由dyld讲述了App的启动流程,而在App的启动流程中又说到了一些关键的名称如:LC_LOAD_DYLINKER、LC_LOAD_DYLIB以及objc的回调函数_dyld_objc_notify_register等等。并且在末尾提出了MachO中还有一些符号表,而有哪些符号表,这些符号表又有些什么用呢?笔者在这篇文章就将一一道来。

老规矩,片头先上福利:点击下载demo,demo中有笔者给fishhook每句代码加的详细注释!!!
这篇文章会用到的工具有:

fishhook

在开始正文之前,假设面试官问了一个问题:
都知道Objective-C最大的特性就是runtime,大家可以用使用runtime对OC的方法进行hook,那么C函数能不能hook?

有兴趣回答的朋友可以先行在评论区回答,答完之后再继续阅读或者预先偷窥一下文末的答案,看看这被炒了无数次冷饭的runtime自己是否真的了然于胸。

本将从以下几方面回答上面所提的问题:

1、Runtime的Hook原理
2、为什么C不能hook
3、如何利用MachO“玩坏”系统C函数
4、fishhook源码分析
5、绑定系统C函数过程验证

一、Runtime的Hook原理

Runtime,从名称上就知道是运行时,也是它造就了OC运行时的特性,而要想彻底明白什么是运行时,那么就需要将之与C语言有相比较。
今天咱们就从汇编的角度看一看OC和C在调用方法(函数)上有什么区别。

注:笔者使用的是iPhone 7征集调试,所有一下汇编都是基于arm64,所以以下所有汇编默认为基于arm64。

新建一个工程取名为:FishhookDemo
敲入两个OC方法mylog和mylog2,挂上断点,如图:


开启汇编断点,如图:


运行工程,会跳转到如下图的汇编断点:


从上图可以看的出来调用了两个objc_msgSend,这两个很像是
我们的mylog和mylog2,但现在还不能确定。
想一想objc_msgSend的定义:

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

第一个参数是self,第二个参数是SEL,所以可以知道SEL是放在x1的寄存器里面(什么是x1?继续关注作者,之后的文章会有相关的汇编的专门篇章)。

马不停蹄,挂上两个汇编断点,查看一下两个x1中存放的到底是什么,如图:



这也就验证了咱们OC方法都是消息转发(objc_msgSend)。而同一个C函数的地址又都是一样的(笔者这次运行的地址就是0x1026ce130) 。

所以在每次调用OC方法的时候就让我们有了一次改变消息转发「目标」的机会。

这里稍微提一下runtime的源码分析流程:
Step 1、方法查找
① 汇编快速查找缓存
② C/C++慢速查找:self->super->NSObject->找到换缓存起来
Step 2、动态方法解析: _class_resolveMethod
① _class_resolveInstanceMethod
② _class_resolveClassMethod
Step 3、消息转发
① _forwardingTargetForSelector
② _methodSignatureForSelector
③ _forwardInvocation
④ _doesNotRecognizeSelector

二、为什么C不能hook

同样我们从汇编的角度切入。
敲入代码一些C函数,挂上断点,如图:


运行工程:
会看到断点断到如下汇编:


可以看到每个NSLog对应跳转的地址都是0x10000a010,每个printf对应跳转的地址都是0x10000a184,也就是说每个C的函数都是一一对应着一个真实的地址空间。每次在调用一个C函数的时候都是执行一句汇编bl 0xXXXXXXXX。

所以上面讲述到的消息转发的机会没有了,也就是没有了利用runtime来Hook的机会了。

三、如何利用MachO“玩坏”系统C函数

既然如此,那么是否C函数就真的那么牢不可破,无法对他进行Hook呢?
答案肯定是否定的!
想要从根上理解这个问题,首先要了解:我们的C函数分为系统C函数和我们自定义的C函数。

1、自定义的C函数

在上面的步骤中我们已经了解到所有C函数的调用都是跳转到一个「固定的地址」,那么就可以推断得出这个「固定的地址」其实是在编译期已经被生成好了,所以才能快速、直接的跳转到这个地址,实现函数调用。
C语言被称之为是静态语言也就是这么个理。

2、系统的C函数

在上篇文章不知MachO怎敢说自己懂DYLD已经提到了在dyld启动app的第二个步骤就是加载共享缓存库,共享缓存库包括Foundation框架,NSLog是被包含在Foundation框架的。那么就可以确定一件事情,在我们将自己工程打包出的MachO文件中是不可能预先确定NSLog的地址的。

但是又因为C语言是静态的特性,没法在运行的时候实时获取共享缓存库中NSLog的地址。而共享缓存库的存在好处太大,既能节省大量内存,又能加快启动速度提升性能,不能弃之而不用。

为了解决这个问题,Apple使用了PIC(Position-independent code)技术,在第一次使用对应函数(NSLog)的时候,从系统内存中将对函数(NSLog)的内存地址取出,绑定到APP中对应函数(NSLog)上,就可以实现正常的C函数(NSLog)调用了。

既然有这么个过程,iOS系统可以动态的绑定系统C函数的地址,那么咱们就也能。

四、fishhook源码分析

1、fishhook的总体思路

Facebook的开源库fishhook就可以完美的实现这个任务。
先上一张官网原理图:


总体来说,步骤是这样的:

先找到四张表Lazy Symbol Pointer Table、Indirect Symbol Table、Symbol Table、String Table。
MachO有个规律:Lazy Symbol Pointer Table中第index行代表的函数和Indirect Symbol Table中第index行代表的函数是一样的。
Indirect Symbol Table中value值表示Symbol Table的index。
找到Symbol Table的中对应index的对象,其data代表String Table的偏移值。
用String Table的基值,也就是第一行的pFile值,加上Symbol Table的中取到的偏移值,就能得到Indirect Symbol Table中value(这个value代表函数的偏移值)代表的函数名了。

2、验证NSLog地址

下面就来验证一下在NSLog的地址是不是真的就存在Indirect Symbol Table中。
同样在NSLog处下好断点,打开汇编断点,运行代码。会发现断点断在如下入位置:


注:笔者的工程重新build了,MachO也重新生成,所以此处的截图和上文中断住NSLog的截图的地址不一样,这是正常情况。

可以发现NSLog的地址是0x104d36010,先记住这个值。

然后查看我们APP在内存中的偏移值。
利用image list命令列出所有image,第一个image就是我们APP的偏移值,也就是内存地址。


可以看到APP在内存中的偏移值为0x104d30000。
接着打开MachOView查看MachO中的Indirect Symbol Table中的value,如图:


其值为0x100006010,去除最高位得到的0x6010就是NSLog在MachO中的偏移值。
最后将NSLog在MachO中的偏移值于APP在内存中的偏移值相加就得到NSLog真实的内存地址:
0x6010+0x104d30000=0x104d36010

最终证明,在Indirect Symbol Table的value中的值就是其对应的函数的地址!!!

3、根据MachO的表查找对应的函数名和函数地址

咱们还是用NSLog来距离查找。

1、Indirect Symbol Table

取出其data值0000010A,用10进制表示,结果为266,如图:


2、Symbol Table

在Symbol Table中找到下标(offset)为266的的对象,取出其data0x124,如图:


3、String Table

将在Symbols中得到的偏移值0x124加上String Table的首个地址DC6C,得到值DD90,然后找到pFile为DD90的值,如下两图:



上述就是根据MachO的表查找对应的函数名和函数地址全过程了。

4、源码分析

fishhook的源码总共只有250行左右,所以结合MachO慢慢看,其实一点也不费劲,在笔者的demo中有对其每一句函数的详细注释。当然也有对fishhook使用的demo。

所以笔者就不在此处对fishhook做太过详细的介绍了。只对其中一些关键参数和关键函数做介绍。

1、fishhook为维护一个链表,用来储存需要hook的所有函数

// 给需要rebinding的方法结构体开辟出对应的空间
// 生成对应的链表结构(rebindings_entry),并将新的entry插入头部
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel)

2、根据linkedit的基值,找到对应的三张表:symbol_table、string_table和indirect_symtab :

// 找到linkedit的头地址
// linkedit_base其实就是MachO的头地址!!!可以通过查看linkedit_base值和image list命令查看验证!!!(文末附有验证图)
/**********************************************************
Linkedit虚拟地址 = PAGEZERO(64位下1G) + FileOffset
MachO地址 = PAGEZERO + ASLR
上面两个公式是已知的 得到下面这个公式
MachO文件地址 = Linkedit虚拟地址 - FileOffset + ASLR(slide)
**********************************************************/
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// 获取symbol_table的真实地址
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
// 获取string_table的真实地址
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Get indirect symbol table (array of uint32_t indices into symbol table)
// 获取indirect_symtab的真实地址
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);

3、最核心的一个步骤,查找并且替换目标函数:

// 在四张表(section,symtab,strtab,indirect_symtab)中循环查找
// 直到找到对应的rebindings->name,将原先的函数复制给新的地址,将新的函数地址赋值给原先的函数
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab)

五、绑定系统C函数过程验证

上面说了这么多,那么咱们来验证一下系统C函数是不是真的会这样被绑定起来,并且看一看,是在什么时候绑定的。

同样,在第一次敲入NSLog函数的地方加上断点,在第二个NSLog处也加上断点:


运行工程后,使用dis -s命令查看该函数的汇编代码,并且继续查看其中第一次b指令,也就是函数调用的汇编,如图:


从上图就可以看到,在我们第一次调用NSLog的时候,系统确实会默认的调用dyld_stub_binder函数对NSLog进行绑定。

继续跳过这个断点,进入下一个NSLog的汇编断点处,同样利用dis -s命令查看该汇编:


得到答案:
系统确实会在第一次调用系统C函数的时候对其进行绑定!

还记得正文开始的时候的那个问题吗?
那么是不是系统C函数可以hook,而自定义的C函数就绝对不能hook了呢?
很显然,国内外大神那么多,肯定是能做到的,有兴趣的读者可以自行查阅Cydia Substrate。

这篇文章利用了一些LLDB命令行看了许多我们想看的内容,如image list,register read还有dis -s,在我们正向开发中,LLDB就是一把利器,而在我们玩逆向的时候,LLDB就成为了我们某些是后的唯一途径了!所以,在下一篇文章中,笔者将会对LLDB进行更加详细的讲解,让大家看到LLBD的伟大。

1、关于道友AmazingYu的提问:
想问下 linkedit_base 地址与 Text 段的初始地址以及 Data 段的初始地址的关系,这三个段在内存中是挨着的吗,还有就是 linkedit_base 大概在进程内存分布中的哪个地方?

在咨询大佬请叫我Hank后,得到最终答案,在下面问回答中有一些问题,再此纠正一下!
linkedit地址(不是linkedit_base,末尾会介绍linkedit_base到底是什么) 与 Text 段的初始地址以及 Data 段确实是连续的,他们的顺序是:
先是Text 段,然后是Data 段,最后是linkedit_base 地址。从下面三幅图的File Offset和File Size可以看出来,两者相加就能得到下一段的地址:




2、几个名词(pFile 、offset 、File Offset)之前解释的有点问题:
1、首先,这三个都是表示相对于MachO的内存偏移,只不过其含义被细分了。
2、pFile 和 offset含义相近,不过offset更详细,能够对应上具体某一个符号(DATA? TEXT?)。比如文件里面有许多类,类里面有许多的属性,pFile就代表各个类的偏移值,offset代表各个属性的偏移值
3、File Offset 这个存在于Segment的字段中。用于从Segment快速找到其代表的「表」真正的偏移值。
最后说一下linkedit_base:
linkedit_base其实代表的就是MachO的真实内存地址!
可以从下图得到验证


因为

Linkedit虚拟地址 = PAGEZERO(64位下1G) + FileOffset 
MachO地址 = PAGEZERO + ASLR
// 上面两个公式是已知的 所以可以得到下面这个公式
MachO地址 = Linkedit虚拟地址 - FileOffset + ASLR(slide)

也就是fishhook中的:

uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;

转自:https://www.jianshu.com/p/b6a72aa6c146

收起阅读 »

iOS利用RunTime来实现万能跳转

1.万能跳转的应用场景:(1)手机App通过推送过来的数据内容来跳转不同的界面,并把界面数据展示出来。(2)手机内部根据不同的cell的点击事件,不同的数据跳转不同的界面。2.工作的流程图:通过动态返回的数据中的class类名,来去查询class是不是存在:(...
继续阅读 »

1.万能跳转的应用场景:

(1)手机App通过推送过来的数据内容来跳转不同的界面,并把界面数据展示出来。
(2)手机内部根据不同的cell的点击事件,不同的数据跳转不同的界面。

2.工作的流程图:

通过动态返回的数据中的class类名,来去查询class是不是存在:(1)存在则获取实例对象然后通过kVC来绑定数据然后去跳转。(2)不存在则动态创建class及其变量,然后手动创建实例对象在通过KVC来绑定数据,最后跳转。


3.主要方法:

//创建Class
objc_allocateClassPair(Class superclass, const char * name, size_t extraBytes)
//注册Class
void objc_registerClassPair(Class cls)
//添加变量
class_addIvar(Class cls, const char * name,size_t size, uint8_t alignment , const char * types)
//添加方法
class_addMethod(Class cls, SEL name, IMP imp, const char * types)
//获取属性
class_getProperty(Class cls, const char * name)
//获取实例变量
class_getInstanceVariable(Class cls, const char * name)

4.代码实现:

1、工程中新建三个控制器,命名为
FirstViewController
SecondViewController
ThredViewController
每一个控制器的viewDidLoad方法里面的内容为

self.view.backgroundColor = [UIColor redColor];

UILabel * titleLab = [[UILabel alloc]initWithFrame:CGRectMake(100, 100, 200, 40)];
titleLab.textColor = [UIColor blackColor];
[self.view addSubview:titleLab];
titleLab.text =self.name;

然后在ViewController模拟根据不同数据跳转不同界面,代码如下

#import "ViewController.h"
#import <objc/message.h>

@interface ViewController ()

@property (nonatomic, weak) UISegmentedControl * seg;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.view.backgroundColor = [UIColor yellowColor];

NSArray * array = @[@"消息1",@"消息2",@"消息3",@"消息4"];
UISegmentedControl * seg = [[UISegmentedControl alloc]initWithItems:array];
seg.frame = CGRectMake(70, 200, 240, 45);
[self.view addSubview:seg];
seg.selectedSegmentIndex = 0;
self.seg = seg;

UIButton * jupBtn = [UIButton buttonWithType:UIButtonTypeCustom];
jupBtn.frame = CGRectMake(100, 250, 60, 45);
[jupBtn setTitle:@"跳转" forState:UIControlStateNormal];
[jupBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
jupBtn.backgroundColor = [UIColor redColor];
[self.view addSubview:jupBtn];
[jupBtn addTarget:self action:@selector(action) forControlEvents:UIControlEventTouchUpInside];

//创建Class
//objc_allocateClassPair(Class superclass, const char * name, size_t extraBytes)
//注册Class
//void objc_registerClassPair(Class cls)
//添加变量
//class_addIvar(Class cls, const char * name,size_t size, uint8_t alignment , const char * types)
//添加方法
//class_addMethod(Class cls, SEL name, IMP imp, const char * types)
//获取属性
//class_getProperty(Class cls, const char * name)
//获取实例变量
//class_getInstanceVariable(Class cls, const char * name)
}

-(void)action{

NSDictionary * infoDic = nil;

switch (self.seg.selectedSegmentIndex) {
case 0:
infoDic = @{@"class":@"FirstViewController",
@"property":@{
@"name":@"尼古拉斯赵四"
}
};
break;
case 1:
infoDic = @{@"class":@"SecondViewController",
@"property":@{
@"age":@"26",
@"sex":@"男"
}
};
break;
case 2:
infoDic = @{@"class":@"ThredViewController",
@"property":@{
@"teacher":@"王老师",
@"money":@"5000"
}
};
break;
case 3:
//NewViewController
infoDic = @{@"class":@"WorkerController",
@"property":@{
@"phoneNumber":@"17710948530"
}
};
break;

default:
break;
}

[self pushToControllerWithData:infoDic];

}
-(void)pushToControllerWithData:(NSDictionary * )vcData{
//1.获取class
const char * className = [vcData[@"class"] UTF8String];
Class cls = objc_getClass(className);
if(!cls){
//创建新的类,并添加变量和方法
Class superClass = [UIViewController class];
cls = objc_allocateClassPair(superClass, className, 0);
//添加phoneNumber变量
class_addIvar(cls, "phoneNumber", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
//添加titleLab控件
class_addIvar(cls, "titleLab", sizeof(UILabel *), log2(sizeof(UILabel *)), @encode(UILabel *));
//添加方法,方法交换,执行viewDidLoad加载
Method method = class_getInstanceMethod([self class], @selector(workerLoad));
IMP methodIMP = method_getImplementation(method);
const char * types = method_getTypeEncoding(method);
class_addMethod(cls, @selector(viewDidLoad), methodIMP, types);
}
//2.创建实例对象,给属性赋值
id instance = [[cls alloc]init];
NSDictionary * values = vcData[@"property"];
[values enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
//检测是否存在为key的属性
if(class_getProperty(cls, [key UTF8String])){
[instance setValue:obj forKey:key];
}
//检测是否存在为key的变量
else if (class_getInstanceVariable(cls, [key UTF8String])){
[instance setValue:obj forKey:key];
}
}];

//2.跳转到对应的界面
[self.navigationController pushViewController:instance animated:YES];

}

-(void)workerLoad{
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
//初始化titleLab
[self setValue:[[UILabel alloc]initWithFrame:CGRectMake(100, 100, 200, 40)] forKey:@"titleLab"];
UILabel * titleLab = [self valueForKey:@"titleLab"];
//添加到视图上
[[self valueForKey:@"view"] performSelector:@selector(addSubview:) withObject:titleLab];
titleLab.text =[self valueForKey:@"phoneNumber"];
titleLab.textColor = [UIColor blackColor];

}

@end

5.demo的下载地址,喜欢的话给个星,谢谢:

iOS根据不同数据跳转不同界面,动态添加属性及其控件等界面内容

转自:https://www.jianshu.com/p/376a3bc7741b

收起阅读 »

AVPlayer封装

说明基于AVPlayer和MVP模式封装的一个视频播放控制器,支持全屏,暂停播放,进度条拖动。Demo地址AVPlayer框架介绍AVPlay既可以用来播放音频也可以用来播放视频,AVPlay在播放音频方面可以直接用来播放网络上的音频。在使用AVPlay的时候...
继续阅读 »

说明

基于AVPlayer和MVP模式封装的一个视频播放控制器,支持全屏,暂停播放,进度条拖动。

Demo地址

AVPlayer框架介绍

AVPlay既可以用来播放音频也可以用来播放视频,AVPlay在播放音频方面可以直接用来播放网络上的音频。在使用AVPlay的时候我们需要导入AVFoundation.framework框架,再引入头文件#import<AVFoundation/AVFoundation.h>。

主要包括下面几个类

1.AVPlayer:播放器类
2.AVPlayerItem:播放单元类,即一个播放源
3.AVPlayerLayer:播放界面

使用时,需要先根据NSURL生成一个播放源,[AVPlayerItem playerItemWithURL:],再根据这个播放源获得一个播放器对象,[AVPlayer playerWithPlayerItem:];,此时播放器已经准备完成,但还需要根据AVPlayer生成一个AVPlayerLayer,设置frame,再加入到superView.layer中,[AVPlayerLayer playerLayerWithPlayer:]; self.playerLayer.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.width*0.6); [self.layer addSublayer:self.playerLayer];

此时一个简单的播放器就已经配置完成。

暂停播放

AVPlayer有一个rate属性,可以根据这个属性来判断当前是否在播放,rate == 0.f为暂停,反之视频播放。

AVPlayerItemStatus
可以对AVPlayerItem设置kvo,监听视频源是否可播放,系统给了三种状态,如下:

typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
AVPlayerItemStatusUnknown,
AVPlayerItemStatusReadyToPlay,
AVPlayerItemStatusFailed
};

设置KVO监听:

[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItemStatus status = [change[NSKeyValueChangeNewKey] intValue];
if (status == AVPlayerItemStatusReadyToPlay) {
isReadyToPlay = YES;
[self.player play];
}else{
//预留
isReadyToPlay = NO;
}
[self.controlView controlItemStatus:status playItem:object];
}
}

全屏操作

Demo中给出的思路是:

1.首先将当前竖屏状态下的播放器的view的frame保存下来,方便退出全屏时,布局;
2.然后新建一个全屏展示View的控制器,重写该控制器的@property(nonatomic, readonly) UIInterfaceOrientation preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;,强制让该控制器旋转;
3.将当前根控制器present到上述的全屏控制器,在completion:回调中,做个简单的动画过渡一下,然后再将承载AVPlayerLayer的view的frame改成横屏状态,然后再修改AVPlayerLayer的frame;

退出全屏:

1.将当前全屏控制器dismiss;
2.再dismiss的成功回调中,设置View的frame为进入全屏前保存的frame;
3.再将AVPlayerLayer的frame修改。

代码如下:

#pragma mark - 进入全屏和退出全屏的动画和present处理
- (void)enterFullScreen:(BOOL)rightOrLeft{
playViewBeforeRect = _playerView.frame;
playViewBeforeCenter = _playerView.center;

TBZAVFullViewController *vc = [[TBZAVFullViewController alloc] init];
vc.type = rightOrLeft;
self.fullVC = vc;

__weak TBZAVPlayerViewController *weakSelf = self;

[self.navigationController presentViewController:vc animated:false completion:^{
[UIView animateWithDuration:0.25 animations:^{
weakSelf.playerView.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
} completion:^(BOOL finished) {
[weakSelf.playerView enterFull];
[weakSelf.fullVC.view addSubview:weakSelf.playerView];
[UIApplication.sharedApplication.keyWindow insertSubview:UIApplication.sharedApplication.keyWindow.rootViewController.view belowSubview:vc.view.superview];

self->isFull = YES;
}];
}];
}

- (void)exitFullScreen{
__weak TBZAVPlayerViewController *weakSelf = self;
[self.fullVC dismissViewControllerAnimated:false completion:^{
[UIView animateWithDuration:0.25 animations:^{
weakSelf.playerView.frame = self->playViewBeforeRect;
} completion:^(BOOL finished) {
[weakSelf.playerView exitFull];
[weakSelf.view addSubview:weakSelf.playerView];

self->isFull = NO;
}];
}];
}

播放进度

主要就是需要对AVPlayer添加监听,且注意需要释放该方法返回的对象。AVPlayerItem有两个属性,currentTime和duration,这两个对象都是CMTime类,可以用CMTimeGetSeconds(CMTime t);得到一个float指,秒数。也就是CMTimeGetSeconds(item.currentTime)可以得到当前播放到第几秒,CMTimeGetSeconds(item.duration)可以得到当前视频的总时长。

/*!
@method addPeriodicTimeObserverForInterval:queue:usingBlock:
@abstract Requests invocation of a block during playback to report changing time.
@param interval
The interval of invocation of the block during normal playback, according to progress of the current time of the player.
@param queue
The serial queue onto which block should be enqueued. If you pass NULL, the main queue (obtained using dispatch_get_main_queue()) will be used. Passing a
concurrent queue to this method will result in undefined behavior.
@param block
The block to be invoked periodically.
@result
An object conforming to the NSObject protocol. You must retain this returned value as long as you want the time observer to be invoked by the player.
Pass this object to -removeTimeObserver: to cancel time observation.
@discussion The block is invoked periodically at the interval specified, interpreted according to the timeline of the current item.
The block is also invoked whenever time jumps and whenever playback starts or stops.
If the interval corresponds to a very short interval in real time, the player may invoke the block less frequently
than requested. Even so, the player will invoke the block sufficiently often for the client to update indications
of the current time appropriately in its end-user interface.
Each call to -addPeriodicTimeObserverForInterval:queue:usingBlock: should be paired with a corresponding call to -removeTimeObserver:.
Releasing the observer object without a call to -removeTimeObserver: will result in undefined behavior.
*/
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

/*!
@method removeTimeObserver:
@abstract Cancels a previously registered time observer.
@param observer
An object returned by a previous call to -addPeriodicTimeObserverForInterval:queue:usingBlock: or -addBoundaryTimeObserverForTimes:queue:usingBlock:.
@discussion Upon return, the caller is guaranteed that no new time observer blocks will begin executing. Depending on the calling thread and the queue
used to add the time observer, an in-flight block may continue to execute after this method returns. You can guarantee synchronous time
observer removal by enqueuing the call to -removeTimeObserver: on that queue. Alternatively, call dispatch_sync(queue, ^{}) after
-removeTimeObserver: to wait for any in-flight blocks to finish executing.
-removeTimeObserver: should be used to explicitly cancel each time observer added using -addPeriodicTimeObserverForInterval:queue:usingBlock:
and -addBoundaryTimeObserverForTimes:queue:usingBlock:.
*/
- (void)removeTimeObserver:(id)observer;

- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;其实就是一个Timer,每隔1秒执行block,可以设置常驻子线程,如果设为NULL,就是在主线程。
主要使用如下:

__weak AVPlayer *weakAVPlayer = self.player;
__weak TBZAVPlayerView *weakSelf = self;
//监听播放进度,需要再destory方法中,释放timeObserve
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, NSEC_PER_SEC) queue:NULL usingBlock:^(CMTime time) {
CGFloat progress = CMTimeGetSeconds(weakAVPlayer.currentItem.currentTime) / CMTimeGetSeconds(weakAVPlayer.currentItem.duration);
if (progress == 1.0f) {
//视频播放完毕
if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(playEnd)]) {
[weakSelf.delegate playEnd];
}
}else{
[weakSelf.controlView controlPlayItem:weakAVPlayer.currentItem];
}
}];

- (void)destroy{
if (self.player || self.playerItem || self.playerLayer) {
[self.player pause];
if (self.timeObserver) {
[self.player removeTimeObserver:self.timeObserver];
}
[self.playerItem removeObserver:self forKeyPath:@"status"];
self.playerItem = nil;
self.player = nil;
[self.playerLayer removeFromSuperlayer];
}
}

总结

1.当视频源切换了之后,需要将当前视频源添加的监听都remove掉,重新给新的视频源添加监听;
2.全屏跟退出全屏,主要是注意AVPlayerLayer的布局,不会跟着superLayer的变动而变动,需要手动再设置一遍;

具体可以结合Demo来看。

Demo下载

觉得有用,请帮忙点亮红心

Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

10人点赞
iOS知识点

链接:https://www.jianshu.com/p/55825996cb11

收起阅读 »

Android仿魅族桌面悬浮球!

背景 游戏内的悬浮窗通常情况下只出现在游戏内,用做切换账号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及我们踩过的坑 1、方案一:应用外悬浮窗+栈顶权限/生命周期回调 通常实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用WindowManager...
继续阅读 »

背景



游戏内的悬浮窗通常情况下只出现在游戏内,用做切换账号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及我们踩过的坑



1、方案一:应用外悬浮窗+栈顶权限/生命周期回调


通常实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用WindowManager在设备界面上addView实现(UI层级较高,应用外显示)


1、弹出悬浮窗需要用到悬浮窗权限

<!--悬浮窗权限-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>



2、判断悬浮窗游戏内外显示


方式一:使用栈顶权限获取当前


//需要声明权限


//判断当前是否在后台
private boolean isAppIsInBackground(Context context) {
boolean isInBackground = true;
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
List runningProcesses = am.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
//前台程序
if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
for (String activeProcess : processInfo.pkgList) {
if (activeProcess.equals(context.getPackageName())) {
isInBackground = false;
}
}
}
}
} else {
List taskInfo = am.getRunningTasks(1);
ComponentName componentInfo = taskInfo.get(0).topActivity;
if (componentInfo.getPackageName().equals(context.getPackageName())) {
isInBackground = false;
}
}

return isInBackground;


这里考虑到这种方案网上有很多具体案例,在这里就不实现了。但是这种方案有如下缺点:


1、适配问题,悬浮窗权限在不同设备上由于不同产商实现不同,适配难。


2、向用户申请权限,打开率较低,体验较差


2、方案二:addContentView实现


原理:Activity的接口中除了我们常用的setContentView接口外,还有addContentView接口。利用该接口可以在Activity上添加View。


这里你可能会问:


1、那只能在一个Activity上添加吧?


没错,是只能在当前Activity上添加,但是由于游戏通常也就在一个Activity跑,因此基本上是可以接受的。


2、只add一个view,那拖动怎么实现?


LayoutParams params = new LayoutParams(mWidth, mHeight);
params.setMargins(mLeft, mTop, 0, 0);
setLayoutParams(params);


通过更新LayoutParams调整子View在父View中的位置就能实现


具体代码如下:


/**
* @author zhuxiaoxin
* 可拖拽贴边的view
*/

public class DragViewLayout extends RelativeLayout {

//手指拖拽得到的位置
int mLeft, mRight, mTop, mBottom;

//view所在的位置
int mLastX, mLastY;

/**
* 屏幕宽度|高度
*/

int mScreenWidth, mScreenHeight;

/**
* view的宽度|高度
*/

int mWidth, mHeight;


/**
* 是否在拖拽过程中
*/

boolean isDrag = false;

/**
* 系统最小滑动距离
* @param context
*/

int mTouchSlop = 0;

public DragViewLayout(Context context) {
this(context, null);
}

public DragViewLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}

@Override
protected void onFinishInflate()
{
super.onFinishInflate();
}

@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLeft = getLeft();
mRight = getRight();
mTop = getTop();
mBottom = getBottom();
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int x = (int) event.getRawX();
int y = (int) event.getRawY();
int dx = x - mLastX;
int dy = y - mLastY;
if (Math.abs(dx) > mTouchSlop) {
isDrag = true;
}
mLeft += dx;
mRight += dx;
mTop += dy;
mBottom += dy;
if (mLeft < 0) {
mLeft = 0;
mRight = mWidth;
}
if (mRight >= mScreenWidth) {
mRight = mScreenWidth;
mLeft = mScreenWidth - mWidth;
}
if (mTop < 0) {
mTop = 0;
mBottom = getHeight();
}
if (mBottom > mScreenHeight) {
mBottom = mScreenHeight;
mTop = mScreenHeight - mHeight;
}
mLastX = x;
mLastY = y;
//根据拖动举例设置view的margin参数,实现拖动效果
LayoutParams params = new LayoutParams(mWidth, mHeight);
params.setMargins(mLeft, mTop, 0, 0);
setLayoutParams(params);
break;
case MotionEvent.ACTION_UP:
//手指抬起,执行贴边动画
if (isDrag) {
startAnim();
isDrag = false;
}
break;
}
return super.dispatchTouchEvent(event);
}

//执行贴边动画
private void startAnim(){
ValueAnimator valueAnimator;
if (mLeft < mScreenWidth / 2) {
valueAnimator = ValueAnimator.ofInt(mLeft, 0);
} else {
valueAnimator = ValueAnimator.ofInt(mLeft, mScreenWidth - mWidth);
}
//动画执行时间
valueAnimator.setDuration(100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
mLeft = (int)animation.getAnimatedValue();
//动画执行依然是使用设置margin参数实现
LayoutParams params = new LayoutParams(mWidth, mHeight);
params.setMargins(mLeft, getTop(), 0, 0);
setLayoutParams(params);
}
});
valueAnimator.start();
}


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
if (mWidth == 0) {
//获取view的高宽
mWidth = getWidth();
mHeight = getHeight();
}
}

}


/**
*
@author zhuxiaoxin
* 37悬浮窗基础view
*/

public class SqAddFloatView extends DragViewLayout {

private RelativeLayout mFloatContainer;

public SqAddFloatView(final Context context, final int floatImgId) {
super(context);
setClickable(true);
final ImageView floatView = new ImageView(context);
floatView.setImageResource(floatImgId);
floatView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "点击了悬浮球", Toast.LENGTH_SHORT).show();
}
});
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
addView(floatView, params);
}

public void show(Activity activity) {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
if(mFloatContainer == null) {
mFloatContainer = new RelativeLayout(activity);
}
RelativeLayout.LayoutParams floatViewParams = new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
floatViewParams.setMargins(0, (int) (mScreenHeight * 0.4), 0, 0);
mFloatContainer.addView(this, floatViewParams);
activity.addContentView(mFloatContainer, params);

}
}


在Activity中使用


SqAddFloatView(this, R.mipmap.ic_launcher).show(this)


3、方案三:WindowManager+应用内层级实现


WindowManger中的层级有如下两个(其实是一样的~)可以实现在Activity上增加View


        /**
* Start of types of sub-windows. The {@link #token} of these windows
* must be set to the window they are attached to. These types of
* windows are kept next to their attached window in Z-order, and their
* coordinate space is relative to their attached window.
*/

public static final int FIRST_SUB_WINDOW = 1000;

/**
* Window type: a panel on top of an application window. These windows
* appear on top of their attached window.
*/

public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;


具体实现时,WindowManger相关的核心代码如下:


    public void show() {
floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
//最最重要的一句 WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.RGBA_8888);
floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
floatLayoutParams.x = mMinWidth;
floatLayoutParams.y = (int)(mScreenHeight * 0.4);
mWindowManager.addView(this, floatLayoutParams);
}


添加完view如何更新位置?


使用WindowManager的updateViewLayout方法


mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);


完整代码如下:


DragViewLayout:


public class DragViewLayout extends RelativeLayout {

//view所在位置
int mLastX, mLastY;

//屏幕高宽
int mScreenWidth, mScreenHeight;

//view高宽
int mWidth, mHeight;

/**
* 是否在拖拽过程中
*/

boolean isDrag = false;

/**
* 系统最小滑动距离
* @param context
*/

int mTouchSlop = 0;

WindowManager.LayoutParams floatLayoutParams;
WindowManager mWindowManager;

//手指触摸位置
private float xInScreen;
private float yInScreen;
private float xInView;
public float yInView;


public DragViewLayout(Context context) {
this(context, null);
}

public DragViewLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
}

@Override
protected void onFinishInflate()
{
super.onFinishInflate();
}

@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
yInView = event.getY();
xInView = event.getX();
xInScreen = event.getRawX();
yInScreen = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int dx = (int) event.getRawX() - mLastX;
int dy = (int) event.getRawY() - mLastY;
if (Math.abs(dx) > mTouchSlop || Math.abs(dy) > mTouchSlop) {
isDrag = true;
}
xInScreen = event.getRawX();
yInScreen = event.getRawY();
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
//拖拽时调用WindowManager updateViewLayout更新悬浮球位置
updateFloatPosition(false);
break;
case MotionEvent.ACTION_UP:
if (isDrag) {
//执行贴边
startAnim();
isDrag = false;
}
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}

//更新悬浮球位置
private void updateFloatPosition(boolean isUp) {
int x = (int) (xInScreen - xInView);
int y = (int) (yInScreen - yInView);
if(isUp) {
x = isRightFloat() ? mScreenWidth : 0;
}
if(y < 0) {
y = 0;
}
if(y > mScreenHeight - mHeight) {
y = mScreenHeight - mHeight;
}
floatLayoutParams.x = x;
floatLayoutParams.y = y;
//更新位置
mWindowManager.updateViewLayout(this, floatLayoutParams);
}

/**
* 是否靠右边悬浮
* @return
*/

boolean isRightFloat() {
return xInScreen > mScreenWidth / 2;
}


//执行贴边动画
private void startAnim(){
ValueAnimator valueAnimator;
if (floatLayoutParams.x < mScreenWidth / 2) {
valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, 0);
} else {
valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, mScreenWidth - mWidth);
}
valueAnimator.setDuration(200);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
floatLayoutParams.x = (int)animation.getAnimatedValue();
mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);
}
});
valueAnimator.start();
}

//悬浮球显示
public void show() {
floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.RGBA_8888);
floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
floatLayoutParams.x = 0;
floatLayoutParams.y = (int)(mScreenHeight * 0.4);
mWindowManager.addView(this, floatLayoutParams);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
super.onLayout(changed, l, t, r, b);
if (mWidth == 0) {
//获取悬浮球高宽
mWidth = getWidth();
mHeight = getHeight();
}
}
}


悬浮窗View


public class SqWindowManagerFloatView extends DragViewLayout {


public SqWindowManagerFloatView(final Context context, final int floatImgId) {
super(context);
setClickable(true);
final ImageView floatView = new ImageView(context);
floatView.setImageResource(floatImgId);
floatView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "点击了悬浮球", Toast.LENGTH_SHORT).show();
}
});
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
addView(floatView, params);
}
}


使用:


SqWindowManagerFloatView(this, R.mipmap.float_icon).show()


4、小结


1、方案一需要用到多个权限,显然是不合适的。


2、方案二简单方便,但是用到了Activity的addContentView方法,在某些游戏引擎上使用会有问题。因为有些游戏引擎不是在Activity上跑的,而是在NativeActivity上跑


3、方案三是我们当前采用的方案,目前还暂未发现有显示不出来之类的问题~


4、本文讲述的方案只是Demo哈,实际使用还需要考虑刘海屏的问题,本文暂未涉及


代码下载:way-Doughnut-master.zip 收起阅读 »

安卓自定义view - 2048 小游戏

为了学习自定义 ViewGroup,正碰巧最近无意间玩了下 2048 的游戏,因此这里就来实现一个 2048 小游戏。想必很多人应该是玩过这个游戏的,如果没有玩过的可以下载玩一下。下图是我实现的效果。2048 游戏规则游戏规则比较简单,共有如下几个步骤:向一个...
继续阅读 »

为了学习自定义 ViewGroup,正碰巧最近无意间玩了下 2048 的游戏,因此这里就来实现一个 2048 小游戏。想必很多人应该是玩过这个游戏的,如果没有玩过的可以下载玩一下。下图是我实现的效果。

2048 游戏规则

游戏规则比较简单,共有如下几个步骤:

  1. 向一个方向移动,所有格子会向那个方向移动
  2. 相同的数字合并,即相加
  3. 每次移动时,空白处会随机出现一个数字2或4
  4. 当界面不可移动时,即格子被数字填满,游戏结束,网格中出现 2048 的数字游戏胜利,反之游戏失败。

2048 游戏算法

算法主要是讨论上下左右四个方向如何合并以及移动,这里我以向左和向上来说明,而向下和向右就由读者自行推导,因为十分相似。

向左移动算法

先来看下面两张图,第一张是初始状态,可以看到网格中有个数字 2。在这里用二维数组来描述。它的位置应该是第2行第2列 。第二张则是它向左移动后的效果图,可以看到 2 已经被移动到最左边啦!

我们最常规的想法就是首先遍历这个二维数组,找到这个数的位置,接着合并和移动。所以第一步肯定是循环遍历。

int i;
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; ) {
Model model = models[x][y];
int number = model.getNumber();
if (number == 0) {
y++;
continue;
} else {
// 找到不为零的位置. 下面就需要进行合并和移动处理

}
}
}


上面的代码非常简单,这里引入了 Model 类,这个类是封装了网格单元的数据和网格视图。定义如下:先不纠结视图的绘制,我们先把算法理清楚,算法搞明白了也就解决一大部分了,其他就是自定义 View 的知识。上述的过程就是,遍历整个网格,找到不为零的网格位置。


public class Model {

private int number;
/**
* 单元格视图.
*/

private CellView cellView;

public Model(int number, CellView cellView) {
this.number = number;
this.cellView = cellView;
}

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public CellView getCellView() {
return cellView;
}

public void setCellView(CellView cellView) {
this.cellView = cellView;
}
}




让我们来思考一下,合并要做什么,那么我们再来看一张图。

从这张图中我们可以看到在第一行的最后两个网格单元都是2,当向左移动时,根据 2048 游戏规则,我们需要将后面的一个2 和前面的 2 进行合并(相加)运算。是不是可以推理,我们找到第一个不为零的数的位置,然后找到它右边第一个不为零的数,判断他们是否相等,如果相等就合并。算法如下:

int i;
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; ) {
Model model = models[x][y];
int number = model.getNumber();
if (number == 0) {
y++;
continue;
} else {
// 找到不为零的位置. 下面就需要进行合并和移动处理
// 这里的 y + 1 就是找到这个数的右侧
for (i = y + 1; i < 4; i++) {
if (models[x][i].getNumber() == 0) {
continue;
} else if (models[x][y].getNumber() == models[x][i].getNumber()) {
// 找到相等的数
// 合并,相加操作
models[x][y].setNumber(
models[x][y].getNumber() + models[x][i].getNumber())

// 将这个数清0
models[x][i].setNumber(0);

break;
} else {
break;
}
}

// 防止陷入死循环,所以必须要手动赋值,将其跳出。
y = i;
}
}
}


通过上面的过程,我们就将这个数右侧的第一个相等的数进行了合并操作,是不是也好理解的。不理解的话可以在草稿纸上多画一画,多推导几次。

搞定了合并操作,现在就是移动了,移动肯定是要将所有数据的单元格都移动到左侧,移动的条件是,找到第一个不为零的数的坐标,继续向前找到第一个数据为零即空白单元格的位置,将数据覆盖它,并将后一个单元格数据清空。算法如下:

for (int x = 0; x < 4; x++) {
for (y = 0; y < 4; y++) {
if (models[x][y].getNumber() == 0) {
continue;
} else {
// 找到当前数前面为零的位置,即空格单元
for (int j = y; j >= 0 && models[x][j - 1].getNumber() == 0; j--) {
// 数据向前移动,即数据覆盖.
models[j - 1][y].setNumber(
models[j][y].getNumber())
// 清空数据
models[j][y].setNumber(0)
}
}
}
}

到此向左移动算法完毕,接着就是向上移动的算法。

向上移动算法

有了向左移动的算法思维,理解向上的操作也就变得容易一些啦!首先我们先来看合并,合并的条件也就是找到第一个不为零的数,然后找到它下一行第一个不为零且相等的数进行合并。算法如下:

int i = 0;
for (int y = 0; y < 4; y++) {
for (x = 3; x >= 0; ) {
if (models[x][y].getNumber() == 0) {
continue;
} else {
for (i = x + 1; i < 4; i++) {
if (models[i][y].getNumber() == 0) {
continue;
} else if (models[x][y].getNumber() == models[i][y].getNumber()) {
models[x][y].setNumber(
models[x][y].getNumber() + models[i][y].getNumber();
)

models[i][y].setNumber(0);

break;
} else {
break;
}
}
}
}
}


移动的算法也类似,即找到第一个不为零的数前面为零的位置,即空格单元的位置,将数据覆盖并将后一个单元格的数据清空。

for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; y++) {
if (models[x][y].getNumber() == 0) {
continue;
} else {
for (int j = x; x >
0 && models[j - 1][y].getNumber() == 0; j--) {
models[j -1][y].setNumber(models[j][y].getNumber());

models[j][y].setNumber(0);
}
}
}
}


到此,向左移动和向上移动的算法就描述完了,接下来就是如何去绘制视图逻辑啦!

网格单元绘制

首先先忽略数据源,我们只是单纯的绘制网格,有人可能说了我们不用自定义的方式也能实现,我只想说可以,但是不推荐。如果使用自定义 ViewGroup,将每一个小的单元格作为单独的视图。这样扩展性更好,比如我做了对随机显示的单元格加上动画。

既然是自定义 ViewGroup, 那我们就创建一个类并继承 ViewGroup,其定义如下:

public class Play2048Group extends ViewGroup {

public Play2048Group(Context context) {
this(context, null);
}

public Play2048Group(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public Play2048Group(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

......
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
.....
}

}


我们要根据子视图的大小来测量容器的大小,在 onLayout 中摆放子视图。为了更好的交给其他开发者使用,我们尽量可以让 view 能被配置。那么就要自定义属性。

  1. 自定义属性

这里只是提供了设置网格单元行列数,其实这里我我只取两个值的最大值作为行列的值。













  1. 布局中加载自定义属性

可以看到将传入的 row 和 column 取大的作为行列数。

public Play2048Group(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Play2048Group);

try {
mRow = a.getInteger(R.styleable.Play2048Group_row, 4);
mColumn = a.getInteger(R.styleable.Play2048Group_column, 4);
// 保持长宽相等排列, 取传入的最大值
if (mRow > mColumn) {
mColumn = mRow;
} else {
mRow = mColumn;
}

init();

} catch (Exception e) {
e.printStackTrace();
} finally {
a.recycle();
}
}


  1. 网格子视图

因为整个网格有一个个网格单元组成,其中每一个网格单元都是一个 view, 这个 view 其实也就只是绘制了一个矩形,然后在矩形的中间绘制文字。考虑文章篇幅,我这里只截取 onMeasure 和 onDraw 方法。

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 我这里直接写死了,当然为了屏幕适配,这个值应该由外部传入的,
// 这里就当我留下的作业吧 😄
setMeasuredDimension(130, 130);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// 绘制矩形.
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

// 如果当前单元格的数据不为0,就绘制。
// 如果为零,就使用背景的颜色作为画笔绘制,这么做就是为了不让它显示出来😳
if (!mNumber.equalsIgnoreCase("0")) {
mTextPaint.setColor(Color.parseColor("#E451CD"));
canvas.drawText(mNumber,
(float) (getMeasuredWidth() - bounds.width()) / 2,
(float) (getMeasuredHeight() / 2 + bounds.height() / 2), mTextPaint);
} else {
mTextPaint.setColor(Color.parseColor("#E4CDCD"));
canvas.drawText(mNumber,
(float) (getMeasuredWidth() - bounds.width()) / 2,
(float) (getMeasuredHeight() / 2 + bounds.height() / 2), mTextPaint);
}
}



  1. 测量容器视图

由于网格是行列数都相等,则宽和高都相等。那么所有的宽加起来除以 row, 所有的高加起来除以 column 就得到了最终的宽高, 不过记得要加上边距。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int width = 0;
int height = 0;

int count = getChildCount();

MarginLayoutParams layoutParams =
(MarginLayoutParams)getChildAt(0).getLayoutParams();

// 每一个单元格都有左边距和上边距
int leftMargin = layoutParams.leftMargin;
int topMargin = layoutParams.topMargin;

for (int i = 0; i < count; i++) {
CellView cellView = (CellView) getChildAt(i);
cellView.measure(widthMeasureSpec, heightMeasureSpec);

int childW = cellView.getMeasuredWidth();
int childH = cellView.getMeasuredHeight();

width += childW;
height += childH;
}

// 需要加上每个单元格的左边距和上边距
setMeasuredDimension(width / mRow + (mRow + 1) * leftMargin,
height / mRow + (mColumn + 1) * topMargin);
}


  1. 布局子视图(网格单元)

布局稍微麻烦点,主要是在换行处的计算有点绕。首先我们找一下什么时候是该换行了,如果是 4 * 4 的 16 宫格,我们可以知道每一行的开头应该是 0、4、8、12,如果要用公式来表示的就是: temp = mRow * (i / mRow), 这里的 mRow 为行数,i 为索引。

我们这里首先就是要确定每一行的第一个视图的位置,后面的视图就好确定了, 下面是推导过程:

第一行: 
网格1:
left = lefMargin;
top = topMargin;
right = leftMargin + width;
bottom = topMargin + height;

网格2:
left = leftMargin + width + leftMargin
top = topMargin;
right = leftMargin + width + leftMargin + width
bottom = topMargin + height

网格3:
left = leftMargin + width + leftMargin + width + leftMargin
right = leftMargin + width + leftMargin + width + leftMargin + width

...
第二行:
网格1:
left = leftMargin
top = topMargin + height
right = leftMargin + width
bottom = topMargin + height + topMargin + height

网格2:
left = leftMargin + width + leftMargin
top = topMargin + height + topMargin
right = leftMargin + width + lefMargin + width
bottom = topMargin + height + topMargin + height


上面的应该很简单的吧,这是根据画图的方式直观的排列,我们可以归纳总结,找出公式。

除了每一行的第一个单元格的 left, right 都相等。 其他的可以用一个公式来总结:

left = leftMargin * (i - temp + 1) + width * (i - temp)
right = leftMargin * (i - temp + 1) + width * (i - temp + 1)

可以随意带数值进入然后对比画图看看结果,比如(1, 1) 即第二行第二列。

temp = row * (i / row) => 4 * 1 = 4

left = leftMargin * (5 - 4 + 1) + width * (5 - 4)
= leftMargin * 2 + width

right = leftMargin * (5 - 4 + 1) + width * (5 - 4 + 1)
= lefMargin * 2 + width * 2

和上面的手动计算完全一样,至于为什么 i = 5 那是因为 i 循环到第二行的第二列为 5


除了第一行第一个单元格其他的 top, bottom 可以用公式:

top = height * row + topMargin * row + topMargin
bottom = height * (row + 1) + topMargin(row + 1)


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
CellView cellView = (CellView) getChildAt(i);
MarginLayoutParams layoutParams = (MarginLayoutParams) cellView.getLayoutParams();
int leftMargin = layoutParams.leftMargin;
int topMargin = layoutParams.topMargin;

int width = cellView.getMeasuredWidth();
int height = cellView.getMeasuredHeight();

int left = 0, top = 0, right = 0, bottom = 0;

// 每一行开始, 0, 4, 8, 12...
int temp = mRow * (i / mRow);
// 每一行的开头位置.
if (i == temp) {
left = leftMargin;
right = width + leftMargin;
} else {
left = leftMargin * (i - temp + 1) + width * (i - temp);
right = leftMargin * (i - temp + 1) + + width * (i - temp + 1);
}

int row = i / mRow;
if (row == 0) {
top = topMargin;
bottom = height + topMargin;
} else {
top = height * row + topMargin * row + topMargin;
bottom = height * (row + 1) + (row + 1) * topMargin;
}

cellView.layout(left, top, right, bottom);
}
}


  1. 初始数据
private void init() {
models = new Model[mRow][mColumn];
cells = new ArrayList<>(mRow * mColumn);

for (int i = 0; i < mRow * mColumn; i++) {
CellView cellView = new CellView(getContext());
MarginLayoutParams params = new MarginLayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

params.leftMargin = 10;
params.topMargin = 10;
cellView.setLayoutParams(params);

Model model = new Model(0, cellView);
cells.add(model);

addView(cellView, i);
}
}


以上就是未带数据源的宫格绘制过程,接下来开始接入数据源来动态改变宫格的数据啦!

动态改变数据

  1. 初始化数据源,随机显示一个数据 2
private void init() {
... 省略部分代码.....

int i = 0;
for (int x = 0; x < mRow; x++) {
for (int y = 0; y < mColumn; y++) {
models[x][y] = cells.get(i);
i++;
}
}

// 生成一个随机数,初始化数据.
mRandom = new Random();
rand = mRandom.nextInt(mRow * mColumn);
Model model = cells.get(rand);
model.setNumber(2);
CellView cellView = model.getCellView();
cellView.setNumber(2);

// 初始化时空格数为总宫格个数 - 1
mAllCells = mRow * mColumn - 1;

// 程序动态变化这个值,用来判断当前宫格还有多少空格可用.
mEmptyCells = mAllCells;


... 省略部分代码.....
}


  1. 计算随机数生成的合法单元格位置

生成的随机数据必须在空白的单元格上。

 private void nextRand() {
// 如果所有宫格被填满则游戏结束,
// 当然这里也有坑,至于怎么发现,你多玩几次机会发现,
// 这个坑我就不填了,有兴趣的可以帮我填一下😄😄
if (mEmptyCells <= 0) {
findMaxValue();
gameOver();
return;
}

int newX, newY;

if (mEmptyCells != mAllCells || mCanMove == 1) {
do {
// 通过伪随机数获取新的空白位置
newX = mRandom.nextInt(mRow);
newY = mRandom.nextInt(mColumn);
} while (models[newX][newY].getNumber() != 0);

int temp = 0;

do {
temp = mRandom.nextInt(mRow);
} while (temp == 0 || temp == 2);

Model model = models[newX][newY];
model.setNumber(temp + 1);
CellView cellView = model.getCellView();
cellView.setNumber(model.getNumber());
playAnimation(cellView);

// 空白格子减1
mEmptyCells--;
}
}


  1. 向左移动

算法是我们前面推导的,最后调用 drawAll() 绘制单元格文字, 以及调用 nextRand() 生成新的随机数。

public void left() {
if (leftRunnable == null) {
leftRunnable = new Runnable() {
@Override
public void run() {
int i;
for (int x = 0; x < mRow; x++) {
for (int y = 0; y < mColumn; ) {
Model model = models[x][y];
int number = model.getNumber();
if (number == 0) {
y++;
continue;
} else {
// 找到不为零的位置. 往后找不为零的数进行运算.
for (i = y + 1; i < mColumn; i++) {
Model model1 = models[x][i];
int number1 = model1.getNumber();
if (number1 == 0) {
continue;
} else if (number == number1) {
// 如果找到和这个相同的,则进行合并运算(相加)。
int temp = number + number1;
model.setNumber(temp);
model1.setNumber(0);

mEmptyCells++;
break;
} else {
break;
}
}

y = i;
}
}
}

for (int x = 0; x < mRow; x++) {
for (int y = 0; y < mColumn; y++) {
Model model = models[x][y];
int number = model.getNumber();
if (number == 0) {
continue;
} else {
for (int j = y; (j > 0) && models[x][j - 1].getNumber() == 0; j--) {
models[x][j - 1].setNumber(models[x][j].getNumber());
models[x][j].setNumber(0);

mCanMove = 1;
}
}
}
}

drawAll();
nextRand();
}
};
}

mExecutorService.execute(leftRunnable);
}

  1. 随机单元格动画
private void playAnimation(final CellView cellView) {
mainHandler.post(new Runnable() {
@Override
public void run() {
ObjectAnimator animator = ObjectAnimator.ofFloat(
cellView, "alpha", 0.0f, 1.0f);
animator.setDuration(300);
animator.start();
}
});
}


代码下载:i1054959069-simple-2048-games-master.zip

收起阅读 »

一个你想象不到的验证码输入框!

之所以造这个轮子,是因为之前有这样的需求,然后也用过其它类似开源的库(VerificationCodeView),但是需求随着需求的变动,之前使用的库就不太满足现有的需求。所以最近抽空写了一个。 支持设置框数量 支持设置框的风格样式&nbs...
继续阅读 »

SplitEditText for Android 是一个灵活的分割编辑框。常常应用于 验证码输入 、密码输入 、等场景。

之所以造这个轮子,是因为之前有这样的需求,然后也用过其它类似开源的库(VerificationCodeView),但是需求随着需求的变动,之前使用的库就不太满足现有的需求。所以最近抽空写了一个。

特性说明

  •  支持设置框数量
  •  支持设置框的风格样式
  •  支持根据状态区分框颜色
  •  基于EditText实现,更优雅


SplitEditText 自定义属性说明

属性值类型默认值说明
setStrokeWidthdimension1dp画笔描边的宽度
setBorderColorcolor#FF666666边框颜色
setInputBorderColorcolor#FF1E90FF已输入文本的边框颜色
setFocusBorderColorcolor焦点框的边框颜色
setBoxBackgroundColorcolor框的背景颜色
setBorderCornerRadiusdimension0dp框的圆角大小(当 BorderSpacing 为 0dp 时,只有最左和最右两端的框有圆角)
setBorderSpacingdimension8dp框与框之间的间距大小
setMaxLengthinteger6允许输入的最大长度(框个数量)
setBorderStyleenumbox边框风格
setTextStyleenumplain_text文本风格(可以是明文或者密文,默认:明文)
setCipherMaskstring*密文掩码(当 TextStyle 为密文时,可自定义密文掩码)
setFakeBoldTextbooleanfalse是否是粗体

引入

Maven:


com.king.view
splitedittext
1.0.0
pom

Gradle:

//AndroidX
implementation 'com.king.view:splitedittext:1.0.0'

如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)

allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

布局示例

    
android:id="@+id/splitEditText"
android:layout_width="match_parent"
android:layout_height="45dp"
android:inputType="number"/>

代码示例

Kotlin

    //设置监听
splitEditText.setOnTextInputListener(object : SplitEditText.OnTextInputListener {
override fun onTextInputChanged(text: String, length: Int) {
//TODO 文本输入改变
}

override fun onTextInputCompleted(text: String) {
//TODO 文本输入完成
}

})

Java

    //设置监听
splitEditText.setOnTextInputListener(new SplitEditText.OnTextInputListener(){

@Override
public void onTextInputChanged(String text, int length) {
//TODO 文本输入改变
}

@Override
public void onTextInputCompleted(String text) {
//TODO 文本输入完成
}
});

更多使用详情,请查看app中的源码使用示例

代码下载:jenly1314-SplitEditText-master



收起阅读 »

JAVA开发MQTT程序总结

JAVA开发MQTT总结MQTT 介绍它是一种 机器之间通讯 machine-to-machine (M2M)、物联网 Internet of Things (IoT)常用的一种轻量级消息传输协议适用于网络带宽较低的场合包含发布、订阅模式,通过一个代理服务器(...
继续阅读 »

JAVA开发MQTT总结

MQTT 介绍

  • 它是一种 机器之间通讯 machine-to-machine (M2M)、物联网 Internet of Things (IoT)常用的一种轻量级消息传输协议
  • 适用于网络带宽较低的场合
  • 包含发布、订阅模式,通过一个代理服务器(broker),任何一个客户端(client)都可以订阅或者发布某个主题的消息,然后订阅了该主题的客户端则会收到该消息

mqtt还是之前公司有需求所以写的一个demo,在这里记录下来,方便有人使用的时候查阅,不涉及mqtt的具体讲解,只是贴代码和运行过程。

MQTT的入门,以及特性,协议,结构的讲解,请看下面这篇文章

www.runoob.com/w3cnote/mqt…

什么是MQTT,它能干什么,它的应用场景在哪里?请参考下面这篇文章

www.ibm.com/developerwo…

本文中采用的MQTT服务器Apache-Apollo的下载配置搭建过程,请参考下面这篇文章

blog.csdn.net/qq_29350001…

下面就开始创建broker,

RaindeMacBook-Pro:bin rain$ ./apollo create mybroker
Creating apollo instance at: mybroker
Generating ssl keystore...

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore keystore -destkeystore keystore -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

You can now start the broker by executing:

"/Users/rain/Documents/Soft/apache-apollo-1.7.1/bin/mybroker/bin/apollo-broker" run

Or you can run the broker in the background using:

"/Users/rain/Documents/Soft/apache-apollo-1.7.1/bin/mybroker/bin/apollo-broker-service" start


进入新生成的broker中

RaindeMacBook-Pro:bin rain$ ls
apollo apollo.cmd mybroker testbroker
RaindeMacBook-Pro:bin rain$ cd mybroker/
RaindeMacBook-Pro:mybroker rain$ ls
bin data etc log tmp
RaindeMacBook-Pro:mybroker rain$ cd bin
RaindeMacBook-Pro:bin rain$ ls
apollo-broker apollo-broker-service

可以看到有两个文件,启动apollo-broker

启动成功以后,就可以在浏览器中访问了,默认用户名和密码是admin,password

刚进去是,Topics选项卡是空的,我是在运行程序后截图的,所以有一个topic列表

配置Maven

在pom.xml中添加以下配置

<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>

再创建下面的类

MqttServer

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class MqttServer2 {
/**
* 代理服务器ip地址
*/
public static final String MQTT_BROKER_HOST = "tcp://127.0.0.1:61613";

/**
* 订阅标识
*/
public static final String MQTT_TOPIC = "test2";

private static String userName = "admin";
private static String password = "password";

/**
* 客户端唯一标识
*/
public static final String MQTT_CLIENT_ID = "android_server_xiasuhuei32";
private static MqttTopic topic;
private static MqttClient client;

public static void main(String... args) {
// 推送消息
MqttMessage message = new MqttMessage();
try {
client = new MqttClient(MQTT_BROKER_HOST, MQTT_CLIENT_ID, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setUserName(userName);
options.setPassword(password.toCharArray());
options.setConnectionTimeout(10);
options.setKeepAliveInterval(20);

topic = client.getTopic(MQTT_TOPIC);

message.setQos(1);
message.setRetained(false);
message.setPayload("message from server222222".getBytes());
client.connect(options);

while (true) {
MqttDeliveryToken token = topic.publish(message);
token.waitForCompletion();
System.out.println("已经发送222");
Thread.sleep(10000);
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

MqttClient

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

public class MyMqttClient {
/**
* 代理服务器ip地址
*/
public static final String MQTT_BROKER_HOST = "tcp://127.0.0.1:61613";

/**
* 客户端唯一标识
*/
public static final String MQTT_CLIENT_ID = "android_xiasuhuei321";

/**
* 订阅标识
*/
// public static final String MQTT_TOPIC = "xiasuhuei321";

/**
*
*/
public static final String USERNAME = "admin";
/**
* 密码
*/
public static final String PASSWORD = "password";
public static final String TOPIC_FILTER = "test2";

private volatile static MqttClient mqttClient;
private static MqttConnectOptions options;

public static void main(String... args) {
try {
// host为主机名,clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,
// MemoryPersistence设置clientid的保存形式,默认为以内存保存

mqttClient = new MqttClient(MQTT_BROKER_HOST, MQTT_CLIENT_ID, new MemoryPersistence());
// 配置参数信息
options = new MqttConnectOptions();
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,
// 这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(true);
// 设置用户名
options.setUserName(USERNAME);
// 设置密码
options.setPassword(PASSWORD.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
// 连接
mqttClient.connect(options);
// 订阅
mqttClient.subscribe(TOPIC_FILTER);
// 设置回调
mqttClient.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable throwable) {
System.out.println("connectionLost");
}

@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
System.out.println("Topic: " + s + " Message: " + mqttMessage.toString());
}

@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {

}
});
} catch (Exception e) {
e.printStackTrace();
}

}

}

PublishSample

import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

/**
*发布端
*/
public class PublishSample {
public static void main(String[] args) {

String topic = "test2";
String content = "hello 哈哈";
int qos = 1;
String broker = "tcp://127.0.0.1:61613";
String userName = "admin";
String password = "password";
String clientId = "pubClient";
// 内存存储
MemoryPersistence persistence = new MemoryPersistence();

try {
// 创建客户端
MqttClient sampleClient = new MqttClient(broker, clientId, persistence);
// 创建链接参数
MqttConnectOptions connOpts = new MqttConnectOptions();
// 在重新启动和重新连接时记住状态
connOpts.setCleanSession(false);
// 设置连接的用户名
connOpts.setUserName(userName);
connOpts.setPassword(password.toCharArray());
// 建立连接
sampleClient.connect(connOpts);
// 创建消息
MqttMessage message = new MqttMessage(content.getBytes());
// 设置消息的服务质量
message.setQos(qos);
// 发布消息
sampleClient.publish(topic, message);
// 断开连接
sampleClient.disconnect();
// 关闭客户端
sampleClient.close();
} catch (MqttException me) {
System.out.println("reason " + me.getReasonCode());
System.out.println("msg " + me.getMessage());
System.out.println("loc " + me.getLocalizedMessage());
System.out.println("cause " + me.getCause());
System.out.println("excep " + me);
me.printStackTrace();
}
}
}

SubscribeSample

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

/**
*订阅端
*/
public class SubscribeSample {

public static void main(String[] args) throws MqttException {
String HOST = "tcp://127.0.0.1:61613";
String TOPIC = "test2";
int qos = 1;
String clientid = "subClient";
String userName = "admin";
String passWord = "password";
try {
// host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
MqttClient client = new MqttClient(HOST, clientid, new MemoryPersistence());
// MQTT的连接设置
MqttConnectOptions options = new MqttConnectOptions();
// 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
options.setCleanSession(true);
// 设置连接的用户名
options.setUserName(userName);
// 设置连接的密码
options.setPassword(passWord.toCharArray());
// 设置超时时间 单位为秒
options.setConnectionTimeout(10);
// 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
options.setKeepAliveInterval(20);
// 设置回调函数
client.setCallback(new MqttCallback() {

public void connectionLost(Throwable cause) {
System.out.println("connectionLost");
}

public void messageArrived(String topic, MqttMessage message) throws Exception {
System.out.println("topic:"+topic);
System.out.println("Qos:"+message.getQos());
System.out.println("message content:"+new String(message.getPayload()));

}

public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println("deliveryComplete---------"+ token.isComplete());
}

});
client.connect(options);
//订阅消息
client.subscribe(TOPIC, qos);
} catch (Exception e) {
e.printStackTrace();
}
}
}

启动程序

1.启动MqttServer2以后,开始循环发送消息。

2.启动MyMqttClient开始接收消息。

到这里,整个程序基本可以运行。

3.启动PublishSample,发布一条消息,在启动SubscribeSample来订阅发布的消息。

4.发布的消息在MyMqttClient中也会显示出来

作者:魏小雨
链接:https://juejin.cn/post/6844904071367753735
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

MQTT在Android端的使用详解以及MQTT服务器搭建、Paho客户端使用

前言最近的项目中使用了MQTT来接收后端推送过来的一些数据,这篇文章来介绍下Android端如何集成使用,关于MQTT相关介绍将不再阐述。由于光写代码不实践的接收下数据很难验证我们写的是否正确,所以我将简单介绍下如何配置个MQTT服务端,并使用工具来发送数据到...
继续阅读 »

前言

最近的项目中使用了MQTT来接收后端推送过来的一些数据,这篇文章来介绍下Android端如何集成使用,关于MQTT相关介绍将不再阐述。

由于光写代码不实践的接收下数据很难验证我们写的是否正确,所以我将简单介绍下如何配置个MQTT服务端,并使用工具来发送数据到服务端,使得手机端能接收到数据。话不多说直接看。

1. MQTT服务器配置

1.1 下载EMQX

下载地址

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

1.2 启动EMQX

在解压后的bin目录下打开cmd命令,输入emqx.cmd start即可启动。

如果你在启动时遇到could't load module...,那就是因为你的路径中包含中文名导致启动不了,将该文件夹放到纯英文目录下即可启动。 在这里插入图片描述

完事后在浏览器内输入http://127.0.0.1:18083即可打开web管理界面,帐号为admin,密码为public

按如图方式将语言改为中文 在这里插入图片描述

1.3 界面说明

左侧的Clients标签下可以看到当前连接的客户端 在这里插入图片描述 左侧的Topics标签下可以看到当前订阅的主题 在这里插入图片描述

1.4 个人理解

到这服务端就算是配置完成了,你可能会问,服务端就是这,那我手机客户端怎么接收消息呢,服务端从哪里发送消息呢?其实EMQX服务是消息中间件服务,有点像是转发。一个客户端发送消息并指定主题,该消息发送到服务端,那么连接了服务端并且订阅了该主题的所有客户端就都能接收到该消息,所以我们手机客户端想要接收到消息,还需要有一端来给EMQX服务端来发送消息才行。

2. MQTT客户端软件 Paho

2.1 下载MQTT客户端软件

下载地址 在这里插入图片描述

下载勾选中的那个文件即可,下载完后解压得到paho.exe,即我们需要的客户端软件。

2.2 MQTT客户端使用

2.2.1 连接服务器

在这里插入图片描述

按如图所示步骤进行点击,1、新增一个连接,2、填写服务器地址和客户标识,这里的标识为自己定义的,服务器地址可在该地址那查看,可以看到是本地地址,端口号是1883或者11883 点击连接后可以看到连接状态变为已连接,就代表我们客户端已经连接到了EMQX。 在这里插入图片描述

2.2.2 发送消息

在这里插入图片描述

在1处填写主题名,2处填写消息然后3处点击发布,然后可以看到4处显示已发布,代表我们已经发送到服务端了。

2.2.3 订阅主题

订阅我们刚才发送消息的那个主题

在这里插入图片描述

点击1处来新增订阅,点击2处输入我们要订阅的主题,这里我们设置为刚才发布消息的那个主题,然后点击3处的订阅,可以看到历史记录那里显示已订阅。

接下来我们再发送一次该主题消息,观察历史记录

在这里插入图片描述

可以看到,当我们发布后,由于我们订阅了该主题,所以就接收到了该主题消息。

在MQTT服务端配置完成以及MQTT客户端软件测试可行后,现在来看我们的安卓端如何订阅并接收消息。

3. Andoird端集成使用

3.1 添加依赖、权限等配置

//MQTT
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'

AndroidManifest文件配置

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.myfittinglife.mqttdemo">

<!--必要的三个权限-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<application
...>
...
<!--添加该Service-->
<service android:name="org.eclipse.paho.android.service.MqttService"/>
</application>

3.2 使用

3.2.1 创建MqttAndroidClient对象

var mClient: MqttAndroidClient? = null

private fun createClient() {

//1、创建接口回调
//以下回调都在主线程中(如果使用MqttClient,使用此回调里面的都是非主线程)
val mqttCallback: MqttCallbackExtended = object : MqttCallbackExtended {
override fun connectComplete(reconnect: Boolean, serverURI: String) {
//连接成功
Log.i(TAG, "connectComplete: ")
showToast("连接成功")
}

override fun connectionLost(cause: Throwable) {
//断开连接
Log.i(TAG, "connectionLost: ")
showToast("断开连接")

}

@Throws(Exception::class)
override fun messageArrived(topic: String, message: MqttMessage) {
//得到的消息
var msg = message.payload
var str = String(msg)
Log.i(TAG, "messageArrived: $str")
showToast("接收到的消息为:$str")

}

override fun deliveryComplete(token: IMqttDeliveryToken) {
//发送消息成功后的回调
Log.i(TAG, "deliveryComplete: ")
showToast("发送成功")

}
}

//2、创建Client对象
try {
mClient = MqttAndroidClient(this, "tcp://192.168.14.57:1883", "客户端名称,可随意")
mClient?.setCallback(mqttCallback) //设置回调函数
} catch (e: MqttException) {
Log.e(TAG, "createClient: ", e)
}
}

3.2.2 设置MQTT连接的配置信息

val mOptions = MqttConnectOptions()
mOptions.isAutomaticReconnect = false //断开后,是否自动连接
mOptions.isCleanSession = true //是否清空客户端的连接记录。若为true,则断开后,broker将自动清除该客户端连接信息
mOptions.connectionTimeout = 60 //设置超时时间,单位为秒
//mOptions.userName = "Admin" //设置用户名。跟Client ID不同。用户名可以看做权限等级
//mOptions.setPassword("Admin") //设置登录密码
mOptions.keepAliveInterval = 60 //心跳时间,单位为秒。即多长时间确认一次Client端是否在线
mOptions.maxInflight = 10 //允许同时发送几条消息(未收到broker确认信息)
mOptions.mqttVersion = MqttConnectOptions.MQTT_VERSION_3_1_1 //选择MQTT版本

3.2.3 建立连接

try {
mClient?.connect(mOptions, this, object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken?) {
Log.i(TAG, "onSuccess:连接成功 ")
}

override fun onFailure(asyncActionToken: IMqttToken?, exception: Throwable?) {
Log.i(TAG, "onFailure: " + exception?.message)
}

})
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ", e)
}

3.2.4 订阅主题

//设置监听的topic
try {
mClient?.subscribe("topicName", 0)
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ", e)
}

3.2.5 发送消息

try {
var str = "要发送的消息"
var msg = MqttMessage()
msg.payload =str.toByteArray()
mClient?.publish(Const.Subscribe.mTopic,msg)
} catch (e: MqttException) {
Log.e(TAG, "onCreate: ",e )
}

3.3 最终效果

在我们的Paho MQTT Utility软件发送消息后,我们的手机端由于订阅了该主题,所以就可以接收到该消息。 在这里插入图片描述

4. 注意事项

  • 别忘记在manifest中添加service,否则在connect()的时候会报mClient为空。

    <service android:name="org.eclipse.paho.android.service.MqttService"/>
  • 别忘记添加localbroadcastmanager依赖,否则会报Failed resolution of: Landroidx/localbroadcastmanager/content/LocalBroadcastManager错误。

    implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
  • 启动emqx服务时,一定要将该文件目录放到纯英文的目录下,不能包含中文,否则会出现could't load module的错误。

5. 总结

按以上步骤即可完成最基本的功能,以上只是简单的使用,其实还可以设置用户登录名和密码、设置服务质量、重连的操作等。关于MQTT的相关内容可以看这篇文章MQTT

项目Github地址

如果本文对你有帮助,请别忘记三连,如果有不恰当的地方也请提出来,下篇文章见。


作者:重拾丢却的梦

链接:https://juejin.cn/post/6939153370267516941

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

学习MQTT协议,与设备沟通

概述 MQTT是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和制动器之间通信的桥梁。 MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控...
继续阅读 »

概述


MQTT是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部分。该协议支持所有平台,几乎可以把所有联网物品和外部连接起来,被用来当做传感器和制动器之间通信的桥梁。


MQTT协议是为大量计算能力有限,且工作在低带宽、不可靠的网络的远程传感器和控制设备通讯而设计的协议。有以下特点:



  • 使用发布/订阅消息模式,提供一对多的消息发布

  • 使用TCP/IP提供网络连接

  • 小型传输,开销很小(固定长度的头部是 2 字节),协议交换最小化,以降低网络流量,传输的内容最大为256MB。

  • 使用 Last Will 和 Testament 特性通知有关各方客户端异常中断的机制。


1.MQTT协议实现方式




MQTT系统由与服务器通信的客户端组成,通常称服务器为“代理Broker”。客户可以是信息发布者Publish或订阅者Subscribe。每个客户端都可以连接到代理。


信息按主题层次结构组织。当发布者具有要分发的新数据时,它会将包含数据的控制消息发送到连接的代理。然后,代理将信息分发给已订阅该主题的任何客户端。发布者不需要有关于订阅者数量或位置的任何数据,而订阅者又不必配置有关发布者的任何数据。


MQTT传输的消息分为:主题(Topic)和负载(payload)两部分: (1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload); (2)payload,可以理解为消息的内容,是指订阅者具体要使用的内容。


2. MQTT协议中的术语




2.1订阅(Subscription)

订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。


2.2会话(Session)

每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。


2.3主题名(Topic Name)

连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。 系统主题:通过定义$SYS开头的主题可以查看一些系统信息,如客户端连接数量等, 详细介绍:github.com/mqtt/mqtt.g…


2.4主题筛选器(Topic Filter)

一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。 多级匹配符 # 单级匹配符 + 更多主题讨论,请移步github wiki github.com/mqtt/mqtt.g…


2.5负载(Payload)

消息订阅者所具体接收的内容。


3.保留消息和最后遗嘱




保留消息 Retained Messages

MQTT中,无论是发布还是订阅都不会有任何触发事件。 1个Topic只有唯一的retain消息,Broker会保存每个Topic的最后一条retain消息。 发布消息时把retain设置为true,即为保留信息。每个Client订阅Topic后会立即读取到retain消息。如果需要删除retain消息,可以发布一个空的retain消息,因为每个新的retain消息都会覆盖最后一个retain消息。


最后遗嘱 Last Will & Testament

MQTT本身就是为信号不稳定的网络设计的,所以难免一些客户端会无故的和Broker断开连接。 当客户端连接到Broker时,可以指定LWT,Broker会定期检测客户端是否有异常。 当客户端异常掉线时,Broker就往连接时指定的topic里推送当时指定的LWT消息。


4.消息服务质量




有三种消息发布服务质量qos(Quality of Service):


4.1“至多一次”




至多一次



消息发布完全依赖底层TCP/IP网络。会发生消息丢失或重复。这一级别可用于如下情况,环境传感器数据,丢失一次读记录无所谓,因为不久后还会有第二次发送。


4.2“至少一次”




至少一次



PUBACK消息是对QoS级别为1的PUBLISH消息的响应.PUBACK消息由服务器发送以响应来自发布端的PUBLISH消息,订阅端也会响应来自服务器的PUBLISH消息。当发布端收到PUBACK消息时,它会丢弃原始消息,因为它也被服务器接收(并记录)。


如果一定时间内,发布端或服务器没有收到PUBACK消息,则会进行重发。这种方式虽然确保了消息到达,但消息重复可能会发生。


4.3“只有一次”




只有一次



PUBREC消息是对QoS级别为2的PUBLISH消息的响应。它是QoS级别2协议流的第二个消息。 PUBREC消息由服务器响应来自发布端的PUBLISH消息,或订阅端响应来自服务器的PUBLISH消息。发布端或服务器收到PUBREC消息时,会响应PUBREL消息。


PUBREL消息是从发布端对PUBREC的响应,或从服务器对订阅端PUBREC消息的响应。 这是QoS 2协议流中第三个消息。当服务器从发布者收到PUBREL消息时,服务器会将PUBLISH消息发送到订阅端,并发送PUBCOMP消息到发布端。 当订阅端收到来自服务器的消息PUBREL时,使得消息可用于应用程序并将PUBCOMP消息发送到服务器。


PUBCOMP消息是服务器对来自发布端的PUBREL消息的响应,或订阅者对来自服务器的PUBREL消息的响应。 它是QoS 2协议流程中的第四个也是最后一个消息。当发布端收到PUBCOMP消息时,它会丢弃原始消息,因为它已经将消息发给了服务器。


在一些要求比较严格的计费系统中,可以使用此级别。在计费系统中,消息重复或丢失会导致不正确的结果。这种最高质量的消息发布服务还可以用于即时通讯类的APP的推送,确保用户收到且只会收到一次。




附录:各编程语言对MQTT客户端/服务器的实现


NameLanguageTypeLast releaseLicense
Adafruit IORuby on RailsNode.jsClient2.0.0?
flespiCBroker?Proprietary License
M2MqttC#Client4.3.0.0Eclipse Public License 1.0
Machine HeadClojureClient1.0.0Creative Commons Attribution 3.0 Unported License
moquetteJavaBroker0.10Apache License 2.0
MosquittoCPythonBroker and client1.4.15Eclipse Public License 1.0, Eclipse Distribution License 1.0 (BSD)
Paho MQTTCC++JavaJavascriptPythonGoClient1.3.0Eclipse Public License 1.0, Eclipse Distribution License 1.0 (BSD)
SharkMQTTCClient1.5Proprietary License
VerneMQErlang/OTPBroker1.4.1Apache License 2.0
wolfMQTTCClient0.14GNU Public License, version 2
MQTTRouteCPythonBroker1.0Proprietary License
HiveMQJavaBroker3.4.0Proprietary License
SwiftMQJavaBroker11.1.0Proprietary License
JoramMQJavaBroker11.1.0Proprietary License

作者:brandonbai
链接:https://juejin.cn/post/6844903829096382471
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

基于环信MQTT消息云,iOS版MQTT客户端快速实现消息收发

本文介绍iOS版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。一、前提条件1.部署iOS开发环境下载安装 Xcode。下载安装cocoapods,本文以cocoapods为例。2.导入项目依赖 在项目的podfile文...
继续阅读 »

本文介绍iOS版MQTT 客户端,如何连接环信MQTT消息云快速实现消息的自收自发。

一、前提条件

1.部署iOS开发环境

下载安装 Xcode

下载安装cocoapods,本文以cocoapods为例。

2.导入项目依赖

 在项目的podfile文件中设置如下:

  source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
pod 'MQTTClient'
pod 'AFNetworking'
pod 'MBProgressHUD'
pod 'Masonry'
pod 'MJRefresh'
pod 'YYModel'
target 'MQTTChat' do
end

二、实现流程

1、获取鉴权

     为保障客户安全性需求,环信MQTT消息云服务为客户提供【token+clientID】方式实现鉴权认证,其中AppID(clientID中的关键字段)及token标识获取流程如下:

【登录console】
    欢迎您登录环信云console控制台,在此控制台中,为您提供应用列表、解决方案、DEMO体验以及常见问题等功能。
     在应用列表中,若您未在APP中开通MQTT业务,可参见APP  MQTT开通流程
     若APP已开通MQTT业务,可在应用列表中选中Appname,点击【查看】操作,进入应用详情。


【获取AppID及连接地址】 
      进入【查看】后,点击左侧菜单栏【MQTT】->【服务概览】,在下图红色方框内获取当前AppID及服务器连接地址。


【获取token】
     为实现对用户管控及接入安全性,环信云console提供用户认证功能,支持对用户账户的增、删、改、查以及为每个用户账户分配唯一token标识,获取token标识可选择以下两种形式。
  形式一:console控制台获取(管理员视角)
  * 点击左侧菜单栏【应用概览】->【用户认证】页面,点击【创建IM用户】按钮,增添新的账户信息(包  括用户名及密码)。
  * 创建成功后,在【用户ID】列表中选中账户,点击【查看token】按钮获取当前账户token信息。


  形式二:客户端代码获取(客户端视角)
  * 获取域名:点击左侧菜单栏【即时通讯】->【服务概览】页面,查看下图中token域名、org_name、app_name。


  * 拼接URL:获取token URL格式为:http:/ /token域名/org_name/app_name/token。 
  * 用户名/密码:使用【用户ID】列表中已有账户的用户名及密码,例“用户名:test/密码:test123”。

客户端获取token代码示例如下:

- (void)getTokenWithUsername:(NSString *)username password:(NSString *)password completion:(void (^)(NSString *token))response {

NSString *urlString = getToken_url;
//初始化一个AFHTTPSessionManager
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
//设置请求体数据为json类型
manager.requestSerializer = [AFJSONRequestSerializer serializer];
//设置响应体数据为json类型
manager.responseSerializer = [AFJSONResponseSerializer serializer];
//请求体,参数(NSDictionary 类型)

NSDictionary *parameters = @{@"grant_type":@"password",
@"username":username,
@"password":password
};
__block NSString *token = @"";

[manager POST:urlString parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:responseObject options:NSJSONWritingPrettyPrinted error:&error];
NSDictionary *jsonDic = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
NSLog(@"%s jsonDic:%@",__func__,jsonDic);
token = jsonDic[@"access_token"];

response(token);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"%s error:%@",__func__,error.debugDescription);
response(token);
}];
}

2、初始化

      在cocoapods工程中创建MQTT客户端,客户端初始配置包括创建clientID,topic名称,QoS质量,连接地址等信息。

  NSString *deviceID = [UIDevice currentDevice].identifierForVendor.UUIDString;

self.clientId = [NSString stringWithFormat:@"%@@%@",deviceID,self.appId];

//初始化manager
self.manager = [[MQTTSessionManager alloc] init];
self.manager.delegate = self;

//订阅的主题 格式为 xxx/xxx/xxx 可以为多级话题
self.manager.subscriptions = @{[NSString stringWithFormat:@"%@/IOS", self.rootTopic]:@(self.qos),[NSString stringWithFormat:@"%@/IOS_TestToic", self.rootTopic]:@(1)};

3、连接服务器

    配置连接密码、cleansession标志、心跳间隔、超时时间等信息,调用connect()函数连接至环信MQTT消息云。

//此处从配置文件导入的Host即为MQTT的接入点,该接入点获取方式请参考资源申请章节文档,在控制台上申请MQTT实例,每个实例会分配一个接入点域名
[self.manager connectTo:self.host

port:self.port
tls:self.tls
keepalive:60
clean:true
auth:true
user:userName
pass:token
will:false
willTopic:nil
willMsg:nil
willQos:0
willRetainFlag:FALSE
withClientId:self.clientId];

4、订阅【subscribe】

【订阅主题】

当客户端成功连接环信MQTT消息云后,通过设置subscriptions参数值实现订阅主题与取消订阅主题 。当subscriptions非空时,订阅主题;当subscriptions为空时,取消订阅主题;

/**
订阅主题
格式为 xxx/xxx/xxx 可以为多级话题 @{@"xxx/xxx/xxx...":@(1)}
qos定义{ 0: 最多一次,1:至少一次 2:仅一次}
*/
self.manager.subscriptions = @{[NSString stringWithFormat:@"%@/IOS", self.rootTopic]:@(self.qos),[NSString stringWithFormat:@"%@/IOS_TestToic", self.rootTopic]:@(1)};

【取消订阅】

self.manager.subscriptions = @{};

【接收消息】

    环信MQTT消息云接收订阅消息。

  /*
* MQTTSessionManagerDelegate
*/
- (void)handleMessage:(NSData *)data onTopic:(NSString *)topic retained:(BOOL)retained {
/*
* MQTTClient: process received message
*/

NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[self.receiveMsgs insertObject:[NSString stringWithFormat:@"RecvMsg from Topic: %@ Body: %@", topic, dataString] atIndex:0];
[self.tableView reloadData];
}

5、发布【publish】

   环信MQTT消息云中指定topic发送消息。

- (void)send {
/*
* MQTTClient: send data to broker
*/

[self.manager sendData:[self.messageTextField.text dataUsingEncoding:NSUTF8StringEncoding]
topic:[NSString stringWithFormat:@"%@/%@",
self.rootTopic,
@"IOS"]//此处设置多级子topic
qos:self.qos
retain:FALSE];
}

6、断开连接

  MQTT client向环信MQTT消息云发送断开连接请求。

  /*
* 断开连接
*/
- (void)disConnect {
[self.manager disconnect];
self.manager.subscriptions = @{};
}

7、重新连接

 MQTT client向环信MQTT消息云发送重新连接请求。

/*
* 重新连接
*/
- (void)connect {
[self.manager connectToLast];
}

三、更多信息

  * 完整demo示例,请参见demo下载

  * 目前MQTT客户端支持多种语言,请参见 SDK下载
  * 如果您在使用环信MQTT消息云服务中,有任何疑问和建议,欢迎您联系我们

收起阅读 »

iOS Crash分析中的Signal

下面是一些信号说明SIGHUP本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的...
继续阅读 »

下面是一些信号说明

  • SIGHUP
    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
    此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

  • SIGINT
    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

  • SIGQUIT
    SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

  • SIGILL
    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

  • SIGTRAP
    由断点指令或其它trap指令产生. 由debugger使用。

  • SIGABRT
    调用abort函数生成的信号。

  • SIGBUS
    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

  • SIGFPE
    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

  • SIGKILL
    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

  • SIGUSR1
    留给用户使用

  • SIGSEGV
    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

  • SIGUSR2
    留给用户使用

  • SIGPIPE
    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

  • SIGALRM
    时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

  • SIGTERM
    程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL

  • SIGCHLD
    子进程结束时, 父进程会收到这个信号。
    如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

  • SIGCONT
    让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

  • SIGSTOP
    停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

  • SIGTSTP
    停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

  • SIGTTIN
    当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

  • SIGTTOU
    类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

  • SIGURG
    有”紧急”数据或out-of-band数据到达socket时产生.

  • SIGXCPU
    超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

  • SIGXFSZ
    当进程企图扩大文件以至于超过文件大小资源限制。

  • SIGVTALRM
    虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

  • SIGPROF
    类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

  • SIGWINCH
    窗口大小改变时发出.

  • SIGIO
    文件描述符准备就绪, 可以开始进行输入/输出操作.

  • SIGPWR
    Power failure

  • SIGSYS
    非法的系统调用。

关键点注意

  • 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP

  • 不能恢复至默认动作的信号有:SIGILL,SIGTRAP

  • 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ

  • 默认会导致进程退出的信号有:
    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM

  • 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

  • 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

  • 此外,SIGIOSVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

作者:Cooci
链接:https://www.jianshu.com/p/3a9dc6bd5e58



收起阅读 »

iOS编译&链接

对于平常的应用程序开发,我们很少需要关注编译和链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc he...
继续阅读 »

对于平常的应用程序开发,我们很少需要关注编译链接过程。我们平常Xcode开发就是集成的的开发环境(IDE),这样的IDE一般都将编译链接的过程一步完成,通常将这种编译链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句gcc hello.c命令就包含了非常复杂的过程!

正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。


编译流程分析

现在我们通过一个C语言的经典例子,来具体了解一下这些机制:


#include <stdio.h>
int main(){
printf("Hello World");
return 0;
}
在linux下只需要一个简单的命令(假设源代码文件名为hello.c):

$ gcc hello.c
$ ./a.out
Hello World

其实上述过程可以分解为四步:

  • 预处理(Prepressing)
  • 编译(Compilation)
  • 汇编(Assembly)
  • 链接(Linking)


预编译

首先是源代码文件hello.c和相关的头文件(如stdio.h等)被预编译器cpp预编译成一个.i文件。第一步预编译的过程相当于如下命令(-E 表示只进行预编译):

$ gcc –E hello.c –o hello.i
还可以下面的表达
$ cpp hello.c > hello.i

预编译过程主要处理源代码文件中以”#”开头的预编译指令。比如#include、#define等,主要处理规则如下:

  • 将所有的#define删除,并展开所有的宏定义
  • 处理所有条件预编译指令,比如#if,#ifdef,#elif,#else,#endif
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
  • 删除所有的注释///**/
  • 添加行号和文件名标识,比如#2 “hello.c” 2。
  • 保留所有的#pragma编译器指令

截图个大家看看效果



经过预编译后的文件(.i文件)不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经插入到.i文件中,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

编译(compliation)

编译过程就是把预处理完的文件进行一系列的:词法分析语法分析语义分析优化后生产相应的汇编代码文件,此过程是整个程序构建的核心部分,也是最复杂的部分之一。其编译过程相当于如下命令:

$ gcc –S hello.i –o hello.s


通过上图我们不难得出,通过命令得到汇编输出文件hello.s.

汇编(assembly)

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎对应一条机器令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。其汇编过程相当于如下命令:

as hello.s –o hello.o

或者

gcc –c hello.s –o hello.o

或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件:

gcc –c hello.c –o hello.o


链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一个目标文件呢?为什么要链接?下面让我们来看看怎么样调用ld才可以产生一个能够正常运行的Hello World程序:

注意默认情况没有gcc / 记得 :
$ brew install gcc

下面在贴出我们的写出的源代码是如何变成目标代码的流程图:

    



主要通过我们的编译器做了以下任务:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化

到这我们就可以得到以下的文件,不知道你是否有和我一起操作,玩得感觉还是不错,继续往下面看


iOS的编译器

iOS现在为了达到更牛逼的速度和优化效果,采用了LLVM

  • 1.LLVM核心库:
    LLVM提供一个独立的链接代码优化器为许多流行CPU(以及一些不太常见的CPU)的代码生成支持。这些库是围绕一个指定良好的代码表示构建的,称为LLVM中间表示(“LLVM IR”)LLVM还可以充当JIT编译器 - 它支持x86 / x86_64和PPC / PPC64程序集生成,并具有针对编译速度的快速代码优化。。

  • 2.LLVM IR 生成器Clang: Clang是一个“LLVM原生”C / C ++ / Objective-C编译器,旨在提供惊人的快速编译(例如,在调试配置中编译Objective-C代码时比GCC快3倍),非常有用的错误和警告消息以及提供构建优秀源代码工具的平台。

  • 3.LLDB项目:
    LLDB项目以LLVMClang提供的库为基础,提供了一个出色的本机调试器。它使用Clang AST表达式解析器LLVM JIT,LLVM反汇编程序等,以便提供“正常工作”的体验。在加载符号时,它也比GDB快速且内存效率更高。

  • 4.libclibc++:
    libc 和libc++ ABI项目提供了C ++标准库的标准符合性和高性能实现,包括对C ++ 11的完全支持。

  • 5.lld项目:
    lld项目旨在成为clang / llvm的内置链接器。目前,clang必须调用系统链接器来生成可执行文件。

LLVM采用三相设计,前端Clang负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR,后端LLVM编译把IR通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码。

编译器前端的任务是进行:

  • 语法分析
  • 语义分析
  • 生成中间代码(intermediate representation )

在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。

iOS程序-详细编译过程

  • 1.写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;
  • 2.运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases中可以看到;
  • 3.编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;
  • 4.链接文件:将项目中的多个可执行文件合并成一个文件;
  • 5.拷贝资源文件:将项目中的资源文件拷贝到目标包;
  • 6.编译 storyboard 文件:storyboard 文件也是会被编译的;
  • 7.链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;
  • 8.编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage
  • 9.运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。
  • 10.生成 .app 包
  • 11.将 Swift 标准库拷贝到包中
  • 12.对包进行签名
  • 13.完成打包

编译过程的确是个比较复杂的过程,还有链接!并不是说难就不需要掌握,我个人建议每一个进阶路上iOS开发人员,都是要了解一下的。不需要你多么牛逼,但是你能在平时的交流讨论,面试中能点出一个两个相应的点,我相信绝对是逼格满满!


作者:Cooci_和谐学习_不急不躁
链接:https://www.jianshu.com/p/b60612c4d9ca





收起阅读 »

RSA概述

RSA概述首先看这个加密算法的命名.很有意思,它其实是三个人的名字.早在1977年由麻省理工学院的三位数学家Rivest、Shamir 和 Adleman一起提出了这个加密算法,并且用他们三个人姓氏开头字母命名.RSA加密算法是一种非对称加密算法,其玩法打破了...
继续阅读 »

RSA概述

首先看这个加密算法的命名.很有意思,它其实是三个人的名字.早在1977年由麻省理工学院的三位数学家Rivest、Shamir 和 Adleman一起提出了这个加密算法,并且用他们三个人姓氏开头字母命名.
RSA加密算法是一种非对称加密算法,其玩法打破了以往所有加密算法的规则.在RSA出现之前,所有的加密方法都是同一种模式:加密解密的规则使用同一种方式.这种长达几个世纪的加密方案有一个致命的缺陷.在传递加密信息时,必须让对方拿到解密的规则才能正常解密.由于加密解密的规则一致,所以保存和传递"密钥",就成了最头疼的问题。
RSA的出现解决了这个问题.我们来看看RSA是怎么玩的.


RSA加密/解密

  • 使用公钥加密的数据,利用私钥进行解密
  • 使用私钥加密的数据,利用公钥进行解密

没错,RSA加密使用了"一对"密钥.分别是公钥私钥,这个公钥和私钥其实就是一组数字!其二进制位长度可以是1024位或者2048位.长度越长其加密强度越大,目前为止公之于众的能破解的最大长度为768位密钥,只要高于768位,相对就比较安全.所以目前为止,这种加密算法一直被广泛使用.

RSA的弊端

由于RSA算法的原理都是大数计算,使得RSA最快的情况也比对称加密算法慢上好几倍。速度一直是RSA的缺陷,一般来说RSA只用于小数据的加密.RSA的速度是对应同样安全级别的对称加密算法的1/1000左右。

RSA终端命令演示

由于Mac系统内置OpenSSL(开源加密库),所以我们可以直接在终端上使用命令来玩RSA.
OpenSSL中RSA算法常用指令主要有三个,其他指令此处不介绍。



命令
含义
genrsa生成并输入一个RSA私钥
rsautl使用RSA密钥进行加密、解密、签名和验证等运算
rsa处理RSA密钥的格式转换等问题

生成RSA私钥,密钥长度为1024bit

hank$ openssl genrsa -out private.pem 1024
Generating RSA private key, 1024 bit long modulus
..++++++
..........................................++++++
e is 65537 (0x10001)

从私钥中提取公钥

hank$ openssl rsa -in private.pem -pubout -out public.pem
writing RSA key

显得非常高大上对吧!那么它里面是什么,我们可以利用终端进行查看.

//查看私钥文件
hank$ cat private.pem
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDbGfA0XdkIpK5h2O9mg5o35pitxwiHDnlpBTCTUH+pkGMdDe6d
9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy1BgSfLxUx50jmm7jnvnS4Hrb
65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNzdmrkaaAQQkQ9liN6awIDAQAB
AoGAU0gdvNn7WES4oCrEfPQDF8KIQG3KOQPwdFHrr+NGU161veKA0/xNhTvFk8IV
BqsjkdO5j2EFfTMfJ+Qg4maCfIZN+xknosXRUF3vz5CUz/rXwBupOlOiWFJbB6cV
/Jee045DjiHjciip/ZVd8A2xnUEg4pIFUujAFPH+22t5TvkCQQD73bRqCQF9sWIA
tBeNR10Mygx5wrwKvjgCvaawsgx82kuAb3CWR0G81GfU+lK0YaHdmcFHsAHlDncM
OtY6IPnNAkEA3rKP6+/jUoylsJPWuN9LyuKjtAlsNtbWaYvs8iCNhLyV9hoWjvow
AAZB1uWy5aLDtQI3v48beExwsJEFAlQtFwJASTkKU21s1or0T/oLgtJFdgtjlx6L
JqBojjtus53/zWh1XNCJLddngCtMSHnCA5kCwvcJXvsHgf0zlQWh9GJT3QJAY0+q
EwN1kpiaQzaKqQMbX6zWaDFTitkf4Q2/avLNaYZYMdnMeZJk2X3w2o6wyutc71m/
1rNRAsLD9lmVrEYxnQJAEAHb0lsRgWe/sXX2attg4NbDsEExqDZ+7GGsyvqZn1Xg
S/UPdt6rVkVQ3N7ZEPKV6SxwN9LySI4lVWmFWhCn6w==
-----END RSA PRIVATE KEY-----
//查看公钥文件
hank$ cat public.pem
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDbGfA0XdkIpK5h2O9mg5o35pit
xwiHDnlpBTCTUH+pkGMdDe6d9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy
1BgSfLxUx50jmm7jnvnS4Hrb65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNz
dmrkaaAQQkQ9liN6awIDAQAB
-----END PUBLIC KEY-----
其实就是一个文本文件,并且一看就知道是base64编码.那么公钥相比私钥要简单很多.我们可以通过命令,将私钥转换成为明文看看.

//转化为明文信息
hank$ openssl rsa -in private.pem -text -out private.txt
writing RSA key
//查看文本信息
hank$ cat private.txt
Private-Key: (1024 bit)
modulus:
00:db:19:f0:34:5d:d9:08:a4:ae:61:d8:ef:66:83:
9a:37:e6:98:ad:c7:08:87:0e:79:69:05:30:93:50:
7f:a9:90:63:1d:0d:ee:9d:f6:75:50:0e:be:b5:41:
41:0c:58:08:1b:9d:bf:e2:ad:35:e1:e5:58:a0:1a:
11:c3:6c:6f:f8:92:65:72:d4:18:12:7c:bc:54:c7:
9d:23:9a:6e:e3:9e:f9:d2:e0:7a:db:eb:98:3d:db:
4c:76:ea:06:81:5b:e2:3d:9f:d7:07:17:f4:12:86:
ba:9a:35:73:fd:0c:37:aa:86:a3:73:76:6a:e4:69:
a0:10:42:44:3d:96:23:7a:6b
publicExponent: 65537 (0x10001)
privateExponent:
53:48:1d:bc:d9:fb:58:44:b8:a0:2a:c4:7c:f4:03:
17:c2:88:40:6d:ca:39:03:f0:74:51:eb:af:e3:46:
53:5e:b5:bd:e2:80:d3:fc:4d:85:3b:c5:93:c2:15:
06:ab:23:91:d3:b9:8f:61:05:7d:33:1f:27:e4:20:
e2:66:82:7c:86:4d:fb:19:27:a2:c5:d1:50:5d:ef:
cf:90:94:cf:fa:d7:c0:1b:a9:3a:53:a2:58:52:5b:
07:a7:15:fc:97:9e:d3:8e:43:8e:21:e3:72:28:a9:
fd:95:5d:f0:0d:b1:9d:41:20:e2:92:05:52:e8:c0:
14:f1:fe:db:6b:79:4e:f9
prime1:
00:fb:dd:b4:6a:09:01:7d:b1:62:00:b4:17:8d:47:
5d:0c:ca:0c:79:c2:bc:0a:be:38:02:bd:a6:b0:b2:
0c:7c:da:4b:80:6f:70:96:47:41:bc:d4:67:d4:fa:
52:b4:61:a1:dd:99:c1:47:b0:01:e5:0e:77:0c:3a:
d6:3a:20:f9:cd
prime2:
00:de:b2:8f:eb:ef:e3:52:8c:a5:b0:93:d6:b8:df:
4b:ca:e2:a3:b4:09:6c:36:d6:d6:69:8b:ec:f2:20:
8d:84:bc:95:f6:1a:16:8e:fa:30:00:06:41:d6:e5:
b2:e5:a2:c3:b5:02:37:bf:8f:1b:78:4c:70:b0:91:
05:02:54:2d:17
exponent1:
49:39:0a:53:6d:6c:d6:8a:f4:4f:fa:0b:82:d2:45:
76:0b:63:97:1e:8b:26:a0:68:8e:3b:6e:b3:9d:ff:
cd:68:75:5c:d0:89:2d:d7:67:80:2b:4c:48:79:c2:
03:99:02:c2:f7:09:5e:fb:07:81:fd:33:95:05:a1:
f4:62:53:dd
exponent2:
63:4f:aa:13:03:75:92:98:9a:43:36:8a:a9:03:1b:
5f:ac:d6:68:31:53:8a:d9:1f:e1:0d:bf:6a:f2:cd:
69:86:58:31:d9:cc:79:92:64:d9:7d:f0:da:8e:b0:
ca:eb:5c:ef:59:bf:d6:b3:51:02:c2:c3:f6:59:95:
ac:46:31:9d
coefficient:
10:01:db:d2:5b:11:81:67:bf:b1:75:f6:6a:db:60:
e0:d6:c3:b0:41:31:a8:36:7e:ec:61:ac:ca:fa:99:
9f:55:e0:4b:f5:0f:76:de:ab:56:45:50:dc:de:d9:
10:f2:95:e9:2c:70:37:d2:f2:48:8e:25:55:69:85:
5a:10:a7:eb
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDbGfA0XdkIpK5h2O9mg5o35pitxwiHDnlpBTCTUH+pkGMdDe6d
9nVQDr61QUEMWAgbnb/irTXh5VigGhHDbG/4kmVy1BgSfLxUx50jmm7jnvnS4Hrb
65g920x26gaBW+I9n9cHF/QShrqaNXP9DDeqhqNzdmrkaaAQQkQ9liN6awIDAQAB
AoGAU0gdvNn7WES4oCrEfPQDF8KIQG3KOQPwdFHrr+NGU161veKA0/xNhTvFk8IV
BqsjkdO5j2EFfTMfJ+Qg4maCfIZN+xknosXRUF3vz5CUz/rXwBupOlOiWFJbB6cV
/Jee045DjiHjciip/ZVd8A2xnUEg4pIFUujAFPH+22t5TvkCQQD73bRqCQF9sWIA
tBeNR10Mygx5wrwKvjgCvaawsgx82kuAb3CWR0G81GfU+lK0YaHdmcFHsAHlDncM
OtY6IPnNAkEA3rKP6+/jUoylsJPWuN9LyuKjtAlsNtbWaYvs8iCNhLyV9hoWjvow
AAZB1uWy5aLDtQI3v48beExwsJEFAlQtFwJASTkKU21s1or0T/oLgtJFdgtjlx6L
JqBojjtus53/zWh1XNCJLddngCtMSHnCA5kCwvcJXvsHgf0zlQWh9GJT3QJAY0+q
EwN1kpiaQzaKqQMbX6zWaDFTitkf4Q2/avLNaYZYMdnMeZJk2X3w2o6wyutc71m/
1rNRAsLD9lmVrEYxnQJAEAHb0lsRgWe/sXX2attg4NbDsEExqDZ+7GGsyvqZn1Xg
S/UPdt6rVkVQ3N7ZEPKV6SxwN9LySI4lVWmFWhCn6w==
-----END RSA PRIVATE KEY-----

通过公钥加密数据,私钥解密数据

//生成明文文件
hank$ vi message.txt
//查看文件内容
hank$ cat message.txt
密码:123456
//通过公钥进行加密
hank$ openssl rsautl -encrypt -in message.txt -inkey public.pem -pubin -out enc.txt
//通过私钥进行解密
hank$ openssl rsautl -decrypt -in enc.txt -inkey private.pem -out dec.txt

通过私钥加密数据,公钥解密数据

//通过私钥进行加密
hank$ openssl rsautl -sign -in message.txt -inkey private.pem -out enc.txt
//通过公钥进行解密
hank$ openssl rsautl -verify -in enc.txt -inkey public.pem -pubin -out dec.txt

小结

那么看到这些之后,对RSA应该有了一定的了解.由于RSA加密运行效率非常低!并不是所有数据加密都会使用它.那么它的主战场在于加密一些小的数据,比如对称加密算法的密钥.又或者数字签名.



作者:Hank
链接:https://www.jianshu.com/p/6280aa136292







收起阅读 »

RunLoop(二):实际应用

前不久我们我们对RunLoop的底层有了简单的了解,那我们现在就要把我们学到的这些东西,实际应用到我们的项目中。Timer定时器问题我们在vc中创建一个定时器,然后在view上面添加一个滚动视图,比如说scrollView,可以发现在scrollView滚动的...
继续阅读 »

前不久我们我们对RunLoop的底层有了简单的了解,那我们现在就要把我们学到的这些东西,实际应用到我们的项目中。

Timer定时器问题

我们在vc中创建一个定时器,然后在view上面添加一个滚动视图,比如说scrollView,可以发现在scrollView滚动的时候,timer定时器会卡住,停止滚动之后才重新生效。

这个问题比较简单,也是我们经常遇到的。

因为定时器默认是添加在了RunLoop的NSDefaultRunLoopMode模式下,scrollView在滚动的时候会进入UITrackingRunLoopMode,RunLoop在同一时间只能处理一种mode,所以在滚动的时候,自然定时器就没法处理,卡住。

解决方法就是我们创建了timer之后,把他add到RunLoop的NSRunLoopCommonModes,NSRunLoopCommonModes其实并不是一种真实的模式,他只是一个标志,意味着timer在标记为common的模式下都能使用 (标记为common 也就是_commonModes数组)。

这个地方多说一句,这个标记为common是啥意思。我们得看回RunLoop结构体的源码

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

可以看到里面有一个set类型的变量,CFMutableSetRef _commonModes;,被放到这个set中的mode就等于是被标记为了common。NSDefaultRunLoopMode和UITrackingRunLoopMode都在里面。

下面是我们创建timer的正确姿势 ~

//我们平时可能都是用scheduledTimerWithTimeInterval这个方法创建,这个会默认把timer添加到runloop的defalut模式下,所以我们使用timerWithTimeInterval创建
NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d",++ count);
}];

//NSRunLoopCommonModes 并不是一个真的模式 他只是一个标记,意味着timer在标记为common的模式下都能使用 (标记为common 也就是_commonModes数组)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

线程保活

线程保活并不是所有的项目都用的到,他适应于那种一直有任务需要处理的场景,而且注意,一定要是串行的任务。这种情况下保活一条线程,就可以免去线程创建和销毁的开销,提高性能。

具体怎么保活线程,我下面先直接把我的代码贴出来,然后针对一些点在做一系列的说明。(模拟的项目场景是进入到一个vc中,开一条线程,然后用这条线程来执行任务,当然vc销毁时,线程也要销毁。)

下面是全部代码,大家可以先跳过代码看下面的一些解析。

#import "SecondViewController.h"

@interface MyThread : NSThread
@end
@implementation MyThread
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end

@interface SecondViewController ()
@property (nonatomic, strong) MyThread * thread;
@property (nonatomic, assign, getter=isStopped) BOOL stopped;
@end

@implementation SecondViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];

self.stopped = NO;

UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom];
btn.frame = CGRectMake(40, 100, 100, 40);
btn.backgroundColor = [UIColor blackColor];
[btn setTitle:@"停止" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(stopThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];

__weak typeof(self) weakSelf = self;
// 初始化thread
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"--begin--");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

NSLog(@"--end--");
}];
[self.thread start];

}

- (void)stopThread {
if (!self.thread) return;

// waitUntilDone YES
[self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 执行这个方法必须要在我们自己创建的这个线程中
- (void)__stopThread {
// 标识
self.stopped = YES;
// 停止runloop
CFRunLoopStop(CFRunLoopGetCurrent());
//
self.thread = nil;
}


#pragma mark - 添加touch事件 (每点击一次 让线程处理一次事件)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (!self.thread) return;

[self performSelector:@selector(threadDoSomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)threadDoSomething {
NSLog(@"work--%@",[NSThread currentThread]);
}


#pragma mark - dealloc
- (void)dealloc {
NSLog(@"%s",__func__);
[self stopThread];
}

@end

最顶部新建了一个继承自NSThread的MyThread类,目的就是为了重写-dealloc方法,在内部有打印内容,方便我调试线程是否被销毁。在我们真是的项目中,可以不需要这部分。

初始化线程,开启RunLoop

__weak typeof(self) weakSelf = self;
// 初始化thread
self.thread = [[MyThread alloc] initWithBlock:^{
NSLog(@"--begin--");
//往runloop里面添加source/timer/observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

NSLog(@"--end--");
}];
[self.thread start];

这部分是初始化我们的线程,线程的初始化我们一般用的多的是self.thread = [[MyThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];这样的方法,我是觉得这样把self传进线程内部,可能造成一些循环引用问题,最后影响vc和thread的销毁,所以我是用了block的形式。

initWithBlock的意思也就是线程初始化完毕会执行block内的代码。一个子线程默认是没有RunLoop的,RunLoop会在第一次获取的时候创建,所以我们先[NSRunLoop currentRunLoop]获取RunLoop,也就是创建了我们当前线程的RunLoop。

在了解RunLoop底层的时候我们了解到,如果一个RunLoop没有timer、observer、source,就会退出。我们新创建的RunLoop这些都是没有的,如果我们不手动的添加,那我们的RunLoop一跑起来就这就会退出的。所以就等于说我们必须手动给RunLoop添加点事情做。

在代码中我们使用了addPort:forMode这个方法,向当前RunLoop添加一个端口让RunLoop监听。RunLoop有工作做了,自然就不会退出的。

我们在开启线程的时候,用了一个while循环,通过一个属性stopped来控制是否跳出循环,然后循环内部使用了- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;这个方法开启RunLoop。有人有可能会问了,这里的开启RunLoop为什么不直接使用- (void)run;这个方法。这里我稍微解释一下:

查阅一下苹果的文档可以了解到,这个run方法,内部其实也是循环的调用了runMode这个方法的,但是这个循环是永远不会停止的,也就是说我们使用run方法开启的RunLoop是永远都不会停下来的,我们调用了stop之后,也只会停止当前的这一次循环,他还是会继续run起来的。所以文档中也提到,如果我们要创建一个可以停下来的RunLoop,用runMode这个方法。所以我们用这个while循环模拟run的运行原理,但是呢,我们通过stopped这个属性可以控制循环的停止。

while里面的条件weakSelf && !weakSelf.isStopped为什么不仅仅使用stopped判断,而是还要判断weakSelf是否有值?我们下面会提到的。

两个stopThread方法

- (void)stopThread {
if (!self.thread) return;

// waitUntilDone YES
[self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 执行这个方法必须要在我们自己创建的这个线程中
- (void)__stopThread {
// 标识置为YES,跳出while循环
self.stopped = YES;
// 停止runloop的方法
CFRunLoopStop(CFRunLoopGetCurrent());
// RunLoop退出之后,把线程置空释放,因为RunLoop退出之后就没法重新开启了
self.thread = nil;
}

stopThread是给我们的停止button调用的,但是实际的停止RunLoop操作在__stopThread里面。在stopThread中调用__stopThread一定要使用performSelector:onThread:这一类的方法,这样就可以保证在我们指定的线程中执行这个方法。如果我们直接调用__stopThread,就说明是在主线程调用的,那就代表我们把主线程的RunLoop停掉了,那我们的程序就完了。

touch模拟事件处理

我们在touchBegin方法中,让我们self.thread执行-threadDoSomething这个方法,代表每点击一次,我们的线程就要处理一次-threadDoSomething中的打印事件。做这个操作是为了检测看我们每次工作的线程是不是都是我们最开始创建的这一个线程,没有重新开新线程。

其他细节

那我们仔细观察的话会发现一个问题,-threadDoSomething和stopThread这两个方法中都是用下面这个方法来处理线程间通信

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait

但是两次调用传入的wait参数是不一样的。我们要先知道这个waitUntilDone:(BOOL)wait代表什么意思。

如果wait传的是YES,就代表我们在主线程用self调用这个performSelector的时候,主线程会等待我们的self.thread这个线程执行他需要执行的方法,等着self.thread执行完方法之后,主线程再继续往下走。那如果是NO,肯定就是主线程不会等了,主线程继续往下走,然后我们的self.thread去调用自己该调用的方法。

那为什么在stop方法中是用的YES?

有这么一个情形,如果我们push进这个vc,线程初始化,然后RunLoop开启,但是我们不想通过点击停止button来停止,当我们点击导航的back的时候,我也需要销毁线程。

所以我们在vc的-dealloc方法中也调用了stopThread方法。那如果stopThread中使用
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
的时候wait不用YES,而是NO,会出现什么情况,那肯定是crash了。

如果wait是NO,代表我们的主线程不会等待self.thread执行__stopThread方法。

#pragma mark - dealloc
- (void)dealloc {
NSLog(@"%s",__func__);
[self stopThread];
}

但是dealloc中主线程调用完stopThread,之后整个dealloc方法就结束了,也就是我们的控制器已经销毁了。但是呢这个时候self.thread还在执行__stopThread方法呢。__stopThread中还要self变量,但是他其实已经销毁了,所以这个地方就会crash了。所以在stopThread中的wait一定要设置为YES。

在当时写代码的时候,这样确实处理了crash的问题,但是我直接返回值后,RunLoop并没有结束,线程没有销毁。这就要讲到上面说的while判断条件是weakSelf && !weakSelf.isStopped的原因了。vc执行了dealloc之后,self被置为nil了,weakSelf.isStopped也是nil,取非之后条件又成立了,while循环还要继续的走,RunLoop又run起来了。所以这里我们加上weakSelf这个判断,也就是self必须不为空。

总结

上面就是我实现的线程保活这一功能的代码和细节分析,当然我们在实际的项目中可能有多个位置需要线程保活这一功能,所以我们应该把这一部分做一下简单的封装,来方便我们在不同的地方调用。大家有兴趣的可以自己封装一下,我在写RunLoop相关的代码时,大多用的是OC层的代码,有兴趣的小伙伴可以尝试一下C语言的API。

RunLoop的应用当前不止这么一点,还可以监控应用卡顿,做性能优化,这些以后研究明白了再继续更博客吧,一起加油。

相关的功能代码和封装已经放到github上面了
https://github.com/Sunxb/RunLoopDemo


链接:https://www.jianshu.com/p/9e0177d40aab

收起阅读 »

细说浏览器输入URL后发生了什么

细说浏览器输入URL后发生了什么总体概览大体上,可以分为六步,当然每一步都可以详细都展开来说,这里先放一张总览图:DNS域名解析在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可...
继续阅读 »

细说浏览器输入URL后发生了什么

总体概览

大体上,可以分为六步,当然每一步都可以详细都展开来说,这里先放一张总览图:


DNS域名解析

在网络世界,你肯定记得住网站的名称,但是很难记住网站的 IP 地址,因而也需要一个地址簿,就是 DNS 服务器。DNS 服务器是高可用、高并发和分布式的,它是树状结构,如图:


  • 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址

  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址

  • 权威 DNS 服务器 :返回相应主机的 IP 地址


DNS的域名查找,在客户端和浏览器,本地DNS之间的查询方式是递归查询;在本地DNS服务器与根域及其子域之间的查询方式是迭代查询;


递归过程:

GitHub


在客户端输入 URL 后,会有一个递归查找的过程,从浏览器缓存中查找->本地的hosts文件查找->找本地DNS解析器缓存查找->本地DNS服务器查找,这个过程中任何一步找到了都会结束查找流程。


如果本地DNS服务器无法查询到,则根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下图:

GitHub


结合起来的过程,可以用一个图表示:

GitHub

在查找过程中,有以下优化点:



  • DNS存在着多级缓存,从离浏览器的距离排序的话,有以下几种: 浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存。

  • 在域名和 IP 的映射过程中,给了应用基于域名做负载均衡的机会,可以是简单的负载均衡,也可以根据地址和运营商做全局的负载均衡。


建立TCP连接


首先,判断是不是https的,如果是,则HTTPS其实是HTTP + SSL / TLS 两部分组成,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据。


进行三次握手,建立TCP连接。




  1. 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;




  2. 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;




  3. 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。




SSL握手过程



  1. 第一阶段 建立安全能力 包括协议版本 会话Id 密码构件 压缩方法和初始随机数

  2. 第二阶段 服务器发送证书 密钥交换数据和证书请求,最后发送请求-相应阶段的结束信号

  3. 第三阶段 如果有证书请求客户端发送此证书 之后客户端发送密钥交换数据 也可以发送证书验证消息

  4. 第四阶段 变更密码构件和结束握手协议


完成了之后,客户端和服务器端就可以开始传送数据。更多 HTTPS 的资料可以看这里:



备注


ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0。TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1。


SYN(SYNchronization):在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。


FIN(finis)即完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。


发送HTTP请求,服务器处理请求,返回响应结果


TCP连接建立后,浏览器就可以利用HTTP/HTTPS协议向服务器发送请求了。服务器接受到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since,则验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200.


这里有发生的一个过程是HTTP缓存,是一个常考的考点,大致过程如图:

GitHub

其过程,比较多内容,可以参考我的这篇文章《浏览器相关原理(面试题)详细总结一》,这里我就不详细说了~


关闭TCP连接




  1. 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;




  2. 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我"同意"你的关闭请求;




  3. 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;




  4. 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。




浏览器渲染


按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、栅格化和显示。如图:

GitHub



  1. 渲染进程将 HTML 内容转换为能够读懂DOM 树结构。

  2. 渲染引擎将 CSS 样式表转化为浏览器可以理解的styleSheets,计算出 DOM 节点的样式。

  3. 创建布局树,并计算元素的布局信息。

  4. 对布局树进行分层,并生成分层树。

  5. 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。

  6. 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。


构建 DOM 树


浏览器从网络或硬盘中获得HTML字节数据后会经过一个流程将字节解析为DOM树,先将HTML的原始字节数据转换为文件指定编码的字符,然后浏览器会根据HTML规范来将字符串转换成各种令牌标签,如html、body等。最终解析成一个树状的对象模型,就是dom树。

GitHub


具体步骤:



  1. 转码(Bytes -> Characters)—— 读取接收到的 HTML 二进制数据,按指定编码格式将字节转换为 HTML 字符串

  2. Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为结构清晰的 Tokens,每个 Token 都有特殊的含义同时有自己的一套规则

  3. 构建 Nodes(Tokens -> Nodes)—— 每个 Node 都添加特定的属性(或属性访问器),通过指针能够确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)

  4. 构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建立起每个结点的父子兄弟关系


样式计算


渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。


CSS 样式来源主要有 3 种,分别是通过 link 引用的外部 CSS 文件、style标签内的 CSS、元素的 style 属性内嵌的 CSS。,其样式计算过程主要为:

GitHub

可以看到上面的 CSS 文本中有很多属性值,如 2em、blue、bold,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。处理完成后再处理样式的继承和层叠,有些文章将这个过程称为CSSOM的构建过程。


页面布局


布局过程,即排除 script、meta 等功能化、非视觉节点,排除 display: none 的节点,计算元素的位置信息,确定元素的位置,构建一棵只包含可见元素布局树。如图:

GitHub

其中,这个过程需要注意的是回流和重绘,关于回流和重绘,详细的可以看我另一篇文章《浏览器相关原理(面试题)详细总结二》,这里就不说了~


生成分层树


页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree),如图:

GitHub

如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。在浏览器中,你可以打开 Chrome 的"开发者工具",选择"Layers"标签。渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。


并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。那么需要满足什么条件,渲染引擎才会为特定的节点创建新的层呢?详细的可以看我另一篇文章《浏览器相关原理(面试题)详细总结二》,这里就不说了~


栅格化


合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。如图:


GitHub


通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。


显示


最后,合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上,渲染过程完成。




链接:https://juejin.cn/post/6844904054074654728
收起阅读 »

浏览器工作原理&前端安全

网络安全 三原则 在传输中,不允许明文传输用户隐私数据; 在本地,不允许明文保存用户隐私数据; 在服务器,不允许明文保存用户隐私数据; http是明文传输,WiFi、路由器、运营商、机房等多个物理设备节点,如果在这中间任意一个节点被监听,传输的内容就会完全...
继续阅读 »

网络安全


三原则



  1. 在传输中,不允许明文传输用户隐私数据;

  2. 在本地,不允许明文保存用户隐私数据;

  3. 在服务器,不允许明文保存用户隐私数据;


http是明文传输,WiFi、路由器、运营商、机房等多个物理设备节点,如果在这中间任意一个节点被监听,传输的内容就会完全暴露,,这一攻击手法叫做MITM(Man In The Middle)中间人攻击。
在网络来说,我们知道不论 POST 请求和 GET 请求都会被抓包,在没有使用 HTTPS 的情况下,抓包我们是防不住的,如果明文传输用户隐私,那后果就不说了。


很多用户密码是通用的,一旦被不法分子窃取,去其他网站撞库,造成损失。
上文说到http传输因为有三大风险



  • 窃听风险(eavesdropping):第三方可以获知通信内容。

  • 篡改风险(tampering):第三方可以修改通信内容。

  • 冒充风险(pretending):第三方可以冒充他人身份参与通信。


所以提到了https
https 可以认为是 http + TLS TLS 是传输层加密协议,它的前身是 SSL 协议,如果没有特别说明,SSL 和 TLS 说的都是同一个协议。


加密传输(避免明文传输)


1. 对称加密

加解密使用同一个密钥
客户端和服务端进行通信,采用对称加密,如果只使用一个秘钥,很容易破解;如果每次用不同的秘钥,海量秘钥的管理和传输成本又会比较高。


2.非对称加密

需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)
非对称加密的模式则是:




  • 乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的




  • 甲方获取乙方的公钥,然后用它对信息加密




  • 乙方得到加密后的信息,用私钥解密。


    但当服务端要返回数据,如果用公钥加密,那么客户端并没有私钥用来解密,而如果用私钥加密,客户端虽然有公钥可以解密,但这个公钥之前就在互联网上传输过,很有可能已经有人拿到,并不安全,所以这一过程只用非对称加密是不能满足的。
    (严格来讲,私钥并不能用来加密,只能用作签名使用,这是由于密码学中生成公钥私钥时对不同变量的数学要求是不同的,因此公钥私钥抵抗攻击的能力也不同)
    所以为了满足即使非对称




image.png


https


HTTPS 的出发点是解决HTTP明文传输时信息被篡改和监听的问题。




  • 为了兼顾性能和安全性,使用了非对称加密+对称加密的方案。




  • 为了保证公钥传输中不被篡改,又使用了非对称加密的数字签名功能,借助CA机构和系统根证书的机制保证了HTTPS证书的公信力。


    只传递证书、明文信息、加签加密后的明文信息,注意不传递CA公钥(防止中间人攻击),客户端浏览器可以通过系统根证书拿到CA公钥。(系统或浏览器中内置的CA机构的证书和公钥成为了至关重要的环节)




加密存储
千万不要用明文存储密码
如果用明文存储密码(不管是存在数据库还是日志中),一旦数据泄露,所有用户的密码就毫无保留地暴露在黑客的面前,开头提到的风险就可能发生,那我们费半天劲加密传输密码也失去了意义。


总结
如果我们想要尽可能保证用户的信息安全,我们需要做以下的工作



  • 使用https请求

  • 利用RSA加密密码并传输数据

  • 用BCrypt或者PBKDF2单向加密,并存储


强制使用HTTPS


一些网站购买了SSL证书并将其配置到Web服务器上,以为这就算完事儿了。但这只是表明你启用了HTTPS选项,而用户很可能不会注意到。为确保每个用户都从HTTPS中受益,你应该将所有传入的HTTP请求重定向至HTTPS。这意味着任何一个访问你的网站的用户都将自动切换到HTTPS,从那以后他们的信息传输就安全了。


配合cookie的secure参数,禁止cookie在最初的http请求中被带出去(中间人拦截)。


TCP三次握手四次挥手


Tcp是传输控制协议(Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议


第一次握手:请求连接client->SYN=1, 随机seq=x(数据包首字节序列号)
第二次握手:同意应答,SYN和ACK都置为1,ack=x+1,随机seq=y,返回确认连接
第三次握手:client检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1;——>Server,Server检查ack是否为y+1,ACK是否为1,正确则连接成功!


认证授权+浏览器存储


什么是认证(Authentication)

验证当前用户的身份,证明“你是你自己”
互联网中的认证:



  • 用户名密码登录

  • 邮箱发送登录链接

  • 手机号接收验证码


什么是授权(Authorization)

用户授予第三方应用访问该用户某些资源的权限
安装手机应用时(是否允许访问相册、地理位置等权限)
登录微信小程序(是否允许获取昵称、头像、地区、性别等个人信息)



  • 实现授权的方式有:cookie、session、token、OAuth


什么是凭证(Credentials)

实现认证和授权的前提是需要一种媒介(证书) 来标记访问者的身份
登录成功后,服务器给用户使用的浏览器颁发一个令牌,表明身份,每次请求时带上。


什么是 Cookie


  • HTTP 是无状态的协议,每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

  • cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

  • cookie 是不可跨域的:每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain


特点:
Cookie 的大小受限,一般为 4 KB;
同一个域名下存放 Cookie 的个数是有限制的,不同浏览器的个数不一样,一般为 20 个;
Cookie 支持设置过期时间,当过期时自动销毁;(max-age单位秒,如果是负数,为临时cookie关闭浏览器失效;默认是-1)
每次发起同域下的 HTTP 请求时,都会携带当前域名下的 Cookie;
支持设置为 HttpOnly,防止 Cookie 被客户端的 JavaScript 访问


什么是 Session


  • session 是另一种记录服务器和客户端会话状态的机制

  • session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中


SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。


什么是localStorage

特点



  • 大小限制为 5MB ~10MB;

  • 在同源的所有标签页和窗口之间共享数据;

  • 数据仅保存在客户端,不与服务器进行通信;

  • 数据持久存在且不会过期,重启浏览器后仍然存在;

  • 对数据的操作是同步的。


什么是sessionStorage


  • sessionStorage 的数据只存在于当前浏览器的标签页;

  • 数据在页面刷新后依然存在,但在关闭浏览器标签页之后数据就会被清除;

  • 与 localStorage 拥有统一的 API 接口;

  • 对数据的操作是同步的。


什么是 Token(令牌)


  • 访问资源接口(API)时所需要的资源凭证

  • 简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)


特点:



  • 服务端无状态化、可扩展性好

  • 支持移动端设备

  • 安全

  • 支持跨程序调用


什么是 JWT


  • JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。(不使用cookie)


方式:通过Authorization;通过url;跨域的时候,可以把 JWT 放在 POST 请求的数据体里


和session、token的区别是JWT已经包含用户信息,所以不用再去数据库里查询了,而且


什么是 XSS

Cross-Site Scripting(跨站脚本攻击),是一种代码注入攻击



  • 存储性(任何可输入存入数据库的地方,注入脚本,服务端渲染时将脚本拼接html中返回给浏览器)

  • 反射性(脚本写入url,如路由传参,诱导用户点击,服务端渲染时将脚本拼接html中返回给浏览器)

  • DOM性(脚本写入url,前端 JavaScript 取出 URL 中的恶意代码并执行)


防范:cookie设置readOnly禁止js脚本访问cookie
前端服务端对输入框设置格式检查
转义 HTML(存储、反射)
改成纯前端渲染(存储、反射)
使用react就在前端 render 阶段避免 innerHTML、outerHTML 的 XSS 隐患用.textContent、.setAttribute()。


什么是 CSRF
跨站请求伪造(英语:Cross-site request forgery)
用户已经登录了安全网站A,诱导用户访问网站B,B利用A获取的凭证去访问A,绕过用户验证



  • 1.登录受信任网站A,并在本地生成Cookie。

  • 2.在不登出A的情况下,访问危险网站B。


防范:同源策略(origin referrer) token samesite


Base64编码由来


因为有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASCII码的控制字符就不能通过邮件传送。Base64就是一种基于64个可打印字符来表示二进制数据的表示方法。


ASCII码
在计算机中,所有的数据在存储和运算时都要使用二进制数表示,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,用来统一规定上述常用符号用哪些二进制数来表示


unicode、utf-8、ASCII、base64、哈希md5
ASCII美国信息互换标准代码,用一个字节存储128个字符(其中包括33个控制字符(具有某些特殊功能但是无法显示的字符)
产生原因:
在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示 [2]  。


Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。.Base64编码是从二进制到字符的过程


浏览器工作原理


异步编程


与同步相对的异步则可以理解为在异步操作完成后所要做的任务,它们通常以回调函数或者 Promise 的形式被放入事件队列,再由事件循环 ( Event Loop ) 机制在每次轮询时检查异步操作是否完成,若完成则按事件队列里面的执行规则来依次执行相应的任务。


javascript的运行机制(单线程、任务队列、EventLoop、微任务、宏任务)


单线程特点


单线程可以避免多线程操作带来的复杂的同步问题。


任务队列(JavaScript的运行机制)


  1. 所有同步任务都在主线程上执行,形成一个执行栈。

  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。


Event Loop

每次 Tick 会查看任务队列中是否有需要执行的任务。每次 Tick 的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中
1. onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。
2. setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
3. ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。


javascript是单线程的,浏览器是多线程的。
进程和线程都是操作系统的概念,进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。


进程(process)


进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,即进程是操作系统进行资源分配和独立运行的最小单元。
当我们启动一个应用,计算机会至少创建一个进程,cpu会为进程分配一部分内存,应用的所有状态都会保存在这块内存中,应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。如果应用关闭,进程会被终结,操作系统会释放相关内存。


线程(thread)



  • 进程内部的一个执行单元,是被系统独立调度和分派的基本单位。系统创建好进程后,实际上就启动执行了该进程的主执行线程

  • 进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情,所以一个进程可以创建多个线程。

  • 线程自己不需要系统重新分配资源,它与同属一个进程的其它线程共享当前进程所拥有的全部资源。 PS: 进程之间是不共享资源和地址空间的,所以不会存在太多的安全问题,而由于多个线程共享着相同的地址空间和资源,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。



Chrome 采用多进程架构


主要进程



  • Browser Process 浏览器的主进程(负责协调、主控) (1)负责包括地址栏,书签栏,前进后退按钮等部分的工作 (2)负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问 (3)负责各个页面的管理,创建和销毁其他进程

  • Renderer Process 负责一个 tab 内关于网页呈现的所有事情,页面渲染,脚本执行,事件处理等


image.png



  • Plugin Process 负责控制一个网页用到的所有插件,如 flash 每种类型的插件对应一个进程,仅当使用该插件时才创建

  • GPU Process 负责处理 GPU 相关的任务,3D 绘制等


优点
由于默认 新开 一个 tab 页面 新建 一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器。
同样,第三方插件崩溃也不会影响到整个浏览器。
多进程可以充分利用现代 CPU 多核的优势。


缺点
系统为浏览器新开的进程分配内存、CPU 等资源,所以内存和 CPU 的资源消耗也会更大。
不过 Chrome 在内存释放方面做的不错,基本内存都是能很快释放掉给其他程序运行的。


一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。


1.JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JavaScript线程在运行JavaScript程序。


2.GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。但需要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时立即被执行。


3.事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其他线程如鼠标点击、Ajax异步请求等,但由于JavaScript的单线程关系所有这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)


问题



  1. 为什么 Javascript 要是单线程的 ?


JavaScript 为处理页面中用户的交互,以及操作 DOM 树、CSS 样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。
如果 JavaScript 是多线程的方式来操作这些 UI DOM,则可能出现 UI 操作的冲突。但为了避免因为引入了锁而带来更大的复杂性,Javascript 在最初就选择了单线程执行。



  1. 为什么 JS 阻塞页面加载 ?


由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。所以为了防止渲染的不可预期结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。



  1. css 加载会造成阻塞吗 ?


CSS 加载不会阻塞 DOM 的解析(并行), Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的所以CSS 加载会阻塞 Dom 的渲染,同时css 会阻塞后面 js 的执行



  1. 什么是 CRP,即关键渲染路径(Critical Rendering Path)? 如何优化 ?


image.png


Html可以逐步解析,和css解析是并行的,但是css不行,因为css的每个属性都是可以改变cssom的,比如后面的把前面设置的font-size覆盖等,所以必须等cssom构建完毕才能进入下一个阶段。CSS的加载速度与构建CSSOM的速度将直接影响首屏渲染速度,因此在默认情况下CSS被视为阻塞渲染的资源。


通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS。


优化围绕三因素


关键资源数量(js、css)


关键路径长度


关键字节的数量(字节越小、下载和处理速度都会更快——压缩)


具体做法:


优化dom


html文件尽可能小,删除冗余代码,压缩代码,使用缓存(http cache)


优化cssom


仅把首屏需要的css通过style标签内嵌到head里,其余的使用异步方式非阻塞加载(如Critical CSS)


避免使用@import


@import会把css引入从并行变成串行加载


异步js


所有文本资源都应该尽可能小,删除未使用的代码、缩小文件的尺寸(Minify)、使用gzip压缩(Compress)、使用缓存(HTTP Cache)


可以为script添加async属性异步加载


5.从输入url浏览器渲染的流程。


解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树
布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
绘制 RenderObject 树 (paint),绘制页面的像素信息
浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面


6.Event Loop至少包含两个队列,macrotask队列和microtask队列


async/await成对出现,async标记的函数会返回一个Promise对象,可以使用then方法添加回调函数。await后面的语句会同步执行。但 await 下面的语句会被当成微任务添加到当前任务队列的末尾异步执行。


先微后宏


回流 (Reflow)


当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:



  • 页面首次渲染

  • 浏览器窗口大小发生改变

  • 元素尺寸或位置发生改变

  • 元素内容变化(文字数量或图片大小等等)

  • 元素字体大小变化

  • 添加或者删除可见的DOM元素

  • 激活CSS伪类(例如::hover)

  • 查询某些属性或调用某些方法


重绘 (Repaint)


当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。


回流比重绘的代价要更高。
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。



  1. 多线程的优点和缺点分别是什么?


优点:


1、将耗时较长的操作(网络请求、图片下载、音频下载、数据库访问等)放在子线程中执行,可以防止主线程的卡死;


2、可以发挥多核处理的优势,提升cpu的使用率。


缺点:


1、每开辟一个子线程就消耗一定的资源;


2、会造成代码的可读性变差;


3、如果出现多个线程同时访问一个资源,会出现资源争夺的情况。


链接:https://juejin.cn/post/6953482213845368863

收起阅读 »

使用transform和left改变位置的性能区别

使用transform和left改变位置的性能区别现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。F(Frames) P(Per) S(Second) 指的画面每秒钟传输的...
继续阅读 »

使用transform和left改变位置的性能区别

现如今大多数设备的屏幕刷新频率是60Hz,也就是每秒钟屏幕刷新60次;因此网页动画的运行速度只要达到60FPS,我们就会觉得动画很流畅。

F(Frames) P(Per) S(Second) 指的画面每秒钟传输的帧数,60FPS指的是每秒钟60帧;换算下来每一帧差不多是16毫秒。 (1 秒 = 1000 毫秒) / 60 帧 = 16.66 毫秒/帧 复制代码但通常浏览器需要花费一些时间将每一帧的内容绘制到屏幕上(包括样式计算、布局、绘制、合成等工作),所以通常我们只有10毫秒来执行JS代码。

那么动画只要接近于60FPS就是比较流畅的,对比一下通过position:left 做动画和transform做动画的性能区别

假设每个人都是用性能最好的手机,浏览器,我们根本用不着去做性能优化,所以在这里为了效果明显,先将环境配置到较低,较差的情况下测试,动画也不能设置为单一的移动

1如何使用google开发者工具查看帧数

1.先按键盘F12, 然后点到performance

2.点击刷新按钮再按确定

image.png

3.把鼠标放在下面就是他对应的帧数

test5.gif

4.现在的浏览器(google为例)已经默认开启了硬件加速器,所以你去对比left和transform其实效果非常不明显,所以先把这个默认关掉

image.png

5.对比效果,应该是在低cpu的情况下测试,将他设置为6

test7.gif

6 查看GPU的使用

image.png

如果你是mac,勾选fps meter, 如果你是windows,勾选我上面写的

我是windows,但是我并看不到帧率的时时变化

7 如果你想查看层级

检查-> layers -> 选择那个可旋转的 -> 查看元素paint code的变化

如果你发现你没有layers, 可以看看三个点里面的more tools,把layers点出来

image.png

4transformcode.gif

2使用position:left (使用left并没有被提升到复合层)

<div class="ball-running"></div>
.ball-running {
width: 100px;
height: 100px;
animation: run-around 2s linear alternate 100;
background: red;
position: absolute;
border-radius: 50%;
}
@keyframes run-around {
0% {
top: 0;
left: 0;
}
25% {
top: 0;
left: 200px;
}
50% {
top: 200px;
left: 200px;
}
75% {
top: 200px;
left: 0;
}
}

3transformcode.gif


test2.gif


在cpu 4slown down的情况下,我们可以看到上面的FPS刚开始在60左右,后面掉到了4FPS,这个动画是不够流畅的.
帧率呈现出锯齿型


这是对应的帧率


image.png


在cpu6 slow down的帧率下甚至会出现掉帧的情况(下面那些红色的就是dropped frame)


test5.gif


3.使用transform进行做动画(transform提升到了复合层)

.ball-running {
width: 100px;
height: 100px;
animation: run-around 2s linear alternate 100;
background: red;
border-radius: 50%;
}
@keyframes run-around {
0% {
transform: translate(0, 0);
}
25% {
transform: translate(200px, 0);
}
50% {
transform: translate(200px, 200px);
}
75% {
transform: translate(0, 200px);
}
}

1transformcode.gif


4.从层级方向解释transform性能优于left


建议看这篇文章:
浏览器层合成与页面渲染优化


基本的渲染流程:


image.png


从左往右边看,我们可以看到,浏览器渲染过程如下:


1.解析HTML,生成DOM树,解析CSS,生成CSSOM树
(CSS Object Model ,CSS 对象模型,里面有很多api,包括style rules-内部样式表中所有的CSS规则)
2.将DOM树和CSSOM树结合,生成渲染树(Render Tree)
3.Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
4.Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
5.display:将像素发送给GPU,展示在页面上。
复制代码

先了解下什么是渲染层


渲染层: 在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),
当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。
复制代码

1先不涉及任何的层级问题

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
}
.small {
width: 100px;
height: 100px;
background-color: red;
}
</style>

1普通的代码.gif

从上面来看,只有一个渲染层

2加上index

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
position: relative;
}
.small {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 0px;
left: 0px;
z-index: 10000;
}
</style>

1zindex.gif

从视觉上来看,small 的div确实是在big之上,但是和big在同一个渲染层上

3加上transform

<div class="big">
</div>
<div class="small"></div>
<style>
.big {
width: 200px;
height: 200px;
background-color: yellow;
position: relative;
}
.small {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
top: 0px;
left: 0px;
z-index: 10000;
transform: translateZ(0);
}

1transform.gif

如何形成合成层


上面产生了一个新的层级,也就是合成层


首先,如果你改了一个影响元素布局信息的CSS样式,比如width、height、left、top等(transform除外),那么浏览器会将当前的Layout标记为dirty,因为元素的位置信息变了,将可能会导致整个网页其他元素的位置情况都发生改变,所以需要执行Layout全局重新计算每个元素的位置,如果提升为合成层能够开启gpu加速,并且在渲染的时候不会影响其他的层


并且在使用left的时候,document的paint code一直在变化,而使用transform的paint code一直都是不变的,可看上面的动画gif


有关于层级方面的东西,希望大家共同交流,我觉得自己也没有深刻的了解有些定义,只写了自己会的理解的,希望在查看操作方面能帮到大家



链接:https://juejin.cn/post/6959089368212439076
收起阅读 »

5个 Chrome 调试混合应用的技巧

对前端开发人员来说,Chrome 真是一个必备的开发工具,大到页面展示,小到 BUG 调试/HTTP 抓包等,本文我将和大家分享自己做混合应用开发过程中经常用到的几个调试技巧。一、调试安卓应用 在进行混合应用开发过程中,经常需要在安卓应用中调试 H5 项目的代...
继续阅读 »

对前端开发人员来说,Chrome 真是一个必备的开发工具,大到页面展示,小到 BUG 调试/HTTP 抓包等,本文我将和大家分享自己做混合应用开发过程中经常用到的几个调试技巧。

一、调试安卓应用


在进行混合应用开发过程中,经常需要在安卓应用中调试 H5 项目的代码,这里我们就需要了解安卓应用如何在 Chrome 上进行调试。
接下来简单介绍一下,希望大家还是能实际进行调试看看:


1. 准备工作


需要准备有以下几个事项:



  1. 安卓包必须为可调试包,如果不可以调试,可以找原生的同事提供;

  2. 安卓手机通过数据线连接电脑,然后开启“开发者模式”,并启用“USB 调试”选项。


2. Chrome 启动调试页面


在 Chrome 浏览器访问“chrome://inspect/#devices”,然后在 WebView 列表中选择你要调试的页面,点击“ Inspect ”选项,跟调试 PC 网页一样,使用 Chrome 控制台进行调试。



然后就可以正常进行调试了,操作和平常 Chrome 上面调试页面是一样的。


3. 注意


如果访问 “chrome://inspect/#devices” 页面会一直提示 404,可以在翻墙情况下,先在 Chrome 访问 chrome-devtools-frontend.appspot.com,然后重新访问“chrome://inspect/#devices”即可。

二、筛选特定条件的请求


在 Network 面板中,我们可以在 Filter 输入框中,通过各种筛选条件,来查看满足条件的请求。



  1. 使用场景:


如只需要查看失败或者符合指定 URL 的请求。



  1. 使用方式:


在 Network 面板在 Filter 输入框中,输入各种筛选条件,支持的筛选条件包括:文本、正则表达式、过滤器和资源类型。
这里主要介绍“过滤器”,包括:


这里输入“-”目的是为了让大家能看到 Chrome 提供哪些高级选项,在使用的时候是不需要输入“-”。
如果输入“-.js -.css”则可以过滤掉“.js”和“.css”类型的文件。


关于过滤器更多用法,可以阅读《Chrome DevTools: How to Filter Network Requests》



三、快速断点报错信息


在 Sources 面板中,我们可以开启异常自动断点的开关,当我们代码抛出异常,会自动在抛出异常的地方断点,能帮助我们快速定位到错误信息,并提供完整的错误信息的方法调用栈。
3速断点报错信息.png



  1. 使用场景:


需要调试抛出异常的情况。



  1. 使用方式:


在 Sources 面板中,开启异常自动断点的开关。
3快速断点报错信息.gif


四、断点时修改代码


在 Sources 面板中,我们可以在需要断点的行数右击,选择“Add conditional breakpoint”,然后在输入框中输入表达式(如赋值操作等),后面代码将使用该结果。
4断点时修改代码1.png
4断点时修改代码2.png



  1. 使用场景:


需要在调试时,方便手动修改数据来完成后续调试的时候。



  1. 使用方式:


在 Sources 面板中,在需要断点的行数右击,选择“Add conditional breakpoint”。
4断点时修改代码.gif


五、自定义断点(事件、请求等)


当我们需要进行自定义断点的时候,比如需要拦截 DOM 事件、网络请求等,就可以在 Source 面板,通过 XHR/fetch Breakpoints 和 Event Listener Breakpoints 来启用对应断点。
5自定义断点.png



  1. 使用场景:


需要在调试时,需要增加自定义断点时(如需要拦截 DOM 事件、网络请求等)。



  1. 使用方式:


在 Sources 面板中,通过 XHR/fetch Breakpoints 和 Event Listener Breakpoints 来启用对应断点。
5自定义断点.gif




链接:https://juejin.cn/post/6955081218723414029



收起阅读 »

如何处理浏览器的断网情况?

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行 坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼 网络问题一直是一个很值得关注的问题。 比如在慢网情况下,增加loading避免重复发...
继续阅读 »

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行


坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼


网络问题一直是一个很值得关注的问题。


比如在慢网情况下,增加loading避免重复发请求,使用promise顺序处理请求的返回结果,或者是增加一些友好的上传进度提示等等。


那么大家有没有想过断网情况下该怎么做呢?比如说网络正常->断网->网络正常。


其实我一直也没想过,直到组里的测试测出一个断网导致的bug,让我意识到重度依赖网络请求的前端,在断网情况下可能会出现严重的bug。


因此我将在这里记录一下自己对系统断网情况下的处理,一方面避免bug产生,一方面保证用户及时在应用内知道网络已经断开连接

概览


为了构建一个 “断网(offline)可用”的web应用,你需要知道应用在什么时候是断网(offline)的。
不仅仅要知道什么时候断网,更要知道什么时候网络恢复正常(online)。
可以分解陈本下面两种常见情况:



  1. 你需要知道用户何时online,这样你可以与服务器之间re-sync(重新同步)。

  2. 你需要知道用户何时offline,这样你可以将你未发出的请求过一段时间再向服务器发出。


通常可以通过online/offline事件去做这个事情。


用于检测浏览器是否连网的navigator.onLine


navigator.onLine



  • true online

  • false offline


可以通过network的online选项切换为offline,打印navigator.onLine验证。


当浏览器不能连接到网络时,这个属性会更新。规范中是这样定义的:

The navigator.onLine attribute must return false if the user agent will not contact the network when the user follows links or when a script requests a remote page (or knows that such an attempt would fail)...

用于检测网络状况的navigator.connection


在youtube观看视频时,自动检测网络状况切换清晰度是如何做到的呢?
国内的视频网站也会给出一个切换网络的提醒,该如何去检测呢?
也就是说,有没有办法检测网络状况?判断当前网络是流畅,拥堵,繁忙呢?
可以通过navigator.connection,属性包括effectiveType,rtt,downlink和变更网络事件change。继承自NetworkInformation API。

navigator.connection

online状态下运行console.log(navigator.connection);

{
onchange: null,
effectiveType: "4g",
rtt: 50,
downlink: 2,
saveData: false
}

通过navigator.connection可以判断出online,fast 3g,slow 3g,和offline,这四种状态下的effectiveType分别为4g,3g,2g,4g(rtt,downlink均为0)。


rtt和downlink是什么?NetworkInformation是什么?


这是两个反映网络状况的参数,比type更加具象且更能反映当前网络的真实情况。


常见网络情况rtt和downlink表


注意:rtt和downlink不是定值,而是实时变化的。online时,可能它现在是rtt 100ms,2.2Mb/s,下一秒就变成125ms,2.1Mb/s了。


rtt


  • 连接预估往返时间

  • 单位为ms

  • 值为四舍五入到25毫秒的最接近倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)

  • 值越小网速越快。类似ping的time吧

  • 在Web Worker中可用


downlink


  • 带宽预估值

  • 单位为Mbit/s(注意是Mbit,不是MByte。)

  • 值也是四舍五入到最接近的25比特/秒的倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)

  • 一般越宽速度越快,也就是,信道上可以传输更多数。(吐槽一句,学过的通信原理还蛮有用。)

  • 值越大网速越快。类似高速一般比国道宽。

  • 在Web Worker中可用


草案(Draft)阶段NetworkInformation API

无论是rtt,还是downlink,都是这个草案中的内容。
除此之外还有downlinkMax,saveData,type等属性。
更多资料可以查询:NetworkInformation


如何检测网络变化去做出响应呢?


NetworkInformation继承自EventTarget,可以通过监听change事件去做一些响应。


例如可以获得网络状况的变更?

var connection = navigator.connection;
var type = connection.effectiveType;

function updateConnectionStatus() {
console.log("网络状况从 " + type + " 切换至" + connection.effectiveType);
type = connection.effectiveType;
}

connection.addEventListener('change', updateConnectionStatus);

监听变更之后,我们可以弹一个Modal提醒用户,也可以出一个Notice通知用户网络有变化,或者可以更高级得去自动切换清晰度(这个应该比较难)。


引出NetworkInformation的概念,只是想起一个抛砖引玉的作用。这种细粒度的网络状况检测,可以结合具体需求去具体实现。


在这篇博文中,我们只处理断网和连网两种情况,下面来看断网事件"offline"和连网事件"online"。


断网事件"offline"和连网事件"online"


浏览器有两个事件:"online" 和 "offline".
这两个事件会在浏览器在online mode和offline mode之间切换时,由页面的<body>发射出去。


事件会按照以下顺序冒泡:document.body -> document -> window。


事件是不能去取消的(开发者在代码上不能手动变为online或者offline,开发时使用开发者工具可以)。


注册上下线事件的几种方式


最最建议window+addEventListener的组合。



  • 通过window或document或document.body和addEventListener(Chrome80仅window有效)

  • 为document或document.body的.ononline或.onoffline属性设置一个js函数。(注意,使用window.ononline和window.onoffline会有兼容性的问题)

  • 也可以通过标签注册事件<body ononline="onlineCb" onoffline="offlineCb"></body>


例子

<div id="status"></div>
<div id="log"></div>
window.addEventListener('load', function() {
var status = document.getElementById("status");
var log = document.getElementById("log");

function updateOnlineStatus(event) {
var condition = navigator.onLine ? "online" : "offline";
status.innerHTML = condition.toUpperCase();

log.insertAdjacentHTML("beforeend", "Event: " + event.type + "; Status: " + condition);
}

window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
});

其中insertAdjacentHTML是在标签节点的邻近位置插入,可以查阅:DOM进阶之insertAdjacentHTML


断网处理项目实战


可以基于vue,react封装出离线处理组件,在需要到的页面引入即可。


思路和效果


只要做到断网提醒+遮罩,上线提醒-遮罩即可。



  • 监听offline,断网给出提醒和遮罩:网络已断开,请检查网络连接。

  • 监听online,连网给出提醒和遮罩:网络已连接。

断网处理组件使用

<OfflineHandle
offlineTitle = "断网处理标题"
desc="断网处理描述"
onlineTitle="连网提醒"
/>
Vue组件
<!--OfflineHandle.vue-->
<template>
<div v-if="mask" class="offline-mask">
<h2 class="offline-mask-title">{{ offlineTitle }}</h2>

<p class="offline-mask-desc">{{ desc }}</p >
</div>
</template>

<script>
export default {
name: "offline-handle",
props: {
offlineTitle: {
type: String,
default: "网络已断开,请检查网络连接。",
},
onlineTitle: {
type: String,
default: "网络已连接",
},
desc: {
type: String,
default: "",
},
duration: {
type: Number,
default: 4.5,
},
},
data() {
return {
mask: false,
};
},
mounted() {
window.addEventListener("offline", this.eventHandle);
window.addEventListener("online", this.eventHandle);
console.log(this.desc);
},
beforeDestroy() {
window.removeEventListener("offline", this.eventHandle);
window.removeEventListener("online", this.eventHandle);
},
methods: {
eventHandle(event) {
const type = event.type === "offline" ? "error" : "success";
this.$Notice[type]({
title: type === "error" ? this.offlineTitle : this.onlineTitle,
desc: type === "error" ? this.desc : "",
duration: this.duration,
});
setTimeout(() => {
this.mask = event.type === "offline";
}, 1500);
},
},
};
</script>

<style lang="css" scoped>
.offline-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
z-index: 9999;
transition: position 2s;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.offline-mask-title {
color: rgba(0, 0, 0, 0.8);
}
.offline-mask-desc {
margin-top: 20px;
color: red;
font-weight: bold;
}
</style>
React组件
// offlineHandle.js
import React, { useState, useEffect } from "react";
import { notification } from "antd";
import "antd/dist/antd.css";
import "./index.css";

const OfflineHandle = (props) => {
const {
offlineTitle = "网络已断开,请检查网络连接。",
onlineTitle = "网络已连接",
desc,
duration = 4.5
} = props;
const [mask, setMask] = useState(false);

const eventHandler = (event) => {
const type = event.type === "offline" ? "error" : "success";
console.log(desc, "desc");
openNotification({
type,
title: type === "error" ? offlineTitle : onlineTitle,
desc: type === "error" ? desc : "",
duration
});
setTimeout(() => {
setMask(event.type === "offline");
}, 1500);
};

const openNotification = ({ type, title, desc, duration }) => {
notification[type]({
message: title,
description: desc,
duration
});
};

useEffect(() => {
window.addEventListener("offline", eventHandler);
window.addEventListener("online", eventHandler);
return () => {
window.removeEventListener("offline", eventHandler);
window.removeEventListener("online", eventHandler);
};
}, []);

const renderOfflineMask = () => {
if (!mask) return null;
return (
<div className="offline-mask">
<h2 className="offline-mask-title">{offlineTitle}</h2>

<p className="offline-mask-desc">{desc}</p >
</div>
);
};

return <>{renderOfflineMask()}</>;
};

export default OfflineHandle;

发现



  • offline和online事件:window有效,document和document.body设置无效


手上的项目只运行在Chrome浏览器,只有为window设置offline和online才生效。
运行环境:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36



  • 为position增加2s的transition的避免屏闪


链接:https://juejin.cn/post/6953868764362309639
收起阅读 »

音视频学习从零到整-关于视频的一些概念

内容1、视频文件格式2、视频封装格式3、视频编解码方式4、音频编解码方式5、颜色模型一.视频相关概念1.1 视频文件格式文件格式这个概念应该是我们比较熟悉的,比如我们常见的 Word 文档的文件格式是 .doc,JPG 图片的文件格式是 .jpg 等等。那对于...
继续阅读 »

内容

1、视频文件格式
2、视频封装格式
3、视频编解码方式
4、音频编解码方式
5、颜色模型

一.视频相关概念

1.1 视频文件格式

文件格式这个概念应该是我们比较熟悉的,比如我们常见的 Word 文档的文件格式是 .doc,JPG 图片的文件格式是 .jpg 等等。那对于视频来说,
我们常见的文件格式则有:.mov、.avi、.mpg、.vob、.mkv、.rm、.rmvb 等等。文件格式通常表现为文件在操作系统上存储时的后缀名,它通常会被操作系统用来与相应的打开程序关联,比如你双击一个 test.doc 文件,系统会调用 Word 去打开它。你双击一个 test.avi 或者 test.mkv 系统会调用视频播放器去打开它。

同样是视频,为什么会有 .mov、.avi、.mpg 等等这么多种文件格式呢?****那是因为它们通过不同的方式实现了视频这件事情,至于这个不同在哪里,那就需要了解一下接下来要说的「视频封装格式」这个概念了。

1.2 视频封装格式

视频封装格式,简称视频格式,相当于一种储存视频信息的容器,它里面包含了封装视频文件所需要的视频信息、音频信息和相关的配置信息(比如:视频和音频的关联信息、如何解码等等)。一种视频封装格式的直接反映就是对应着相应的视频文件格式。


下面我们就列举一些文件封装格式:

1、AVI 格式,对应的文件格式为 .avi,英文全称 Audio Video Interleaved,是由 Microsoft 公司于 1992 年推出。这种视频格式的优点是图像质量好,无损 AVI 可以保存 alpha 通道。缺点是体积过于庞大,并且压缩标准不统一,存在较多的高低版本兼容问题。

2、DV-AVI 格式,对应的文件格式为 .avi,英文全称 Digital Video Format,是由索尼、松下、JVC 等多家厂商联合提出的一种家用数字视频格式。常见的数码摄像机就是使用这种格式记录视频数据的。它可以通过电脑的 IEEE 1394 端口传输视频数据到电脑,也可以将电脑中编辑好的的视频数据回录到数码摄像机中。

3、WMV 格式,对应的文件格式是 .wmv、.asf,英文全称 Windows Media Video,是微软推出的一种采用独立编码方式并且可以直接在网上实时观看视频节目的文件压缩格式。在同等视频质量下,WMV 格式的文件可以边下载边播放,因此很适合在网上播放和传输。

4、MPEG 格式,对应的文件格式有 .mpg、.mpeg、.mpe、.dat、.vob、.asf、.3gp、.mp4 等等,英文全称 Moving Picture Experts Group,是由运动图像专家组制定的视频格式,该专家组于 1988 年组建,专门负责视频和音频标准制定,其成员都是视频、音频以及系统领域的技术专家。MPEG 格式目前有三个压缩标准,分别是 MPEG-1、MPEG-2、和 MPEG-4。MPEG-4 是现在用的比较多的视频封装格式,它为了播放流式媒体的高质量视频而专门设计的,以求使用最少的数据获得最佳的图像质量。

5、Matroska 格式,对应的文件格式是 .mkv,Matroska 是一种新的视频封装格式,它可将多种不同编码的视频及 16 条以上不同格式的音频和不同语言的字幕流封装到一个 Matroska Media 文件当中。

6、Real Video 格式,对应的文件格式是 .rm、.rmvb,是 Real Networks 公司所制定的音频视频压缩规范称为 Real Media。用户可以使用 RealPlayer 根据不同的网络传输速率制定出不同的压缩比率,从而实现在低速率的网络上进行影像数据实时传送和播放。

7、QuickTime File Format 格式,对应的文件格式是 .mov,是 Apple 公司开发的一种视频格式,默认的播放器是苹果的 QuickTime。这种封装格式具有较高的压缩比率和较完美的视频清晰度等特点,并可以保存 alpha 通道。

8、Flash Video 格式,对应的文件格式是 .flv,是由 Adobe Flash 延伸出来的一种网络视频封装格式。这种格式被很多视频网站所采用。

从上面的介绍中,我们大概对视频文件格式以及对应的视频封装方式有了一个概念,接下来则需要了解一下关于视频更本质的东西,那就是视频编解码。

1.3 容器(视频封装格式)
封装格式:就是将已经编码压缩好的视频数据 和音频数据按照一定的格式放到一个文件中.这个文件可以称为容器. 当然可以理解为这只是一个外壳.

通常我们不仅仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不同的程序来处理,但是它们在传输和存储的时候,这多种数据都是被绑定在一起的.

常见的视频容器格式:
1、AVI: 是当时为对抗quicktime格式(mov)而推出的,只能支持固定CBR恒定定比特率编码的声音文件
2、MOV:是Quicktime封装
3、WMV:微软推出的,作为市场竞争
4、mkv:万能封装器,有良好的兼容和跨平台性、纠错性,可带外挂字幕
5、flv: 这种封装方式可以很好的保护原始地址,不容易被下载到,目前一些视频分享网站都采用这种封装方式
6、MP4:主要应用于mpeg4的封装,主要在手机上使用。

2.1视频编解码方式
视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程.

在做视频编解码时,需要考虑以下这些因素的平衡:视频的质量、用来表示视频所需要的数据量(通常称之为码率)、编码算法和解码算法的复杂度、针对数据丢失和错误的鲁棒性(Robustness)、编辑的方便性、随机访问、编码算法设计的完美性、端到端的延时以及其它一些因素。

2.2 常见视频编码方式:

H.26X 系列,由国际电传视讯联盟远程通信标准化组织(ITU-T)主导,包括 H.261、H.262、H.263、H.264、H.265

H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,之后的所有的标准视频编解码器都是基于它设计的。
H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它之前的视频编码标准在性能上有了较大的提升。尤其是在低码率端,它可以在保证一定质量的前提下大大的节约码率。
H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被广泛使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的能够大大提高压缩性能的技术,并能够同时在高码率端和低码率端大大超越以前的诸标准。
H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不仅提升图像质量,同时也能达到 H.264 两倍的压缩率(等同于同样画面质量下比特率减少了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
MPEG 系列,由国际标准组织机构(ISO)下属的运动图象专家组(MPEG)开发。

MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大致上和原有的 VHS 录像带相当。
MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
MPEG-4 第二部分,可以使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和第一版的 H.263,它的压缩性能有所提高。
MPEG-4 第十部分,等同于 H.264,是这两个编码组织合作诞生的标准。
其他,AMV、AVS、Bink、CineForm 等等,这里就不做多的介绍了。

介绍了上面这些「视频编解码方式」后,我们来说说它和上一节讲的「视频封装格式」的关系。

可以把「视频封装格式」看做是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」可以支持多种「视频编解码方式」,比如:QuickTime File Format(.MOV) 支持几乎所有的「视频编解码方式」,MPEG(.MP4) 也支持相当广的「视频编解码方式」。当我们看到一个视频文件名为 test.mov 时,我们可以知道它的「视频文件格式」是 .mov,也可以知道它的视频封装格式是 QuickTime File Format,

但是无法知道它的「视频编解码方式」。那比较专业的说法可能是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。比如:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264

3.1 音频编码方式
视频中除了画面通常还有声音,所以这就涉及到音频编解码。在视频中经常使用的音频编码方式有

AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony等公司共同开发,在 1997 年推出的基于 MPEG-2 的音频编码技术。2000 年,MPEG-4 标准出现后,AAC 重新集成了其特性,加入了 SBR 技术和 PS 技术,为了区别于传统的 MPEG-2 AAC 又称为 MPEG-4 AAC。
MP3,英文全称 MPEG-1 or MPEG-2 Audio Layer III,是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在 1991 年,由位于德国埃尔朗根的研究组织 Fraunhofer-Gesellschaft 的一组工程师发明和标准化的。MP3 的普及,曾对音乐产业造成极大的冲击与影响。
WMA,英文全称 Windows Media Audio,由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。

3.2 直播/小视频中的编码格式
视频编码格式

H264编码的优势:
低码率
高质量的图像
容错能力强
网络适应性强
总结: H264最大的优势,具有很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
举例: 原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
音频编码格式:

AAC是目前比较热门的有损压缩编码技术,并且衍生了LC-AAC,HE-AAC,HE-AAC v2 三种主要编码格式.

LC-AAC 是比较传统的AAC,主要应用于中高码率的场景编码(>= 80Kbit/s)
HE-AAC 主要应用于低码率场景的编码(<= 48Kbit/s)
优势:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码

适合场景:于128Kbit/s以下的音频编码,多用于视频中的音频轨的编码

4.1 关于H264
H.264 是现在广泛采用的一种编码方式。关于 H.264 相关的概念,从大到小排序依次是:序列、图像、片组、片、NALU、宏块、亚宏块、块、像素。

图像


H.264 中,「图像」是个集合的概念,帧、顶场、底场都可以称为图像。一帧通常就是一幅完整的图像。

当采集视频信号时,如果采用逐行扫描,则每次扫描得到的信号就是一副图像,也就是一帧。

当采集视频信号时,如果采用隔行扫描(奇、偶数行),则扫描下来的一帧图像就被分为了两个部分,这每一部分就称为「场」,根据次序分为:「顶场」和「底场」。

「帧」和「场」的概念又带来了不同的编码方式:帧编码、场编码逐行扫描适合于运动图像,所以对于运动图像采用帧编码更好;隔行扫描适合于非运动图像,所以对于非运动图像采用场编码更好。




片(Slice),每一帧图像可以分为多个片

网络提取层单元(NALU, Network Abstraction Layer Unit),

NALU 是用来将编码的数据进行打包的,一个分片(Slice)可以编码到一个 NALU 单元。不过一个 NALU 单元中除了容纳分片(Slice)编码的码流外,还可以容纳其他数据,比如序列参数集 SPS。对于客户端其主要任务则是接收数据包,从数据包中解析出 NALU 单元,然后进行解码播放。

宏块(Macroblock),分片是由宏块组成。

4.2 颜色模型
我们开发场景中使用最多的应该是 RGB 模型


在 RGB 模型中每种颜色需要 3 个数字,分别表示 R、G、B,比如 (255, 0, 0) 表示红色,通常一个数字占用 1 字节,那么表示一种颜色需要 24 bits。那么有没有更高效的颜色模型能够用更少的 bit 来表示颜色呢?

现在我们假设我们定义一个「亮度(Luminance)」的概念来表示颜色的亮度,那它就可以用含 R、G、B 的表达式表示为:

Y = kr*R + kg*G + kb*B

Y 即「亮度」,kr、kg、kb 即 R、G、B 的权重值。

这时,我们可以定义一个「色度(Chrominance)」的概念来表示颜色的差异:

Cr = R – Y
Cg = G – Y
Cb = B – Y

Cr、Cg、Cb 分别表示在 R、G、B 上的色度分量。上述模型就是 YCbCr 颜色模型基本原理。

YCbCr 是属于 YUV 家族的一员,是在计算机系统中应用最为广泛的颜色模型,就比如在本文所讲的视频领域。在 YUV 中 Y 表示的是「亮度」,也就是灰阶值,U 和 V 则是表示「色度」。

YUV 的关键是在于它的亮度信号 Y 和色度信号 U、V 是分离的。那就是说即使只有 Y 信号分量而没有 U、V 分量,我们仍然可以表示出图像,只不过图像是黑白灰度图像。在YCbCr 中 Y 是指亮度分量,Cb 指蓝色色度分量,而 Cr 指红色色度分量。

现在我们从 ITU-R BT.601-7 标准中拿到推荐的相关系数,就可以得到 YCbCr 与 RGB 相互转换的公式

Y = 0.299R + 0.587G + 0.114B
Cb = 0.564(B - Y)
Cr = 0.713(R - Y)
R = Y + 1.402Cr
G = Y - 0.344Cb - 0.714Cr
B = Y + 1.772Cb

这样对于 YCbCr 这个颜色模型我们就有个初步认识了,但是我们会发现,这里 YCbCr 也仍然用了 3 个数字来表示颜色啊,有节省 bit 吗?为了回答这个问题,我们来结合视频中的图像和图像中的像素表示来说明

假设图片有如下像素组成


一副图片就是一个像素阵列.每个像素的 3 个分量的信息是完整的,YCbCr 4:4:4


下图中,对于每个像素点都保留「亮度」值,但是省略每行中偶素位像素点的「色度」值,从而节省了 bit。YCbCr4:2:2


上图,做了更多的省略,但是对图片质量的影响却不会太大.YCbCr4:2:0


转自:https://www.jianshu.com/p/15f28fe89329

收起阅读 »

RunLoop(一):源码与逻辑

简述什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。...
继续阅读 »

简述

什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。

那RunLoop是怎样保持程序的运行状态,到底处理了哪些事件?下面我们就从源码的层面来了解一下RunLoop。

RunLoop

获取runloop对象

NSRunLoop和CFRunLoopRef都代表RunLoop对象,NSRunLoop是对CFRunLoopRef的封装。

Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

RunLoop相关类

从源码的代码结构中我们可以找出来一下5个跟RunLoop相关的结构

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopObserverRef
CFRunLoopTimerRef

下面是CFRunLoopRef的结构代码

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

变量很多,我们不需要全部看,只需要注意这两个

CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

每一个runloop里面有很多mode(存在一个set集合里面),然后之后后一个mode叫做currentMode,也就是说runloop一次只能处理一种mode。

然后我们再看CFRunLoopModeRef的结构,我已经给大家省略了里面那些我们不需要关注的变量

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};

根据上面这些我们大概的可以概括出来RunLoop这些相关类的关系。


CFRunLoopModeRef
由上面的源码我们可以稍微总结一下这个CFRunLoopModeRef:

1、CFRunLoopModeRef代表RunLoop的运行模式
2、一个RunLoop包含多个CFRunLoopModeRef,每个CFRunLoopModeRef又包含多个_sources0,_sources1,_observers,_timers。
3、RunLoop每次只能运行一种mode,切换mode的时候,要先退出之前的mode。
4、如果mode中没有_sources0、_sources1、_observers、_timers,程序会立刻退出。
常用的两种Mode

kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行

UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

CFRunLoopObserverRef
源码中给出了可以监听的RunLoop状态

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 进入RunLoop
kCFRunLoopEntry = (1UL << 0),
// 即将处理timers
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理Sources
kCFRunLoopBeforeSources = (1UL << 2),
// 即将休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出循环
kCFRunLoopExit = (1UL << 7),
// 所有状态
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

具体的怎么样添加observer来监听RunLoop状态我就不贴代码了,网上一搜有很多的。

RunLoop的运行逻辑

前面我们已经了解了RunLoop相关的结构的源码,知道了RunLoop大概的数据结构,那RunLoop到底是如何工作的呢?它的运行逻辑是什么?

我们了解过了每个mode中会存放不同的_sources0、_sources1、_observers、_timers,这些我们可以全部统称是RunLoop要处理的东西,那每一种具体对应我们了解的哪写事件呢?

Source0
触摸事件处理
performSelector:onThread:

Source1
基于系统Port(端口)的线程间通信
系统事件捕捉

Timers
NSTimer定时器
performSelector:withObject:afterDelay:

Observers
用于监听RunLoop的状态
UI刷新(BeforeWating)
Autorelease Pool (BeforWaiting)

注: UI的刷新并不是即时生效,比如说我们改变了view的backgroundColor,当执行到这行代码是并不是立刻生效,而是先记录下有这么一个任务,然后在RunLoop处理完所有的时间,进入休眠之前UI刷新。


这是大神总结的RunLoop的运行逻辑图,我直接拿过来用了。我们主要是看左边这部分,右边的这些标注是在源码中对应的主要方法名称。

这个图很容易理解,只有从06跳转到08这一步,单从图上看的话不是很清晰,这一块结合源码就比较明了了。第06步,如果存在Source1就直接跳转到08,在代码中使用了goto这个关键字,其实就是跳过了runloop休眠和唤醒这一部分的代码,直接跳转到了处理各种事件的这一部分。

下面我把源码做了一些删减,方便大家可以更清楚的梳理整个过程

// 这个是runloop入口函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */

// 通知Observers 即将进入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心方法
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers 即将退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

return result;
}

下面是核心方法

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

int32_t retVal = 0;
do {

//通知Observers 即将处理Timers
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

//通知Observers 即将处理Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

//处理Blocks
__CFRunLoopDoBlocks(rl, rlm);

//处理source0,根据返回值决定在处理一次blocks
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);


// source1相关
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
// 是否有Source1 有的话跳转到handle_msg
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}

didDispatchPortLastTime = false;

// 通知Observers: 即将休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//休眠
__CFRunLoopSetSleeping(rl);


//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);


__CFRunLoopUnsetSleeping(rl);

// 通知Observers: 即将醒来
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

// 标识标识 !!!!!
handle_msg:;

__CFRunLoopSetIgnoreWakeUps(rl);

//下面根据是什么唤醒的runloop来分别处理

if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}

// 被Timer唤醒
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被Timer唤醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被GCD唤醒
else if (livePort == dispatchPort) {

// 处理GCD相关
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

} else {
//被Source1唤醒
//处理Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;

}


//在处理一遍BLocks
__CFRunLoopDoBlocks(rl, rlm);


// 设置返回值 决定是否继续循环
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}

} while (0 == retVal);

return retVal;
}

图和源码结合来看,整个流程就清晰了很多。流程里面的有些东西不需要我们太过深入的研究,我们把这个流程掌握一下就OK了。

细节补充

第一点

我们都知道RunLoop有一个优势,那就是可以使线程在有工作的时候工作,没有工作的时候休眠,来减少占用CPU资源,提高程序性能。

这说明代码在执行到

//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

的时候,会阻塞当前的线程。但这种阻塞跟我们之前所用到过的阻塞线程不是一回事。

举个例子,我们可以使用while(1){};这句代码来阻塞线程,这句代码在底层会转换为汇编的代码,我们的线程一直在重读执行这几句代码,所以他仅仅是阻塞线程,并没有使线程休眠,我们的线程一直在工作。但是runloop,通过mach_msg使用了一些内核层的API,真的是实现了线程的休眠,让线程不再占用CPU资源。

第二点

RunLoop与线程的关系?

一个线程对应一个RunLoop对象。
RunLoop默认不创建,在第一次获取的时候创建,主线程中的默认存在RunLoop也是因为在底层代码中,提前获取过一次。
RunLoop储存在一个全局的字典中,线程是key,RunLoop是value。(源码中有所体现)
RunLoop会在线程结束时销毁。

链接:https://www.jianshu.com/p/705aa44405c0

收起阅读 »

微信小程序自定义实现toast进度百分比动画组件

目录结构wxml {{number}} {{ content }} 搭建组件结构jsComponent({ options: { multipleSlots: true // 在组件定义时的选项中...
继续阅读 »

目录结构


wxml



{{number}}



{{ content }}


搭建组件结构

js

Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
/**
* 私有数据,组件的初始数据
* 可用于模版渲染
*/
data: { // 弹窗显示控制
animationData: {},
content: '提示内容',
number: 0,
level_box:-999,
},
/**
* 组件的方法列表
*/
methods: {
/**
* 显示toast,定义动画
*/
numberChange() {
let _this = this
for (let i = 0; i < 101; i++) {
(function () {
setTimeout(() => {
_this.setData({
number: i + '%'
})
}, 100 * i)
})()
}
},
showToast(val) {
this.setData({
level_box:999
})
this.numberChange()
var animation = wx.createAnimation({
duration: 300,
timingFunction: 'ease',
})
this.animation = animation
animation.opacity(1).step()
this.setData({
animationData: animation.export(),
content: val
})
/**
* 延时消失
*/
setTimeout(function () {
animation.opacity(0).step()
this.setData({
animationData: animation.export()
})
}.bind(this), 10000)
}
}
})

json

```javascript
{
"component": true,
"usingComponents": {}
}

wxss

.wx-toast-box {
display: flex;
width: 100%;
justify-content: center;
position: fixed;
top: 400rpx;
opacity: 0;
}

.wx-toast-content {
max-width: 80%;
border-radius: 30rpx;
padding: 30rpx;
background: rgba(0, 0, 0, 0.6);
}

.wx-toast-toast {
height: 100%;
width: 100%;
color: #fff;
font-size: 28rpx;
text-align: center;
}

.progress {
display: flex;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
text-align: CENTER;
color: #07c160;
}

.img_box {
display: flex;
justify-content: center;
margin: 20rpx 0;
}

@keyframes rotate {
from {
transform: rotate(360deg)
}

to {
transform: rotate(0deg)
}
}

.circle {
animation: 3s linear 0s normal none infinite rotate;
}

@keyframes translateBox {
0% {
transform: translateX(0px)
}

50% {
transform: translateX(10px)
}
100% {
transform: translateX(0px)
}
}

.anima_position {
animation: 3s linear 0s normal none infinite translateBox;
}

效果截图



原文:https://juejin.cn/post/6968731176492072968



收起阅读 »

让我们一起实现微信小程序国际化吧

常见的国际化方式官方方案官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面强依赖目录结构由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图特别好笑的一点官方示例里居然不是这个目录结构,不过依然是...
继续阅读 »

常见的国际化方式

官方方案

官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面

强依赖目录结构

由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图


特别好笑的一点官方示例里居然不是这个目录结构,不过依然是强依赖目录结构,因为gulp中路径是写死的

文档简陋

通过官方文档快速入门居然无法搭建起项目,暂时只用这种方式搭建起来了 官方github demo,通过对比发现好多必要代码都没有在文档中说明。


比如需要在app.js中开始便需要执行getI18nInstance(),否则全局都无法正常国际化。这么重要的信息居然在快速入门中没有说明


调试麻烦

每次修改代码都要重新执行npm run build,注意是每次


由于国际化必须通过npm run build来实现,而每次npm run build过后dist文件就会被覆盖,所以每次只能修改src,而小程序预览的却是dist文件,这也就导致必须频繁的执行build命令。下面演示一下增加一个表头的操作步骤

2021-05-22 07-57-21.2021-05-22 08_03_21.gif

说下优点

代码简洁。解释一下,从上图可以看到,他的书写方式和其他主流框架的国际化书写方式很类似(vue,react)。view层都是类似t(key,参数),对js侵入也很小,下面是上图页面对应的js部分。

import { I18nPage } from '@miniprogram-i18n/core'

I18nPage({
onLoad() {
this.onLocaleChange((locale) => {
console.log('current locale:', this.getLocale(), locale)
})

this.setLocale('zh-CN')
},

toggleLocale() {
this.setLocale(
this.getLocale() === 'zh-CN' ? 'en-US' : 'zh-CN'
)
},

nativate() {
wx.navigateTo({
url: '/pages/logs/logs'
})
}
})

可以说除了I18nPage以外没有别的侵入,剩下那些代码都是用于切换语言所需,如果只是最简单国际化,只需要I18nPage({})即可

聊一下为什每次都需build

其实咱们看下dist/i18n/locales.wxs文件即可

var fallbackLocale = "zh-CN";
var translations = {
"en-US": {
test: ["test messages"],
test2: ["test message 2, ", ["label"], ", ", ["label2"]],
nested: ["nested message: ", ["test"]],
toggle: ["Toggle locale"],
navigate: ["Navigate to Log"],
"window.title": ["I18n test"],
"index.test": ["Test fallback"],
navigate2: ["Navigation 2nd"],
},
"zh-CN": {
test: ["测试消息"],
test2: ["测试消息二, ", ["label"], ", ", ["label2"]],
nested: ["嵌套消息: ", ["test"]],
toggle: ["切换语言"],
navigate: ["跳转"],
"window.title": ["国际化测试"],
"index.test": ["备选"],
navigate2: ["导航2"],
},
};
var Interpreter = (function (r) {
var i = "";
function f(r, n) {
return r
? "string" == typeof r
? r
: r
.reduce(function (r, t) {
return r.concat([
(function (n, e) {
if (((e = e || {}), "string" == typeof n)) return n;
if (n[2] && "object" == typeof n[2]) {
var r = Object.keys(n[2]).reduce(function (r, t) {
return (r[t] = f(n[2][t], e)), r;
}, {}),
t = r[e[0]],
u = e[n[0]];
return void 0 !== u
? r[u.toString()] || r.other || i
: t || r.other || i;
}
if ("object" == typeof n && 0 < n.length) {
return (function r(t, n, e) {
void 0 === e && (e = 0);
if (!n || !t || t.length <= 0) return "";
var n = n[t[e]];
if ("string" == typeof n) return n;
if ("number" == typeof n) return n.toString();
if (!n) return "{" + t.join(".") + "}";
return r(t, n, ++e);
})(n[0].split("."), e, 0);
}
return "";
})(t, n),
]);
}, [])
.join("")
: i;
}
function c(r, t, n) {
t = r[t];
if (!t) return n;
t = t[n];
return t || n;
}
return (
(r.getMessageInterpreter = function (i, o) {
function e(r, t, n) {
var e, u;
return f(
((e = r),
(u = o),
((n = (r = i)[(n = n)]) && (n = n[e])) || c(r, u, e)),
t
);
}
return function (r, t, n) {
return 2 === arguments.length
? e(r, null, t)
: 3 !== arguments.length
? ""
: e(r, t, n);
};
}),
r
);
})({});

module.exports.t = Interpreter.getMessageInterpreter(
translations,
fallbackLocale
);
其实搞这么麻烦构建方式主要是为了生成这个wxs文件,我们之所以能在vue中看到{{t('key')}}的方式进行国际化,是因为wxml层本身支持函数调用且函数可以调用外部资源(国际化文件),而小程序只允许通过wxs(官方文档)的方式在页面使用函数,出于性能考虑又不允许wxs引用任何外部资源(除了其他的wxs),所以这个build最核心的诉求是把 国际化文件copy一份到wxs文件中。下面是wxs无法引用外部资源的官方说明

优化

我们国际化的核心诉求就是解决官方国际化问题同时保留他的优点,这里我们列下此次的目标

  •  路径灵活,不强依赖路径减少后期添加国际化的路径改动成本
  •  调试方便,和原始开发调试方式相同
  •  书写简洁,保持和vue一样的书写方式
2021-05-22 10-27-02.2021-05-22 10_28_39.gif

wxml代码

<wxs src="../wxs/i18n.wxs" module="i18n" />
<!-- 标题国际化 -->
<page-meta>
<navigation-bar title="{{i18n.t(locales['主页'])}}" />
</page-meta>
<!-- 一般国际化 -->
<view>{{i18n.t(locales['通往爱人家里的路总不会漫长。'])}}</view>
<!-- js国际化 -->
<view>{{jsMsg}}</view>
<!-- 支持变量国际化 -->
<view>{{i18n.t(locales['当前页面:{path}'],[{key:'path',value:'home-page/home-page'}])}}</view>
<!-- 切换中英文按钮 -->
<button bindtap="zhClick" type="default">{{i18n.t(locales['切换中文'])}}</button>
<button bindtap="enClick" type="warn">{{i18n.t(locales['切换英语'])}}</button>

js代码

const i18n = require('../behaviors/i18n');
// home-page/home-page.js
Component({
behaviors: [i18n],
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
zhClick() {
this.switchLanguage('zh_CN')
},
enClick() {
this.switchLanguage('en_US')
},
}
})

基本维持和官方相同的写法,尽可能少的代码写入

代码段链接

解决思路

利用behaviors(官方文档)将国际化文案进行引入每个页面。然后将所有国际化数据和key值以参数的形式传递给wxs函数,这样就可以避开wxs外部资源限制实现和vue i18n相同的效果。



  • behaviors负责将国际化方法和文案导入全局,以下是behaviors/i18n.js源码:

// behaviors/i18n.js

const {
t
} = require('../utils/index')
const i18n = Behavior({
data: {
language:{}, // 当前语种
locales: {}, // 当前语言的全部国际化信息
},
pageLifetimes: {
// 每次页面打开拉取对应语言国际化数据
show() {
if (this.data.language === 'en_US') {
this.setData({
locales: require('../i18n/en_US')
})
} else {
this.setData({
locales: require('../i18n/zh_CN')
})
}
}
},
methods: {
// 全局js国际化便捷调用
$t(key, option) {
return t(key, option)
},
// 由于tab只能通过js修改,所以每次语言切换需要重新更新tab国际化内容
refreshTab() {
wx.setTabBarItem({
index: 0,
text: this.data.locales['主页']
})
wx.setTabBarItem({
index: 1,
text: this.data.locales['我的']
})
},
// 切换语种
switchLanguage(language) {
this.setData({
language
})
if (language === 'zh_CN') {
this.setData({
locales: require('../i18n/zh_CN')
})
} else {
this.setData({
locales: require('../i18n/en_US')
})
}
// 切换下方tab
this.refreshTab()
},
}
})

module.exports = i18n
wxs负责为wxml层提供国际化方法,此处逻辑比较简单,先找到国际化文件中key值对应的语句,然后根据第二参数(arr)将变量进行替换,此处替换逻辑比较粗暴,使用{key}方式代表变量。如:"我的年龄是{age}",age代表变量,参数传递格式为 [{key:'xxx',value:'xxxx'}]

// 国际化.js
{
"ageText":"my age is {age}",
}
// wxml
<view>{{i18n.t(locales['ageText'],[{key:'age',value:'18'}])}}</view>

wxs源码如下:

var i18n = {
t: function (str, arr) {
var result = str;
if (arr) {
arr.forEach(function (item) {
if(result){
result = result.replace('{'+item.key+'}', item.value)
}
})
}
return result
}
}
module.exports = i18n

同时提供一个在js里获取国际化的util方法

// 国际化
const t = (key, option = {}) => {
const language = wx.getStorageSync('language');
let locales = null
if (language === 'en_US') {
locales = require('../i18n/en_US')
} else {
locales = require('../i18n/zh_CN')
}
let result = locales[key]
for (let optionKey in option) {
result = result.replace(`{${optionKey}}`, option[optionKey])
}
return result
}

module.exports = {
t
}

这样就基本实现了同时在wxml,js中进行国际化的基本需求,同时也解决了官方调试体验不足的缺点。

不足




  • 其实调试体验还不是那么完美,由于只有在show中初始化国际化文件内容,所以当开启“热重载”时对国际化文件进行修改,国际化内容不会自动进行更新




  • 每个page的js文件都需要引入behaviors/i18n.js,wxml文件引入<wxs src="../wxs/i18n.wxs" module="i18n" />有些略显繁琐




  • require('../i18n/xxx')整个项目引入了3遍略显繁琐,可以封装一下




  • i18n.t(locales['key']) 其中locals每次都要写一遍比较繁琐,不过由于wxs的限制也想不到太好的方法

使用建议


由于只是写一个demo很多逻辑并没有写完整,所以如果使用的话需要根据项目进行修改




  1. 如果i18n路径和命名方式不同需要同时修改behaviors/i18n.js,以及utils/index.js中的路径




  2. 现在每次刷新页面国际化都会被重置成中文,建议在behaviors/i18n.js的show方法中从全局获取当前的国际化语言。这样就不会每次都被重置成中文了。


链接:https://juejin.cn/post/6964963316493975588


收起阅读 »

要不要打造一个轻量的小程序引擎玩玩?

我们的小程序框架的底层,我把它分为四个部分,主要是多线程模型runtime 框架js 沙箱其他我们一个一个来多线程模型和线程通信多线程模型多线程模型是一个非常常见的 UI 模型,包括 RN、flutter 统统都是使用的这个模型,小程序也不例外 它们其实只是线...
继续阅读 »


我们的小程序框架的底层,我把它分为四个部分,主要是

  • 多线程模型
  • runtime 框架
  • js 沙箱
  • 其他

我们一个一个来

多线程模型和线程通信


多线程模型


多线程模型是一个非常常见的 UI 模型,包括 RN、flutter 统统都是使用的这个模型,小程序也不例外


它们其实只是线程主体的不同,比如 RN 主要是 shadow tree 和 jscore,而 flutter 则是 skia 和 dart engine,小程序则是 webview 充当渲染层,js engine(或 worker)充当逻辑层


尽管本质一样,但因为业务场景的不同,小程序的诉求却和 RN/flutter 完全不同


在 RN 中,app 作为一个主体,我们更乐意分配更多资源,以至于 RN 一直在跑 react 这种 runtime 浪费的框架,在过去,这被认为是值得的


但是小程序与之相反,它作为 app 的附属品,我们不乐意给小程序分配更多资源,不乐意分配内存,不乐意分配更多线程,所以我们这次分享的前提是

基于最小分配的前提,探讨小程序的每个环节

请记住前提,然后我们接着往下看


线程通信


说到多线程,我们首先想到的就是多线程的调度和通信,我们先讲通信,通常来说,多线程的非 UI 线程都是没有 dom 环境的,无论是 js 引擎还是 worker


所以为了能跑一个前端框架,我们不得另寻出路,主要方案有三种,其中幻灯片的第二种,写一个 dom 无关的 diff 算法,这是写不出来什么好算法的,所以我们主要看剩下两种思路



幻灯片中,左边的代码是 [ 使用 Proxy 劫持 dom ],右边的是 [ 模拟 dom API ]


这两种思路其实是类似的,模拟 dom API 是最为常见的,比如 react-reconciler,vue3 的 renderer,都是用的这个思路,就是它把用到的 dom API 暴露出来,你在不同的端去模拟 dom 的行为


甚至还有 taro-next,kbone 这种框架,他们模拟了一整个 dom/bom 层


这个思路好处是粗暴方便好用,坏处就是抽象程度低,比如 taro-next 中就用了千行代码做这件事,属于 case by case,没啥逼格


所以我提出了 Proxy 劫持 dom 的思路,其实这个思路在微前端中比较常用,只不过我们现在用 Proxy 不再是劫持一两个 dom 操作了,而是将所有 dom 操作通通记录下来,然后批量发送给 UI 线程


这个实现抽象程度非常高,我使用了不到 200 行代码就可以劫持所有 dom 操作


代码在这里:github.com/yisar/fard/…


除了线程通信,更重要的是线程的调度,因为很重要,我们放到最后说


前端框架


还记得小程序架构的前提吗?没错,就是最小资源分配


因为我们不想给小程序分配过多的资源,所以像 react、vue 这种 runtime 特别重的框架,其实是不适合用作小程序的


甚至 fre 也不适合,因为大家可能对“轻量”这个词有误解,不是代码越少就越轻量,代码量是很重要的一个方面,但是更重要的是代码的内存占用,以及算法的算力和复杂度


fre 虽然代码量少,但它的算法和 vue 是一样的,算力相同,内存占用也不少


所以我们不得不将目光转向 svelte 这类框架,幻灯片可以看到,svelte 通过编译,直接生成原生 dom 操作,没有多余的算法和 vdom


实际上,我们在做性能优化的时候,讲究一个“换”字,react 这种框架,通过浪费 runtime 去做算法,“换”一个最小结果,而 svelte 则是通过编译(浪费用户电脑),去换 runtime



JS 沙箱



然后我们来讲讲沙箱,也就是 js 引擎和 worker,这部分适合语言爱好者

选型


通常来说,一提到 js 引擎,大家都是 v8 v8 v8

但是实际上,v8 是一个高度优化的 JIT 引擎,它跑 js 确实是足够快的,但对于 UI 来说,我们更多要的不是跑得快


实际上,AOT 的语言或引擎更适合 UI 框架,比如 RN 的 hermes,dart 也支持 AOT,它可以编译成字节码,只需要一次构建即可,当然,AOT 也有缺点,就是 热更新 比较难做


另外除了 js 引擎,worker 也是一个非常赞的选择,方便好用,而且还提供了 bom 接口,比如 offscreen canvas,fetch,indexdb,requestAnimationFrame……

总结



哈哈哈总结,我们基于最小分配的前提去设计这个架构,每个环节都选择节省资源的方案


事实上写代码就是这样的,比如我写 fre,那么我追求 1kb,0 依赖,我写业务框架,我追求 0 配置,1mb 的 node_modules 总大小


我写小程序,我追求最小资源分配,不管做啥,有痛点然后有追求然后才有设计


其他

其实小程序还有很多东西可以做,比如现在的小程序都需要兼容微信小程序,也就是类似 wxml,wxss,wxs这些非标准的文件,还要得是个多 Page 的 mpa


比如 ide,我们可以使用 nobundle 的思路来加快构建速度


当然,为了服务业务,在我们公司我没有使用 nobundle


比如剧透一下,我在公司中为了兼容微信小程序,开的新坑


原理是将微信的文件(wxml,wxss,wxs)先编译成可识别的文件(jsx,css,js),然后使用 babel、postCss 去转移,形成一个个 umd 的子应用


然后通过 berial(微前端框架)的路由,沙箱,生命周期,将它们跑在 h5 端,这样就可以在浏览器中模拟和调试啦



最后我们通过三张图和一个问题,来补充和结束一下这次分享


第一张图是微信小程序的后台桌面,有没有感觉和操作系统有点像,但其实不是的,操作系统的软件是进程的关系,只能切换,不能共存,而小程序是多进程,这些小程序可以在后台留驻,随时保持唤醒


第二张图是钉钉的仪表盘,这也是小程序最常用的场景,就是和这种一堆子应用的 app


第三张图是 vscode 的插件系统,是的,想不到吧,这玩意也是小程序架构,而且也是同样的思想,我不让你操作 dom

然后最后的问题:canvas 怎么办?



这个问题实际上非常难搞,如果我们使用 worker 作为 js 沙箱还好,有 offscreen canvas 和 requestAnimationFrame


如果我们使用 js 引擎怎么办呢,走上面提到的线程通信成本就太高了,动画走这个通信,等接收到消息,动画已经卡死了


所以还有什么办法,这里不得不再次提多线程的特性,也就是多线程的内存是共享的,我们可以 js 引擎中,将 canvas 整个元素放到堆栈里,然后 UI 线程和 js 线程共享这一块内存


这样就不需要走线程通信了,适合 canvas,动画这种场景



链接:https://juejin.cn/post/6962028699872919559


收起阅读 »

微信小程序-自定义日期组件实现

   今日份跟大佬聊到说前端基础架构平台的东西、涉及到基础组件库、公共工具类等内容, 然后期望能自我驱动带头去做, 陷入深深滴沉思, 该考虑如何做?思绪一片混乱, 那就写篇文章冷静冷静,故有了此篇, 仅供参考。微信小程序原生有提供一套日期组...
继续阅读 »

   今日份跟大佬聊到说前端基础架构平台的东西、涉及到基础组件库、公共工具类等内容, 然后期望能自我驱动带头去做, 陷入深深滴沉思, 该考虑如何做?
思绪一片混乱, 那就写篇文章冷静冷静,故有了此篇, 仅供参考。

微信小程序原生有提供一套日期组件, 大概如下:



跟UI预期不一致的点有如下几个:

A. 期望弹窗居中显示、而不是从底部弹出;

B. 期望小于10的展示为1月,1日这种, 而不是01月, 01日;

C. UI样式跟微信原生差别有点大;

D. 不需要头部的取消&确定按钮、预期底部整个确定按钮即可;

想着让产品接受原生日期组件的, But拗不过产品的思维, 只能开干、自己撸一个自定义日期组件, 造轮子=.= 

既然原生的不能用, 那么我们看看小程序是否有提供这种类似的滚动器, 查看官方文档发现: 



那就开干, 为尽可能保持代码的最小颗粒度(这里不考虑弹窗外壳的封装、纯日期组件).
话不多说、这里直接贴上代码、预留的坑位都会在代码内有备注, 请参考:

// 组件wxml
<!-- 预留坑位: 按道理该日期组件应该是做在弹窗上的、这里为了简化代码故直接写在了页面上;
后期使用者烦请自己做个弹窗套上、用showModal属性控制其显示隐藏-->
<view class="picker" wx:if="{{showModal}}">
<picker-view indicator-class="picker-indicator" value="{{pickerIndexList}}" bindchange="bindChangeDate">
<picker-view-column>
<view wx:for="{{yearList}}" wx:key="index" class="{{pickerIndexList[0]==index?'txt-active':''}}">{{item}}年</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{monthList}}" wx:key="index" class="{{pickerIndexList[1]==index?'txt-active':''}}">{{item}}月</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{dayList}}" wx:key="index" class="{{pickerIndexList[2]==index?'txt-active':''}}">{{item}}日</view>
</picker-view-column>
</picker-view>
<!-- 预留坑位: 日期组件可能仅允许数据回选、不允许修改。
思路: 通过自定义蒙层盖在日期控件上从而达到禁止控件滚动的效果.
-->
<view wx:if="{{!canEdit}}" class="disabled-picker"></view>
</view>

// 组件wxss
.picker{
position: relative;
height: 300rpx;
width: 600rpx;
margin: 0 auto;
border: 1rpx solid red;
}
.picker picker-view {
width: 100%;
height: 100%;
}
.picker-indicator {
height: 60rpx;
line-height: 60rpx;
}
.picker picker-view-column view {
font-size: 40rpx;
line-height: 60rpx;
text-align: center;
}
.txt-active {
color: #2c2c2c;
}
/* 预留坑位: 为便于区分真的有遮罩层盖住、特意加了个背景色、实际使用过程可改成透明色 */
.disabled-picker{
width: 600rpx;
position: absolute;
top: 0;
left: 0;
height: 300rpx;
z-index: 999;
background: rgb(255,222,173,0.7);
}


// 组件js
Component({
properties: {},
data: {
yearList: [],
monthList: [],
dayList: [],
pickerIndexList: [0, 0, 0]
},
methods: {
// dateString格式: 'YYYY-MM-DD'
initPicker (dateString) {
let nowDate = new Date()
// 预留个坑位: 若需要指定某一日期则从外面传入、否则默认当天
if(dateString){
// 预留个坑位: 判定传入的数据类型是否符合要求、若不符合该报错的报错
nowDate = new Date(dateString)
}

// 预留个坑位: 因为下面的日期指定在1900.01.01-2100.12.31、故这里最好校验下传入日期是否在区间内.
let nowYear = nowDate.getFullYear()
let nowMonth = nowDate.getMonth() + 1
let yearList = this.getYearList(nowYear)
let monthList = this.getMonthList()
let dayList = this.getDayList(nowYear, nowMonth)

// 获取多列选择器的选中值下标
let pickerIndexList = []
pickerIndexList[0] = yearList.findIndex(o => o === nowDate.getFullYear())
pickerIndexList[1] = monthList.findIndex(o => o === nowDate.getMonth()+1)
pickerIndexList[2] = dayList.findIndex(o => o === nowDate.getDate())
this.setData({
yearList,
monthList,
dayList,
pickerIndexList,
showModal: true
})
},
// 获取年份
getYearList (nowYear) {
let yearList = []
if(nowYear < 1900 || nowYear > 2100){
return false
}
for (let i = 1900; i <= 2100; i++) {
yearList.push(i)
}
return yearList
},
// 获取月份
getMonthList () {
let monthList = []
for (let i = 1; i <= 12; i++) {
monthList.push(i)
}
return monthList
},
// 获取日期 -> 根据年份&月份
getDayList (year, month) {
let dayList = []
month = parseInt(month, 10)
// 特别注意: 这里要根据年份&&月份去计算当月有多少天[切记切记]
let temp = new Date(year, month, 0)
let days = temp.getDate()
for (let i = 1; i <= days; i++) {
dayList.push(i)
}
return dayList
},
// 日期选择改变事件
bindChangeDate (e) {
let pickerColumnList = e.detail.value
const { yearList=[], monthList=[] } = this.data
const nowYear = yearList[pickerColumnList[0]]
const nowMonth = monthList[pickerColumnList[1]]
this.setData({
dayList: this.getDayList(nowYear, nowMonth),
pickerIndexList: pickerColumnList
})
},
show (birthday) {
// 预留坑位: 这里也许会有一定的逻辑判定是否允许编辑日期控件, 故预留canEdit属性去控制
this.setData({
canEdit: true
})
this.initPicker(birthday)
},
// 预留坑位、点击确定按钮获取到选中的日期
surePicker () {
const { pickerIndexList, yearList, monthList, dayList } = this.data
// 预留坑位: 月份&日期补0
let txtDate = `${yearList[pickerIndexList[0]]}-${monthList[pickerIndexList[1]]}-${dayList[pickerIndexList[2]]}`
console.log(txtDate)
},
}
})

接下来我们看看使用方是怎么使用的?

// 页面wxml
<!-- 预留坑位: 这里仅展示触发事件、开发者替换成实际业务即可-->
<view bind:tap="openPicker" style="margin:20rpx; text-align:center;">打开日期控件</view>

// 页面json: 记得在使用页面的json处引入该组件、配置组件路径

// 页面js
methods: {
openPicker (){
// 获取组件实例、这里可选择是否传入日期
this.date_picker = this.selectComponent && this.selectComponent('#date_picker')
this.date_picker && this.date_picker.show()
},
}

一切准备就绪、我们看看效果图!
这是日期可编辑时、你是可滚动选择器的:


我们看看日期不可编辑时、仅可查看的效果图:



样式是稍微有点丑、到时开发者按照实际UI去做微调即可、这不难的=.=.

这里预留了几个扩展点:

1.支持外部传入日期、默认选中预设值;

2.支持在弹窗内显示日期控件、需要使用者自行开发弹窗;

3.支持日期控件仅可查看、不可编辑;

4.支持日期控件的关闭、一般是弹窗上有个关闭按钮或者是点击弹窗的蒙层可关闭、使用者自行开发;

Tips: 具体的代码改动点都有在上面的code中有备注、欢迎对号改代码, 若有任何不懂的欢迎留言或者私信、很愿意帮您解答。


链接:https://juejin.cn/post/6967201721265160199

收起阅读 »

Onboard,迷人的引导页样式制作库

简介Onboard主要用于引导页制作,源码写的相当规范,值得参考.项目主页: https://github.com/mamaral/Onboard实例下载: https://github.com/mamaral/Onboard/archiv...
继续阅读 »

简介




Onboard主要用于引导页制作,源码写的相当规范,值得参考.

项目主页: https://github.com/mamaral/Onboard
实例下载: https://github.com/mamaral/Onboard/archive/master.zip

样式

设置背景图片或者背景movie,然后在它们之上生成数个ViewController,默认是顶部一张图片,下面是标题和详细介绍,最下面是按钮和page

导入

pod 'Onboard'

使用

导入头文件#import “OnboardingViewController.h”

图片为背景

蒙板控制器生成方法

1、title是标题
2、body是介绍
3、image是顶部图片
4、buttonText是按钮文本
5、block是按钮点击事件

OnboardingContentViewController *firstPage = [OnboardingContentViewController contentWithTitle:@"What A Beautiful Photo" body:@"This city background image is so beautiful." image:[UIImage imageNamed:@"blue"] buttonText:@"Enable Location Services" action:^{
}];

OnboardingContentViewController *secondPage = [OnboardingContentViewController contentWithTitle:@"I'm so sorry" body:@"I can't get over the nice blurry background photo." image:[UIImage imageNamed:@"red"] buttonText:@"Connect With Facebook" action:^{
}];
secondPage.movesToNextViewController = YES;
secondPage.viewDidAppearBlock = ^{
};

OnboardingContentViewController *thirdPage = [OnboardingContentViewController contentWithTitle:@"Seriously Though" body:@"Kudos to the photographer." image:[UIImage imageNamed:@"yellow"] buttonText:@"Get Started" action:^{
}];
```


#### 底部图片控制器

```objc
OnboardingViewController *onboardingVC = [OnboardingViewController onboardWithBackgroundImage:[UIImage imageNamed:@"milky_way.jpg"] contents:@[firstPage, secondPage, thirdPage]];




<div class="se-preview-section-delimiter"></div>

底部video控制器

NSBundle *bundle = [NSBundle mainBundle];
NSString *moviePath = [bundle pathForResource:@"yourVid" ofType:@"mp4"];
NSURL *movieURL = [NSURL fileURLWithPath:moviePath];
OnboardingViewController *onboardingVC = [OnboardingViewController onboardWithBackgroundVideoURL:movieURL contents:@[firstPage, secondPage, thirdPage]];




<div class="se-preview-section-delimiter"></div>

定制
1、默认的会给背景图片或者movie加一层黑色的蒙板,可以去掉它们:

onboardingVC.shouldFadeTransitions = YES;




<div class="se-preview-section-delimiter"></div>

2、可以给图片加上模糊效果(相当漂亮):

onboardingVC.shouldBlurBackground = YES;




<div class="se-preview-section-delimiter"></div>

3、可以给蒙板上的文字加上淡出效果:

onboardingVC.shouldFadeTransitions = YES;

转自:https://blog.csdn.net/sinat_30800357/article/details/50016319

收起阅读 »

RAC解析 - 自定义KVO

知识点概述1.KVO实现原理2.runtime使用目的给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。大体思路添加观察者的方法中- (void)SQ_addObserver:(NSObjec...
继续阅读 »

知识点概述

1.KVO实现原理
2.runtime使用

目的

给NSObject添加一个Category,用于给实例对象添加观察者,当该实例对象的某个属性发生变化的时候通知观察者。

大体思路

添加观察者的方法中

- (void)SQ_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;

会用runtime的方式手动创建一个其子类,并且将该对象变为该子类。该子类会复写观察方法中keyPath的setter方法,使这个setter被调用时利用runtime去调用observer的回调方法

-(void)observeValueForKeyPath:(NSString *)keyPath 
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context;

实现

这里只做KVO的基本功能,当被观察者改变属性的时候通知观察者,所以定义如下方法

NSObject+SQKVO.h

/**
添加观察者

@param observer 观察者
@param keyPath 被观察的属性名
*/
- (void)SQ_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;


/**
当被观察的观察属性改变的时候的回调函数

@param keyPath 所观察被观察者的属性名
@param object 被观察者
@param value 被观察的属性的新值
*/
- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value;

@end

因为这里要用到runtime所以需要添加runtime的头文件

#import <objc/message.h>

而且因为用到objc_msgSend所以要改变一下工程的环境变量


一.动态生成子类

在被观察者调用- SQ_addObserver:forKeyPath:时首先动态生成一个其子类。

// 1.生成子类
// 1.1获取名称
Class selfClass = [self class];
NSString *className = NSStringFromClass(selfClass);
NSString *KVOClassName = [className stringByAppendingString:@"_SQKVO"];
const char *KVOClassNameChar = [KVOClassName UTF8String];
// 1.2创建子类
Class KVOClass = objc_allocateClassPair(selfClass, KVOClassNameChar, 0);
// 1.3注册
objc_registerClassPair(KVOClass);

这里可以看到,我们将子类的类名命名为“类名”+“SQKVO”,譬如类名为“Person”,这个子类是“Person_SQKVO”。
这里有个注意点,一般为动态创建的类名应尽量复杂一些避免重复。最好加上“”。

二.根据KeyPath动态添加对应的setter

1 确定setter的名字
举个例子,如果用户给的keyPath是name,应该动态添加一个-setName:的方法。而这个setter的名字是 "set" + "把keyPath变为首字母大写" + ":"
所以可以得出

NSString *setterString =
[NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);

2 利用class_addMethod()给子类动态添加方法

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

1、cls:
给哪个类添加方法。即新生成的子类,上面生成的 KVOClass。
2、name:
所添加方法的名称。即上一步生成的字符串 setterString。
3、imp:
所添加方法的实现。即这个方法的C语言实现,首先在下面先写一个C语言的方法。稍后会讲具体实现。

void setValue(id self, SEL _cmd, id newVale) {
}

4、types:
所添加方法的编码类型。setter的返回值是void,参数是一个对象(id)。void用"v"表示,返回值和参数之间用“@:”隔开,对象用"@"表示。最后我们可以得出结果"v@:@"。
具体其他的编码类型可以参考苹果文档。
ps: 这里说下为什么返回值和参数之间用“@:”隔开。“:”代表字符串,所有的OC方法都有两个隐藏参数在参数列表的最前面,“发起者”和 “方法描述符”,“@”就是这个发起者,“:”是方法描述符。而这个types其实是imp返回值和参数的编码。因为OC方法中返回值和参数之间必然有“发起者”和“SEL”隔着,所以“@:”自然而然就成了返回值和参数之间的分隔符。
当然我们还可以用@encode来得到我们想要的编码类型

NSString *encodeString =
[NSString stringWithFormat:@"%s%s%s%s",
@encode(void),
@encode(id),
@encode(SEL),
@encode(id)];

3 将当前对象的类变为我们所创建的子类的类型,即更改isa指针

object_setClass(self, KVOClass);

4 将keyPath和观察者关联(associate)到我们的对象上

用下面这个函数可以很方便的将一个对象用键值对的方式绑定到一个目标对象上。
*如果想了解跟多可以查找《Effective Objective-C》的第10条

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

1、object
目标对象

2、key
绑定对象的键,相当于NSDictionary的key
这里的key一般采用下面的方式声明:

static const void *SQKVOObserverKey = &SQKVOObserverKey;
static const void *SQKVOKeyPathKey = &SQKVOKeyPathKey;

这样做是因为若想令两个键匹配到同一个值,则两者必须是完全相同的指针才行。

3、value
绑定对象,相当于NSDictionary的value

4、policy
绑定对象的缓存策略
@property (nonatomic, weak) :OBJC_ASSOCIATION_ASSIGN
@property (nonatomic, strong) :OBJC_ASSOCIATION_RETAIN_NONATOMIC
@property (nonatomic, copy) :OBJC_ASSOCIATION_COPY_NONATOMIC
@property (atomic, strong) :OBJC_ASSOCIATION_RETAIN
@property (atomic, weak) :OBJC_ASSOCIATION_COPY

最后关联的代码:

objc_setAssociatedObject(self, SQKVOObserverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, SQKVOKeyPathKey, keyPath, OBJC_ASSOCIATION_COPY_NONATOMIC);

三.setValue()的实现

这个函数的目的主要是:
1.利用objc_msgSend触发原先类的setter
2.利用objc_msgSend触发观察者的回调方法

1. 触发原先的setter方法

// 保存子类
Class KVOClass = [self class];

// 变回原先的类型,去触发setter
object_setClass(self, class_getSuperclass(KVOClass));
NSString *keyPath = objc_getAssociatedObject(self, SQKVOKeyPathKey);
NSString *setterString = [NSString stringWithFormat:@"set%@:", [keyPath capitalizedString]];
SEL setter = NSSelectorFromString(setterString);
objc_msgSend(self, setter, newVale);

2. 调用观察者的回调方法

id observer = objc_getAssociatedObject(self, SQKVOObserverKey);
objc_msgSend(observer, @selector(SQ_observeValueForKeyPath:ofObject:changeValue:), keyPath, self, newVale);

3.改回KVO类

object_setClass(self, KVOClass);

四.实现空的回调方法

- (void)SQ_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object changeValue:(id)value {

}

五.调用自定义的KVO

恭喜你看到这里,并且恭喜你已经成功了!

六.代码

代码下载地址

转自:https://www.jianshu.com/p/eb067f68c2b7

收起阅读 »

Objective-C高级编程笔记一(自动引用计数)

示例代码下载手动引用计数MRC内存管理的思考方式1、自己生成的对象自己持有2、不是自己生成的对象,自己也能持有3、不在需要自己持有的对象时释放4、不是自己持有的对象无法释放对象操作与Objective-C方法的对应实现一个MRCObject类:@impleme...
继续阅读 »

示例代码下载

手动引用计数

MRC内存管理的思考方式

1、自己生成的对象自己持有
2、不是自己生成的对象,自己也能持有
3、不在需要自己持有的对象时释放
4、不是自己持有的对象无法释放

对象操作与Objective-C方法的对应


实现一个MRCObject类:

@implementation MRCObject
- (void)dealloc {
NSLog(@"%@(%@)销毁了", NSStringFromClass(self.class), self);

[super dealloc];
}
+ (instancetype)object {
MRCObject *obj = [self allocObject];
[obj autorelease];
return obj;
}

+ (instancetype)allocObject {
MRCObject *obj = [[MRCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);

return obj;
}

@end

自己生成并持有对象:

MRCObject *obj = [MRCObject allocObject];

不是自己生成的对象也能持有:

MRCObject *obj = [MRCObject object];
[obj retain];

不在需要自己持有的对象时释放:

MRCObject *obj = [self allocObject];
[obj release];

无法释放自己没有持有的对象:

MRCObject *obj = [self allocObject];
[obj release];
[obj release];//会奔溃

autorelease

autorelease像c语言的自动变量来对待对象实例,当超出其作用域(相当于变量作用域),对象实例的release方法被调用。与c语言自动变量不同的是,可以autorelease的作用域。

autorelease的使用方法:

1、生成NSAutoreleasePool对象
2、调用已分配对象实例的autorelease方法
3、废弃NSAutoreleasePool对象

在应用程序中,由于主线程的NSRunloop对NSAutoreleasePool对象进行生成、持有和废弃处理。因此开发者不一定非得使用NSAutoreleasePool对象来进行开发工作。如下图:


在大量产生autorelease对象时,只要不废弃NSAutoreleasePool对象,autorelease对象就不会被释放,因此会产生内存不足的现象。如下两段代码:

for (int index = 0; index < 1000; index++) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"1553667540126" ofType:@"jpeg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
[image autorelease];
}
for (int index = 0; index < 1000; index++) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *path = [[NSBundle mainBundle] pathForResource:@"1553667540126" ofType:@"jpeg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:path];
[image autorelease];
[pool drain];
}

ARC

ARC规则

ARC有效时,id类型和对象类型同c语言其他类型不同,必须添加所有权修饰符。共如下4种所有权修饰符:

1、__strong修饰符
2、__weak修饰符
3、__unsafe_unretained修饰符
4、__outoreleasing修饰符

import "ARCObject.h"

实现一个ARCObject类:

@interface ARCObject ()
{
__strong id _strongObj;
__weak id _weakObj;
}

@end

@implementation ARCObject

- (void)dealloc {
NSLog(@"%@(%@)销毁了", NSStringFromClass(self.class), self);
}
+ (instancetype)allocObject {
ARCObject *obj = [[ARCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);
return obj;
}
- (void)setStrongObject:(id)obj {
_strongObj = obj;
}
- (void)setWeakObject:(id)obj {
_weakObj = obj;
}

@end

__strong修饰符

__strong修饰符是所有id类型和对象类型默认的所有权修饰符,表示对对象的强引用,在超出其作用域或被重新赋值时被废弃。

{
ARCObject *obj = [ARCObject allocObject];
NSLog(@"作用域最后一行%@", obj);
}
NSLog(@"作用域已经结束");
ARCObject *obj = [ARCObject allocObject];
NSLog(@"重新赋值前%@", obj);
obj = [ARCObject allocObject];
NSLog(@"重新赋值前后%@", obj);

__strong、__weak、__outoreleasing修饰符的自动变量默认初始化为nil。

__weak修饰符

__weak修饰符与__strong修饰符相反,提供弱引用,弱引用不持有对象实例。

循环引用容易发生内存泄漏,内存泄漏就是应当废弃的对象在超出其生存周期后依然存在。可以使用__weak修饰符来避免。

ARCObject *aObj = [ARCObject allocObject];
ARCObject *bObj = [ARCObject allocObject];
[aObj setStrongObject:bObj];
[bObj setStrongObject:aObj];
ARCObject *obj = [ARCObject allocObject];
[obj setStrongObject:obj];
ARCObject *aObj = [ARCObject allocObject];
ARCObject *bObj = [ARCObject allocObject];
ARCObject *cObj = [ARCObject allocObject];
[aObj setWeakObject:bObj];
[bObj setWeakObject:aObj];
[cObj setWeakObject:cObj];

__weak修饰符有一个优点就是:在持有某对象的弱引用时,如果该对象被废弃,则该对象弱引用自动失效且被置为nil。

__unsafe_unretained修饰符

__unsafe_unretained修饰符,正如其名一样是不安全的所有权修饰符。尽管ARC的内存管理是编译器的工作,但是这一点需要注意特别注意,__unsafe_unretained修饰符的变量不属于编译器内存管理的对象。

__unsafe_unretained修饰符和__weak修饰符的变量一样不会持有对象,但是__unsafe_unretained修饰符的变量在销毁时并不会自动置为nil,在其地址被覆盖后就会因为反问垂悬指正而造成奔溃。因此__unsafe_unretained修饰符变量赋值给__strong修饰符变量时要确保对象的真实存在。因为__weak修饰符是在iOS5中实现的,__unsafe_unretained修饰符存在的意义就是在iOS4中代替__weak修饰符的作用。

ARCObject __unsafe_unretained *obj = nil;
{
ARCObject *obj1 = [ARCObject allocObject];
obj = obj1;
}
NSLog(@"%@(%@)", NSStringFromClass(obj.class), obj);

__outoreleasing修饰符

ARC有效时不能使用outorelease方法,也不能使用NSAutoreleasePool类。这样就导致outorelease无法直接使用,但实际上outorelease功能是起作用的。使用@outoreleasepool{}块代码来代替NSAutoreleasePool类对象的生成持有以及废弃。通过赋值给__outoreleasing修饰符的变量来代替调用outorelease方法,也就是说对象被注册到autoreleasepool中。

@autoreleasepool {
ARCObject __autoreleasing *obj1 = [ARCObject allocObject];
NSLog(@"autoreleasepool块最后一行%@", obj1);
}
NSLog(@"autoreleasepool块已经结束");

ARC有效时,cocoa中由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回值的对象注册到outoreleasepool中。所以非显示的使用__outoreleasing修饰符也是可以的。

NSMutableArray __weak *array = nil;
NSLog(@"作用域块开始前%@", array);
{
NSMutableArray *arr = [NSMutableArray arrayWithObject:@(1)];
array = arr;
NSLog(@"作用域块最后一行%@", array);
}
NSLog(@"作用域块已经结束%@", array);

打印结果:

2019-03-28 11:56:52.316360+0800 ProfessionalExample[82984:16680615] 作用域块开始前(null)
2019-03-28 11:56:52.316538+0800 ProfessionalExample[82984:16680615] 作用域块最后一行(
1
)
2019-03-28 11:56:52.316627+0800 ProfessionalExample[82984:16680615] 作用域块已经结束(
1
)

id的指针和对象的指针在没有显式指定修饰符时会被附加上__outoreleasing修饰符。

- (BOOL)performOperationWithError:(ARCObject **)obj {
*obj = [ARCObject object];
return NO;
}

调用方法则为如下所示,自动转化为__autoreleasing修饰符:

[self performOperationWithError:<#(ARCObject *__autoreleasing *)#>];

id的指针和对象的指针变量必须指明所有权修饰符,并且赋值的所有权修饰符必须一致:

NSObject **pObj;//编报错,没有所有权修饰符
NSObject *obj = [[NSObject alloc] init];
NSObject *__autoreleasing*pObj = &obj;//编译报错,会更改所有权属性

纠正一个比较普遍的错误认知,for循环中并不是循环结束才释放循环内的局部变量,并不是所有产生大量对象的for循环中都需要加NSAutoreleasePool,而是产生大量autorelease对象时才需要添加。如下示例代码:

for (int index = 0; index < 2; index++) {
if (index == 0) {
NSLog(@"-------------begin");
ARCObject *obj = [[ARCObject alloc] init];
NSLog(@"%@(%@)生成了", NSStringFromClass(obj.class), obj);
}
if (index == 1) {
NSLog(@"-------------end");
}
}

下面是这段代码的打印内容:

2019-03-28 15:27:19.179194+0800 ProfessionalExample[85692:16955598] -------------begin
2019-03-28 15:27:19.179366+0800 ProfessionalExample[85692:16955598] ARCObject(<ARCObject: 0x600001ded3a0>)生成了
2019-03-28 15:27:19.179449+0800 ProfessionalExample[85692:16955598] ARCObject(<ARCObject: 0x600001ded3a0>)销毁了
2019-03-28 15:27:19.179521+0800 ProfessionalExample[85692:16955598] -------------end

ARC编码规则

1、不能使用retain/release/retainCount/autorelease
2、不能使用NSAllocateObject/NSDeallocateObject
3、须遵守内存管理的方法命名规则
4、不能显式调用dealloc方法
5、使用@autoreleasepool{}代替NSAutoreleasePool
6、不能使用NSZone
7、对象变量不能作为c语言结构体的成员
8、显式转换id和void *

内存管理的方法命名规则

以alloc/new/copy/mutableCopy开头的方法返回对象时,必须返回给调用方应当持有的对象。这在ARC有效时是一样的,不同的是以init开头的方法必须是实例方法且需要返回对象,该返回对象并不注册到autoreleasepool上。

对象变量不能作为c语言结构体的成员

要把对象类型变量加入到结构体中,需强制转为void *或者前面附加__unsafe_unretained修饰符。

显式转换id和void *

可以使用(__bridge)转换void *和OC对象,但是其安全性和赋值给__unsafe_unretained修饰符相近或者更低。如果管理时不注意赋值对象的所有者就会因为垂悬指针而奔溃或者内存泄漏。

NSObject *obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;
obj = (__bridge NSObject *)p;

__bridge_retained转换可使要赋值的变量持有所赋值的变量。__bridge_transfer则与之相反。

NSObject *obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)obj;
obj = (__bridge_transfer NSObject *)p;

NSObject对象与Core Fundation对象之间的相互转换,即免费桥(Toll-Freee-Bridge)转换。CFBridgingRetain函数(等价于__bridge_retained转换),CFBridgingRelease函数(等价于__bridge_transfer)。

NSObject *obj = [[NSObject alloc] init];
CFTypeRef ref = CFBridgingRetain(obj);
obj = CFBridgingRelease(ref);

属性

属性声明的属性与所有权修饰符对应关系


c数组

c静态数组,各修饰符的使用OC对象一样没有区别。

以__strong为例,其初始化为nil,超过作用域销毁:

{
ARCObject *array[2];
array[0] = [ARCObject allocObject];
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
array[1] = nil;
NSLog(@"array第二个元素:%@", array[1]);
}
NSLog(@"作用域块已经结束");

打印结果:

2019-03-28 19:19:26.697408+0800 ProfessionalExample[88859:17353905] ARCObject(<ARCObject: 0x6000005f8500>)生成了
2019-03-28 19:19:26.697661+0800 ProfessionalExample[88859:17353905] array第一个元素:<ARCObject: 0x6000005f8500>
2019-03-28 19:19:26.697761+0800 ProfessionalExample[88859:17353905] array第二个元素:(null)
2019-03-28 19:19:26.697845+0800 ProfessionalExample[88859:17353905] array第二个元素:(null)
2019-03-28 19:19:26.697930+0800 ProfessionalExample[88859:17353905] ARCObject(<ARCObject: 0x6000005f8500>)销毁了
2019-03-28 19:19:26.697995+0800 ProfessionalExample[88859:17353905] 作用域块已经结束

c动态数组,c语言中动态数组声明用指针即id *array(NSObject **array)。需要注意如下几点:

1、_strong/__weak修饰符的OC变量初始化为nil,并不代表其指针初始化为nil。所以分配内存后,需要对其初始化为nil,否则非常危险。calloc函数分配的就是nil初始化后的内存,malloc函数分配内存后必须使用memset将内存填充为0(nil)。
2、必须置空_strong修饰符的态数数组内的元素,使其强引用失效,元素才能释放。因为动态数组的生命周期有开发者管理,编译器不能确定销毁动态数组内元素的时机。

{
ARCObject *__strong *array;
array = (ARCObject *__strong *)calloc(2, sizeof(ARCObject *));
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
array[0] = [ARCObject allocObject];
array[1] = [ARCObject allocObject];
array[0] = nil;
NSLog(@"array第一个元素:%@", array[0]);
NSLog(@"array第二个元素:%@", array[1]);
free(array);
}
NSLog(@"作用域块已经结束");

打印结果:

2019-03-28 19:29:26.162245+0800 ProfessionalExample[89048:17394552] array第一个元素:(null)
2019-03-28 19:29:26.162586+0800 ProfessionalExample[89048:17394552] array第二个元素:(null)
2019-03-28 19:29:26.162763+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a32b40>)生成了
2019-03-28 19:29:26.162867+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a395c0>)生成了
2019-03-28 19:29:26.162945+0800 ProfessionalExample[89048:17394552] ARCObject(<ARCObject: 0x600001a32b40>)销毁了
2019-03-28 19:29:26.163011+0800 ProfessionalExample[89048:17394552] array第一个元素:(null)
2019-03-28 19:29:26.163083+0800 ProfessionalExample[89048:17394552] array第二个元素:<ARCObject: 0x600001a395c0>
2019-03-28 19:29:26.163160+0800 ProfessionalExample[89048:17394552] 作用域块已经结束

转自:https://www.jianshu.com/p/82849c350b0b

收起阅读 »

CYLTabBarController的使用

CYLTabBarController 是一个自定义的TabBarController, 集成非常简单https://github.com/ChenYilong/CYLTabBarController1.首先使用CocoaPods 进行集成: pod...
继续阅读 »

CYLTabBarController 是一个自定义的TabBarController, 集成非常简单

https://github.com/ChenYilong/CYLTabBarController

1.首先使用CocoaPods 进行集成: 

pod 'CYLTabBarController'
在终端上执行: 
pod install --verbose --no-repo-update

2. 创建TabBar对应的视图控制器


3.创建CYLTabBarControllerConfig

#import <Foundation/Foundation.h>  

#import "CYLTabBarController.h"
@interface CYLTabBarControllerConfig : NSObject

@property (nonatomic, retain) CYLTabBarController * tabBarController;

@end
#import "CYLTabBarControllerConfig.h"  

#import "FirstViewController.h"
#import "SecondViewController.h"
#import "ThirdViewController.h"
#import "FourthViewController.h"


@implementation CYLTabBarControllerConfig

- (CYLTabBarController *)tabBarController {
if (_tabBarController == nil) {
FirstViewController * firstViewController = [[FirstViewController alloc] init];
UIViewController * firstNavigationController = [[UINavigationController alloc] initWithRootViewController:firstViewController];

SecondViewController * secondViewController = [[SecondViewController alloc] init];
UIViewController * secondNavigationController = [[UINavigationController alloc] initWithRootViewController:secondViewController];

ThirdViewController * thirdViewController = [[ThirdViewController alloc] init];
UIViewController * thirdNavigationController = [[UINavigationController alloc] initWithRootViewController:thirdViewController];

FourthViewController * fourthViewController = [[FourthViewController alloc] init];
UIViewController * fourthNavigationController = [[UINavigationController alloc] initWithRootViewController:fourthViewController];


NSArray * tabBarItemsAttributes = [self tabBarItemsAttributes];
NSArray * viewControllers = @[firstNavigationController, secondNavigationController, thirdNavigationController, fourthNavigationController];

CYLTabBarController * tabBarController = [[CYLTabBarController alloc] init];

tabBarController.tabBarItemsAttributes = tabBarItemsAttributes;
tabBarController.viewControllers = viewControllers;

_tabBarController = tabBarController;

}

return _tabBarController;
}


- (NSArray *)tabBarItemsAttributes {
NSDictionary * tabBarItem1Attribute = @{
CYLTabBarItemTitle : @"首页",
CYLTabBarItemImage : @"home_normal",
CYLTabBarItemSelectedImage : @"home_highlight"
};
NSDictionary * tabBarItem2Attribute = @{
CYLTabBarItemTitle : @"同城",
CYLTabBarItemImage : @"mycity_normal",
CYLTabBarItemSelectedImage : @"mycity_highlight"
};
NSDictionary * tabBarItem3Attribute = @{
CYLTabBarItemTitle : @"消息",
CYLTabBarItemImage : @"message_normal",
CYLTabBarItemSelectedImage : @"message_highlight"
};
NSDictionary * tabBarItem4Attribute = @{
CYLTabBarItemTitle : @"我的",
CYLTabBarItemImage : @"account_normal",
CYLTabBarItemSelectedImage : @"account_highlight"
};
NSArray * tarBarItemsAttrbutes = @[tabBarItem1Attribute, tabBarItem2Attribute, tabBarItem3Attribute, tabBarItem4Attribute];

return tarBarItemsAttrbutes;
}


/**
* 更多TabBar自定义设置:比如:tabBarItem 的选中和不选中文字和背景图片属性、tabbar 背景图片属性
*/
+ (void)customizeTabBarAppearance {

//去除 TabBar 自带的顶部阴影
[[UITabBar appearance] setShadowImage:[[UIImage alloc] init]];

// set the text color for unselected state
// 普通状态下的文字属性
NSMutableDictionary *normalAttrs = [NSMutableDictionary dictionary];
normalAttrs[NSForegroundColorAttributeName] = [UIColor blackColor];

// set the text color for selected state
// 选中状态下的文字属性
NSMutableDictionary *selectedAttrs = [NSMutableDictionary dictionary];
selectedAttrs[NSForegroundColorAttributeName] = [UIColor blackColor];

// set the text Attributes
// 设置文字属性
UITabBarItem *tabBar = [UITabBarItem appearance];
[tabBar setTitleTextAttributes:normalAttrs forState:UIControlStateNormal];
[tabBar setTitleTextAttributes:selectedAttrs forState:UIControlStateSelected];

// Set the dark color to selected tab (the dimmed background)
// TabBarItem选中后的背景颜色
[[UITabBar appearance] setSelectionIndicatorImage:[self imageFromColor:[UIColor colorWithRed:26 / 255.0 green:163 / 255.0 blue:133 / 255.0 alpha:1] forSize:CGSizeMake([UIScreen mainScreen].bounds.size.width / 5.0f, 49) withCornerRadius:0]];

// set the bar background color
// 设置背景图片
// UITabBar *tabBarAppearance = [UITabBar appearance];
// [tabBarAppearance setBackgroundImage:[UIImage imageNamed:@"tabbar_background_ios7"]];
}

+ (UIImage *)imageFromColor:(UIColor *)color forSize:(CGSize)size withCornerRadius:(CGFloat)radius {
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIGraphicsBeginImageContext(rect.size);

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// Begin a new image that will be the new image with the rounded corners
// (here with the size of an UIImageView)
UIGraphicsBeginImageContext(size);

// Add a clip before drawing anything, in the shape of an rounded rect
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius] addClip];
// Draw your image
[image drawInRect:rect];

// Get the image, here setting the UIImageView image
image = UIGraphicsGetImageFromCurrentImageContext();

// Lets forget about that we were drawing
UIGraphicsEndImageContext();
return image;
}

4. AppDelegate 设置根视图控制器

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {  
// TabBar
CYLTabBarControllerConfig * TabBarControllerConfig = [[CYLTabBarControllerConfig alloc] init];
self.window.rootViewController = TabBarControllerConfig.tabBarController;
[self customizeInterface];

return YES;
}
- (void)customizeInterface {
[self setUpNavigationBarAppearance];
}

/**
* 设置navigationBar样式
*/
- (void)setUpNavigationBarAppearance {
UINavigationBar *navigationBarAppearance = [UINavigationBar appearance];

UIImage *backgroundImage = nil;
NSDictionary *textAttributes = nil;
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
backgroundImage = [UIImage imageNamed:@"navigationbar_background_tall"];

textAttributes = @{
NSFontAttributeName: [UIFont boldSystemFontOfSize:18],
NSForegroundColorAttributeName: [UIColor blackColor],
};
} else {
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0
backgroundImage = [UIImage imageNamed:@"navigationbar_background"];

textAttributes = @{
UITextAttributeFont: [UIFont boldSystemFontOfSize:18],
UITextAttributeTextColor: [UIColor blackColor],
UITextAttributeTextShadowColor: [UIColor clearColor],
UITextAttributeTextShadowOffset: [NSValue valueWithUIOffset:UIOffsetZero],
};
#endif
}

[navigationBarAppearance setBackgroundImage:backgroundImage
forBarMetrics:UIBarMetricsDefault];
[navigationBarAppearance setTitleTextAttributes:textAttributes];
}

运行即可实现效果,如果想实现凸起的加号效果需要 CYLPlusButtonSubclass

#import "CYLPlusButton.h"  

@interface CYLPlusButtonSubclass : CYLPlusButton <CYLPlusButtonSubclassing>

@end
#import "CYLPlusButtonSubclass.h"  

@interface CYLPlusButtonSubclass ()<UIActionSheetDelegate> {
CGFloat _buttonImageHeight;
}

@end

@implementation CYLPlusButtonSubclass

#pragma mark -
#pragma mark - Life Cycle

+ (void)load {
[super registerSubclass];
}

- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.titleLabel.textAlignment = NSTextAlignmentCenter;
self.adjustsImageWhenHighlighted = NO;
}
return self;
}

//上下结构的 button
- (void)layoutSubviews {
[super layoutSubviews];

// 控件大小,间距大小
CGFloat const imageViewEdge = self.bounds.size.width * 0.6;
CGFloat const centerOfView = self.bounds.size.width * 0.5;
CGFloat const labelLineHeight = self.titleLabel.font.lineHeight;
CGFloat const verticalMarginT = self.bounds.size.height - labelLineHeight - imageViewEdge;
CGFloat const verticalMargin = verticalMarginT / 2;

// imageView 和 titleLabel 中心的 Y 值
CGFloat const centerOfImageView = verticalMargin + imageViewEdge * 0.5;
CGFloat const centerOfTitleLabel = imageViewEdge + verticalMargin * 2 + labelLineHeight * 0.5 + 5;

//imageView position 位置
self.imageView.bounds = CGRectMake(0, 0, imageViewEdge, imageViewEdge);
self.imageView.center = CGPointMake(centerOfView, centerOfImageView);

//title position 位置
self.titleLabel.bounds = CGRectMake(0, 0, self.bounds.size.width, labelLineHeight);
self.titleLabel.center = CGPointMake(centerOfView, centerOfTitleLabel);
}

#pragma mark -
#pragma mark - Public Methods

/*
*
Create a custom UIButton with title and add it to the center of our tab bar
*
*/
+ (instancetype)plusButton {

CYLPlusButtonSubclass *button = [[CYLPlusButtonSubclass alloc] init];

[button setImage:[UIImage imageNamed:@"post_normal"] forState:UIControlStateNormal];
[button setTitle:@"发布" forState:UIControlStateNormal];

[button setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
button.titleLabel.font = [UIFont systemFontOfSize:9.5];
[button sizeToFit];

[button addTarget:button action:@selector(clickPublish) forControlEvents:UIControlEventTouchUpInside];
return button;
}
/*
*
Create a custom UIButton without title and add it to the center of our tab bar
*
*/
//+ (instancetype)plusButton
//{
//
// UIImage *buttonImage = [UIImage imageNamed:@"hood.png"];
// UIImage *highlightImage = [UIImage imageNamed:@"hood-selected.png"];
//
// CYLPlusButtonSubclass* button = [CYLPlusButtonSubclass buttonWithType:UIButtonTypeCustom];
//
// button.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
// button.frame = CGRectMake(0.0, 0.0, buttonImage.size.width, buttonImage.size.height);
// [button setBackgroundImage:buttonImage forState:UIControlStateNormal];
// [button setBackgroundImage:highlightImage forState:UIControlStateHighlighted];
// [button addTarget:button action:@selector(clickPublish) forControlEvents:UIControlEventTouchUpInside];
//
// return button;
//}

#pragma mark -
#pragma mark - Event Response

- (void)clickPublish {
UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
UIViewController *viewController = tabBarController.selectedViewController;

UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil
delegate:self
cancelButtonTitle:@"取消"
destructiveButtonTitle:nil
otherButtonTitles:@"拍照", @"从相册选取", @"淘宝一键转卖", nil nil];
[actionSheet showInView:viewController.view];
}

#pragma mark - UIActionSheetDelegate
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
NSLog(@"index: %ld", buttonIndex);
}


#pragma mark - CYLPlusButtonSubclassing
//+ (NSUInteger)indexOfPlusButtonInTabBar {
// return 3;
//}

+ (CGFloat)multiplerInCenterY {
return 0.3;
}

@end

Demo 下载地址:

http://download.csdn.net/detail/vbirdbest/9431253

实现效果如图:


转自:https://blog.csdn.net/man_liang/article/details/56671353

收起阅读 »

iOS 开源项目-FXBlurView

PurposeFXBlurView is a UIView subclass that replicates the iOS 7 realtime background blur effect, but works on iOS 5 and above. It...
继续阅读 »

Purpose
FXBlurView is a UIView subclass that replicates the iOS 7 realtime background blur effect, but works on iOS 5 and above. It is designed to be as fast and as simple to use as possible. FXBlurView offers two modes of operation: static, where the view is rendered only once when it is added to a superview (though it can be updated by calling setNeedsDisplay or updateAsynchronously:completion:) or dynamic, where it will automatically redraw itself on a background thread as often as possible.
FXBlurView 是一个 UIView 的子类,复制了 iOS7 的实时背景模糊效果,但是可以运行在 iOS5 以上的版本。它的设计简单易用。FXBlurView 提供两种模式:静态模糊和动态模糊。

FXBlurView methods

+(void)setBlurEnabled:(BOOL)blurEnabled;

This method can be used to globally enable/disable the blur effect on all FXBlurView instances. This is useful for testing, or if you wish to disable blurring on iPhone 4 and below (for consistency with iOS7 blur view behavior). By default blurring is enabled.

这个方法用来设置全局 使能/不使能 模糊效果在 FXBlurView 的实例上。默认情况下模糊效果是启动。

+(void)setUpdatesEnabled;
+(void)setUpdatesDisabled;

These methods can be used to enable and disable updates for all dynamic FXBlurView instances with a single command. Useful for disabling updates immediately before performing an animation so that the FXBlurView updates don’t cause the animation to stutter. Calls can be nested, but ensure that the enabled/disabled calls are balanced, or the updates will be left permanently enabled or disabled.

这两个方法用来设置所以的动态 FXBlurView 是否进行更新,通过一条指令执行。在展示动画之前立即对没有用的更新进行更新,使 FXBlurView 更新不会产生动画断断续续的效果。调用可以嵌套,但确保 启用/禁用 调用平衡,否则更新会留下永久 启用/禁用。

-(void)updateAsynchronously:(BOOL)async completion:(void (^)())completion;

This method can be used to trigger an update of the blur effect (useful when dynamic = NO). The async argument controls whether the blur will be redrawn on the main thread or in the background. The completion argument is an optional callback block that will be called when the blur is completed.

这个方法可以用于触发更新模糊效果。(在属性 "dynamic = NO"情况下有用)。异步参数控制是否模糊将要在主线程上或在后台进行重绘。完成参数是一个可供选择的回调块,将在模糊完成的时候进行调用。

-(void)setNeedsDisplay;

Inherited from UIView, this method can be used to trigger a (synchronous) update of the view. Calling this method is more-or-less equivalent to calling [view updateAsynchronously:NO completion:NULL].

继承 UIView,这个方法用于触发一个(同步)更新视图。调用这个方法或多或少等同于调用[view updateAsynchronously:NO completion:NULL].

FXBlurView properties

@property (nonatomic, getter = isBlurEnabled) BOOL blurEnabled;

This property toggles blurring on and off for an individual FXBlurView instance. Blurring is enabled by default. Note that if you disable blurring using the +setBlurEnabled method then that will override this setting.

这个属性用来切换 FXBlurView 单独实例模糊启动还是关闭。默认情况下模糊是使能的。请注意,如果您禁用模糊方法 setBlurEnabled,那么它将覆盖此设置。

@property (nonatomic, getter = isDynamic) BOOL dynamic;

This property controls whether the FXBlurView updates dynamically, or only once when the view is added to its superview. Defaults to YES. Note that if dynamic is set to NO, you can still force the view to update by calling setNeedsDisplay or updateAsynchronously:completion:. Dynamic blurring is extremely cpu-intensive, so you should always disable dynamic views immediately prior to performing an animation to avoid stuttering. However, if you have multiple FXBlurViews on screen then it is simpler to disable updates using the setUpdatesDisabledmethod rather than setting the dynamic property to NO.

这个属性控制 FXBlurView 是否动态更新,还是只有在视图加入到它的父视图中。默认情况下是 YES ,请注意,如果你设置 dynamic 属性为 NO,你可以强制视图更新通过调用 setNeedsDisplay或者updateAsynchronously:completion:。动态模糊非常消耗 CPU 内存,所以您应该禁用立即执行的动态视图避免出现断断续续的动画。然而,如果您在屏幕上有多个 FXBlurViews ,通过设置方法 setUpdatedsDisabled来禁止更新比用设置动态属性为 NO 更为简单。

@property (nonatomic, assign) NSUInteger iterations;

The number of blur iterations. More iterations improves the quality but reduces the performance. Defaults to 2 iterations.

模糊迭代的次数。更多的迭代提高质量,但会降低性能。默认值为 2 的迭代。

@property (nonatomic, assign) NSTimeInterval updateInterval;

This controls the interval (in seconds) between successive updates when the FXBlurView is operating in dynamic mode. This defaults to zero, which means that the FXBlurView will update as fast as possible. This yields the best frame rate, but is also extremely CPU intensive and may cause the rest of your app’s performance to degrade, especially on older devices. To alleviate this, try increasing the updateInterval value.

此属性控制 FXBlurView 在动态模式下,距离成功更新的时间间隔(以秒计)。默认值为 0 ,这表示 FXBlurView 更新越快越好。 这将生成最佳的帧速率,但是也是非常消耗 CPU内存,导致你的其他 apps 无法无法加载,特别是旧设备。为了减缓这些情况,尝试增加updateInterval 的值。

@property (nonatomic, assign) CGFloat blurRadius;

This property controls the radius of the blur effect (in points). Defaults to a 40 point radius, which is similar to the iOS 7 blur effect.

此属性控制模糊效果的半径 (以像素点计)。默认是半径为40个像素点,这个值与 iOS7 模糊效果相似。

@property (nonatomic, strong) UIColor *tintColor;

This in an optional tint color to be applied to the FXBlurView. The RGB components of the color will be blended with the blurred image, resulting in a gentle tint. To vary the intensity of the tint effect, use brighter or darker colors. The alpha component of the tintColor is ignored. If you do not wish to apply a tint, set this value to nil or [UIColor clearColor]. Note that if you are using Xcode 5 or above, FXBlurViews created in Interface Builder will have a blue tint by default.

这是应用在 FXBlurView 可选的色调选择。颜色的 RGB 分量将会掺入到模糊图像上,导致产生一个柔和的色调。为了验证色调效果,若要改变色调效果的强度,使用更亮或更暗的颜色。颜色的透明参数是被忽略的。如果您不想应用色调,设置[UIColor clearColor]。请注意,如果您现在使用 Xcode5及以上,FXBlurViews 产生一个接口生成器将默认有一个一个蓝色的色调。

@property (nonatomic, weak) UIView *underlyingView;

This property specifies the view that the FXBlurView will sample to create the blur effect. If set to nil (the default), this will be the superview of the blur view itself, but you can override this if you need to.

此属性表明该视图是 FXBlurView 产生模糊效果的子视图。如果设置为 nil(默认),则该视图的父视图是模糊视图本身,但是如果您有需要您可以进行覆盖它。

总结

今天通过学习 FXBlurView ,提高对英语文档的理解和翻译能力,增加了自己的学习兴趣,也懂得了如何去使用 FXBlurView 的模糊效果特效。在实际中提升自己的能力,年轻,就是资本!Oh Yeah!

参考

https://github.com/cnbin/FXBlurView

转自:https://cnbin.github.io/blog/2015/05/25/ioskai-yuan-xiang-mu-fxblurview/

收起阅读 »

你还没用Logger?用了他我才知道屌

Logger简单,漂亮,强大的android日志 配置下载 implementation 'com.orhanobut:logger:2.2.0' 初始化 Logger.addLogAdapter(new AndroidLogAdapter()); 使用 ...
继续阅读 »

Logger

简单,漂亮,强大的android日志


配置

下载


implementation 'com.orhanobut:logger:2.2.0'

初始化


Logger.addLogAdapter(new AndroidLogAdapter());

使用


Logger.d("hello");

输出


属性

Logger.d("debug");
Logger.e("error");
Logger.w("warning");
Logger.v("verbose");
Logger.i("information");
Logger.wtf("What a Terrible Failure");

支持字符串格式参数


Logger.d("hello %s", "world");

支持集合(仅适用于调试日志)


Logger.d(MAP);
Logger.d(SET);
Logger.d(LIST);
Logger.d(ARRAY);

Json和Xml支持(输出将处于调试级别)


Logger.json(JSON_CONTENT);
Logger.xml(XML_CONTENT);

高级用法

FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
.showThreadInfo(false) // (可选)是否显示线程信息。默认为 true
.methodCount(0) // (可选)要显示的方法行数。默认为 2
.methodOffset(7) // (可选)隐藏内部方法调用直到偏移量。默认值5
.logStrategy(customLog) // (可选)将日志策略更改为打印输出。默认LogCat
.tag("My custom tag") // (可选)每个日志的全局标记。默认PRETTY_LOGGER
.build();

Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));

日志开启

日志适配器通过检查此功能来检查日志是否应打印。


如果要禁用/隐藏输出日志,请重写isLoggable'方法。true会打印日志消息,false` 会忽略它。


Logger.addLogAdapter(new AndroidLogAdapter() {
@Override public boolean isLoggable(int priority, String tag) {
return BuildConfig.DEBUG;
}
});

将日志保存到文件

//TODO: 稍后将添加更多信息


Logger.addLogAdapter(new DiskLogAdapter());

将自定义标记添加到Csv格式策略


FormatStrategy formatStrategy = CsvFormatStrategy.newBuilder()
.tag("custom")
.build();

Logger.addLogAdapter(new DiskLogAdapter(formatStrategy));

工作原理


更多


  • 使用过滤器以获得更好的结果。或者你的自定义标签


  • 确保已禁用“环绕”选项


  • 也可以通过更改设置来简化输出。





  • Timber 集成
    // 将methodOffset设置为5以隐藏内部方法调用
    Timber.plant(new Timber.DebugTree() {
    @Override protected void log(int priority, String tag, String message, Throwable t) {
    Logger.log(priority, tag, message, t);
    }
    });


github地址:https://github.com/orhanobut/logger
下载地址:
master.zip


收起阅读 »

java设计模式:命令模式

前言在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。 在现...
继续阅读 »

前言

在软件开发系统中,“方法的请求者”与“方法的实现者”之间经常存在紧密的耦合关系,这不利于软件功能的扩展与维护。例如,想对方法进行“撤销、重做、记录”等处理都很不方便,因此“如何将方法的请求者与实现者解耦?”变得很重要,命令模式就能很好地解决这个问题。


在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。


再比如,我们去餐厅吃饭,菜单不是等到客人来了之后才定制的,而是已经预先配置好的。这样,客人来了就只需要点菜,而不是任由客人临时定制。餐厅提供的菜单就相当于把请求和处理进行了解耦,这就是命令模式的体现。


定义与特点

命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。


优点

通过引入中间件(抽象接口)降低系统的耦合度。
扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。

缺点

可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。
命令模式的结构与实现
可以将系统中的相关操作抽象成命令,使调用者与实现者相关分离,其结构如下。

结构

抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
在这里插入图片描述

实现

命令模式的代码如下:


package command;
public class CommandPattern {
public static void main(String[] args) {
Command cmd = new ConcreteCommand();
Invoker ir = new Invoker(cmd);
System.out.println("客户访问调用者的call()方法...");
ir.call();
}
}

//调用者


class Invoker {
private Command command;
public Invoker(Command command) {
this.command = command;
}
public void setCommand(Command command) {
this.command = command;
}
public void call() {
System.out.println("调用者执行命令command...");
command.execute();
}
}

//抽象命令


interface Command {
public abstract void execute();
}

//具体命令



class ConcreteCommand implements Command {
private Receiver receiver;
ConcreteCommand() {
receiver = new Receiver();
}
public void execute() {
receiver.action();
}
}

//接收者


class Receiver {
public void action() {
System.out.println("接收者的action()方法被调用...");
}
}

程序的运行结果如下:



客户访问调用者的call()方法...
调用者执行命令command...
接收者的action()方法被调用...

实例


假如我们开发一个播放器,播放器播放功能、拖动进度条功能、停止播放功能、暂停功能,我们在操作播发器的时候并不知道之间调用播放器
哪个功能,而是通过一个控制传达去传递指令给播放器内核,具体传达什么指令,会被封装成一个个按钮。那么每个按钮就相当于一条命令的封装。
用控制条实现了用户发送指令与播放器内核接收指令的解耦。下面来看代码,首先创建播放器内核类:



public class GPlayer {
public void play() {
System.out.println("正常播放");
}

public void speed() {
System.out.println("拖动进度条");
}

public void stop() {
System.out.println("停止播放");
}

public void pause() {
System.out.println("暂停播放");
}
}

创建命令接口:



public interface IAction {
void execute();
}

创建播放指令类:


public class PlayAction implements IAction {
private GPlayer gplayer;

public PlayAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.play();
}
}

创建暂停指令类:


public class PauseAction implements IAction {
private GPlayer gplayer;

public PauseAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.pause();
}
}

创建拖动进度条类:


public class SpeedAction implements IAction {
private GPlayer gplayer;

public SpeedAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.speed();
}
}

创建停止播放指令:


public class StopAction implements IAction {
private GPlayer gplayer;

public StopAction(GPlayer gplayer) {
this.gplayer = gplayer;
}

public void execute() {
gplayer.stop();
}
}

创建控制条controller类:



public class Controller {
private List<IAction> actions = new ArrayList<IAction>();

public void addAction(IAction action) {
actions.add(action);
}

public void execute(IAction action) {
action.execute();
}

public void executes() {
for (IAction action : actions) {
action.execute();
}
actions.clear();
}
}

从上面代码来看,控制条可以执行单条命令,也可以批量执行多条命令。下面看客户端的测试代码:



public class Test {
public static void main(String[] args) {

GPlayer player = new GPlayer();
Controller controller = new Controller();
controller.execute(new PlayAction(player));

controller.addAction(new PauseAction(player));
controller.addAction(new PlayAction(player));
controller.addAction(new StopAction(player));
controller.addAction(new SpeedAction(player));
controller.executes();
}
}

由于控制条已经与播放器内核解耦了,以后如果想扩展新命令,只需要增加命令即可,控制条的结构无须改动。


java源码中的命令模式


首先来看 JDK 中的 Runnable 接口,Runnable 相当于命令模式中的抽象命令角色。Runnable 中的 run() 方法就当于 execute() 方法。



public interface Runnable {
public abstract void run();
}
public class T implements Runnable {
@Override
public void run() {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " : " + lazySingleton);
}
}

public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new T());
Thread t2 = new Thread(new T());
t1.start();
t2.start();
System.out.println("Program End");
}
}

只要是实现了 Runnable 接口的类都被认为是一个线程,相当于命令模式中的具体命令角色。


实际上调用线程的 start() 方法之后,就有资格去抢 CPU 资源,而不需要编写获得 CPU 资源的逻辑。而线程抢到 CPU 资源后,就会执行 run() 方法中的内容,用 Runnable 接口把用户请求和 CPU 执行进行解耦。


收起阅读 »

Java设计模式:迭代器模式

前言在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。 既然将遍历方法封装在聚合...
继续阅读 »

前言

在现实生活以及程序设计中,经常要访问一个聚合对象中的各个元素,如“数据结构”中的链表遍历,通常的做法是将链表的创建和遍历都放在同一个类中,但这种方式不利于程序的扩展,如果要更换遍历方法就必须修改程序源代码,这违背了 “开闭原则”。


既然将遍历方法封装在聚合类中不可取,那么聚合类中不提供遍历方法,将遍历方法由用户自己实现是否可行呢?答案是同样不可取,因为这种方式会存在两个缺点:



  1. 暴露了聚合类的内部表示,使其数据不安全;
  2. 增加了客户的负担。

“迭代器模式”能较好地克服以上缺点,它在客户访问类与聚合类之间插入一个迭代器,这分离了聚合对象与其遍历行为,对客户也隐藏了其内部细节,且满足“单一职责原则”和“开闭原则”,如 Java 中的 Collection、List、Set、Map 等都包含了迭代器。


迭代器模式在生活中应用的比较广泛,比如:物流系统中的传送带,不管传送的是什么物品,都会被打包成一个个箱子,并且有一个统一的二维码。这样我们不需要关心箱子里是什么,在分发时只需要一个个检查发送的目的地即可。再比如,我们平时乘坐交通工具,都是统一刷卡或者刷脸进站,而不需要关心是男性还是女性、是残疾人还是正常人等信息。


定义与特点

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。迭代器模式是一种对象行为型模式


优点


  • 访问一个聚合对象的内容而无须暴露它的内部表示。
  • 遍历任务交由迭代器完成,这简化了聚合类。
  • 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  • 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  • 封装性良好,为遍历不同的聚合结构提供一个统一的接口。

缺点

增加了类的个数,这在一定程度上增加了系统的复杂性。


在日常开发中,我们几乎不会自己写迭代器。除非需要定制一个自己实现的数据结构对应的迭代器,否则,开源框架提供的 API 完全够用。


结构

迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。现在我们来分析其基本结构与实现方法。



  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合对象以及创建迭代器对象的接口。
  • 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、first()、next() 等方法。
  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

在这里插入图片描述

模式的实现

package net.biancheng.c.iterator;
import java.util.*;
public class IteratorPattern {
public static void main(String[] args) {
Aggregate ag = new ConcreteAggregate();
ag.add("中山大学");
ag.add("华南理工");
ag.add("韶关学院");
System.out.print("聚合的内容有:");
Iterator it = ag.getIterator();
while (it.hasNext()) {
Object ob = it.next();
System.out.print(ob.toString() + "\t");
}
Object ob = it.first();
System.out.println("\nFirst:" + ob.toString());
}
}
//抽象聚合
interface Aggregate {
public void add(Object obj);
public void remove(Object obj);
public Iterator getIterator();
}
//具体聚合
class ConcreteAggregate implements Aggregate {
private List<Object> list = new ArrayList<Object>();
public void add(Object obj) {
list.add(obj);
}
public void remove(Object obj) {
list.remove(obj);
}
public Iterator getIterator() {
return (new ConcreteIterator(list));
}
}
//抽象迭代器
interface Iterator {
Object first();
Object next();
boolean hasNext();
}
//具体迭代器
class ConcreteIterator implements Iterator {
private List<Object> list = null;
private int index = -1;
public ConcreteIterator(List<Object> list) {
this.list = list;
}
public boolean hasNext() {
if (index < list.size() - 1) {
return true;
} else {
return false;
}
}
public Object first() {
index = 0;
Object obj = list.get(index);
;
return obj;
}
public Object next() {
Object obj = null;
if (this.hasNext()) {
obj = list.get(++index);
}
return obj;
}
}

运行结果


聚合的内容有:中山大学    华南理工    韶关学院   
First:中山大学

java源码分析

Iterator


public interface Iterator<E> {

boolean hasNext();

E next();

default void remove() {
throw new UnsupportedOperationException("remove");
}

//剩余元素迭代
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

上面是迭代器Iterator接口的代码,定义了一些需要子类实现的方法和默认的方法。在这里说一下上面两个default方法都是JDK1.8之后才有的接口新特性,在JDK1.8之前接口中不能有方法实体。


ArrayList


public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
}

上面是简化的ArrayList类,因为具体实现迭代器Itr的类在ArrayList中作为内部类存在,这个内部类将接口中的方法做了具体实现,并且是只对ArrayList这个类进行实现的。


public interface List<E> extends Collection<E> {
Iterator<E> iterator();
}

上面是简化的List接口,充当的是聚合接口,可以看见内部创建了相应迭代器接口的方法。


public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public Iterator<E> iterator() {
return new Itr();
}
}

上面是简化的ArrayList类,充当的是具体聚合类角色,在这里是直接返回了一个具体实现迭代器的类。


public class Test1 {

public static void main(String[] args) {
List<Integer> a=new ArrayList<>();
a.add(1);
a.add(2);
a.add(3);
Iterator itr=a.iterator();
while(itr.hasNext()){
System.out.println(itr.next());
}
}
}

收起阅读 »

java设计模式:中介者模式

前言在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵...
继续阅读 »

前言

在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵一发而动全身”,非常复杂。


如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。这样的例子还有很多,例如,你刚刚参加工作想租房,可以找“房屋中介”;或者,自己刚刚到一个陌生城市找工作,可以找“人才交流中心”帮忙。


在软件的开发过程中,这样的例子也很多,例如,在 MVC 框架中,控制器(C)就是模型(M)和视图(V)的中介者;还有大家常用的 QQ 聊天程序的“中介者”是 QQ 服务器。所有这些,都可以采用“中介者模式”来实现,它将大大降低对象之间的耦合性,提高系统的灵活性。
模式的定义与特点

定义

定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。


优点

类之间各司其职,符合迪米特法则。
降低了对象之间的耦合性,使得对象易于独立地被复用。
将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。

缺点

中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
模式的结构与实现
中介者模式实现的关键是找出“中介者”,下面对它的结构和实现进行分析。

结构

抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
在这里插入图片描述

实现

中介者模式的实现代码如下:


package net.biancheng.c.mediator;
import java.util.*;
public class MediatorPattern {
public static void main(String[] args) {
Mediator md = new ConcreteMediator();
Colleague c1, c2;
c1 = new ConcreteColleague1();
c2 = new ConcreteColleague2();
md.register(c1);
md.register(c2);
c1.send();
System.out.println("-------------");
c2.send();
}
}
//抽象中介者
abstract class Mediator {
public abstract void register(Colleague colleague);
public abstract void relay(Colleague cl); //转发
}
//具体中介者
class ConcreteMediator extends Mediator {
private List<Colleague> colleagues = new ArrayList<Colleague>();
public void register(Colleague colleague) {
if (!colleagues.contains(colleague)) {
colleagues.add(colleague);
colleague.setMedium(this);
}
}
public void relay(Colleague cl) {
for (Colleague ob : colleagues) {
if (!ob.equals(cl)) {
((Colleague) ob).receive();
}
}
}
}
//抽象同事类
abstract class Colleague {
protected Mediator mediator;
public void setMedium(Mediator mediator) {
this.mediator = mediator;
}
public abstract void receive();
public abstract void send();
}
//具体同事类
class ConcreteColleague1 extends Colleague {
public void receive() {
System.out.println("具体同事类1收到请求。");
}
public void send() {
System.out.println("具体同事类1发出请求。");
mediator.relay(this); //请中介者转发
}
}
//具体同事类
class ConcreteColleague2 extends Colleague {
public void receive() {
System.out.println("具体同事类2收到请求。");
}
public void send() {
System.out.println("具体同事类2发出请求。");
mediator.relay(this); //请中介者转发
}
}

程序的运行结果如下:


具体同事类1发出请求。
具体同事类2收到请求。
-------------
具体同事类2发出请求。
具体同事类1收到请求。

应用场景

前面分析了中介者模式的结构与特点,下面分析其以下应用场景。
当对象之间存在复杂的网状结构关系而导致依赖关系混乱且难以复用时。
当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

java源码中的体现

在看其他人写的关于Timer 的中介者设计模式,我觉得写的都不是很清楚。我大概用源码来解释一下,顺便再分析一下Timer的所有关联类的源码:


private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

synchronized(queue) {
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}

说明:所有的schedule方法都调用了sched ,那这个类的主要作用是啥呢?



将timertask加入到队列里,然后从队列里取出min任务(二叉堆的数据结构,下面会说明),判断如果min任务等于当前任务的话让队列wait的状态变为运行状态,如果不等于的话,那么线程的mainloop方法肯定是一直再运行状态的,其他任务就可以依次执行



看如下的源码


private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die

// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}

Timer相当于中介者来执行队列里的任务,用户只管将任务抛给timer就可以了。


如下详细timer源码分析

在Java中,很常见的一个定时器的实现就是 Timer 类,用来实现定时、延迟执行、周期性执行任务的功能。


Timer 是定义在 java.util 中的一个工具类,提供简单的实现定时器的功能。和它配合使用的,是 TimerTask 类,这是对一个可以被调度的任务的封装。使用起来非常简单,如下示例:


// 定义一个可调度的任务,继承自 TimerTask
class FooTimerTask extends TimerTask {

@Override
public void run() {
// do your things
}
}

// 初始化Timer 定时器对象
Timer timer = new Timer("barTimer");

// 初始化需要被调度的任务对象
TimerTask task = new FooTimerTask();

// 调度任务。延迟1000毫秒后执行,之后每2000毫秒定时执行一次
timer.schedule(task, 1000, 2000);

以上,就是一个简单的使用Timer 的示例,下文将会分析Timer的源码实现。


概述

在Timer 机制中,涉及到的关键类如下:



  • Timer: 主要的调用的,提供对外的API;
  • TimerTask: 是一个抽象类,定义一个任务,继承自Runnable
  • TimerThread: 继承自 Thread,是一个自定义的线程类;
  • TaskQueue: 一个任务队列,包含有当前Timer的所有任务,内部使用二叉堆来实现。

以上几个关键类的引用关系如下:
在这里插入图片描述
简要描述的话,是:

1个 TimerThread —-> 实现1个 线程


1个 Timer对象 —-> 持有1个 TimerThread 对象


1个 Timer对象 —-> 持有1个 TimerQueue 对象


1个 TimerQueue 对象 —-> 持有 n个 TimerTask 对象


源码分析

Timer类的源码分析
源码分析的话,我们最好是按照Timer 的使用流程来分析。 首先,是Timer 的创建:

// Timer有四个构造方法,但是本质上其实是做的相同的事情,即
// 1. 使用name 和 isDeamon 两个参数给 thread 对象做了参数设置;
// 2. 调用 thread 的 start() 方法启动线程
public Timer() {
this("" + serialNumber());
}

public Timer(boolean isDaemon) {
this("" + serialNumber(), isDaemon);
}


public Timer(String name) {
thread.setName(name);
thread.start();
}

public Timer(String name, boolean isDaemon) {
thread.setName(name);
thread.setDaemon(isDaemon);
thread.start();
}

那么,或许大家会有一个疑问,thread 成员的初始化呢?这个时候,在代码里面找,就能发现:



// 这两个成员都是直接在声明的时候进行了初始化。
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);

可以看到 thread 和 queue两个成员都是在声明的时候直接初始化的,并且有意思的是,两个成员都是 final 类型的,这也就意味着这两个成员一旦创建就不会再改了,等于说把 thread、queue 和 Timer 对象这三者的生命周期强行绑定在一起了,大家一起创建,并且一经创建将会无法改变。


然后,创建了Timer 后,与之相关的队列也已经创建成功,而且相关联的线程也启动了,就可以进行任务的调度了,我们看下它的任务调度方法:



// Timer 包含有一组重载方法,参数为以下几个:
// 1. TimerTask task:需要被调度的任务
// 2. long delay: 指定延迟的时间;
// 3. long period: 指定调度的执行周期;
schedule(TimerTask task, long delay, long period)

多个重载的调度方法在经过一些一些列的状态判断、参数设置、以及把delay时间转换成实际的执行时间等之后, 最终完成该功能的是 sched 方法,详情见注释部分:


这里涉及到一个需要留意的点,是在调用schedule 方法的时候,会根据TimerTask 的类型来进行不同的计算,进而给TimerTask设置不同的 period 参数,TimerTask 的类型有以下几种:



  • 非周期性任务;对应 TimerTask.period 值为0;
  • 周期性任务,但是没有delay值,即立即执行;对应 TimerTask.period 值为正数;
  • 周期性任务,同时包含有 delay值;对应 TimerTask.period 值为负数;

在schedule 方法中,会



// 执行任务调度的方法
// 这里的 time 已经是经过转换的,表示该task 需要被执行的时间戳
private void sched(TimerTask task, long time, long period) {
// 参数的合法性检查
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

// 核心的调度逻辑
// 由于是在多线程环境中使用的,这里为了保证线程安全,使用的是 synchronized 代码段
// 对象锁使用的是在 Timer 对象中唯一存在的 queue 对象
synchronized(queue) {

// thread.newTasksMayBeScheduled 是一个标识位,在timer cancel之后 或者 thread 被停止后该标识位会被设为false
// newTasksMayBeScheduled 为false 则表示该timer 的关联线程已经停止了。
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");

// 这里是把外部的参数,如执行时间点、执行周期、设置状态等等。
// 这里为了线程安全的考虑,使用对 task 内部的 lock 对象加锁来保证。
synchronized(task.lock) {
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException(
"Task already scheduled or cancelled");
task.nextExecutionTime = time;
task.period = period;
task.state = TimerTask.SCHEDULED;
}

// 最后,把新的 task 添加到关联队列里面
queue.add(task);

// 这里,会使用打 TimerQueue 对象的 getMin() 方法,这个方法是获取到接下来将要被执行的TimerTask 对象
// 这里的逻辑是check 新添加的 task 对象是不是接下来马上会被执行
// 如果刚添加的对象是需要马上执行的话,会使用 queue.notify 来通知在等待的线程。

// 那么,会有谁在等待这个 notify 呢?是TimerThread 内部,TimerThread 会有一个死循环,在不停从queue中取任务来执行
// 当queue为空的时候,TimerThread 会进行 queue.wait() 来进行休眠的状态,直到有新的来任务来唤醒它
// 下面的代码就是,当queue为空的时候,这个判断条件会成立,然后就通知 TimerThread 重新唤醒
// 当然,下面的条件成立也不全是 queue 为空的情况下
if (queue.getMin() == task)
queue.notify();
}
}

TimerTask 的源码分析

接下来,本文将会分析 TimerTask 的源码。相对于Timer 来说,它的源码其实很简单,TimerTask 是实现了Runnable 接口,同时也是一个抽象类,它并没有对 Runnable 的 run() 方法提供实现,而是需要子类来实现。


它对外提供了以下几个功能:


包含有一段可以执行的代码(实现的Runnable 接口的run方法)
包含状态的定义。它有一个固定的状态:VIRGIN(新创建)、SCHEDULED(被调度进某一个 timer 的队列中了,但是还没有执行到)、EXECUTED(以及执行过了)、CANCELLED(任务被取消了)。
包含有取消的方法。
包含有获取下一次执行时间的方法。

相关的源码如下:



// 取消该任务
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}

// 根据执行周期,和设置的执行时间,来确定Task的下一次执行时间。
public long scheduledExecutionTime() {
synchronized(lock) {
// 其中,period 的值分为3种情况:
// 取值为0: 表示该Task是非周期性任务;
// 取值为正数: 表示该Task 是立即执行没有delay的周期性任务,period 的数值表示该Task 的周期
// 取值为负数: 表示该Task 是有 delay 的周期性任务,period 相反数是该Task 的周期
return (period < 0 ? nextExecutionTime + period
: nextExecutionTime - period);
}
}

TimerThread 的源码分析

TimerThread 首先是一个 Thread 的子类,而且我们知道,在Java中,一个Thread 的对象就是代表了一个JVM虚拟机线程。那么,这个 TimerThread 其实也就是一个线程。


对于一个线程来说,那么它的关键就是它的 run() 方法,在调用线程的 start() 方法启动线程之后,接下来就会执行线程的 run() 方法,我们看下 TimerThread 的run() 方法:



public void run() {
try {
// 启动 mainLoop() 方法,这是一个阻塞方法,正常情况下会一只阻塞在这里
// 当 mainLoop() 执行完毕的时候,也即是这个线程退出的时候。
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
// 做一些收尾工作
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}

从以上可以明确得看出,TimerThread 里的实现是调用 mainLoop() 启动了一个死循环,这个死循环内部的工作就是这个线程的具体工作了,一旦线程的死循环执行完毕,线程的 run 方法就执行完了,线程紧接着就退出了。熟悉Android的朋友可能已经觉得这里的实现非常眼熟了,没错,这里的实现和Android平台的 Handler + HandlerThread + Looper 的机制非常相像,可以认为Android平台最初研发这套机制的时候,就是参考的Timer 的机制,然后在上面做了些升级和适合Android平台的一些改动。


下面是 mainLoop() 方法:



private void mainLoop() {
// 一个死循环
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 会等待到队列不为空,结合上面章节的分析,我们可以确定在新添加 TimerTask 到queue中的时候
// 会触发到 queue.notify() 然后通知到这里。
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();

// queue 为空,说明 timer 被取消了
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die


long currentTime, executionTime;
// 又一次看到这个 queue.getMin() ,这个是根据接下来的执行时间来获取下一个需要被执行的任务
task = queue.getMin();

// 需要修改 task对象的内部数值,使用synchronized 保证线程安全
synchronized(task.lock) {
// TimerTask 有多种状态,一旦一个 TimerTask 被取消之后,它就不会被执行了。
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}

// 获取到当前时间,和这个取出来的task 的下一次执行时间
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;

// 这里会check 当前这个 task 是不是已经到时间了
// 这里会把是否到时间了这个状态保存在 taskFired 里面
if (taskFired = (executionTime<=currentTime)) {
// 根据上文的分析,TimerTask 根据 task.period 值的不同,被分为3种类型
// 这里的 task.period == 0 的情况,是对应于一个非周期性任务
if (task.period == 0) {
// 非周期性任务,处理完就完事了,改状态,移除队列
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
// 周期性任务,会被重新调度,也不会被移除队列
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}

// 这里是另一个会等待的地方,这个是为了等待任务的到期,等待时间就是距离到执行之间的时长
if (!taskFired)
queue.wait(executionTime - currentTime);
}

// taskFired 变量经过上面的步骤以及判断过了,如果是 true,说明task以及到时间了
// 到时间就运行完事。
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}

TimerThread 中除了上面的主要逻辑之外,还有一些需要关注的地方,那就是它持有一个 TimerQueue 的对象,这个对象是在创建的时候外部传进来的,也是和当前的Timer 关联的TimerQueue:



// 这里的官方注释,说明了为什么是在TimerThread 中引用了 TimerQueue而不是引用了 Timer。
// 这么做是为了避免循环引用(因为Timer中引用了TimerThread),进而避免循环引用可能导致的JVM gc 失败的问题
// 我们都知道,Java 是一门通用的语言,虽然官方的HotSpot JVM中是能解决循环引用的GC问题的,但是这并不意味着
// 其他第三方的JVM也能解决循环引用导致的GC问题,所以这里干脆就避免了循环引用。

/**
* Our Timer's queue. We store this reference in preference to
* a reference to the Timer so the reference graph remains acyclic.
* Otherwise, the Timer would never be garbage-collected and this
* thread would never go away.
*/
private TaskQueue queue;

TimerQueue 的源码分析(主要是实现一个二叉堆)

TimerQueue 的逻辑上是一个队列,所有它包含有一个队列常见的那些方法,如 size()、add()、clear()等方法。下面我们找一些重要的方法进行分析:


首先,在上文的分析中,我们以及见过TimeQueue 的 getMin() 方法了,这个方法是获取当前的队列里面,接下来应该被执行的TimerTask,也就是说,是执行时间点 数值最小的那一个,那么我们就先看下它的源码:


/**
* Return the "head task" of the priority queue. (The head task is an
* task with the lowest nextExecutionTime.)
*/
TimerTask getMin() {
return queue[1];
}

What??? 就这吗?为啥这么简单?为啥就返回 queue[1] 就对了?


你是不是也有这样的疑问,那么带着疑问往下看吧。


接下来,是添加一个TimerTask 到队列中:



// 内部存放TimerTask 数据的,是一个数组,设置的数组初始大小是128
private TimerTask[] queue = new TimerTask[128];

// 存放当前的TimerTask 的数量
// 而且 TimerTask 是存放在 [1 - size] 位置的,数组的第0位置没有数据
// 至于为什么要 存放在 [1 - size] 请看下文。
private int size = 0;

/**
* Adds a new task to the priority queue.
*/
void add(TimerTask task) {
// check 下数据的容量是否还够添加,不够的话会先进行数组的扩容
// 这扩容一次就是2倍增加
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);

// 把新的TimerTask 放在数组的最后一个位置
// size 的初始化值是0,从这里可以看出来,这里会先把size自增1,然后再添加到数组中
// 其实是从数组位置的 1 开始添加 TimerTask 的,0的位置是空的
queue[++size] = task;

// 然后调用了这个数据上浮的方法
fixUp(size);
}

从上文看出,add 方法本身也没什么奇特的,就是很简单地把新的 TimerTask 放在了数据的最新的位置,只是里面调用了一下另一个方法 fixUp() ,好,那么我们接着分析这个方法:





// 从上文可以看出,参数 k 是当前的数组size 值,也是最后一个TimerTask 的下标索引
private void fixUp(int k) {
// 首先,这是一个循环,循环条件是 k > 1
while (k > 1) {
// 位运算,操作,把 k 右移一位,得到的结果是:偶数相当于除以2,奇数相当于先减1再除以2
int j = k >> 1;

// 比较 j 和 k 两个位置的下次执行时间,j 不大于 k 的话,就停止循环了
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;

// j 大于 k 位置的时间的话,就要进行下面的动作
// 这是一个典型的交换操作
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;

// k 值缩小到j,去逼近循环条件 k>1
k = j;
}
}

看了上面对 fixUp() 的分析,是不是仍然一脸懵?或许也有些熟悉算法的朋友已经觉察出些什么了,那么这个地方的逻辑是什么呢?


有了右移一位、[1, size]的区间等蛛丝马迹,我想聪明的你已经猜出来了,这个数组queue 里面,是存放了一个完全二叉树。


在发现 queue 数组是一个二叉树之后,再去理解上面的 fixUp() 方法其实就很简单了,里面的过程是这样的:


从二叉树的最后一个叶子结点开始循环;
获取这个叶子结点的父结点(完全二叉树中对应的父结点的索引是:子结点位运算右移一位得到的)
判断父结点和子结点中对应的 TimerTask 的 nextExecutionTime 的大小,如果父比子的小,则停止循环;如果父比子的大,则交互负责结点;
重复以上循环,直到遍历到根结点;

通过以上分析,能发现在每一次新增一个结点后,使用 fixUp(),方法直接对整个二叉树进行了重排序,使得 TimerTask 的nextExecutionTime 值最小的结点,永远被放置在了二叉树的根结点上,也即是queue[1]。这也就搞明白了为什么 getMin 的实现,是直接获取的 queue[1] 。


同样的道理,在每一次执行 Timer.purge() 方法,清理了TimerQueue中已经取消的Task之后,会执行另一个 fixDown() 方法,它的逻辑正好是和 fixUp() 相反的,它是从根结点开始遍历的,然后到达每一个叶子结点以整理这个二叉树,这里就不再赘述。


回过头来,我们再看下TimerQueue中的实现,会发现它其实是一个二叉堆,二叉堆是一个带有权重的二叉树,这里不再多说。


总结

通过以上的分析,总的来说,就是每一个 Timer对象中,包含有一个线程(TaskThread)和一个队列(TaskQueue)。TaskQueue 的实现是一个二叉堆(Binary Heap)的结构,二叉堆的每一个节点,就是 TimerTask 的对象。


收起阅读 »

Android RecyclerView 通用适配器

使用方式【最新版本号以这里为准】由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!由于JCenter关...
继续阅读 »

使用方式

【最新版本号以这里为准】

由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!
由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!
由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!

#last-version请查看上面的最新版本号

#只支持AndroidX

#从1.4.5版本开始GroupId、ArtifactId均有更新,请按如下方式引用
implementation "com.lwkandroid.library:rcvadapter:last-version"

基础功能

  • 快速实现适配器,支持多种ViewType模式
  • 支持添加HeaderView、FooterView、EmptyView
  • 支持滑到底部加载更多
  • 支持每条Item显示的动画
  • 支持嵌套Section(1.1.0版本新增)
  • 支持悬浮标签StickyLayout(1.2.0版本新增)

效果图






使用方式

1. 当Item样式一样时,只需继承RcvSingleAdapter<T>即可,示例:

public class TestSingleAdapter extends RcvSingleAdapter<TestData>
{
public TestSingleAdapter(Context context, List<TestData> datas)
{
super(context, android.R.layout.simple_list_item_1, datas);
}

@Override
public void onBindView(RcvHolder holder, TestData itemData, int position)
{
//在这里绑定UI和数据,RcvHolder中提供了部分快速设置数据的方法,详情请看源码
holder.setTvText(android.R.id.text1, itemData.getContent());
}
}


2. 当Item样式不一样时,即存在多种ViewType类型的Item,需要将每种ViewType的Item单独实现,再关联到RcvMultiAdapter<T>中,示例:

//第一步:每种Item分别继承RcvBaseItemView<T>
public class LeftItemView extends RcvBaseItemView<TestData>
{
@Override
public int getItemViewLayoutId()
{
//这里返回该Item的布局id
return R.layout.layout_item_left;
}

@Override
public boolean isForViewType(TestData item, int position)
{
//这里判断何时引用该Item
return position % 2 == 0;
}

@Override
public void onBindView(RcvHolder holder, TestData testData, int position)
{
//在这里绑定UI和数据,RcvHolder中提供了部分快速设置数据的方法,详情请看源码
holder.setTvText(R.id.tv_left, testData.getContent());
}
}

//第二步:将所有Item关联到适配器中
public class TestMultiAdapter extends RcvMultiAdapter<TestData>
{
public TestMultiAdapter(Context context, List<TestData> datas)
{
super(context, datas);
//只需在构造方法里将所有Item关联进来,无论多少种ViewType都轻轻松松搞定
addItemView(new LeftItemView());
addItemView(new RightItemView());
}
}


3.优雅的添加HeaderView、FooterView、EmptyView,只需要在RecyclerView设置LayoutManager后调用相关方法即可:

//要先设置LayoutManager
mRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));

//添加HeaderView(若干个)
mAdapter.addHeaderView(headerView01,headerView02,headerView03...);

//添加FooterView(若干个)
mAdapter.addFooterView(footerView01,footerView02,footerView03...);

//添加EmptyView(只能设置一个)
//设置了EmptyView后,当数据量为0的时候会显示EmptyView
mAdapter.setEmptyView(emptyView);
或者
mAdapter.setEmptyView(layoutId);


4.设置滑动到底部自动加载更多,先上示例代码吧:

自1.4.3版本开始删除了之前的调用方式

//可以先设置加载样式,继承RcvBaseLoadMoreView实现自定义样式
//不设置的话会使用默认的样式,参考RcvDefLoadMoreView源码
RcvDefLoadMoreView loadMoreView = new RcvDefLoadMoreView.Builder()
.setBgColor(Color.GREEN)
.setTextColor(Color.RED)
.build(this);
mAdapter.setLoadMoreLayout(loadMoreView);
//再开启并设置监听
mAdapter.enableLoadMore(true);
mAdapter.setOnLoadMoreListener(RcvLoadMoreListener listener);
//禁止加载更多,通常用在配合下拉刷新的过程中
mAdapter.enableLoadMore(false);

注:
① 默认的样式实现是类RcvDefLoadMoreView
② 如需自定义样式,只需继承RcvBaseLoadMoreView,只要重写各状态UI的实现,无须关心状态切换,可参考RcvDefLoadMoreView内的实现方式。

5.设置Item显示动画,先直接上代码:

//使用默认的动画(Alpha动画)
mAdapter.enableItemShowingAnim(true);

//使用自定义动画
mAdapter.enableItemShowingAnim(true, ? extends RcvBaseAnimation);

注:
①默认动画的实现是类RcvAlphaInAnim
②自定义样式需要继承RcvBaseAnimation,可参考RcvAlphaInAnim内部实现。

6.设置Item点击监听:

    //设置OnItemClickListener
mAdapter.setOnItemClickListener(new RcvItemViewClickListener<TestData>()
{
@Override
public void onItemViewClicked(RcvHolder holder, TestData testData, int position)
{
//onClick回调
}
});

//设置OnItemLongClickListener
mAdapter.setOnItemLongClickListener(new RcvItemViewLongClickListener<TestData>()
{
@Override
public void onItemViewLongClicked(RcvHolder holder, TestData testData, int position)
{
//onLongClick回调
}
});


7. 添加分割线,直接上代码:

1.2.9版本针对分割线进行了重写,原有方法不变,新增支持自定义颜色和部分快速创建的方法:

#适用于LinearLayoutManager
//创建默认竖直排列的分割线
RcvLinearDecoration.createDefaultVertical(Context context);
//创建自定义色值默认竖直排列的分割线
RcvLinearDecoration.createDefaultVertical(int color);
//创建默认水平排列的分割线
RcvLinearDecoration.createDefaultHorizontal(Context context);
//创建自定义色值默认水平排列的分割线
RcvLinearDecoration.createDefaultHorizontal(int color);
//构造方法:默认Drawable分割线
new RcvLinearDecoration(Context context, int orientation);
//构造方法:自定义Drawable分割线
new RcvLinearDecoration(Context context, Drawable drawable, int orientation);
//构造方法:自定义Drawable分割线
new RcvLinearDecoration(Context context, @DrawableRes int drawableResId, int orientation);
//构造方法:自定义Color分割线(宽度或者高度默认1px)
new RcvLinearDecoration(@ColorInt int color, int orientation);
//构造方法:自定义Color分割线
new RcvLinearDecoration(@ColorInt int color, int size, int orientation);

#适用于GridLayoutManager、StaggeredGridLayoutManager
//创建默认分割线
RcvGridDecoration.createDefault(Context context);
//创建自定义色值默认分割线
RcvGridDecoration.createDefault(int color);
//构造方法:默认Drawable的分割线
new RcvGridDecoration(Context context);
//构造方法:自定义Drawable的分割线
new RcvGridDecoration(Context context, Drawable drawable);
//构造方法:自定义Drawable的分割线
new RcvGridDecoration(Context context, @DrawableRes int drawableResId);
//构造方法:自定义Color的分割线(默认分割线宽高均为1px)
new RcvGridDecoration(@ColorInt int color);
//构造方法:自定义Color的分割线
new RcvGridDecoration(@ColorInt int color, int width, int height);

注:
①是直接设置给RecyclerView的,不是设置给适配器的,不要看错哦
②支持自定义drawable当分割线

8.嵌套Section,稍微复杂一点,配合代码讲解:

1.4.0版本开始删除以前的使用方法,采用下面的方式

带有Section功能的适配器为RcvSectionMultiLabelAdapterRcvSectionSingleLabelAdapter,需要指定两个泛型,第一个代表Section,第二个代表普通数据Data, 两者都支持多种Data类型的子布局,唯一不同的是,RcvSectionMultiLabelAdapter还支持多种Section类型的子布局,但不可以和RcvStickyLayout联动,而RcvSectionSingleLabelAdapter 仅支持一种Section类型的子布局,但是可以和RcvStickyLayout联动。需要注意的是,传给适配器的数据均需要自行预处理,用RcvSectionWrapper封装后才可传入适配器。

#只有一种Section类型,配合多种Data类型的适配器
public class TestSectionAdapter extends RcvSectionSingleLabelAdapter<TestSection, TestData>
{
public TestSectionAdapter(Context context, List<RcvSectionWrapper<TestSection, TestData>> datas)
{
super(context, datas);
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createDataItemViews()
{
return new RcvBaseItemView[]{new DataItemView01(), new DataItemView02()};
}

@Override
public int getSectionLabelLayoutId()
{
return R.layout.layout_section_label;
}

@Override
public void onBindSectionLabelView(RcvHolder holder, TestSection section, int position)
{
holder.setTvText(R.id.tv_section_label, section.getSection());
}

//第一种Data ItemView
private class DataItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_long;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_long);
textView.setBackgroundColor(Color.GREEN);
textView.setText("第一种数据类型:" + wrapper.getData().getContent());
}
}

//第二种Data ItemView
private class DataItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_short;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_short);
textView.setBackgroundColor(Color.RED);
textView.setText("第二种数据类型:" + wrapper.getData().getContent());
}
}
}

#多种Section类型,配合多种Data类型的适配器
public class TestSectionMultiLabelAdapter extends RcvSectionMultiLabelAdapter<TestSection, TestData>
{
public TestSectionMultiLabelAdapter(Context context, List<RcvSectionWrapper<TestSection, TestData>> datas)
{
super(context, datas);
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createLabelItemViews()
{
return new RcvBaseItemView[]{new LabelItemView01(), new LabelItemView02()};
}

@Override
protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createDataItemViews()
{
return new RcvBaseItemView[]{new DataItemView01(), new DataItemView02()};
}


//第一种Label ItemView
private class LabelItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.layout_section_label;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return item.isSection() && item.getSection().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
holder.setTvText(R.id.tv_section_label, wrapper.getSection().getSection());
}
}

//第二种Label ItemView
private class LabelItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.layout_section_label02;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return item.isSection() && item.getSection().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
holder.setTvText(R.id.tv_section_label, wrapper.getSection().getSection());
}
}

//第一种Data ItemView
private class DataItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_long;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() == 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_long);
textView.setBackgroundColor(Color.GREEN);
textView.setText("第一种数据类型:" + wrapper.getData().getContent());
}
}

//第二种Data ItemView
private class DataItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
{
@Override
public int getItemViewLayoutId()
{
return R.layout.adapter_item_short;
}

@Override
public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
{
return !item.isSection() && item.getData().getType() != 0;
}

@Override
public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
{
TextView textView = holder.findView(R.id.tv_item_short);
textView.setBackgroundColor(Color.RED);
textView.setText("第二种数据类型:" + wrapper.getData().getContent());
}
}
}

注:
①传给适配器的数据集合内实体类必须经过RcvSectionWrapper包装。
②向外公布的方法(例如点击监听)的实体类泛型不能传错。

9.悬浮标签StickyLayout

适配器方面无需改动,直接使用RcvSectionSingleLabelAdapter即可,在RecyclerView同级布局下添加RcvStickyLayout,然后在代码中关联起来即可:

    // xml布局中添加RcvStickyLayout:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/rcv_sticky"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

<com.lwkandroid.rcvadapter.ui.RcvStickyLayout
android:id="@+id/stickyLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</FrameLayout>



//代码中关联RecyclerView
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rcv_sticky);
/...省略设置RecyclerView的LayoutMananger和Adapter.../
RcvStickyLayout stickyLayout = (RcvStickyLayout) findViewById(R.id.stickyLayout);
stickyLayout.attachToRecyclerView(recyclerView);

上面就是大部分基础功能的使用方法了,想了解更多方法请看源码。

混淆配置

-dontwarn com.lwkandroid.rcvadapter.**
-keep class com.lwkandroid.rcvadapter.**{*;}


待实现功能

  • 暂时未想到

开源参考

  1. https://github.com/hongyangAndroid/baseAdapter
  2. https://github.com/CymChad/BaseRecyclerViewAdapterHelper
收起阅读 »

一行代码解决RxJava 内存泄漏

xLifeRxLife,相较于trello/RxLifecycle、uber/AutoDispose,具有如下优势:直接支持在主线程回调支持在子线程订阅观察者简单易用,学习成本低性能更优,在实现上更加简单友情提示: RxLife与RxHttp搭配使用,味道更佳...
继续阅读 »

xLife

RxLife,相较于trello/RxLifecycleuber/AutoDispose,具有如下优势:

  • 直接支持在主线程回调
  • 支持在子线程订阅观察者
  • 简单易用,学习成本低
  • 性能更优,在实现上更加简单

友情提示: RxLife与RxHttp搭配使用,味道更佳

RxLife详细介绍:https://juejin.im/post/5cf3e1235188251c064815f1

Gradle引用

jitpack添加到项目的build.gradle文件中,如下:

allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}

注:RxLife 2.1.0 版本起,已全面从JCenter迁移至jitpack

新版本仅支持AndroidX项目

dependencies {
//kotlin协程
implementation 'com.github.liujingxing.rxlife:rxlife-coroutine:2.1.0'

//rxjava2
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.1.0'

//rxjava3
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.1.0'
}

注意:RxJava2 使用Rxlife.asXxx方法;RxJava3使用Rxlife.toXxx方法

非AndroidX项目

非AndroidX项目,请使用旧版本RxLife

implementation 'com.rxjava.rxlife:rxlife:2.0.0'

由于Google在19年就停止了非AndroidX库的更新,故rxlife旧版本不再维护,请尽快将项目迁移至AndroidX

#Usage

1、FragmentActivity/Fragment

FragmentActivity/Fragment销毁时,自动关闭RxJava管道

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.as(this)) //此时的this FragmentActivity/Fragment对象
.subscribe(aLong -> {
Log.e("LJX", "accept =" + aLong);
});

2、View

View被移除时,自动关闭RxJava管道

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.as(this)) //此时的this 为View对象
.subscribe(aLong -> {
Log.e("LJX", "accept =" + aLong);
});

3、ViewModel

Activity/Fragment销毁时,自动关闭RxJava管道,ViewModel需要继承ScopeViewModel类,如下

public class MyViewModel extends ScopeViewModel {

public MyViewModel(@NonNull Application application) {
super(application);
}

public void test(){
Observable.interval(1, 1, TimeUnit.SECONDS)
.as(RxLife.asOnMain(this)) //继承ScopeViewModel后,就可以直接传this
.subscribe(aLong -> {
Log.e("LJX", "MyViewModel aLong=" + aLong);
});
}
}

注意: 一定要在Activity/Fragment通过以下方式获取ViewModel对象,否则RxLife接收不到生命周期的回调


MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);

4、任意类

Activity/Fragment销毁时,自动关闭RxJava管道,任意类需要继承BaseScope类,如P层:

public class Presenter extends BaseScope {

public Presenter(LifecycleOwner owner) {
super(owner); //添加生命周期监听
}

public void test(){
Observable.interval(1, 1, TimeUnit.SECONDS)
.as(RxLife.as(this)) //继承BaseScope后,就可以直接传this
.subscribe(aLong -> {
Log.e("LJX", "accept aLong=" + aLong);
});
}
}

5、kotlin用户

由于as是kotlin中的一个关键字,所以在kotlin中,我们并不能直接使用as(RxLife.as(this)),可以如下编写

Observable.intervalRange(1, 100, 0, 200, TimeUnit.MILLISECONDS)
.`as`(RxLife.`as`(this))
.subscribe { aLong ->
Log.e("LJX", "accept=" + aLong)
}

当然,相信没多少人会喜欢这种写法,故,RxLife针对kotlin用户,新增更为便捷的写法,如下:

Observable.intervalRange(1, 100, 0, 200, TimeUnit.MILLISECONDS)
.life(this)
.subscribe { aLong ->
Log.e("LJX", "accept=" + aLong)
}

使用life 操作符替代as操作符即可,其它均一样

6、小彩蛋

asOnMain操作符

RxLife还提供了asOnMain操作符,它可以指定下游的观察者在主线程中回调,如下:

Observable.timer(5, TimeUnit.SECONDS)
.as(RxLife.asOnMain(this))
.subscribe(aLong -> {
//在主线程回调
Log.e("LJX", "accept =" + aLong);
});

//等价于
Observable.timer(5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.as(RxLife.as(this))
.subscribe(aLong -> {
//在主线程回调
Log.e("LJX", "accept =" + aLong);
});

kotlin 用户使用lifeOnMain替代asOnMain操作符,其它均一样

注意: RxLife类里面as操作符,皆适用于Flowable、ParallelFlowable、Observable、Single、Maybe、Completable这6个被观察者对象

混淆

RxLife作为开源库,可混淆,也可不混淆,如果不希望被混淆,请在proguard-rules.pro文件添加以下代码

-keep class com.rxjava.rxlife.**{*;}


代码下载:327744707-rxjava-RxLife-master.zip

收起阅读 »

Android替换系统dialog风格后的通用提示框工具类

DialogUtilsApp一、介绍替换系统dialog风格后的通用提示框工具类,可以覆盖lib下的定义资源,改变现有的颜色风格,需要改变布局风格,可下载项目后自行调整APP 使用示例项目,libs下含有已编译最新的aar资源。dialogutilslib a...
继续阅读 »

DialogUtilsApp

一、介绍

替换系统dialog风格后的通用提示框工具类,可以覆盖lib下的定义资源,改变现有的颜色风格,需要改变布局风格,可下载项目后自行调整

  • APP 使用示例项目,libs下含有已编译最新的aar资源。
  • dialogutilslib arr资源项目,需要引入的资源包项目。
  • aar文件生成,在工具栏直接Gradle - (项目名) - dialogutilslib - Tasks - build - assemble,直到编译完成
  • aar文件位置,打开项目所在文件夹,找到 dialogutilslib\build\outputs\aar 下。

二、工程引入工具包准备

下载项目,可以在APP项目的libs文件下找到DialogUtilsLib.aar文件(已编译为最新版),引入自己的工程 引入aar

dependencies {
implementation files('libs/DialogUtilsLib-release.aar')
...
}

三、使用

注意下方只做了基础展示,dialog的都会返回对应的utils对象,registerActivityLifecycleCallbacks方法设置后,activity销毁时会自动把显示在此activity上的dialog一起关闭。

  • application初始化设置
public class App extends Application {

@Override
public void onCreate() {
super.onCreate();

//初始化dialog工具类设置
DialogLibInitSetting.getInstance()
//设置debug
.setDebug(BuildConfig.DEBUG)
//注册全局activity生命周期监听
.registerActivityLifecycleCallbacks(this);

}
}
  • 普通dialog
            DialogLibCommon.create(this)
.setMessage("普通对话框1")
.setAlias("text1")
.setOnBtnMessage(()->{
//描述区域点击时触发
})
.noShowCancel()
.show();
  • 自定义dialog
            ImageView imageView = new ImageView(this);
imageView.setImageDrawable(getResources().getDrawable(R.mipmap.ic_launcher));
DialogLibCustom.create(this)
.noShowCancel()
.setAlias("text2")
.show(imageView);
  • 输入型dialog
            DialogLibInput.create(this)
.setMessage("输入信息")
.setAlias("text3")
//自动弹出键盘
.setPopupKeyboard()
.setOnBtnOk(str -> {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
return true;
})
.show();
  • 等待型dialog
            DialogLibLoading.create(this)
.setTimeoutClose(2000)
.setAlias("text4")
.setOnLoading(() -> {
Toast.makeText(MainActivity.this, "我是显示对话框前触发的", Toast.LENGTH_SHORT).show();
})
.show();
  • 完全自定义型dialog
            final DialogLibAllCustom dialog = DialogLibAllCustom.create(this)
.setCancelable(true)
.setAlias("text5");

TextView view = new TextView(this);
view.setBackgroundResource(R.color.design_default_color_secondary);
view.setText("这是一个完全自定义布局的对话框,对话框显示后需要手动关闭");
view.setOnClickListener(v2 -> {
dialog.closeDialog();
});

dialog.show(view);
  • 密码输入型dialog
              DialogLibInput.create(this)
.setMessage("123")
.setLength(6)
.setInputType(EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD)
.setAlias("text6")
//设置显示密码隐藏/显示图片
.setShowLookPassword()
//自动弹出键盘
.setPopupKeyboard()
.setOnBtnOk(str -> {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
return true;
})
.show();

四、资源覆盖,改变颜色、字体大小、默认文字

  • colors下可覆盖资源及注释,默认黑色和白色不建议覆盖,前景色:字体的颜色,背景色:布局的背景色
<resources>
<!--黑色-->
<color name="dialog_utils_lib_black">#FF000000</color>
<!--白色-->
<color name="dialog_utils_lib_white">#FFFFFFFF</color>

<!--dialog的标题文字的前景色,适用于所有带标题的dialog-->
<color name="dialog_utils_lib_title_fg">@color/dialog_utils_lib_black</color>
<!--dialog的 确认 按钮文字的前景色-->
<color name="dialog_utils_lib_ok_fg">@color/dialog_utils_lib_white</color>
<!--dialog的 取消 按钮文字的前景色-->
<color name="dialog_utils_lib_cancel_fg">@color/dialog_utils_lib_white</color>
<!--dialog的 确认 按钮文字的背景色-->
<color name="dialog_utils_lib_ok_bg">#22C5A3</color>
<!--dialog的 取消 按钮文字的背景色-->
<color name="dialog_utils_lib_cancel_bg">#F8A01A</color>
<!--dialog的输入框下方显示2个按钮时,中间分隔的背景色-->
<color name="dialog_utils_lib_button_split_bg">@color/dialog_utils_lib_white</color>

<!--dialog的内容文字的前景色,适用于 DialogLibCommonUtils-->
<color name="dialog_utils_lib_content_fg">@color/dialog_utils_lib_black</color>

<!--dialog的输入框文字的前景色,适用于 DialogLibInputUtils-->
<color name="dialog_utils_lib_input_fg">@color/dialog_utils_lib_black</color>
<!--dialog的输入框下方分割线的背景色,适用于 DialogLibInputUtils-->
<color name="dialog_utils_lib_input_split_line">@color/dialog_utils_lib_ok_bg</color>

<!--dialog的加载框加载等待区域的背景色-->
<color name="dialog_utils_lib_loading_content_bg">#FFc4c4c4</color>
<!--dialog的加载框加载等待区域文字提示的前景色-->
<color name="dialog_utils_lib_loading_content_text_fg">@color/dialog_utils_lib_white</color>
</resources>
  • dimens下字体大小资源
<resources>
<dimen name="dialog_utils_lib_text_size_normal">14sp</dimen>

<!--标题字体大小,统一设定-->
<dimen name="dialog_utils_lib_title_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--确定 字体大小,统一设定-->
<dimen name="dialog_utils_lib_ok_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--取消 字体大小,统一设定-->
<dimen name="dialog_utils_lib_cancel_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--内容 字体大小,适用于 DialogLibCommonUtils的提示内容区域-->
<dimen name="dialog_utils_lib_content_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--输入框 字体大小,适用于 DialogLibInputUtils 输入区域-->
<dimen name="dialog_utils_lib_input_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
<!--加载框 字体大小,适用于 DialogLibLoadingUtils 提示内容区域-->
<dimen name="dialog_utils_lib_loading_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>

<!--dialog 宽度占屏幕宽度的百分比,取值0-1之间,不包含边界,竖屏时的系数-->
<item name="dialog_utils_lib_portrait_width_factor" format="float" type="dimen">0.85</item>
<!--dialog 宽度占屏幕宽度的百分比,取值0-1之间,不包含边界,横屏时的系数-->
<item name="dialog_utils_lib_landscape_width_factor" format="float" type="dimen">0.5</item>
</resources>
  • strings下资源定义,注意:如果你的项目存在多语言,则必须覆盖
<resources>
<string name="dialog_utils_lib_ok">确定</string>
<string name="dialog_utils_lib_cancel">取消</string>
<string name="dialog_utils_lib_default_title">提示</string>
<string name="dialog_utils_lib_data_processing">数据处理中…</string>
</resources>
  • mipmap下资源定义,注意:此2张图片为密码输入时显示/隐藏按钮的图片,png格式
dialog_utils_lib_password_hide 隐藏图片命名
dialog_utils_lib_password_show 显示图片命名

代码下载:mjsoftking-dialog-utils-app-master.zip

收起阅读 »

30秒上手的HTTP请求库

RxHttp主要优势1. 30秒即可上手,学习成本极低2. 史上最优雅的支持 Kotlin 协程3. 史上最优雅的处理多个BaseUrl及动态BaseUrl4. 史上最优雅的对错误统一处理,且不打破Lambda表达式5. 史上最优雅的文件上传/下载/断点下载/...
继续阅读 »

RxHttp


主要优势

1. 30秒即可上手,学习成本极低

2. 史上最优雅的支持 Kotlin 协程

3. 史上最优雅的处理多个BaseUrl及动态BaseUrl

4. 史上最优雅的对错误统一处理,且不打破Lambda表达式

5. 史上最优雅的文件上传/下载/断点下载/进度监听,已适配Android 10

6. 支持Gson、Xml、ProtoBuf、FastJson等第三方数据解析工具

7. 支持Get、Post、Put、Delete等任意请求方式,可自定义请求方式

8. 支持在Activity/Fragment/View/ViewModel/任意类中,自动关闭请求

9. 支持全局加解密、添加公共参数及头部、网络缓存,均支持对某个请求单独设置

请求三部曲

上手教程

30秒上手教程:30秒上手新一代Http请求神器RxHttp

协程文档:RxHttp ,比Retrofit 更优雅的协程体验

掘金详细文档:RxHttp 让你眼前一亮的Http请求框架

wiki详细文档:https://github.com/liujingxing/rxhttp/wiki (此文档会持续更新)

自动关闭请求用到的RxLife类,详情请查看RxLife库

更新日志      遇到问题,点击这里,99%的问题都能自己解决

上手准备

Maven依赖点击这里

1、RxHttp目前已适配OkHttp 3.12.0 - 4.9.1版本(4.3.0版本除外), 如你想要兼容21以下,请依赖OkHttp 3.12.x,该版本最低要求 API 9

2、asXxx方法内部是通过RxJava实现的,而RxHttp 2.2.0版本起,内部已剔除RxJava,如需使用,请自行依赖RxJava并告知RxHttp依赖的Rxjava版本

必须

jitpack添加到项目的build.gradle文件中,如下:

allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}

注:RxHttp 2.6.0版本起,已全面从JCenter迁移至jitpack

//使用kapt依赖rxhttp-compiler时必须
apply plugin: 'kotlin-kapt'

android {
//必须,java 8或更高
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
implementation 'com.github.liujingxing.rxhttp:rxhttp:2.6.1'
implementation 'com.squareup.okhttp3:okhttp:4.9.1' //rxhttp v2.2.2版本起,需要手动依赖okhttp
kapt 'com.github.liujingxing.rxhttp:rxhttp-compiler:2.6.1' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt
}

可选

android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [
//使用asXxx方法时必须,告知RxHttp你依赖的rxjava版本,可传入rxjava2、rxjava3
rxhttp_rxjava: 'rxjava3',
rxhttp_package: 'rxhttp' //非必须,指定RxHttp类包名
]
}
}
}
}
dependencies {
implementation 'com.github.liujingxing.rxlife:rxlife-coroutine:2.1.0' //管理协程生命周期,页面销毁,关闭请求

//rxjava2 (RxJava2/Rxjava3二选一,使用asXxx方法时必须)
implementation 'io.reactivex.rxjava2:rxjava:2.2.8'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.1.0' //管理RxJava2生命周期,页面销毁,关闭请求

//rxjava3
implementation 'io.reactivex.rxjava3:rxjava:3.0.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.1.0' //管理RxJava3生命周期,页面销毁,关闭请求

//非必须,根据自己需求选择 RxHttp默认内置了GsonConverter
implementation 'com.github.liujingxing.rxhttp:converter-fastjson:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-jackson:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-moshi:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-protobuf:2.6.1'
implementation 'com.github.liujingxing.rxhttp:converter-simplexml:2.6.1'
}

最后,rebuild一下(此步骤是必须的) ,就会自动生成RxHttp类

混淆

RxHttp v2.2.8版本起,无需添加任何混淆规则(内部自带混淆规则),v2.2.8以下版本,请查看混淆规则,并添加到自己项目中

代码下载:327744707-okhttp-RxHttp-master.zip







收起阅读 »

你确定你会写代码---iOS规范补充

Pod update注意1、先执行pod repo update 公司内部库specs2、再执行pod update --no-repo-update这样就不会update github_specs,速度快JSONSerialization涉及到JSON Ob...
继续阅读 »

Pod update注意

1、先执行pod repo update 公司内部库specs
2、再执行pod update --no-repo-update这样就不会update github_specs,速度快

JSONSerialization

涉及到JSON Object<->NSData数据转换的地方,注意对NSError的处理和JSON Object合法性的校验,如:

BOOL validate = [NSJSONSerialization isValidJSONObject:parament];
if (!validate) {
// 对不是合法的JSON对象错误进行处理
return;
}
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parament options:NSJSONWritingPrettyPrinted error:&error];
if (error) {
// 对数据转换错误进行处理
return;
}

合法JSON对象满足:
1、Top level object is an NSArray or NSDictionary
2、All objects are NSString, NSNumber, NSArray, NSDictionary, or NSNull
3、All dictionary keys are NSStrings
4、NSNumbers are not NaN or infinity

补充一些代码规范、开发约定

写if else语句时可以else不换行紧跟if的}括号,但写if else if时,为了保持条件{}的可读性,务必请换行书写:

// if else
BOOL flag = YES;
if (flag) {

} else {

}

// if else if
BOOL flag = YES;
BOOL elseIfFlag = (1+1-1+2 == 5);
if (flag) {

}
// 这里换行书写
else if(elseIfFlag) {

}

对于@property声明的属性,如果初始化设置复杂,请采用懒加载getters方式,对于简单初始化的,应在.m文件中提供统一的-initData初始化数据的方法。

/// 懒加载方式-内部配置
@property(nonatomic, strong)UIView *redView;
/// 统一初始化
@property(nonatomic, strong)NSMutableArray *dataSourceArray;

/// 统一数据初始化
- (void)initData{
_dataSourceArray = [NSMutableArray new];
}

/// 懒加载
- (UIView *)redView{
if (!_redView) {
_redView = [UIView new];
_redView.backgroundColor = UIColor.redColor;
}
return _redView;
}

对于NSDictionary、NSArray等的初始化,为提高可读性起见,建议采用语法糖的初始化方式:

_dataSourceArray = [@[@"1", @"2"] mutableCopy];
_parameters = [@{@"action": @"add", @"id": @"22"} mutableCopy];

// X: 不推荐这样做
_dataSourceArray = [NSMutableArray new];
[_dataSourceArray addObject:@"1"];
[_dataSourceArray addObject:@"2"];

_parameters = [NSMutableDictionary new];
[_parameters setValue:@"add" forKey:@"action"];
[_parameters setValue:@"22" forKey:@"id"];

对于Category中的对外公有方法,务必采用categoryName_funcName的命名方式,以区别于主类里没有前缀的方法:

// TALPlayer+LogReport.h

/// 加载播放器
- (void)logReport_loadPlayer;
/// 开始播放
- (void)logReport_startPlay;

对于Category里的私有同名方法,可采用下划线方式如_mainClassFuncName以区别.
对于主类里的私有属性,在多个Category访问时,可采用属性中间件的方式,拆出一个独立的MainClass+InternalProperty来提供一些getters方法:

// TALPlayer+InternalProperty.h

- (TALPlayerLogModel *)internalProperty_logModel;
- (TALPlayerStaticsModel *)internalProperty_staticsModel;


// TALPlayer+InternalProperty.m

- (TALPlayerLogModel *)internalProperty_logModel{
// 这里为方便以后调试断点用,建议拆开2行写
id value = [self valueForKey:@"logModel"];
return value;
}

对于需要跟服务器交互的网络请求参数字符串,务必独立出对应Category的DataInfoKeys扩展文件,方便查询、注释、全局引用、修改和拼写纠错:

// TALPlayer+LogReportDataInfoKeys.h

/// action
extern NSString *const LogReportDataInfoActionKey;
/// 心跳
extern NSString *const LogReportDataInfoActionHeartBeatKey;
/// 严重卡顿
extern NSString *const LogReportDataInfoActionSeriousBufferKey;


// TALPlayer+LogReportDataInfoKeys.m

// action
NSString *const LogReportDataInfoActionKey = @"action";
/// 心跳
NSString *const LogReportDataInfoActionHeartBeatKey = @"heartbeat";
/// 严重卡顿
NSString *const LogReportDataInfoActionSeriousBufferKey = @"seriousbuffer";


// 使用
#import "TALPlayerLogReportDataInfoKeys.h"

NSMutableDictionary *info = [NSMutableDictionary new];
info[LogReportDataInfoActionKey] = LogReportDataInfoActionHeartBeatKey;
// info[XXXKey] = value;

对于.h及.m文件中默认#pragma mark的规范,推荐如下:

// XXX.h

#pragma mark - Protocol

#pragma mark - Properties

#pragma mark - Methods

// XXX.m

#pragma mark - Consts

#pragma mark - UI Components

#pragma mark - Data Properties

#pragma mark - Initial Methods

#pragma mark - Lifecycle Methods

#pragma mark - Override Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - XXXDelegate

#pragma mark - Getters

#pragma mark - Setters

如上相关#pragma字符在Xcode中的自动配置,有机会我会单独分享给大家。
Xcode FileTemplate路径:Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/Source/Cocoa Touch Class.xctemplate/


对于OC文件中注释规范说明:

//MARK: mark here(类似于#pragma mark,只不过没有横线)

//TODO: todo here

//FIXME: fix me later

//???: What is this shit

//!!!: DO NOT TOUCH MY CODE

说明:
对于单行注释,尽量用//代替/**/格式,,务必请在//后加一个空格,再进行内容补充,如:// 这是单行注释而不要写成//这是单行注释
对于SDK内部私有方法,如果无参数,则采用/// 这是无参数方法注释格式;
对于SDK需要向外暴露的接口方法注释,请务必按照AppleDoc编写,写明@param、@return等:

/**
* 这是一个demo方法
* @param param1 第一个参数
* @param param2 第二个参数
* @return BOOL值
*/
- (BOOL)thisIsADemoFuncWithParam1: (NSString *)param1
param2: (NSInteger)param2{
return NO;
}

1、对于@property申明的SDK公开属性,务必写成/* 这是SDK公开属性注释 */,方便调用时Xcode提示;

2、对于SDK内部使用的属性,最好写成/// 这是属性注释而不是/**/;

3、另外,务必让对于>=2个参数的方法,各个参数折行对齐,务必保持.h和.m方法参数格式一致;

4、对于方法名的统一性说明:

  4.1 如果方法是功能性的,处理某些事件、计算、数据处理等的私有方法,则可定义方法名为handleXXX:,如-handleRedBtnClick:、-handleResponsedData:、-handlePlayerEvent:等;

  4.2 对于一些需要暴露的公有方法,则命名最好按照n的功能命名,如对于一个TALPlayer它可以play、stop、resume等;

  4.3 对于可以switch两种状态切换的状态方法,最好命名为toggleXXX:(BOOL)on,如- (void)toggleMute:(BOOL)on;

5、对于一些状态描述性的属性,可以用needs、is、should、has+adj/vb组合形式,如needsAutoDisplay、shouldAutoRotateOrientation、isFinished、hasData或hasNoData等;

对于一些NS_ENUM枚举定义,务必遵循统一前缀方式:

typedef enum : NSUInteger {
TALPlayerEventA = 0,
TALPlayerEventB,
TALPlayerEventC,
} TALPlayerEvent;
// 或者
typedef NS_ENUM(NSUInteger, MyEnum) {
MyEnumValueA,
MyEnumValueB,
MyEnumValueC,
};

6、对于一些全局宏的定义,务必SDK前缀全大写_NAME_组合如TALPLAYER_GLOBAL_MACRO_NAME,对于const类型的常量,务必加上k前缀,命名为kConstValue;

7、对于一些typedef的Block,命名最好指明Block的类别+用途,如TALPlayerLogReportHandler,如果有功能性区分的话,则可以定义为TALPlayerLogReportCompletionHandler、TALPlayerLogReportSuccessHandler、TALPlayerLogReportFailureHandler注意是名词组合形式;

8、调用Block时,一定要对block对象进行nil值判断,防止崩溃handler ? handler() : nil;

9、所有对于NSString校验的地方,都应该校验其length > 0,而不是!str;

10、所有对于NSURL校验的地方,都应该校验其[URL.scheme.lowercaseString isEqualToString: @"https"]方式,而不是!URL;

链接:https://www.jianshu.com/p/deb117eca9ea

收起阅读 »

iOS Cateogry的深入理解&&initialize方法调用理解(二)

上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调...
继续阅读 »
  • 上一篇文章我们讲到了load方法,今天我们来看看initialize

新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下

将原来的load方法换成initialize






先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize

    1. 相信大家在想什么叫第一次接收消息了,我们回到main()





说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码

从输出结果可以看到没有任何关于initialize的打印,程序直接退出

  • 2.initialize的打印

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
}
return 0;
}
2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印

load是直接函数指针直接调用,类,分类,继承等等

[TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用


多次向TCPerson发送消息的输出结果

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
}
return 0;
}
2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

initialize只会调用一次

我们再来看看继承关系中,initialize的调用

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCStudent alloc];

}
return 0;
}

输出结果:

2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0


从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCStudent alloc];
[TCStudent alloc];
}
return 0;
}
020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

#import "TCStudent.h"

@implementation TCStudent
//+ (void)initialize{
// NSLog(@"TCStudent +initialize");
//}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCStudent alloc];
}
return 0;
}
2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
Program ended with exit code: 0
如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

#import "TCStudent.h"

@implementation TCStudent
+ (void)initialize{
NSLog(@"TCStudent +initialize");
}
@end
#import "TCStudent+TCStudentTest1.h"

@implementation TCStudent (TCStudentTest1)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest1) +initialize");
}
@end#import "TCStudent+TCStudentTest2.h"

@implementation TCStudent (TCStudentTest2)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest2) +initialize");
}
@end
2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0




作者:枫紫_6174
链接:https://www.jianshu.com/p/f0150edc0f42


收起阅读 »

iOS Cateogry的深入理解&&load方法调用&&分类重写方法的调用顺序(一)

首先先看几个面试问题Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类2.新建一个TCStudent类继承自TCPerson,并且给T...
继续阅读 »

首先先看几个面试问题

  • Cateogry里面有load方法么? load方法什么时候调用?load方法有继承么?

1. 新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类


2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类


Cateogry里面有load方法么?

  • 答:分类里面肯定有load

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{

}
@end
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{

}
@end
#import "TCPerson+TCTest2.h"

@implementation TCPerson (TCTest2)
+ (void)load{

}
@end

load方法什么时候调用?

load方法在runtime加载类和分类的时候调用load

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {

}
return 0;
}

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
@end


@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
@end
@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
@end
可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出

从输出结果我们可以看出,三个load方法都被调用

问题:分类重写方法,真的是覆盖原有类的方法么?如果不是,到底分类的方法是怎么调用的?

  • 首先我们在TCPerson申明一个方法+ (void)test并且在它的两个分类都重写+ (void)test


#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject
+ (void)test;
@end

NS_ASSUME_NONNULL_END

#import "TCPerson.h"

@implementation TCPerson
+ (void)load{
NSLog(@"TCPerson +load");
}
+ (void)test{
NSLog(@"TCPerson +test");
}
@end
分类重写test
#import "TCPerson+TCtest1.h"

@implementation TCPerson (TCtest1)
+ (void)load{
NSLog(@"TCPerson (TCtest1) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest1) +test1");
}
@end
#import "TCPerson+TCTest2.h"

@implementation TCPerson (TCTest2)
+ (void)load{
NSLog(@"TCPerson (TCtest2) +load");
}
+ (void)test{
NSLog(@"TCPerson (TCtest2) +test2");
}
@end

在main里面我们调用test

#import <Foundation/Foundation.h>
#import "TCPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
}
return 0;
}

从输出结果中我们可以看到,只有分类2中的test被调用,为什么只调用分类2中的test了?





因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(拖动文件就行了)




细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test覆盖了,所以才出现这种情况么?

我们打印TCPerson的类方法

void printMethodNamesOfClass(Class cls)
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);

// 存储方法名
NSMutableString *methodNames = [NSMutableString string];

// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}

// 释放
free(methodList);

// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson test];
printMethodNamesOfClass(object_getClass([TCPerson class]));
}
return 0;
}


可以看到,TCPerson的所有类方法名,并不是覆盖,三个load,三个test,方法都在

load源码分析:查看objc底层源码我们可以看到:

void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;

loadMethodLock.assertLocked();

// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;

void *pool = objc_autoreleasePoolPush();

do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}

// 2. Call category +loads ONCE
more_categories = call_category_loads();

// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);

objc_autoreleasePoolPop(pool);

loading = NO;
}
load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的load,和编译顺序无关,都会调用
我们查看call_class_loads()方法

static void call_class_loads(void)
{
int I;

// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;

if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}

// Destroy the detached list.
if (classes) free(classes);
}
其通过的是load_method_t函数指针直接调用
函数指针直接调用
typedef void(*load_method_t)(id, SEL);

其分类load方法调用也是一样

static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;

// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;

// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;

cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
(*load_method)(cls, SEL_load);
cats[i].cat = nil;
}
}

为什么test不一样了

因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的

load只在加载类的时候调用一次,且先调用类的load,再调用分类的

load的继承关系调用
首先我们先看TCStudent
#import "TCStudent.h"

@implementation TCStudent

@end

不写load方法调用

TCStudent写上load


从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的

总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



作者:枫紫
链接:https://www.jianshu.com/p/f66921e24ffe









收起阅读 »

在iOS中运用React Component的思路,效率更高的开发UI,更好的复用UI组件

最近一直在看React的一些东西,其实很早前就想开始重拾前端,但是一直提不起兴趣再去看JavaScript,对CSS这种布局方式也不是很来感,说白了,就是懒吧😂。去年年底开始在公司app里开始尝试接入Weex,所以不得不把JavaScript再重新撸了一遍,顺...
继续阅读 »

最近一直在看React的一些东西,其实很早前就想开始重拾前端,但是一直提不起兴趣再去看JavaScript,对CSS这种布局方式也不是很来感,说白了,就是懒吧😂。去年年底开始在公司app里开始尝试接入Weex,所以不得不把JavaScript再重新撸了一遍,顺带着把ES6的一些新特性也了解了一下,更好的函数调用方式,Class的引入,Promise的运用等等,其实最吸引我的还是在用了Weex之后,感受到了Component带来的UI复用,高效开发的快感。Weex是运用Vue.js来调用,渲染native控件,来达到one code, run everywhere。不管是Vue.js,还是React,最终都是朝着W3C WebComponent的标准走了(今年会发布的Vue 3.0在组件上的语法基本上跟React一样了)。这篇就来讲讲我对React Component的理解,还有怎么把这个标准也能在native上面做运用

demo源码

iOS UI开发的痛点

对iOS开发来说,最常用的UI组件就是UICollectionView了,就是所谓的一个列表页,现在的app大部分页面都是由一个列表来呈现内容的。对iOS开发者来说,我们可以封装每个UICollectionViewCell,从而可以在每个页面的UICollectionView中能够复用,但是痛点是,这个复用仅仅是UI上的复用,在每写一个新的页面(UIViewController)的时候,还是需要新建一个UICollectionView,然后再把UICollectionView的DataSource和Delegate方法再实现一遍,把这些Cell再在这些方法里重新生成一遍,才能让列表展现出来。比方说我们首页列表底部有猜你喜欢的cell,个人中心页面底部也有猜你喜欢的cell,这两个页面,都需要在自己拥有的UICollectionView中注册这个猜你喜欢的cell,返回这个猜你喜欢cell的高度,设置这个cell的model并刷新数据,如果有Header或者Footer的话,还得重新设置这些Header跟Footer。所以新写一个列表页面,对iOS开发者来说,还是很麻烦。

使用Weex或者RN开发原生列表页

使用Weex开发列表页的时候,我们组内的小伙伴都觉得很爽,很高效,基本上几行代码就能绘制出一个列表页,举个RN和weex的例子

// React
render() {
const cells = this.state.items.map((item, index) => {
if (item.cellType === 'largeCell') {
return <LargeCell cellData={item.entity}></LargeCell>
} else if (item.cellType === 'mediumCell') {
return <MediumCell cellData={item.entity}></MediumCell>
} else if (item.cellType === 'smallCell') {
return <SmallCell cellData={item.entity}></SmallCell>
}
});

return(
<Waterfall>
{ cells }
</Waterfall>
);
}

// Vue
<template>
<waterfall>
<cell v-for="(item, index) in itemsArray" :key="index">
<div class="cell" v-if="item.cellType === 'largeCell'">
<LargeCell :cellData="item.entity"></LargeCell>
</div>
<div class="cell" v-if="item.cellType === 'mediumCell'">
<MediumCell :cellData="item.entity"></MediumCell>
</div>
<div class="cell" v-if="item.cellType === 'smallCell'">
<SmallCell :cellData="item.entity"></SmallCell>
</div>
</cell>
</waterfall>
</template>

const

waterfall对应的就是iOS中的UICollectionView,waterfall这个组件中有cell的子组件,这些cell的子组件可以是我们自己定义的不同类型样式的cell组件。LargeCell,MediumCell,SmallCell对应的就是原生中的我们自定义的UICollectionViewCell。这些Cell子组在任何waterfall组件下面都可以使用,在一个waterfall组件下面,我们只需要把我们把在这个列表中需要展示的cell放进来,通过props把数据传到cell组件中即可。这种方式对iOS开发者来说,真的是太舒服了。在觉得开发很爽的同时,我也在思考,既然这种Component的方式用起来很爽,那么能不能也运用到原生开发中呢?毕竟我们大部分的业务需求还是基于原生来开发的。

React的核心思想

1、先来解释下React中的React Element和React Component
1.1、React Elements

const element = <div id='login-button>Login</div>

这段JSX表达式返回的就是一个React Element,React element描述了用户将在屏幕上看到的那个UI,跟DOM elements不一样的是,React elements是一个单纯的对象,仅仅是对将要呈现到屏幕上的UI的一个描述,并不是真正渲染好的UI,创建一个React element开销是极其小的,渲染的事情是由背后的React DOM来处理的。上面的那段代码相当于:

const element = React.createElement(
'div',
{id: 'login-button'},
'Login'
)

返回的React element对象相当于 =>

{
type: 'div',
props: {
children: 'Login',
id: 'login-button'
}
}

1.2 React Components

React中最核心的一个思想就是Component了,官方的解释是Component允许我们将UI拆分为独立可复用的代码片段,组件中可以包含多个其他组件,这样将组件一个个单独抽离出来,并最终再组合到一起,大大提高了代码的可读性(Readability)、可维护性(Maintainability)、可复用性(Reusability)和可测试性(Testability)。这也是 React 里用 Component 抽象所有 UI 的意义所在。

class Button extends React.Component {
render() {
const element = <div id='login-button>{ this.props.title }</div>
return (
<div>
{ element }
</div>
)
}

这段代码中Button就是一个React Component,这个component接受一个叫props的参数,返回描述UI的React element。

2、可以看出React Component接受props是一个对象,也就是所谓的一种数据结构,返回React Element也是一种对象,所谓的另外一种数据结构,所以我认为的React Component其实就是一个function,这个function的主要功能就是将一种数据结构(描述原始数据)转换成另外一种数据结构(描述UI)。React element仅仅是一个描述UI的对象,可以认为是一个中间状态,我们可以用最小的开销来创建或者销毁element对象。

3、React的核心思想总结下来就是这样的一个流程
1、原始数据到UI数据的转化 props -> React Component -> React Element
2、React Element的作用是将Component的创建跟描述状态分离,Component内部主要负责这个Component的构建,React Element主要用来做描述这个Component的状态
3、多个Component返回的多个Elements,这个流程是进行UI组合
4、React Element并不是一个渲染结果,React DOM的作用是将UI的状态(即Element)和UI的渲染分离,React DOM负责element的渲染
5、最后一个流程就是UI渲染了
上述这几个流程基本上代表了React的核心概念

怎么在iOS中运用React Component概念

说了这么多,其实iOS中缺少的就是这个Component概念,iOS原生的流程是原始数据到UI布局,再到UI绘制。复用的只是UI绘制结果的那个view(e.g. UICollectionViewCell)

在使用UICollectionView的时候,我们的数据都是通过DataSource方法返回给UICollectionView,UICollectionView拿到这些数据之后,就直接去绘制UICollectionViewCell了。所以每个列表页都得重新建一个UICollectionView,再引入自定义的UICollectionViewCell来绘制列表,所有的DataSource跟Delegate方法都得走一遍。所以我在想,我们可以按照React的那种方式来绘制列表么?将一个个UI控件抽象成一个个组件,再将这些组件组合到一起,绘制出最后的页面,React或者Weex的绘制列表其实就是waterfall这个列表component里面按照列表顺序插入自定义的cell component(组合)。那么我们其实可以在iOS中也可以有这个waterfall的component,这个component支持一个insertChildComponent:的方法,这个方法里就是插入自定义的CellComponent到waterfall这个组件中,并通过传入props来创建这个component。所以我就先定义了一个组件的基类BaseComponent

@protocol ComponentProtocol <NSObject>

/**
* 绘制组件
*
* @param view 展示该组件的view
*/
- (void)drawComponentInView:(UIView *)view withProps:(id)props;

/**
* 组件的尺寸
*
* @param props 该component的数据model
* @return 该组件的size
*/
+ (CGSize)componentSize:(id)props;

@end

@interface BaseComponent : NSObject <ComponentProtocol>

- (instancetype)initWithProps:(id)props;

@property (nonatomic, strong, readonly) id props;

所有的Component的创建都是通过传入props参数,来返回一个组件实例,每个Component还遵守一个ComponentProtocol的协议,协议里两个方法:

1、- (void)drawComponentInView:(UIView *)view withProps:(id)props; 每个component通过这个方法来进行native控件的绘制,参数中view是将会展示该组件的view,比方说WaterfallComponent中的该方法view为UIViewController的view,因为UIViewController的view会用来展示WaterfallComponent这个组件,'props'是该组件创建时传入的参数,这个参数用来告诉组件应该怎样绘制UI
2、+ (CGSize)componentSize:(id)props; 来描述组件的尺寸。

有了这个Component概念之后,我们原生的绘制流程就变成

1、创建Component,传入参数props
2、Component内部执行创建代码,保存props
3、当页面需要绘制的时候(React中的render命令),component内部会执行- (void)drawComponentInView:(UIView *)view withProps:(id)props;方法来描述并绘制UI

原生代码中想实现React element,其实不是一件简单的事情,因为原生没有类似JSX这种语言来生成一套只用来描述UI,并不绘制UI的中间状态的对象(可以做,比方说自己定义一套语法来描述UI),所以目前我的做法是在component内部,等到绘制命令来了之后,通过在- (void)drawComponentInView:(UIView *)view withProps:(id)props方法中,调用原生自定义的UIKit控件,通过props来绘制该UIKit

所以将通过封装component的方式,我们之前UIKit代表的UI组件转换成组件,把这些组件一个个单独抽离出来,再通过搭积木的方式,将各种组件一个个组合到一起,怎么绘制交给component内部去描述,而不是交给每个页面对应的UIViewController

Demo

Demo中,我会创建一个WaterfallComponent组件,还有多个CellComponent来绘制列表页,每个不一样列表页面(UIViewController)都可以创建一个WaterfallComponent组件,然后将不一样的CellComponent按照顺序插入到WaterfallComponent组件中,即可完成绘制列表,不需要每个页面再去处理UICollectionView的DataSource,Delegate方法。


WaterfallComponent内部会有一个UICollectionView,WaterfallComponent的insertChildComponent方法中,会创建一个dataController来管理数据源,并用来跟UICollectionView的DataSource方法进行交互从而绘制出列表页,最终UIViewController中绘制列表的方法如下:

self.waterfallComponent = [[WaterfallComponent alloc] initWithProps:nil];

for (NSDictionary *props in datas) {
if ([props[@"type"] isEqualToString:@"1"]) {
FirstCellComponent *cellComponent = [[FirstCellComponent alloc] initWithProps:props];
[self.waterfallComponent insertChildComponent:cellComponent];
} else if ([props[@"type"] isEqualToString:@"2"]) {
SecondCellComponent *cellComponent = [[SecondCellComponent alloc] initWithProps:props];
[self.waterfallComponent insertChildComponent:cellComponent];
}
}
[self.waterfallComponent drawComponentInView:self.view withProps:nil];

这样,每个我们自定义的Cell就可以以CellComponent的形式,被按照随意顺序插入到WaterfallComponent,从而做到了真正意义上的复用,Demo已上传到GitHub上,有兴趣的可以看

总结

React的核心思想是将组件一个个单独抽离出来,并最终再组合到一起,大大提高了代码的可读性、可维护性、可复用性和可测试性。这也是 React 里用 Component 抽象所有 UI 的意义所在。
原生开发中,使用Component的概念,用Component去抽象UIKit控件,也能达到同样的效果,这样也能统一每个开发使用UICollectionView时候的规范,也能统一对所有列表页的数据源做一些统一处理,比方说根据一个逻辑,统一在所有列表页,插入一个广告cell,这个逻辑完全可以在WaterfallComponent里统一处理。

思考

目前我们只用到了Component这个概念,其实React中,React Element的概念也是非常核心的,React Element隔离了UI描述跟UI绘制的逻辑,通过JSX来描述UI,并不去生成,绘制UI,这样我们能够以最小的代价来生成或者销毁React Elements,然后在交付给系统绘制elements里描述的UI,那么如果原生里也有这一套模板语言,那么我们就能真正做到在Component里,传入props,返回一个element描述UI,然后再交给系统去绘制,这样还能省去cell的创建,只创建CellComponent即可。其实我们可以通过定义一套语义去描述UI布局,然后通过解析这套语义,通过Core Text去做绘制,这一套还是值得我再去思考的。

链接:https://www.jianshu.com/p/bc4b13a0d312

收起阅读 »

Swift 5.0 值得关注的特性:增加 Result<T, E: Error> 枚举类型

HackingSwift: What’s new in Swift 5.0Result<T> 还是 Result<T, E: Error>背景在异步获取数据的场景中,常见的回调的数据结构是这样的:表示获取成功的数据,表示获取失败的 er...
继续阅读 »

HackingSwift: What’s new in Swift 5.0
Result<T> 还是 Result<T, E: Error>

背景

在异步获取数据的场景中,常见的回调的数据结构是这样的:表示获取成功的数据,表示获取失败的 error。因为数据可能获取成功,也可能失败。因此回调中的数据和错误都是 optional 类型。
比如 CloudKit 中保存数据的一个函数就是这样:

func save(_ record: CKRecord, completionHandler: @escaping (CKRecord?, Error?) -> Void)

这种形式的缺点是没有体现出两种结果的互斥关系:如果数据成功获取到了,那么 error 一定为空。如果 error 有值,数据一定是获取失败了。

Swift 中枚举的能力相比 OC 有着很大的进步,每个枚举值除了可以是常规的基础类型,还可以是一个关联的类型。有了这样的特性后用枚举来优化返回结果的数据结构显得水到渠成:

enum Result<Success, Failure> where Failure : Error {

/// A success, storing a `Success` value.
case success(Success)

/// A failure, storing a `Failure` value.
case failure(Failure)
}

基本用法

定义异步返回结果是 Int 类型的函数:

func fetchData(_ completionHandler: @escaping (Result<Int, Error>) -> Void) {
DispatchQueue.global().async {
let isSuccess = true
if isSuccess {
let resultValue = 6
return completionHandler(.success(resultValue))
} else {
let error = NSError(domain: "custom error", code: -1, userInfo: nil)
return completionHandler(.failure(error))
}
}
}

返回值的类型通过泛型进行约束,Result 第一个泛型类型表示返回值的类型,第二个类型表示错误的类型。对 Result 赋值和常规的枚举一样:

let valueResult: Result<Int, CustomError> = Result.success(4)

// 因为 swift 中会进行类型推断,编译器在确认返回的是 `Result` 类型后,可以省略枚举类型的声明
let errorResult = .failure(CustomError.inputNotValid)

取出 Result 值和获取普通的关联类型枚举是一样的:

fetchData { (result) in
switch result {
case .success(let value):
print(value)
case .failure(let error)
print(error.localizedDescription)
}
}

如果你只想要获取其中一项的值,也可以直接用 if case 拆包:

fetchDate { (result) in
if case .success(let value) = result {
print(value)
}
}

可以判等

Enum 是一个值类型,是一个值就应该可以判断是否相等。如果 Result 的成功和失败的类型都是 Equatable,那么 Result就可以判等,源码如下:

extension Result : Equatable where Success : Equatable, Failure : Equatable { }

类似的,如果是成功和失败的类型都是 Hashable,那么 Result 也是 Hashable:

extension Result : Hashable where Success : Hashable, Failure : Hashable { }

如果实现了 Hashable ,可以用来当做字典的 key。

辅助的 API

map、mapError
与 Dictionary 类似,Swift 为 Result 提供了几个 map value 和 error 的方法。

let intResult: Result<Int, Error> = Result.success(4)
let stringResult = x.map { (value) -> Result<String, Error> in
return .success("map")
}

let originError = NSError(domain: "origin error", code: -1, userInfo: nil)
let errorResult: Result<Int, Error> = .failure(originError)
let newErrorResult = errorResult.mapError { (error) -> Error in
let newError = NSError(domain: "new error", code: -2, userInfo: nil)
return newError
}

flatMap、flatMapError
map 返回的是具体的结果和错误, flatMap 闭包中返回的是 Result 类型。如果 Result 中包含的是数据,效果和 map 一致,替换数据;如果 Result 中包含的是错误,那么不替换结果。

let intResult: Result<Int, Error> = Result.success(4)

// 替换成功
let flatMapResult = intResult.flatMap { (value) -> Result<String, Error> in
return .success("flatMap")
}

// 没有执行替换操作,flatMapIntResult 值还是 intResult
let flatMapIntResult = intResult.flatMap { (value) -> Result<String, Error> in
return .failure(NSError(domain: "origin error", code: -1, userInfo: nil))
}

get
很多时候只关心 Result 的值,Swift 提供了 get() 函数来便捷的直接获取值,需要注意的是这个函数被标记为 throws,使用时语句前需要加上 try:

let intResult: Result<Int, Error> = Result.success(4)

let value = try? intResult.get()

可抛出异常的闭包初始化器

很多时候获取返回值的闭包中可能会发生异常代表获取失败的错误,基于这个场景 Swift 提供了一个可抛出异常的闭包初始化器:

enum CustomError: Error, Equatable {
case inputNotValid
}

let fetchInt = { () -> Int in
if true {
return 4
} else {
throw CustomError.inputNotValid
}
}

let result: Result<Int, Error> = Result { try fetchInt() }

需要提醒是通过这种方式声明的 Result 的 error 类型只能是 Error,不能指定特定的 Error。

转自:https://www.jianshu.com/p/a3712edc9367

收起阅读 »

运行时Hook所有Block方法调用的技术实现

1.方法调用的几种Hook机制iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控拦截或者统计一些系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。当我们想Hook一...
继续阅读 »

1.方法调用的几种Hook机制

iOS系统中一共有:C函数、Block、OC类方法三种形式的方法调用。Hook一个方法调用的目的一般是为了监控拦截或者统计一些系统的行为。Hook的机制有很多种,通常良好的Hook方法都是以AOP的形式来实现的。

当我们想Hook一个OC类的某些具体的方法时可以通过Method Swizzling技术来实现、当我们想Hook动态库中导出的某个C函数时可以通过修改导入函数地址表中的信息来实现(可以使用开源库fishhook来完成)、当我们想Hook所有OC类的方法时则可以通过替换objc_msgSend系列函数来实现。。。

那么对于Block方法呢而言呢?

2.Block的内部实现原理和实现机制简介

这里假定你对Block内部实现原理和运行机制有所了解,如果不了解则请参考文章《深入解构iOS的block闭包实现原理》或者自行通过搜索引擎搜索。

源程序中定义的每个Block在编译时都会转化为一个和OC类对象布局相似的对象,每个Block也存在着isa这个数据成员,根据isa指向的不同,Block分为__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三种类型。也就是说从某种程度上Block对象也是一种OC对象。下面的类图描述了Block类的层次结构。


Block类以及其派生类在CoreFoundation.framework中被定义和实现,并且没有对外公开。

每个Block对象在内存中的布局,也就是Block对象的存储结构被定义如下(代码出自苹果开源出来的库实现libclosure中的文件Block_private.h):

//需要注意的是下面两个只是模板,具体的每个Block定义时总是按这个模板来定义的。

//Block描述,每个Block一个描述并定义在全局数据段
struct Block_descriptor_1 {
uintptr_t reserved; //记住这个变量和结构体,它很重要!!
uintptr_t size;
};

//Block对象的内存布局
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block对象的实现函数
struct Block_descriptor_1 *descriptor;
// imported variables,这里是每个block对象的特定数据成员区域
};

这里要关注一下struct Block_descriptor_1中的reserved这个数据成员,虽然系统没有用到它,但是下面就会用到它而且很重要!

在了解了Block对象的类型以及Block对象的内存布局后,再来考察一下一个Block从定义到调用是如何实现的。就以下面的源代码为例:

int main(int argc, char *argv[])
{
//定义
int a = 10;
void (^testblock)(void) = ^(){
NSLog(@"Hello world!%d", a);
};

//执行
testblock();

return 0;
}

在将OC代码翻译为C语言代码后每个Block的定义和调用将变成如下的伪代码:

//testblock的描述信息
struct Block_descriptor_1_fortestblock {
uintptr_t reserved;
uintptr_t size;
};

//testblock的布局存储结构体
struct Block_layout_fortestblock {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
uintptr_t invoke; //Block对象的实现函数
struct Block_descriptor_1_fortestblock *descriptor;
int m_a; //外部的传递进来的数据。
};

//testblock函数的实现。
void main_invoke_fortestblock(struct Block_layout_fortestblock *cself)
{
NSLog(@"Hello world!%d", cself->m_a);
}

//testblock对象描述的实例,存储在全局内存区
struct Block_descriptor_1_fortestblock _testblockdesc = {0, sizeof(struct Block_layout_fortestblock)};

int main(int argc, char *argv[])
{
//定义部分
int a = 10;
struct Block_layout_fortestblock testblock = {
.isa = __NSConcreteStackBlock,
.flags =0,
.reserved = 0,
.invoke = main_invoke_fortestblock,
.descriptor = & _testblockdesc,
.m_a = a
};

//调用部分
testblock.invoke();

return 0;
}

可以看出Block对象的生成和调用都是在编译期间就已经固定在代码中了,它不像其他OC对象调用方法时需要通过runtime来执行间接调用。并且线上程序中所有关于Block的符号信息都会被strip掉。所以上述的所介绍的几种Hook方法都无法Hook住一个Block对象的函数调用。

如果想要Hook住系统的所有Block调用,需要解决如下几个问题:
a. 如何在运行时将所有的Block的invoke函数替换为一个统一的Hook函数。
b. 这个统一的Hook函数如何调用原始Block的invoke函数。
c. 如何构建这个统一的Hook函数。

3.实现Block对象Hook的方法和原理

一个OC类对象的实例通过引用计数来管理对象的生命周期。在MRC时代当对象进行赋值和拷贝时需要通过调用retain方法来实现引用计数的增加,而在ARC时代对象进行赋值和拷贝时就不再需要显示调用retain方法了,而是系统内部在编译时会自动插入相应的代码来实现引用计数的添加和减少。不管如何只要是对OC对象执行赋值拷贝操作,最终内部都会调用retain方法。

Block对象也是一种OC对象!!

每当一个Block对象在需要进行赋值或者拷贝操作时,也会激发对retain方法的调用。因为Block对象赋值操作一般是发生在Block方法执行之前,因此我们可以通过Method Swizzling的机制来Hook 类的retain方法,然后在重写的retain方法内部将Block对象的invoke数据成员替换为一个统一的Hook函数!

通过考察__NSStackBlock、__NSMallocBlock、__NSGlobalBlock 三个类的实现发现这三个类都重载了NSObject的retain方法,这样在执行Method Swizzling时就不需要对NSObject的retain方法执行替换,而只要对上述三个类的retain执行替换即可。

你可以说出为什么这三个派生类都会对retain方法进行重载吗?答案可以从这三种Block的类型定义以及所表示的意义中去寻找。

Block技术不仅可以用在OC语言中,LLVM对C语言进行的扩展也能使用Block,比如gcd库中大量的使用了Block。在C语言中如果对一个Block进行赋值或者拷贝系统需要通过C库函数:

//函数声明在Block.h头文件汇总
// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

来实现,这个函数定义在libsystem_blocks.dylib库中,并且库实现已经开源:libclosure。因此可以借助fishhook库来对__Block_copy这个函数进行替换处理,然后在替换的函数函数中将一个Block的原始的invoke函数替换为统一的Hook函数。

另外一个C语言函数objc_retainBlock,也是实现了对Block进行赋值时的引用计数增加,这个函数内部就是简单的调用__Block_copy方法。因此我们也可以添加对objc_retainBlock的替换处理。

解决了第一个问题后,接下来再解决第二个问题。还记得上面提到过的struct Block_descriptor_1中的reserved这个数据成员吗? 当我们通过上述的方法对所有Block对象的invoke成员替换为一个统一的Hook函数前,可以将Block对象的原始invoke函数保存到这个保留字段中去。然后就可以在统一的Hook函数内部读取这个保留字段中的保存的原始invoke函数来执行真实的方法调用了。

因为一个Block对象函数的第一个参数其实是一个隐藏的参数,这个隐藏的参数就是Block对象本身,因此很容易就可以从隐藏的参数中来获取到对应的保留字段。

下面的代码将展示通过方法交换来实现Hook处理的伪代码

struct Block_descriptor {
void *reserved;
uintptr_t size;
};

struct Block_layout {
void *isa;
int32_t flags; // contains ref count
int32_t reserved;
void *invoke;
struct Block_descriptor *descriptor;
};

//统一的Hook函数,这里以伪代码的形式提供
void blockhook(void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//调用原始的invoke函数
layout->descriptor->reserved(...);
}
//模拟器下如果返回类型是结构体并且大于16字节那么第一个参数是返回值保存的内存地址,block对象变为第二个参数
void blockhook_stret(void *pret, void *obj, ...)
{
struct Block_layout *layout = (struct Block_layout*) obj;
//调用原始的invoke函数
layout->descriptor->reserved(...);
}

//执行Block对象的方法替换处理
void replaceBlockInvokeFunction(const void *blockObj)
{
struct Block_layout *layout = (struct Block_layout*)blockObj;
if (layout != NULL && layout->descriptor != NULL){
int32_t BLOCK_USE_STRET = (1 << 29); //如果模拟器下返回的类型是一个大于16字节的结构体,那么block的第一个参数为返回的指针,而不是block对象。
void *hookfunc = ((layout->flags & BLOCK_USE_STRET) == BLOCK_USE_STRET) ? blockhook_stret : blockhook;
if (layout->invoke != hookfunc){
layout->descriptor->reserved = layout->invoke;
layout->invoke = hookfunc;
}
}
}

void *(*__NSStackBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSStackBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSStackBlock_retain_old(obj, cmd);
}

void *(*__NSMallocBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSMallocBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSMallocBlock_retain_old(obj, cmd);
}

void *(*__NSGlobalBlock_retain_old)(void *obj, SEL cmd) = NULL;
void *__NSGlobalBlock_retain_new(void *obj, SEL cmd)
{
replaceBlockInvokeFunction(obj);
return __NSGlobalBlock_retain_old(obj, cmd);
}

int main(int argc, char *argv[])
{
//因为类名和方法名都不能直接使用,所以这里都以字符串的形式来转换获取。
__NSStackBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSStackBlock"), sel_registerName("retain"), (IMP)__NSStackBlock_retain_new, nil);
__NSMallocBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSMallocBlock"), sel_registerName("retain"), (IMP)__NSMallocBlock_retain_new, nil);
__NSGlobalBlock_retain_old = (void *(*)(void*,SEL))class_replaceMethod(NSClassFromString(@"__NSGlobalBlock"), sel_registerName("retain"), (IMP)__NSGlobalBlock_retain_new, nil);

return 0;
}

解决了第二个问题后,就需要解决第三个问题。上面的统一Hook函数blockhook和block_stret只是伪代码实现,因为任何一个Block中的函数的参数类型和个数是不一样的,而且统一Hook函数也需要在适当的时候调用原始的默认Block函数实现,并且不能破坏参数信息。为了解决这些问题就使得这个统一的Hook函数不能用高级语言来实现,而只能用汇编语言来实现。下面就是在arm64位体系下的实现代码:

.text
.align 5
.private_extern _blockhook
_blockhook:
//为了不破坏原有参数,这里将所有参数压入栈中
stp q6, q7, [sp, #-0x20]!
stp q4, q5, [sp, #-0x20]!
stp q2, q3, [sp, #-0x20]!
stp q0, q1, [sp, #-0x20]!
stp x6, x7, [sp, #-0x10]!
stp x4, x5, [sp, #-0x10]!
stp x2, x3, [sp, #-0x10]!
stp x0, x1, [sp, #-0x10]!
stp x8, x30, [sp, #-0x10]!

//这里可以添加任意逻辑来进行hook处理。

//这里将所有参数还原
ldp x8, x30, [sp], #0x10
ldp x0, x1, [sp], #0x10
ldp x2, x3, [sp], #0x10
ldp x4, x5, [sp], #0x10
ldp x6, x7, [sp], #0x10
ldp q0, q1, [sp], #0x20
ldp q2, q3, [sp], #0x20
ldp q4, q5, [sp], #0x20
ldp q6, q7, [sp], #0x20

ldr x16, [x0, #0x18] //将block对象的descriptor数据成员取出
ldr x16, [x16] //获取descriptor中的reserved成员
br x16 //执行reserved中保存的原始函数指针。
LExit_blockhook:

对于x86_64/arm32位系统来说,如果block函数的返回是一个结构体并且长度超过16字节(arm32是8字节)。那么block对象里面的flags属性就会设置为BLOCK_USE_STRET。而x86_64/arm32位系统对于这种返回类型的函数就会将返回值存放到第一个参数所指向的内存中,同时会把原本的block对象变化为第二个参数,因此需要对这种情况进行特殊处理。

关于在运行时Hook所有Block方法调用的技术实现原理就介绍到这里了。当然一个完整的系统可能需要其他一些能力:

1、如果你只想Hook可执行程序中定义的Block,那么请参考我的文章:深入iOS系统底层之映像操作API介绍 中的内容来实现Hook函数的过滤处理。
2、如果你不想借助Block_descriptor中的reserved来保存原始的invoke函数,那么可以参考我的文章:Thunk程序的实现原理以及在iOS中的应用(二)中介绍的技术来实现统一Hook函数以及完成对原始invoke函数的调用技术。

具体完整的代码可以访问我的github中的项目:YSBlockHook。这个项目以AOP的形式实现了真机arm64位模式下对可执行程序中所有定义的Block进行Hook的方法,Hook所做的事情就是在所有Block调用前,打印出这个Block的符号信息。

链接:https://www.jianshu.com/p/0a3d00485c7f

收起阅读 »

性能超高的UI库-AsyncDisplayKit

AsyncDisplayKit 已移动并重命名:Texture性能提升AsyncDisplayKit 的基本单位是node. ASDisplayNode 是对 的抽象UIView,而后者又是对 的抽象CALayer。与只能在主线程上使用的视图不同,节...
继续阅读 »

AsyncDisplayKit 已移动并重命名:Texture

性能提升

AsyncDisplayKit 的基本单位是nodeASDisplayNode 是对 的抽象UIView,而后者又是对 的抽象CALayer与只能在主线程上使用的视图不同,节点是线程安全的:您可以在后台线程上并行实例化和配置它们的整个层次结构。

为了保持其用户界面流畅和响应迅速,您的应用程序应以每秒 60 帧的速度呈现——这是 iOS 的黄金标准。这意味着主线程有六十分之一秒来推动每一帧。执行所有布局和绘图代码需要 16 毫秒!并且由于系统开销,您的代码在导致丢帧之前的运行时间通常不到 10 毫秒。

AsyncDisplayKit 允许您将图像解码、文本大小调整和渲染、布局和其他昂贵的 UI 操作移出主线程,以保持主线程可用于响应用户交互。


随着框架的发展,添加了许多功能,通过消除现代 iOS 应用程序中常见的样板样式结构,可以为开发人员节省大量时间。如果您曾经处理过单元格重用错误,尝试为页面或滚动样式界面高效地预加载数据,或者甚至只是试图防止您的应用丢失太多帧,您都可以从集成 ASDK 中受益。


详细的api介绍:

https://texturegroup.org/appledocs.html


常见问题及demo下载:

https://github.com/facebookarchive/AsyncDisplayKit


源码下载:




收起阅读 »

java设计模式:享元模式

前言在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。 例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些...
继续阅读 »

前言

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。


例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。


定义

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。


优点

相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。


缺点

为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
读取享元模式的外部状态会使得运行时间稍微变长。

享元模式的结构与实现

享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。

比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。


享元模式的本质是缓存共享对象,降低内存消耗。


结构

抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

享元模式的实现

应用实例的话,其实上面的模板就已经是一个很好的例子了,类似于String常量池,没有的对象创建后存在池中,若池中存在该对象则直接从池中取出。


  为了更好的理解享元模式,这里再举一个实例,比如接了我一个小型的外包项目,是做一个产品展示网站,后来他的朋友们也希望做这样的网站,但要求都有些不同,我们当然不能直接复制粘贴再来一份,有任希望是新闻发布形式的,有人希望是博客形式的等等,而且因为经费原因不能每个网站租用一个空间。


  其实这里他们需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,这是造成服务器的大量资源浪费。如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源;而对于代码,由于是一份实例,维护和扩展都更加容易。


  那么此时就可以用到享元模式了。UML图如下:
  在这里插入图片描述
网站抽象类
 

 public abstract class WebSite {

public abstract void use();

}

具体网站类


public class ConcreteWebSite extends WebSite {

private String name = "";

public ConcreteWebSite(String name) {
this.name = name;
}

@Override
public void use() {
System.out.println("网站分类:" + name);
}

}

网络工厂类
  这里使用HashMap来作为池,通过put和get方法实现加入池与从池中取的操作。

public class WebSiteFactory {

private HashMap<String, ConcreteWebSite> pool = new HashMap<>();

//获得网站分类
public WebSite getWebSiteCategory(String key) {
if(!pool.containsKey(key)) {
pool.put(key, new ConcreteWebSite(key));
}

return (WebSite)pool.get(key);
}

//获得网站分类总数
public int getWebSiteCount() {
return pool.size();
}

}

Client客户端
  这里测试用例给了两种网站,原先我们需要做三个产品展示和三个博客的网站,也即需要六个网站类的实例,但其实它们本质上都是一样的代码,可以利用用户ID号的不同,来区分不同的用户,具体数据和模板可以不同,但代码核心和数据库却是共享的。

public class Client {

public static void main(String[] args) {
WebSiteFactory factory = new WebSiteFactory();

WebSite fx = factory.getWebSiteCategory("产品展示");
fx.use();

WebSite fy = factory.getWebSiteCategory("产品展示");
fy.use();

WebSite fz = factory.getWebSiteCategory("产品展示");
fz.use();

WebSite fa = factory.getWebSiteCategory("博客");
fa.use();

WebSite fb = factory.getWebSiteCategory("博客");
fb.use();

WebSite fc = factory.getWebSiteCategory("博客");
fc.use();

System.out.println("网站分类总数为:" + factory.getWebSiteCount());
}

}

源码中的享元模式

享元模式很重要,因为它能帮你在一个复杂的系统中大量的节省内存空间。在JAVA语言中,String类型就是使用了享元模式。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,JAVA会确保一个字符串常量在常量池中只有一个拷贝。String a=”abc”,其中”abc”就是一个字符串常量。


熟悉java的应该知道下面这个例子:


Stringa="hello";
Stringb="hello";
if(a==b)
 System.out.println("OK");
else
 System.out.println("Error");

输出结果是:OK。可以看出if条件比较的是两a和b的地址,也可以说是内存空间 核心总结,可以共享的对象,也就是说返回的同一类型的对象其实是同一实例,当客户端要求生成一个对象时,工厂会检测是否存在此对象的实例,如果存在那么直接返回此对象实例,如果不存在就创建一个并保存起来,这点有些单例模式的意思。通常工厂类会有一个集合类型的成员变量来用以保存对象,如hashtable,vector等。在java中,数据库连接池,线程池等即是用享元模式的应用。


首先String不属于8种基本数据类型,String是一个对象。
因为对象的默认值是null,所以String的默认值也是null;但它又是一种特殊的对象,有其它对象没有的一些特性。
new String()和new String(“”)都是申明一个新的空字符串,是空串不是null;
String str=”kvill”;
String str=new String (“kvill”);的区别:
在这里,我们不谈堆,也不谈栈,只先简单引入常量池这个简单的概念。
常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。
看例1:

String s0=”kvill”; 

String s1=”kvill”;

String s2=”kv” + “ill”;

System.out.println( s0==s1 );

System.out.println( s0==s2 );

结果为:


true 

true

首先,我们要知结果为道Java会确保一个字符串常量只有一个拷贝。
因为例子中的s0和s1中的”kvill”都是字符串常量,它们在编译期就被确定了,所以s0==s1为true;而”kv”和”ill”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,所以s2也是常量池中”kvill”的一个引用。

所以我们得出s0==s1==s2;


用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。


看例2:


String s0=”kvill”; 

String s1=new String(”kvill”);

String s2=”kv” + new String(“ill”);

System.out.println( s0==s1 );

System.out.println( s0==s2 );

System.out.println( s1==s2 );

结果为:


false 

false

false

例2中s0还是常量池中”kvill”的应用,s1因为无法在编译期确定,所以是运行时创建的新对象”kvill”的引用,s2因为有后半部分new String(“ill”)所以也无法在编译期确定,所以也是一个新创建对象”kvill”的应用;明白了这些也就知道为何得出此结果了。


String.intern():

再补充介绍一点:存在于.class文件中的常量池,在运行期被JVM装载,并且可以扩充。String的intern()方法就是扩充常量池的一个方法;当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用;看例3就清楚了


例3:


String s0= “kvill”; 

String s1=new String(”kvill”);

String s2=new String(“kvill”);

System.out.println( s0==s1 );

System.out.println( “**********” );

s1.intern();

s2=s2.intern(); //把常量池中“kvill”的引用赋给s2

System.out.println( s0==s1);

System.out.println( s0==s1.intern() );

System.out.println( s0==s2 );

结果为:


false 

**********

false //虽然执行了s1.intern(),但它的返回值没有赋给s1

true //说明s1.intern()返回的是常量池中”kvill”的引用

true

最后我再破除一个错误的理解:


有人说,“使用String.intern()方法则可以将一个String类的保存到一个全局String表中,如果具有相同值的Unicode字符串已经在这个表中,那么该方法返回表中已有字符串的地址,如果在表中没有相同值的字符串,则将自己的地址注册到表中“如果我把他说的这个全局的String表理解为常量池的话,他的最后一句话,“如果在表中没有相同值的字符串,则将自己的地址注册到表中”是错的:


看例4:


String s1=new String("kvill"); 

String s2=s1.intern();

System.out.println( s1==s1.intern() );

System.out.println( s1+" "+s2 );

System.out.println( s2==s1.intern() );

结果:


false 

kvill kvill

true

在这个类中我们没有声名一个”kvill”常量,所以常量池中一开始是没有”kvill”的,当我们调用s1.intern()后就在常量池中新添加了一个”kvill”常量,原来的不在常量池中的”kvill”仍然存在,也就不是“将自己的地址注册到常量池中”了。


s1==s1.intern()为false说明原来的“kvill”仍然存在;


s2现在为常量池中“kvill”的地址,所以有s2==s1.intern()为true。


关于equals()和==:

这个对于String简单来说就是比较两字符串的Unicode序列是否相当,如果相等返回true;而==是比较两字符串的地址是否相同,也就是是否是同一个字符串的引用。


关于String是不可变的

这一说又要说很多,大家只要知道String的实例一旦生成就不会再改变了,比如说:String str=”kv”+”ill”+” “+”ans”;


就是有4个字符串常量,首先”kv”和”ill”生成了”kvill”存在内存中,然后”kvill”又和” “ 生成 ”kvill “存在内存中,最后又和生成了”kvill ans”;并把这个字符串的地址赋给了str,就是因为String的“不可变”产生了很多临时变量,这也就是为什么建议用StringBuffer的原因了,因为StringBuffer是可改变的。


okhttp3 kotlin ConnectionPool 源码分析

ConnectionPool的说明:
管理http和http/2的链接,以便减少网络请求延迟。同一个address将共享同一个connection。该类实现了复用连接的目标。

class RealConnectionPool(
/** 每个address的最大空闲连接数 */
private val maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
/**
* Background threads are used to cleanup expired connections. There will be at most a single
* thread running per connection pool. The thread pool executor permits the pool itself to be
* garbage collected.
*/
//这是一个用于清楚过期链接的线程池,每个线程池最多只能运行一个线程,并且这个线程池允许被垃圾回收
private val executor = ThreadPoolExecutor(
0, // corePoolSize.
Int.MAX_VALUE, // maximumPoolSize.
60L, TimeUnit.SECONDS, // keepAliveTime.
SynchronousQueue(),
threadFactory("OkHttp ConnectionPool", true)
)
//双向队列
private val connections = ArrayDeque<RealConnection>()
//路由的数据库
val routeDatabase = RouteDatabase()
//清理任务正在执行的标志
var cleanupRunning: Boolean = false


  1. 主要就是connections,可见ConnectionPool内部以队列方式存储连接;
  2. routDatabase是一个黑名单,用来记录不可用的route,但是看代码貌似ConnectionPool并没有使用它。所以此处不做分析。
  3. 剩下的就是和清理有关了,所以executor是清理任务的线程池,cleanupRunning是清理任务的标志,cleanupRunnable是清理任务。

class ConnectionPool(
maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) {
//创建一个适用于单个应用程序的新连接池。
//该连接池的参数将在未来的okhttp中发生改变
//目前最多可容乃5个空闲的连接,存活期是5分钟
constructor() : this(5, 5, TimeUnit.MINUTES)
}

init {
//保持活着的时间,否则清理将旋转循环
require(keepAliveDuration > 0L) { "keepAliveDuration <= 0: $keepAliveDuration" }
}

通过这个构造器我们知道了这个连接池最多维持5个连接,且每个链接最多活5分钟。并且包含一个线程池包含一个清理任务。
所以maxIdleConnections和keepAliveDurationNs则是清理中淘汰连接的的指标,这里需要说明的是maxIdleConnections是值每个地址上最大的空闲连接数。所以OkHttp只是限制与同一个远程服务器的空闲连接数量,对整体的空闲连接并没有限制。

这时候说下ConnectionPool的实例化的过程,一个OkHttpClient只包含一个ConnectionPool,其实例化也是在OkHttpClient的过程。这里说一下ConnectionPool各个方法的调用并没有直接对外暴露,而是通过OkHttpClient的Internal接口统一对外暴露。


然后我们来看下他的transmitterAcquirePooledConnection(获取连接)和put方法


fun transmitterAcquirePooledConnection(
address: Address,
transmitter: Transmitter,
routes: List<Route>?,
requireMultiplexed: Boolean
): Boolean {
//断言,判断线程是不是被自己锁住了
assert(Thread.holdsLock(this))
// 遍历已有连接集合
for (connection in connections) {
if (requireMultiplexed && !connection.isMultiplexed) continue
//如果connection和需求中的"地址"和"路由"匹配
if (!connection.isEligible(address, routes)) continue
//复用这个连接
transmitter.acquireConnectionNoEvents(connection)

return true
}
return false
}

put方法更为简单,就是异步触发清理任务,然后将连接添加到队列中


  fun put(connection: RealConnection) {
assert(Thread.holdsLock(this))
if (!cleanupRunning) {
cleanupRunning = true
executor.execute(cleanupRunnable)
}
connections.add(connection)
}

private val cleanupRunnable = object : Runnable {
override fun run() {
while (true) {
val waitNanos = cleanup(System.nanoTime())
if (waitNanos == -1L) return
try {
this@RealConnectionPool.lockAndWaitNanos(waitNanos)
} catch (ie: InterruptedException) {
// Will cause the thread to exit unless other connections are created!
evictAll()
}
}
}
}
这个逻辑也很简单,就是调用cleanup方法执行清理,并等待一段时间,持续清理,其中cleanup方法返回的值来来决定而等待的时间长度。那我们继续来看下cleanup函数:
fun cleanup(now: Long): Long {
var inUseConnectionCount = 0
var idleConnectionCount = 0
var longestIdleConnection: RealConnection? = null
var longestIdleDurationNs = Long.MIN_VALUE

// Find either a connection to evict, or the time that the next eviction is due.
synchronized(this) {
for (connection in connections) {
// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++
continue
}
//统计空闲连接数量
idleConnectionCount++

// If the connection is ready to be evicted, we're done.
val idleDurationNs = now - connection.idleAtNanos
if (idleDurationNs > longestIdleDurationNs) {
//找出空闲时间最长的连接以及对应的空闲时间
longestIdleDurationNs = idleDurationNs
longestIdleConnection = connection
}
}

when {
longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections -> {
// We've found a connection to evict. Remove it from the list, then close it below
// (outside of the synchronized block).
//在符合清理条件下,清理空闲时间最长的连接
connections.remove(longestIdleConnection)
}
idleConnectionCount > 0 -> {
// A connection will be ready to evict soon.
//不符合清理条件,则返回下次需要执行清理的等待时间,也就是此连接即将到期的时间
return keepAliveDurationNs - longestIdleDurationNs
}
inUseConnectionCount > 0 -> {
// All connections are in use. It'll be at least the keep alive duration 'til we run
// again.
//没有空闲的连接,则隔keepAliveDuration(分钟)之后再次执行
return keepAliveDurationNs
}
else -> {
// No connections, idle or in use.
cleanupRunning = false
return -1
}
}
}
//关闭socket资源
longestIdleConnection!!.socket().closeQuietly()

// Cleanup again immediately.
//这里是在清理一个空闲时间最长的连接以后会执行到这里,需要立即再次执行清理
return 0
}

这里的首先统计空闲连接数量,然后通过for循环查找最长空闲时间的连接以及对应空闲时长,然后判断是否超出最大空闲连接数(maxIdleConnections)或者或者超过最大空闲时间(keepAliveDurationNs),满足其一则清除最长空闲时长的连接。如果不满足清理条件,则返回一个对应等待时间。
这个对应等待的时间又分二种情况:


  1. 有连接则等待下次需要清理的时间去清理:keepAliveDurationNs-longestIdleDurationNs;
  2. 没有空闲的连接,则等下一个周期去清理:keepAliveDurationNs
    如果清理完毕返回-1。

综上所述,我们来梳理一下清理任务,清理任务就是异步执行的,遵循两个指标,最大空闲连接数量和最大空闲时长,满足其一则清理空闲时长最大的那个连接,然后循环执行,要么等待一段时间,要么继续清理下一个连接,知道清理所有连接,清理任务才结束,下一次put的时候,如果已经停止的清理任务则会被再次触发


private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
val references = connection.transmitters
var i = 0
//遍历弱引用列表
while (i < references.size) {
val reference = references[i]
//若StreamAllocation被使用则接着循环
if (reference.get() != null) {
i++
continue
}

// We've discovered a leaked transmitter. This is an application bug.
val transmitterRef = reference as TransmitterReference
val message = "A connection to ${connection.route().address.url} was leaked. " +
"Did you forget to close a response body?"
Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace)
//若StreamAllocation未被使用则移除引用,这边注释为泄露
references.removeAt(i)
connection.noNewExchanges = true
//如果列表为空则说明此连接没有被引用了,则返回0,表示此连接是空闲连接
// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs
return 0
}
}

return references.size
}

pruneAndGetAllocationCount主要是用来标记泄露连接的。内部通过遍历传入进来的RealConnection的StreamAllocation列表,如果StreamAllocation被使用则接着遍历下一个StreamAllocation。如果StreamAllocation未被使用则从列表中移除,如果列表中为空则说明此连接连接没有引用了,返回0,表示此连接是空闲连接,否则就返回非0表示此连接是活跃连接。
接下来让我看下ConnectionPool的connectionBecameIdle()方法,就是当有连接空闲时,唤起cleanup线程清洗连接池

fun connectionBecameIdle(connection: RealConnection): Boolean {
assert(Thread.holdsLock(this))
//该连接已经不可用
return if (connection.noNewExchanges || maxIdleConnections == 0) {
connections.remove(connection)
true
} else {
// Awake the cleanup thread: we may have exceeded the idle connection limit.
//欢迎clean 线程
this.notifyAll()
false
}
}

connectionBecameIdle标示一个连接处于空闲状态,即没有流任务,那么久需要调用该方法,由ConnectionPool来决定是否需要清理该连接。
再来看下evictAll()方法

fun evictAll() {
val evictedConnections = mutableListOf<RealConnection>()
synchronized(this) {
val i = connections.iterator()
while (i.hasNext()) {
val connection = i.next()
if (connection.transmitters.isEmpty()) {
connection.noNewExchanges = true
evictedConnections.add(connection)
i.remove()
}
}
}

for (connection in evictedConnections) {
connection.socket().closeQuietly()
}
}

该方法是删除所有空闲的连接,比较简单,不说了


Integer中的享元模式

那么我们来看看Integer中的享元模式具体是怎么样的吧。
通过如下代码了解一下integer的比较

public static void main(String[] args)
{
Integer integer1 = 9;
Integer integer2 = 9;
System.out.println(integer1==integer2);

Integer integer3 = 129;
Integer integer4 = 129;
System.out.println(integer3==integer4);
}

输出:


true
false

在通过等号赋值的时候,实际上是通过调用valueOf方法的返回一个对象。然后我们观察一下这个方法的源码。


public final class Integer extends Number implements Comparable<Integer> {
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private final int value;
public Integer(int value) {
this.value = value;
}
}

上面是我简化了的Integer类。平常在使用Integer类的时候。你是否思考过用valueOf还是用new创建Integer对象。看完源码就会发现在valueOf这个方法中它会先判断传进去的值是否在IntegerCache中,如果不在就创建新的对象,在就直接返回缓存池里的对象。这个valueOf方法就用到享元模式。它将-128到127的Integer对象先在缓存池里创建好,等我们需要的时候直接返回即可。所以在-128到127中的数值我们用valueOf创建会比new更快。因此我们在使用Integer对象的时候,也一定要记住使用equals(),而不是单纯的使用”==”,否则有可能出现不相等的情况。


收起阅读 »

java设计模式:桥接模式

桥接模式的定义与特点桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。 通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了...
继续阅读 »

桥接模式的定义与特点

桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。


通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。


优点


  • 抽象与实现分离,扩展能力强
  • 符合开闭原则
  • 符合合成复用原则
  • 其实现细节对客户透明

缺点

由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。


桥接模式的结构与实现

可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。


模式的结构

抽象化角色:定义抽象类,并包含一个对实现化对象的引用。
扩展抽象化角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
实现化角色:定义实现化角色的接口,供扩展抽象化角色调用。
具体实现化角色:给出实现化角色接口的具体实现。

桥接模式的应用场景

当一个类内部具备两种或多种变化维度时,使用桥接模式可以解耦这些变化的维度,使高层代码架构稳定。


桥接模式通常适用于以下场景。



  1. 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  2. 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
  3. 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。

桥接模式的一个常见使用场景就是替换继承。我们知道,继承拥有很多优点,比如,抽象、封装、多态等,父类封装共性,子类实现特性。继承可以很好的实现代码复用(封装)的功能,但这也是继承的一大缺点。


示例代码:
在这里插入图片描述


//抽象类:建筑
public abstract class Building {
protected Paint paint;
public Building(Paint paint) {
this.paint = paint;
}
public abstract void decorate();
}
//接口:油漆
public interface Paint {
void decorateImpl();
}
//教学楼
public class TeachingBuilding extends Building {
public TeachingBuilding(Paint paint) {
super(paint);
}

@Override
public void decorate() {
System.out.print("普通的教学楼");
paint.decorateImpl();
}
}
//实验楼
public class LaboratoryBuilding extends Building {
public LaboratoryBuilding(Paint paint) {
super(paint);
}

@Override
public void decorate() {
System.out.print("普通的实验楼");
paint.decorateImpl();
}
}
public class RedPaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被红色油漆装饰过。");
}
}
public class GreenPaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被绿色油漆装饰过。");
}
}
public class BulePaint implements Paint {
@Override
public void decorateImpl() {
System.out.println("被蓝色油漆装饰过。");
}
}
public class BridgePatternDemo {
public static void main(String[] args) {
//普通的教学楼被红色油漆装饰。
Building redTeachingBuilding=new TeachingBuilding(new RedPaint());
redTeachingBuilding.decorate();
//普通的教学楼被绿色油漆装饰。
Building greenTeachingBuilding1=new TeachingBuilding(new GreenPaint());
greenTeachingBuilding1.decorate();
//普通的实验楼被红色油漆装饰。
Building redLaboratoryBuilding=new LaboratoryBuilding(new RedPaint());
redLaboratoryBuilding.decorate();
//普通的实验楼被绿色油漆装饰。
Building greenLaboratoryBuilding=new LaboratoryBuilding(new GreenPaint());
greenLaboratoryBuilding.decorate();
//普通的实验楼被蓝色油漆装饰。
Building blueLaboratoryBuilding=new LaboratoryBuilding(new BulePaint());
blueLaboratoryBuilding.decorate();
}
}

运行结果:
普通的教学楼被红色油漆装饰过。
普通的教学楼被绿色油漆装饰过。
普通的实验楼被红色油漆装饰过。
普通的实验楼被绿色油漆装饰过。
普通的实验楼被蓝色油漆装饰过。

桥接模式与装饰模式对比:

两个模式都是为了解决子类过多问题, 但他们的诱因不同:



  1. 桥接模式对象自身有 沿着多个维度变化的趋势 , 本身不稳定;
  2. 装饰者模式对象自身非常稳定, 只是为了增加新功能/增强原功能。

收起阅读 »

你有原则么?懂原则么?想了解么?快看设计模式原则篇,让你做个有原则的程序员

前言无论做啥,要想好设计,就得多扩展,少修改 开闭原则此原则是由”Bertrand Meyer”提出的。原文是:”Software entities should be open for extension,but closed for modificatio...
继续阅读 »

前言

无论做啥,要想好设计,就得多扩展,少修改



开闭原则

此原则是由”Bertrand Meyer”提出的。原文是:”Software entities should be open for extension,but closed for modification”。就是说模块应对扩展开放,而对修改关闭。模块应尽量在不修改原(是”原”,指原来的代码)代码的情况下进行扩展


开闭原则的含义

当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。


开闭原则的作用


  1. 对软件测试的影响
    软件遵守开闭原则的话,软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行。
  2. 可以提高代码的可复用性
    粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
  3. 可以提高软件的可维护性
    遵守开闭原则的软件,其稳定性高和延续性强,从而易于扩展和维护。

里氏替换原则

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。


里氏替换原则的作用

它克服了继承中重写父类造成的可复用性变差的缺点。
它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

里氏替换原则的实现方法

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。


依赖倒置原则

要面向接口编程,不要面向实现编程。


依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。


由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。


使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给它们的实现类去完成。


依赖、倒置原则的作用


  • 依赖倒置原则的主要作用如下。
  • 依赖倒置原则可以降低类间的耦合性。
  • 依赖倒置原则可以提高系统的稳定性。
  • 依赖倒置原则可以减少并行开发引起的风险。
  • 依赖倒置原则可以提高代码的可读性和可维护性。

单一职责原则

单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分


单一职责原则的优点

单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。



  • 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
  • 提高类的可读性。复杂性降低,自然其可读性会提高。
  • 提高系统的可维护性。可读性提高,那自然更容易维护了。
  • 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。

接口隔离原则

尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。
一个类对另一个类的依赖应该建立在最小的接口上

要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。


接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:



  • 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。

    接口隔离原则的优点

    接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。


  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
  3. 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
  4. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
  5. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

迪米特法则

如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。


迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
迪米特法则的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。


  • 降低了类之间的耦合度,提高了模块的相对独立性。
  • 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。

但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。


合成复用原则

要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。


如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。


合成复用原则的重要性

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。



  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。



  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

收起阅读 »

华为手机升级HarmonyOS全攻略:公测&内测&线下升级

写在前面:本文旨在帮助社区各位小伙伴选择合适的渠道尽早升级HarmonyOS系统,深夜撸稿,还望三连支持一哈!!目前正在进行的升级活动:消费者公测、消费者内测、HarmonyOS体验官(线下)必要说明:所有消费者公测渠道最终都会跳转到花粉俱乐部;初期申请量巨大...
继续阅读 »

写在前面:

本文旨在帮助社区各位小伙伴选择合适的渠道尽早升级HarmonyOS系统,深夜撸稿,还望三连支持一哈!!

目前正在进行的升级活动:消费者公测、消费者内测、HarmonyOS体验官(线下)

必要说明:

所有消费者公测渠道最终都会跳转到花粉俱乐部;

初期申请量巨大,花粉俱乐部很容易就挂掉,心急的小伙伴可尝试线下渠道或者多次尝试或者深夜(两点以后)申请;

申请前务必将“花粉俱乐部”、“我的华为”、“会员中心”升级到最新版本,尤其是“花粉俱乐部”。

消费者公测

包含机型:

Mate X2

Mate40、Mate40E、Mate 40 Pro、Mate 40 Pro+、Mate 40 RS 保时捷设计

P40 5G、P40 4G、P40 Pro、P40 Pro+

Mate 30 4G、Mate 30 Pro 4G、Mate 30 5G、Mate 30 Pro 5G、Mate 30 RS保时捷设计、Mate 30E Pro 5G

MatePad Pro、MatePad Pro 5G

我的华为/花粉俱乐部:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.打开“我的华为”,点击“升级尝鲜” / 打开“花粉俱乐部”,点击“公测尝鲜”。

△我的华为

△花粉俱乐部

2.页面加载完成后,点击“公测尝鲜”下的“立即尝鲜”按钮。(花粉俱乐部进入的请忽略此步骤,直接进入下步)

3.接下来在列表中找到当前手机型号对应的公测活动,点击“报名公测”。由于不同手机对应的系统版本不一样,请务必仔细核实你的机器型号。

4.此处会跳转到“花粉论坛”的一篇帖子,划到这篇帖子的末尾,点击“参加公测活动”。接下来系统会引导用户签订《华为公测协议》和《华为公测与隐私声明》,等待10秒点击通过。

5.通过两个协议后,系统会引导你下载协议文件。这个过程会验证你的机型是否符合要求,且下载的文件也是将来升级为正式版的必要文件,如果找到(反正我是没找到)请勿删除!!

6.下载并提示安装完描述文件后,就可以去检测系统更新,下载并更新HarmonyOS了。(P40系列当前版本116)

消费者内测

包含机型:

Mate XS、Mate 20、Mate 20 Pro、Mate 20 RS(保时捷)、Mate 20 X(4G)

nova 8、nova 8 Pro、nova 8 SE、nova 7 5G、nova 7 Pro 5G、nova 7 SE 5G、nova 7 SE 5G活力版、nova 7 SE 5G乐活版、nova 6、nova 6 5G、nova 6 SE

华为畅享20 Plus 5G、华为畅享Z 5G、华为畅享20 Pro5G

华为麦芒9 5G

MatePad 10.8、MatePad 5G 10.4、MatePad 10.4

内测时间:6月2日~6月9日上午10:00

渠道一

会员中心:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.打开会员中心,首页上找到“体验先锋”,点击进入。

2.点击顶部的HarmonyOS 2升级尝鲜。

3.进入页面点击报名。如果机型不符合,会弹出提示框。

4.接下来根据流程提示,填写信息,等待审核,坐等更新就好了。

渠道二

花粉俱乐部:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

1.进入首页点击内测报名

2.跳转后,点击立即报名

3.接下来根据流程提示,填写信息,等待审核,坐等更新就好了。

HarmonyOS体验官(线下)

包含机型:

我的华为:请先更新到最新版本,如果卸载了,请在华为应用商店下载安装。

方法:

在APP首页点击“HarmonyOS体验官”海报,经过简单的互动问答即可参加。期间需要提交信息、预约门店时间和信息,最终会生成一张包含数字的海报,用户需要保存此海报才可参与活动。

活动仅在部分门店进行,具体店面和城市请在活动页面查询。到店会提供礼品,并可在线下由店面工作人员协助完成升级。

重要的补充说明

1.消费者公测仅审核设备型号是否合规,避免出现系统和硬件不适配的情况。

2.消费者内测仍然会存在审核机制,但仅审核设备型号是否合规,避免出现系统和硬件不适配的情况。

3.最终稳定的系统版本号预计为:HarmonyOS 2.0.0.116(以实际推送版本号为准!)

4.老荣耀系列机型不在本次消费者公测列表中。

收起阅读 »