iOS开发中网络请求数据如何减少焦虑用户焦虑的方法有哪些

麦子学院教你在Windows中搭建ios开发环境__新浪博客
麦子学院教你在Windows中搭建ios开发环境
很多初学者在选择android和ios的时候,都很焦虑,到底该选择哪种呢?因为学习IOS开发
,一般需要在mac计算机里搭建开发环境,由于硬件成本较高,现在麦子学院的老师为我们介绍如何在Windows
XP下VMWare虚拟机中安装开发环境:本文在Windows
Xp + VMware Workstation 7.0 + MAC OS X SnowLeopard
10.6&环境下安装iPhone SDK
3.1.2成功!&在PC机上安装MAC
X系统有两种方法:&方法一:在硬盘上分区,专门安装MAC
X;&方法二:在现有Windows系统中,使用VMWare等虚拟机软件安装Mac
OS。&注意:一般都需要将苹果系统的DMG光盘镜像文件刻录到D9光盘上,这个步骤比较麻烦。在Windows
XP中使用VMWare虚拟机,安装MAC
X时,无需刻录D9光盘,无需转换成ISO格式,&直接使用MAC
X和iPhoneSDK的DMG镜像文件进行安装的方法。&安装条件:&硬件:一台拥有支持虚拟技术的64位双核处理器和2GB以上内存的PC。&本人采用笔记本:DELL
D630&&4G内存&注意:运行MAC
OS,需要电脑支持虚拟技术(VT),安装时,需要将VT启动,在BIOS中开启。&软件:&Windows
XP:&VMware&:VMware-workstation-7.0.0-203739&&EXE安装文件&Mac
OS:Mac_OS_10.6_Snow_Leopard_10.6&&DMG光盘镜像文件&iPhone
SDK:iphone_sdk_3.1.2_with_xcode_3.2.1&&DMG光盘镜像文件&Darwin.iso或Rebel
EFI.iso&&引导光盘ISO镜像文件这里有更多的ios高手在等你跟他们交流哦:&
如果想看ios开发视频教程的可以去/course/ios/
这个网址看看!我只能帮到这里了,以后就看你了!
麦子学院移动开发_phg
博客等级:
博客积分:0
博客访问:220
关注人气:0
荣誉徽章:&p&整理了一份macOS软件安全工程师技能表。&/p&&p&&br&&/p&&p&&b&未经本人允许,任何个人与组织禁止转载!!!&/b&&/p&&p&&br&&/p&&p&&br&&/p&&img src=&/v2-1b174e1a14f7ac9d5f9bc2_b.png& data-rawwidth=&1704& data-rawheight=&2661& class=&origin_image zh-lightbox-thumb& width=&1704& data-original=&/v2-1b174e1a14f7ac9d5f9bc2_r.png&&&p&&/p&
整理了一份macOS软件安全工程师技能表。 未经本人允许,任何个人与组织禁止转载!!!
&img src=&/v2-07da9d9b9ef4dcb478c2c55d_b.jpg& data-rawwidth=&1024& data-rawheight=&768& class=&origin_image zh-lightbox-thumb& width=&1024& data-original=&/v2-07da9d9b9ef4dcb478c2c55d_r.jpg&&&blockquote&&p&念桥边红药,年年知为谁生&/p&&p& ——杨州慢 姜夔&/p&&/blockquote&&h1&缘起&/h1&&p&libco 协程库在单个线程中实现了多个协程的创建和切换。按照我们通常的编程思路,单个线程中的程序执行流程通常是顺序的,调用函数同样也是 “调用——返回”,每次都是从函数的入口处开始执行。而libco 中的协程却实现了函数执行到一半时,切出此协程,之后可以回到函数切出的位置继续执行,即函数的执行可以被“拦腰斩断”,这种在函数任意位置 “切出——恢复” 的功能是如何实现的呢? 本文从libco 代码层面对协程的切换进行了剖析,希望能让初次接触 libco 的同学能快速了解其背后的运行机理。&/p&&h1&函数调用与协程切换的区别&/h1&&p&下面的程序是我们常规调用函数的方法:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span&&/span&&span class=&kt&&void&/span& &span class=&nf&&A&/span&&span class=&p&&()&/span& &span class=&p&&{&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&mi&&1&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&mi&&2&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&mi&&3&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&kt&&void&/span& &span class=&nf&&B&/span&&span class=&p&&()&/span& &span class=&p&&{&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&s&&&x&&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&s&&&y&&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&s&&&z&&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&kt&&int&/span& &span class=&nf&&main&/span&&span class=&p&&(&/span&&span class=&kt&&void&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&n&&A&/span&&span class=&p&&();&/span&
&span class=&n&&B&/span&&span class=&p&&();&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&在单线程中,上述函数的输出为:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&1 2 3 x y z
&/code&&/pre&&/div&&p&如果我们用 libco 库将上面程序改造一下:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span&&/span&&span class=&kt&&void&/span& &span class=&nf&&A&/span&&span class=&p&&()&/span& &span class=&p&&{&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&mi&&1&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&mi&&2&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&n&&co_yield_ct&/span&&span class=&p&&();&/span&
&span class=&c1&&// 切出到主协程&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&mi&&3&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&kt&&void&/span& &span class=&nf&&B&/span&&span class=&p&&()&/span& &span class=&p&&{&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&s&&&x&&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&n&&co_yield_ct&/span&&span class=&p&&();&/span&
&span class=&c1&&// 切出到主协程&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&s&&&y&&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&n&&cout&/span& &span class=&o&&&&&/span& &span class=&s&&&z&&/span& &span class=&o&&&&&/span& &span class=&s&&& &&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&span class=&kt&&int&/span& &span class=&nf&&main&/span&&span class=&p&&(&/span&&span class=&kt&&void&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&p&&...&/span&
&span class=&c1&&// 主协程&/span&
&span class=&n&&co_resume&/span&&span class=&p&&(&/span&&span class=&n&&A&/span&&span class=&p&&);&/span&
&span class=&c1&&// 启动协程 A&/span&
&span class=&n&&co_resume&/span&&span class=&p&&(&/span&&span class=&n&&B&/span&&span class=&p&&);&/span&
&span class=&c1&&// 启动协程 B&/span&
&span class=&n&&co_resume&/span&&span class=&p&&(&/span&&span class=&n&&A&/span&&span class=&p&&);&/span&
&span class=&c1&&// 从协程 A 切出处继续执行&/span&
&span class=&n&&co_resume&/span&&span class=&p&&(&/span&&span class=&n&&B&/span&&span class=&p&&);&/span&
&span class=&c1&&// 从协程 B 切出处继续执行&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&同样在单线程中,改造后的程序输出如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&1 2 x 3 y z
&/code&&/pre&&/div&&p&可以看出,切出操作是由 co_yield_ct() 函数实现的,而协程的启动和恢复是由 co_resume 实现的。函数 A() 和 B() 并不是一个执行完才执行另一个,而是产生了 “交叉执行“ 的效果,那么,在单个线程中,这种 ”交叉执行“,是如何实现的呢?&/p&&h1&Read the f**king source code!&/h1&&blockquote&Talk is cheap, show me code.&/blockquote&&p&下面我们就深入 libco 的代码来看一下,协程的切换是如何实现的。通过分析代码看到,无论是 co_yield_ct() 还是 co_resume,在协程切出和恢复时,都调用了同一个函数co_swap,在这个函数中调用了 coctx_swap 来实现协程的切换,这一函数的原型是:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span&&/span&&span class=&kt&&void&/span& &span class=&nf&&coctx_swap&/span&&span class=&p&&(&/span& &span class=&n&&coctx_t&/span& &span class=&o&&*&/span&&span class=&p&&,&/span&&span class=&n&&coctx_t&/span&&span class=&o&&*&/span& &span class=&p&&)&/span& &span class=&k&&asm&/span&&span class=&p&&(&/span&&span class=&s&&&coctx_swap&&/span&&span class=&p&&);&/span&
&/code&&/pre&&/div&&p&两个参数都是 coctx_t *指针类型,其中第一个参数表示要切出的协程,第二个参数表示切出后要进入的协程。&/p&&p&在上篇文章 “x86-64 下函数调用及栈帧原理” 中已经指出,调用子函数时,父函数会把两个调用参数放入了寄存器中,并且把返回地址压入了栈中。即在进入 coctx_swap 时,第一个参数值已经放到了 %rdi 寄存器中,第二个参数值已经放到了 %rsi 寄存器中,并且栈指针 %rsp 指向的位置即栈顶中存储的是父函数的返回地址。进入 coctx_swap 后,堆栈的状态如下:&/p&&img src=&/v2-cfc041baaed11e6e8ee98_b.png& data-rawwidth=&1113& data-rawheight=&970& class=&origin_image zh-lightbox-thumb& width=&1113& data-original=&/v2-cfc041baaed11e6e8ee98_r.png&&&p&由于coctx_swap 是在 co_swap() 函数中调用的,下面所提及的协程的返回地址就是 co_swap() 中调用 coctx_swap() 之后下一条指令的地址:&/p&&div class=&highlight&&&pre&&code class=&language-c&&&span&&/span&&span class=&kt&&void&/span& &span class=&nf&&co_swap&/span&&span class=&p&&(&/span&&span class=&n&&stCoRoutine_t&/span&&span class=&o&&*&/span& &span class=&n&&curr&/span&&span class=&p&&,&/span& &span class=&n&&stCoRoutine_t&/span&&span class=&o&&*&/span& &span class=&n&&pending_co&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&p&&....&/span&
&span class=&c1&&// 从本协程切出&/span&
&span class=&n&&coctx_swap&/span&&span class=&p&&(&/span&&span class=&o&&&&/span&&span class=&p&&(&/span&&span class=&n&&curr&/span&&span class=&o&&-&&/span&&span class=&n&&ctx&/span&&span class=&p&&),&/span&&span class=&o&&&&/span&&span class=&p&&(&/span&&span class=&n&&pending_co&/span&&span class=&o&&-&&/span&&span class=&n&&ctx&/span&&span class=&p&&)&/span& &span class=&p&&);&/span&
&span class=&c1&&// 此处是返回地址,即协程恢复时开始执行的位置&/span&
&span class=&n&&stCoRoutineEnv_t&/span&&span class=&o&&*&/span& &span class=&n&&curr_env&/span& &span class=&o&&=&/span& &span class=&n&&co_get_curr_thread_env&/span&&span class=&p&&();&/span&
&span class=&p&&....&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&coctx_swap 函数是用汇编实现的,我们这里只关注 x86-64 相关的部分,其代码如下:&/p&&div class=&highlight&&&pre&&code class=&language-c&&&span&&/span&&span class=&nl&&coctx_swap&/span&&span class=&p&&:&/span&
&span class=&n&&leaq&/span& &span class=&mi&&8&/span&&span class=&p&&(&/span&&span class=&o&&%&/span&&span class=&n&&rsp&/span&&span class=&p&&),&/span&&span class=&o&&%&/span&&span class=&n&&rax&/span&
&span class=&n&&leaq&/span& &span class=&mi&&112&/span&&span class=&p&&(&/span&&span class=&o&&%&/span&&span class=&n&&rdi&/span&&span class=&p&&),&/span&&span class=&o&&%&/span&&span class=&n&&rsp&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&rax&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&rbx&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&rcx&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&rdx&/span&
&span class=&n&&pushq&/span& &span class=&o&&-&/span&&span class=&mi&&8&/span&&span class=&p&&(&/span&&span class=&o&&%&/span&&span class=&n&&rax&/span&&span class=&p&&)&/span& &span class=&c1&&//ret func addr&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&rsi&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&rdi&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&rbp&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&r8&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&r9&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&r12&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&r13&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&r14&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&r15&/span&
&span class=&n&&movq&/span& &span class=&o&&%&/span&&span class=&n&&rsi&/span&&span class=&p&&,&/span& &span class=&o&&%&/span&&span class=&n&&rsp&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&r15&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&r14&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&r13&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&r12&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&r9&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&r8&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&rbp&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&rdi&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&rsi&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&rax&/span& &span class=&c1&&//ret func addr&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&rdx&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&rcx&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&rbx&/span&
&span class=&n&&popq&/span& &span class=&o&&%&/span&&span class=&n&&rsp&/span&
&span class=&n&&pushq&/span& &span class=&o&&%&/span&&span class=&n&&rax&/span&
&span class=&n&&xorl&/span& &span class=&o&&%&/span&&span class=&n&&eax&/span&&span class=&p&&,&/span& &span class=&o&&%&/span&&span class=&n&&eax&/span&
&span class=&n&&ret&/span&
&/code&&/pre&&/div&&p&可以看出,coctx_swap 中并未像常规被调用函数一样创立新的栈帧。先看前两条语句:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&
leaq 8(%rsp),%rax
leaq 112(%rdi),%rsp
&/code&&/pre&&/div&&p&leaq 用于把其第一个参数的值赋值给第二个寄存器参数。第一条语句用来把 8(%rsp) 的本身的值存入到 %rax 中,注意这里使用的并不是 8(%rsp) 指向的值,而是把 8(%rsp) 表示的地址赋值给了 %rax。这一地址是父函数栈帧中除返回地址外栈帧顶的位置。&/p&&p&在第二条语句 leaq 112(%rdi), %rsp 中,%rdi 存放的是coctx_swap 第一个参数的值,这一参数是指向 coctx_t 类型的指针,表示当前要切出的协程,这一类型的定义如下:&/p&&div class=&highlight&&&pre&&code class=&language-cpp&&&span&&/span&&span class=&k&&struct&/span& &span class=&n&&coctx_t&/span& &span class=&p&&{&/span&
&span class=&kt&&void&/span& &span class=&o&&*&/span&&span class=&n&&regs&/span&&span class=&p&&[&/span& &span class=&mi&&14&/span& &span class=&p&&];&/span&
&span class=&kt&&size_t&/span& &span class=&n&&ss_size&/span&&span class=&p&&;&/span&
&span class=&kt&&char&/span& &span class=&o&&*&/span&&span class=&n&&ss_sp&/span&&span class=&p&&;&/span&
&span class=&p&&};&/span&
&/code&&/pre&&/div&&p&因而 112(%rdi) 表示的就是第一个协程的 coctx_t 中 regs[14] 数组的下一个64位地址。而接下来的语句:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&
pushq %rax
pushq %rbx
pushq %rcx
pushq %rdx
pushq -8(%rax) //ret func addr
pushq %rsi
pushq %rdi
pushq %rbp
pushq %r12
pushq %r13
pushq %r14
pushq %r15
&/code&&/pre&&/div&&p&第一条语句 pushq %rax 用于把 %rax 的值放入到 regs[13] 中,resg[13] 用来存储第一个协程的 %rsp 的值。这时 %rax 中的值是第一个协程 coctx_swap 父函数栈帧除返回地址外栈帧顶的地址。由于 regs[] 中有单独的元素存储返回地址,栈中再保存返回地址是无意义的,因而把父栈帧中除返回地址外的栈帧顶作为要保存的 %rsp 值是合理的。当协程恢复时,把保存的 regs[13] 的值赋值给 %rsp 即可恢复本协程 coctx_swap 父函数堆栈指针的位置。第一条语句之后的语句就是用pushq 把各CPU 寄存器的值依次从 regs 尾部向前压入。即通过调整%rsp 把 regs[14] 当作堆栈,然后利用 pushq 把寄存器的值和返回地址存储到 regs[14] 整个数组中。regs[14] 数组中各元素与其要存储的寄存器对应关系如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&//-------------
//low | regs[0]: r15 |
| regs[1]: r14 |
| regs[2]: r13 |
| regs[3]: r12 |
| regs[4]: r9
| regs[5]: r8
| regs[6]: rbp |
| regs[7]: rdi |
| regs[8]: rsi |
| regs[9]: ret |
//ret func addr, 对应 rax
| regs[10]: rdx |
| regs[11]: rcx |
| regs[12]: rbx |
//hig | regs[13]: rsp |
&/code&&/pre&&/div&&p&接下来的汇编语句:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&
movq %rsi, %rsp
popq %rax //ret func addr
&/code&&/pre&&/div&&p&这里用的方法还是通过改变%rsp 的值,把某块内存当作栈来使用。第一句 movq %rsi, %rsp 就是让%rsp 指向 coctx_swap 第二个参数,这一参数表示要进入的协程。而第二个参数也是coctx_t 类型的指针,即执行完 movq 语句后,%rsp 指向了第二个参数 coctx_t 中 regs[0],而之后的pop 语句就是用 regs[0-13] 中的值填充cpu 的寄存器,这里需要注意的是popq 会使得 %rsp 的值增加而不是减少,这一点保证了会从 regs[0] 到regs[13] 依次弹出到 cpu 寄存器中。在执行完最后一句 popq %rsp 后,%rsp 已经指向了新协程要恢复的栈指针(即新协程之前调用 coctx_swap 时父函数的栈帧顶指针),由于每个协程都有一个自己的栈空间,可以认为这一语句使得%rsp 指向了要进入协程的栈空间。&/p&&p&coctx_swap 中最后三条语句如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&
pushq %rax
xorl %eax, %eax
&/code&&/pre&&/div&&p&pushq %rax 用来把 %rax 的值压入到新协程的栈中,这时 %rax 是要进入的目标协程的返回地址,即要恢复的执行点。然后用 xorl 把 %rax 低32位清0以实现地址对齐。最后ret 语句用来弹出栈的内容,并跳转到弹出的内容表示的地址处,而弹出的内容正好是上面 pushq %rax 时压入的 %rax 的值,即之前保存的此协程的返回地址。即最后这三条语句实现了转移到新协程返回地址处执行,从而完成了两个协程的切换。可以看出,这里通过调整%rsp 的值来恢复新协程的栈,并利用了 ret 语句来实现修改指令寄存器 %rip 的目的,通过修改 %rip 来实现程序运行逻辑跳转。注意%rip 的值不能直接修改,只能通过 call 或 ret 之类的指令来间接修改。&/p&&p&整体上看来,协程的切换其实就是cpu 寄存器内容特别是%rip 和 %rsp 的写入和恢复,因为cpu 的寄存器决定了程序从哪里执行(%rip) 和使用哪个地址作为堆栈 (%rsp)。寄存器的写入和恢复如下图所示:&/p&&img src=&/v2-9d61b78baf175_b.png& data-rawwidth=&843& data-rawheight=&535& class=&origin_image zh-lightbox-thumb& width=&843& data-original=&/v2-9d61b78baf175_r.png&&&p&执行完上图的流程,就将之前 cpu 寄存器的值保存到了协程A 的 regs[14] 中,而将协程B regs[14] 的内容写入到了寄存器中,从而使执行逻辑跳转到了 B 协程 regs[14] 中保存的返回地址处开始执行,即实现了协程的切换(从A 协程切换到了B协程执行)。&/p&&h1&结语&/h1&&p&为实现单线程中协程的切换,libco 使用汇编直接读写了 cpu 的寄存器。由于通常我们在高级语言层面很少接触上下文切换的情形,因而会觉得在单线程中切换上下文的方法会十分复杂,但当我们对代码抽丝剥茧后,发现其实现机理也是很容易理解的。从libco 上下文切换中可以看出,用汇编与 cpu 硬件寄存器配合竟然可以设计出如此神奇的功能,不得不惊叹于 cpu 硬件设计的精妙。&/p&&p&libco 库的说明中提及这种上下文切换的方法取自 glibc,看来基础库中隐藏了不少 “屠龙之技”。&/p&&br&&p&看来,想要提高编程技能,无他,Read the f**king source code !&/p&&p&The End.&/p&&p&我就是我,疾驰中的企鹅。&/p&&br&&p&我就是我,不一样的焰火。&/p&&img src=&/v2-cd2accb4d5cb6c_b.jpg& data-rawwidth=&520& data-rawheight=&346& class=&origin_image zh-lightbox-thumb& width=&520& data-original=&/v2-cd2accb4d5cb6c_r.jpg&&
念桥边红药,年年知为谁生 ——杨州慢 姜夔缘起libco 协程库在单个线程中实现了多个协程的创建和切换。按照我们通常的编程思路,单个线程中的程序执行流程通常是顺序的,调用函数同样也是 “调用——返回”,每次都是从函数的入口处开始执行。而libco 中的…
&img src=&/v2-f5a0148cfc005d6d6d86_b.png& data-rawwidth=&1259& data-rawheight=&690& class=&origin_image zh-lightbox-thumb& width=&1259& data-original=&/v2-f5a0148cfc005d6d6d86_r.png&&&p&在 iOS 10.3 之前,App 图标是固定,只能通过发新版来更新。但现在有些 App 可以让用户自己选取图标,并及时生效。这是因为 10.3 里引入了一个新的 API,它允许在 App 运行的时候,通过代码为 app 更换 icon。&/p&&p&(注:这篇文章是写给开发者看的,不是针对普通用户)&/p&&h2&&b&快速上手&/b&&/h2&&p&虽然提供了更换的功能,但更换的 icon 是有限制的。它需要开发者提前预置在工程里,并做好相应配置。更改 icon 的时候,只能在有限的选项中进行选择。&/p&&p&具体方法很简单:&/p&&ul&&li&配置 info.plist 文件,添加对应的 alternate icons 的信息。 &/li&&/ul&&img src=&/v2-61b7fa72f626f_b.png& data-rawwidth=&816& data-rawheight=&190& class=&origin_image zh-lightbox-thumb& width=&816& data-original=&/v2-61b7fa72f626f_r.png&&&ul&&li&调用 UIApplication.setAlternateIconName(_:completionHandler:) 方法设置即可。其中这里的 name 就是上一步中设定的配置名称(如 anotherIcon)。&/li&&/ul&&p&info.plist 的配置方式如下: &/p&&div class=&highlight&&&pre&&code class=&language-xml&&&span&&/span&&span class=&nt&&&key&&/span&CFBundleIcons&span class=&nt&&&/key&&/span&
&span class=&nt&&&dict&&/span&
&span class=&nt&&&key&&/span&CFBundleAlternateIcons&span class=&nt&&&/key&&/span&
&span class=&nt&&&dict&&/span&
&span class=&nt&&&key&&/span&我是一个图标&span class=&nt&&&/key&&/span&
&span class=&nt&&&dict&&/span&
&span class=&nt&&&key&&/span&CFBundleIconFiles&span class=&nt&&&/key&&/span&
&span class=&nt&&&array&&/span&
&span class=&nt&&&string&&/span&文件名&span class=&nt&&&/string&&/span&
&span class=&nt&&&/array&&/span&
&span class=&nt&&&key&&/span&UIPrerenderedIcon&span class=&nt&&&/key&&/span&
&span class=&nt&&&false/&&/span&
&span class=&nt&&&/dict&&/span&
&span class=&nt&&&/dict&&/span&
&span class=&nt&&&key&&/span&CFBundlePrimaryIcon&span class=&nt&&&/key&&/span&
&span class=&nt&&&dict&&/span&
&span class=&nt&&&key&&/span&CFBundleIconFiles&span class=&nt&&&/key&&/span&
&span class=&nt&&&array&&/span&
&span class=&nt&&&string&&/span&AppIcon60x60&span class=&nt&&&/string&&/span&
&span class=&nt&&&/array&&/span&
&span class=&nt&&&/dict&&/span&
&span class=&nt&&&/dict&&/span&
&/code&&/pre&&/div&&h2&&b&详细配置&/b&&/h2&&p&虽然步骤很简单,但实际使用起来却有诸多模糊之处。官方一共有三处描述, 但都比较简略:&/p&&ol&&li&&a href=&/?target=https%3A///documentation/uikit/uiapplication/2806818-setalternateiconname& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&setAlternateIconName 方法文档&i class=&icon-external&&&/i&&/a&&/li&&li&&a href=&/?target=https%3A///library/content/documentation/General/Reference/InfoPlistKeyReference/Introduction/Introduction.html%23//apple_ref/doc/uid/TP& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&About Info.plist Keys and Values&i class=&icon-external&&&/i&&/a&&/li&&li&&a href=&/?target=https%3A///ios/human-interface-guidelines/graphics/app-icon/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Human Interface Guidelines: App Icon&i class=&icon-external&&&/i&&/a&&/li&&/ol&&p&下面介绍一些需要注意的地方:&/p&&p&&br&&/p&&p&&b&省略 CFBundlePrimaryIcon&/b&&/p&&p&虽然文档 1 中写着 「You must declare your app's primary and alternate icons using the CFBundleIcons key of your app's Info.plist file. 」,但经测试,CFBundlePrimaryIcon 可以省略掉。在工程配置 `App Icons and Launch Image` - `App Icons Source` 中使用
asset catalog(默认配置),删除 CFBundlePrimaryIcon 的配置也是没有问题的。&/p&&p&省略这个配置的好处是,避免处理 App icon 的尺寸。现在的工程中,大家一般都使用
asset catalog 进行 icon 的配置,而一个 icon 对应有很多尺寸的文件。省略 CFBundlePrimaryIcon 就可以沿用 Asset 中的配置。&/p&&p&如果想设置回默认 icon,在 `setAlternateIconName` 中传入 nil 即可。&/p&&p&&br&&/p&&p&&b&图片存放位置和引用名称&/b&&/p&&p&Alternate Icon 的图片不能放在
asset catalog 中,而应该直接放在工程里。&/p&&p&info.plist 的配置中,图片的文件名应该尽量不带 @2x/@3x 后缀扩展名,而让它自动选择 。&/p&&p&&br&&/p&&p&&b&提供多种尺寸的 icons&/b&&/p&&p&文档 3 中说明:&/p&&blockquote&Like your primary app icon, each alternate app icon is delivered as a collection of related images that vary in size. When the user chooses an alternate icon, the appropriate sizes of that icon replace your primary app icon on the Home screen, in Spotlight, and elsewhere in the system. To ensure that alternate icons appear consistently throughout the system—the user shouldn't see one version of your icon on the Home screen and a completely different version in Settings, for example—provide them in the same sizes you provide for your primary app icon (with the exception of the large App Store icon). See &a href=&/?target=https%3A///ios/human-interface-guidelines/graphics/app-icon/%23app-icon-sizes& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&App Icon Sizes&i class=&icon-external&&&/i&&/a&.&/blockquote&&p&即需要为 AlternateIcon 提供多种尺寸的 image。具体的尺寸在引用文字里的链接中可以找到,一个 icon 最多可能需要十几种尺寸。&/p&&p&对于 info.plist 中的每个 icon 配置,CFBundleIconFiles 的值是一个数组,我们可以在其中填入这十几种规格的图片名称。经测试:&/p&&ul&&li&文件的命名没有强制的规则,可以随意取,&/li&&li&数组中的文件名也不关心先后顺序。&/li&&/ul&&p&总之把对应的文件名填进去即可,它会自动选择合适分辨率的文件(比如在 setting 中显示 icon 时,它会找到提供的数组中分辨率为 29pt 的那个文件)。&/p&&p&&br&&/p&&h2&&b&演示&/b&&/h2&&p&demo 在这里 &a href=&/?target=https%3A///leavez/alternate-icon-demo/tree/master& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&leavez/alternate-icon-demo&i class=&icon-external&&&/i&&/a&&/p&&p&&br&&/p&&img src=&/v2-d754fc565ce0dd6be1abf9c_b.jpg& data-rawwidth=&480& data-rawheight=&697& data-thumbnail=&/v2-d754fc565ce0dd6be1abf9c_b.jpg& class=&origin_image zh-lightbox-thumb& width=&480& data-original=&/v2-d754fc565ce0dd6be1abf9c_r.gif&&&p&(demo 中特意为 Setting App 设置了不同的图片,以证明图片选择的方)&/p&
在 iOS 10.3 之前,App 图标是固定,只能通过发新版来更新。但现在有些 App 可以让用户自己选取图标,并及时生效。这是因为 10.3 里引入了一个新的 API,它允许在 App 运行的时候,通过代码为 app 更换 icon。(注:这篇文章是写给开发者看的,不是针对普通…
&img src=&/v2-01d1d10cdeb9_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-01d1d10cdeb9_r.jpg&&&p&&/p&&img src=&/v2-01d1d10cdeb9_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-01d1d10cdeb9_r.jpg&&&p&&br&&/p&&p&这其实是之前在北京 Laravel Meetup 的一次分享内容,不过考虑到有很多人在公众号想听听关于我是如何做开源这个话题,所以就再次拿它讲一个文字版。&/p&&p&&br&&/p&&img src=&/v2-052cefceb729_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-052cefceb729_r.jpg&&&p&&br&&/p&&p&关于我,这个就没啥可讲的了,EasyWeChat 作者、Laravel China 创始人之一。&/p&&p&&br&&/p&&img src=&/v2-dacaed0df5829b_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-dacaed0df5829b_r.jpg&&&p&要想做好开源,这 8 个步骤缺一不可,当然这个过程周期是持续的,你会在不断开源过程中提升自己,学到新的东西。&/p&&p&&br&&/p&&img src=&/v2-a3a502efc5658e76bcced0fe_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-a3a502efc5658e76bcced0fe_r.jpg&&&p&第一件头疼的事情当然是 “做什么?”,不过根据我的个人经验来看,找一个开源项目 idea 并没有想象的那么难,一般有以下三个渠道:&/p&&p&第一个渠道是项目,因为大部分都是来自工作生活中,所以上图我把“项目”排到第一位。很多时候在我们的开发工作中,会经常遇到重复性的工作,比如你每启动一个项目都要搞一遍短信的发送,又得去找一遍用哪家的服务,还得折腾一遍权限系统,其实这些都是激发你创意的好时机。你会发现不是你一个人在重复,而是大家都在这样不停的重复做很多原本不需要重复做的事情。所以这时候就是你造轮子的好时机。&/p&&p&第二个创意来源就是交流群,我相信大部分同学的 QQ 都有不少技术交流群吧,你会发现很多人在群里会提重复的问题,或者一些伸手党会经常来问一些“有没有基于xxx的项目啊”、“有没有人会xxx” 诸如此类的问题。如果你发现这个需求确实挺多的,并且也没有一个好用的轮子,你就动手吧!&/p&&p&第三就是社区,一些论坛或者博客,也是发现需求的地方,基本都是从别人的讨论中发现创意,这些用户就是你的项目最直接用户。&/p&&p&&br&&/p&&img src=&/v2-f07cebb58bb1f9ca554abc_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-f07cebb58bb1f9ca554abc_r.jpg&&&p&&br&&/p&&p&做开源项目其实是一件比较费时费心的工作,它的最大难点并不在于代码,而是后期的维护持续的跟进。但是要想制作出一个受欢迎的开源项目,写好代码永远是关键的点,试想一下,一个人从你各种吹 B 的链接点到 GitHub,看到源码乱七八糟,格式不统一,驼峰+下划线各种混写,对齐也是 tab + space,注释基本为 0 的时候那个场景,是很吓人的,所以不管你的抽象能力怎么样,也不管你这个模块写得是否是那么的科学,请做好第一步:写好代码。&/p&&p&上图我列举了一些名词,难免有同学不认识,我这里大概介绍一下:&/p&&p&PSR 是国际框架组织 PHP-FIG(PHP Framework Interop Group) 制定的一系列规范,包括不限于自动加载,编码规范、缓存以及其它一系列接口规范。它虽然不是 PHP 官方标准,但是目前大多数开源项目都是按这个标准来的,所以,有必要认真了解一下。&/p&&p&PHPCS 是 PHP Code Sniffer,一款代码规范检查工具,可以根据你的设置来检查代码规范性问题。&/p&&p&PHPCBF 是 PHPCS 内置的代码规范修复工具,大部分的代码规范问题它都可以自动修掉。&/p&&p&PHPMD 是代码复杂度检测工具,能够很方便的检查你的代码是不是写得复杂度过高。&/p&&p&StyleCI 是一款在线的代码规范检查服务,最初的版本是开源的,后来闭源了,核心是基于 PHPCS 来完成。我的所有 PHP 开源项目基本都开通了这个服务来自动检查。&/p&&p&Scrutinizer 同样是一款在线服务,不过它的功能比较强大,主要用于检查代码质量问题,比如潜在的 bug,未使用的变量,错误的类型约束,或者重复的代码等,总之是一款很棒的工具。&/p&&p&Travis-CI 是一款在线持续集成服务,自动化执行单元测试,或者部署任务等。&/p&&p&GitHub 上有很多类似的服务,你可以查看上面的链接来了解它们,它们都是对开源项目免费使用的。&/p&&p&&br&&/p&&img src=&/v2-d273c8bb597977eee04c535a3be8a117_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-d273c8bb597977eee04c535a3be8a117_r.jpg&&&p&代码写好了别着急直接放到网上,做好了上面第二步我们提到的各种规范检查外,充分的测试也是一个必要的工作。&/p&&p&一般的做法是写单元测试,如果你还对单元测试这个东西不够熟悉的话,是时候发起一波学习了。&/p&&p&在保证足够覆盖度的前提下,结合第二步提到的持续集成服务,这个项目差不多就是可以打 80 分了。&/p&&p&单元测试不仅能保证代码的可靠程度,同时在写测试过程中你会发现你代码设计得不好的地方,我一直使用的一个评判标准就是:&b&编写单元测试的难度与代码质量成反比&/b&。&/p&&p&&br&&/p&&img src=&/v2-df2f27dd262f5cb50bc30_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-df2f27dd262f5cb50bc30_r.jpg&&&p&写完测试可以把代码先提交了,但是不要忘记还有一个非常重要的事情没做:写文档。&/p&&p&友好的文档是你项目能否吸引别人目光的首要的判断标准。文档都看不明白是个啥根本不可能有人敢用嘛。所以,在推广前,我们第一件事情就是完善文档。如果你的项目不限于国内使用,那最好提供两个版本,或者起码提供英文版本的文档。有一些项目国外是没法用的,比如我最近的轮子 overtrue/easy-sms,它是基于国内的短信运营商的,就算国外能用估计也没有人用,所以这种场景下就写中文说明就够。&/p&&p&一说到写英文,好多朋友第一句就会说:我英文很差啊。其实不要怕出错,大胆去做就好了,错了再改就是了。我一个四级都没过的人不也一样写吗,用好各种工具就行了。&/p&&p&如果项目比较小,一般 README.md 就够,如果项目比较复杂,写在 README 感觉太长,可以考虑三种方案:&/p&&p&第一种,最便捷的方案就是直接在项目里放一个 docs 文件夹,使用 Markdown 分文件写文档。&/p&&p&第二种,使用 GitHub 官方提供的 Pages 服务建立一个站点,这个具体使用可以去了解一下官方指南。&/p&&p&第三种,就是最复杂的方案了,自建网站,这种适用于中大型项目。&/p&&p&那么,文档应该怎么写呢?请你一定要关注以下这些必要的点。&/p&&p&&br&&/p&&img src=&/v2-ae11e82f2df15e3206965cb_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-ae11e82f2df15e3206965cb_r.jpg&&&p&&br&&/p&&img src=&/v2-e3836086ecb8dfa6f093c4d_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-e3836086ecb8dfa6f093c4d_r.jpg&&&p&文档写好了,我们应该发布我们的版本,具体关于如何把 GitHub 项目提交到 packagist 我就不细讲了,这个网上实在是太多讲它的,如果你还是没找到,就去 Laravel China 找到作者 Ryan 的文章《&a href=&/?target=https%3A//laravel-china.org/articles/1714/laravel-composer-package-development-concise-tutorial& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Laravel Composer Package 开发简明教程&i class=&icon-external&&&/i&&/a&》。&/p&&p&发布版本并不是那么随便的一件事情,错误的版本发布将会给用户带来灾难性的问题。&/p&&p&&br&&/p&&img src=&/v2-8fffb82dffe374_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-8fffb82dffe374_r.jpg&&&p&正确的 release 的前提是弄清楚语义化版本(Semantic Versioning),它包含 3 个部分:&/p&&ol&&li&&b&主版本号&/b&:当你做了不兼容的 API 修改,&/li&&li&&b&次版本号&/b&:当你做了向下兼容的功能性新增,&/li&&li&&b&修订号&/b&:当你做了向下兼容的问题修正。&/li&&/ol&&p&所以,请根据你的修改正确的使用版本号。&/p&&p&语义化版本的更多可以去官网(有中文支持)了解。&/p&&p&&br&&/p&&p&接下来的事情就是推广了,我的一句总结如下:&/p&&p&&br&&/p&&img src=&/v2-ab78e648b1556cda99c08b_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-ab78e648b1556cda99c08b_r.jpg&&&p&让真正需要它的或者可能需要它的都知道它的存在,这其中有三个核心:&/p&&p&1. 真正需要它的:这些人已经有这个需求了,但是还没解决或者没有很好的解决。&/p&&p&2. 可能需要它的:正在启动项目或者将要在项目中用到的一类人。&/p&&p&3. 知道它的存在:你需要正确的引导到你的项目而不是吹了一大堆后来连个链接都不给。&/p&&p&&br&&/p&&p&一些在推广过程中更细节的点:&/p&&img src=&/v2-e49de1f7b24ca26f4c2d8_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-e49de1f7b24ca26f4c2d8_r.jpg&&&p&你需要有自己的品牌,一个易识别的 GitHub ID、微博账号、微信号等。&/p&&p&在推广过程中你会遇到不少喷子或者闲得蛋疼我就是要骂你两句才舒服的人(根据经验这类人异常的多,知乎尤甚)。不要和他们喷,切记!&/p&&p&另外就是选对目标,不要跑去什么妈妈群或者什么老年健康中心去推广你的项目,他们不需要。去 Laravel China 吧,这里有一群搞 PHP 的小伙伴(当然还搞其它我就不说了)。&/p&&p&然后选择一个正确的时间点,图里我已经列举,这些时间点大家相对比较活跃,周末都约妹去了你就别浪费时间了。&/p&&p&然后一定要准确并富有表现力的表达,你写的帖子就一行我反正肯定不看的。&/p&&p&然后你需要有一定的反馈渠道,小项目的话 GitHub issue 就足够了,大项目你搞个 Q 群,论坛都可以。&/p&&p&&br&&/p&&img src=&/v2-6d3da71d1bb69a665ce31c29aac75f3d_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-6d3da71d1bb69a665ce31c29aac75f3d_r.jpg&&&p&那有了反馈就要关注了,每天抽点时间来看看 issue,分类并加入到改进计划。&/p&&p&请注意,这是最耗时的工作,所以不要影响到你的工作,如果感觉时间已经安排不过来了,就找小伙伴一起维护。&/p&&p&如果这个问题很小,那就立即响应修掉再说,比较复杂,就安排到周末,把约妹的时间挤挤。&/p&&p&&br&&/p&&img src=&/v2-a9f7f98b1489_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-a9f7f98b1489_r.jpg&&&p&很多时候决定是否要使用你这个项目判断,除了前面提到的文档与代码外,还有以下条件的:最近提交、issue 堆积量。&/p&&p&所以,一个项目的更新活跃度,也是比较重要的。保持项目更新,别人才敢用,不然刚用几天就进你的坑了,结果你半月不给人处理,那人家只能弃坑喷人了。&/p&&p&&br&&/p&&p&说了这么多,其实无非就那么8个步骤,每一步都走稳了,你的项目不会差到哪里去。&/p&&p&以下是题外话:&/p&&p&最近在做 EasyWeChat 4.0 的开发工作,我希望是月底能 release,如果没有完成的话你们也不要怪我,因为创业阶段事情比较多。&/p&&p&昨天在 overtrue/laravel-wechat 上一个朋友因为没有细看 README 的说明踩坑了,另外一个热心网友就去提示他,结果他就喷人家,各种骂,实在惨不忍睹,我后来去看了这哥们的博客,他自己不认真所致,这位热心网友并没有与他争吵,这里得点赞,我去解释了一下,结果提问的哥们又来骂我了。做开源心态很重要,这种情商低的朋友很多,不要跟他们吵,你要想一下,你做为一个能写开源项目的人,怎么能跟这种 loser 喷子骂呢是不是,那么多人围观呢,可丢人了。&/p&&p&&br&&/p&&p&对了,下一个话题是《基于 Composer 的模块化开发》,敬请期待。&/p&&p&&br&&/p&&p&还在等什么,快转发到朋友圈。&/p&&p&&br&&/p&&p&&br&&/p&&img src=&/v2-a91f0ee1d8c8b0f9295d2cbf4c695165_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-a91f0ee1d8c8b0f9295d2cbf4c695165_r.jpg&&&p&&br&&/p&&img src=&/v2-a7ee6b1a628a311b5024f2_b.jpg& data-rawwidth=&1920& data-rawheight=&1080& class=&origin_image zh-lightbox-thumb& width=&1920& data-original=&/v2-a7ee6b1a628a311b5024f2_r.jpg&&&p&&/p&
这其实是之前在北京 Laravel Meetup 的一次分享内容,不过考虑到有很多人在公众号想听听关于我是如何做开源这个话题,所以就再次拿它讲一个文字版。 关于我,这个就没啥可讲的了,EasyWeChat 作者、Laravel China 创始人之一。 要想做好开源,这 8 个步骤缺…
&img src=&/v2-eb1b7cba9a877cb88ab3c2_b.png& data-rawwidth=&1200& data-rawheight=&224& class=&origin_image zh-lightbox-thumb& width=&1200& data-original=&/v2-eb1b7cba9a877cb88ab3c2_r.png&&&h2&前言&/h2&&p&贝聊目前开发的两款App分别是贝聊家长版和贝聊老师版,最近因为在快速迭代开发新功能,项目规模急速增长,单个端业务代码约23万行,私有库约6万行,第三方库代码约15万行,单个客户端的代码行数约60万。现在打包一次耗时需要11~12分钟。虽然还远远比不上 Facebook 的40分钟,但是我们在内测的时候,经常一天要发布内测版两到三次。打包时CPU占用基本上是百分百的,因为没有专门的 CI 机器,对负责打包的同事(其实就是我自己)的工作时间占用比较多,所以最近一直在寻找加快打包速度的方案。&/p&&h2&目前的项目架构&/h2&&p&我们的项目使用 CocoaPods 来管理第三方库和私有库的依赖,对大部分项目来说应该是标配了。目前还是纯 Objective-C 的项目,没有引入 Swift。&/p&&h2&调研过的方案&/h2&&p&下面列出我研究过的一些主流方案以及我最后没有采用的原因,这些方案有各自的局限性,但是也给了我不少启发,思考过程跟最终方案一样有价值。&/p&&h3&cocoapods-packager&/h3&&p&&a href=&/?target=https%3A///CocoaPods/cocoapods-packager& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&cocoapods-packager&i class=&icon-external&&&/i&&/a& 可以将任意的 pod 打包成 Static Library,省去重复编译的时间,一定程度上可以加快编译时间,但是也有自身的缺点:&/p&&ol&&li&优化不彻底,只能优化第三方和私有 Pod 的编译速度,对于其他改动频繁的业务代码无能为力&/li&&li&私有库和第三方库的后续更新很麻烦,当有源码修改后,需要重新打包上传到内部的 Git 仓库&/li&&li&过多的二进制文件会拖慢 Git 的操作速度(目前还没部署 Git 的 &a href=&/?target=https%3A//git-/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&LFS&i class=&icon-external&&&/i&&/a&)&/li&&li&难以调试源码&/li&&/ol&&h3&Carthage&/h3&&p&这个方案跟 cocoapods-packager 比较类似,优缺点都差不多,但 Carthage 可以比较方便地调试源码。因为我们目前已经大规模使用 CocoaPods,转用 Carthage 来做包管理需要做大量的转换工作,所以不考虑这个方案了。&/p&&h3&Buck&/h3&&p&&a href=&& data-editable=&true& data-title=&Buck&&Buck&/a& 是一套通用的构建系统,由 Facebook 开源。最大的特色是智能的增量编译可以极大地提高构建速度。最早听说 Buck 的时候,它还只能用在安卓上,现在已经适配了 iOS。&/p&&p&它能增快构建速度的主要原因是缓存了编译结果,通过持续监视项目目录的文件变化,每次编译时只编译有改动的文件。另外一个让我很受启发的功能是 HTTP Cache Server,通过一台缓存文件服务器来保存大家的编译结果,这样只要团队里其中一人编译过的文件,其他人就不用再编译了,直接下载就行。&/p&&p&Buck 是个相当完备的解决方案,很多国外的大公司例如 Uber 都已经用上。我也花了很多时间来研究,最终还是认为对我们的项目和团队来说,目前并不是很适合,主要原因是:&/p&&ol&&li&Buck 抛弃了 Xcode 的项目文件,需要手工编写配置文件来指定编译规则,这要对现有项目作出大幅度的调整。我们目前还在快速迭代新功能,没有余暇和人手来实施。&/li&&li&开发和调试的流程都得做出很大的改变。因为 Buck 接管了项目编译的过程,想调试项目不能简单地在 Xcode 里面 ?+R 了,得先反过来让 Buck 生成 Xcode 的项目文件。Uber 的工程师甚至推荐使用 &a href=&/?target=https%3A//nuclide.io& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Nuclide&i class=&icon-external&&&/i&&/a& 来代替 Xcode 作为开发环境。虽然原理上是可行的,但是团队需要花不少时间来适应,短期内效率降低无可避免。&/li&&li&用 Xcode 调试代码享受不到加快编译速度的好处。虽然可以用 buck 命令启动 App,然后在命令行里启动 lldb 来调试,但那就无法使用 Xcode 的调试工具 例如 View Debugging 和 Memory Graph Debugger。&/li&&/ol&&h3&Bazel&/h3&&p&&a href=&/?target=https%3A//bazel.build/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Bazel&i class=&icon-external&&&/i&&/a& 跟 Buck 很相似,是 Google 开源的,优缺点跟 Buck 都差不多,不再详细说了。&/p&&h3&distcc 分布式编译&/h3&&p&原理是把一部分需要编译的文件发送到服务器上,服务器编译完成后把编译产物传回来。我尝试了一下比较出名的 distcc,搭建过程比较简单,最后也能成功地把编译任务分派到内网的多台服务器上。但是其他编译服务器的 CPU 占用总是很低,只有 20% 左右;也就是说分派任务的速度甚至还赶不上服务器编译的速度,分派任务然后回传编译产物这个过程所耗费的时间超过了本地直接编译。不停调整参数反复试验了很多次,最后发现编译时间完全没有变快,甚至还有点变慢了。可能以我们目前项目的规模并不适合使用分布式编译。&/p&&h2&最终方案:CCache&/h2&&p&先来看看我对于解决方案的诉求:&/p&&ol&&li&能大幅度地提升编译速度,起码要减少掉 50% 的编译时间&/li&&li&不需要对项目作出重大调整&/li&&li&不需要改变开发工具链&/li&&/ol&&p&&a href=&/?target=https%3A//ccache.samba.org& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&CCache&i class=&icon-external&&&/i&&/a& 是一个能够把编译的中间产物缓存起来的工具,在其他领域已经有不少应用,只是在 iOS 界的实践比较少。经过我的实践,它能够满足我前面的三点要求。我最早认识到它是搜到了这篇文章:&a href=&/?target=https%3A///blog/2015/ccache-for-fun-and-profit/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Using ccache for Fun and Profit | Inside PSPDFKit&i class=&icon-external&&&/i&&/a&&/p&&p&如果你不使用 CocoaPods,参照上面的文章即可。因为针对 CocoaPods 需要作出一些额外的调整,所以还是说明一下。下面就来说说要怎样把 CCache 应用在用 CocoaPods 作为包管理工具的 iOS 项目中。&/p&&h3&安装步骤:&/h3&&p&&strong&注意:项目路径不能有中文,否则会影响 CCache 的正常工作&/strong&&/p&&h4&安装 CCache&/h4&&p&首先你需要在电脑上安装 Homebrew,对使用 macOS 的程序员来说应该是标配,略过。&/p&&p&通过 Homebrew 安装 CCache, 在命令行中执行&br&$ brew install ccache&/p&&p&命令跑完后即安装成功。&/p&&h4&创建 CCache 编译脚本&/h4&&p&为了能让 CCache 介入到整个编译的过程,我们要把 CCache 作为项目的 C 编译器,当 CCache 找不到编译缓存时,它会再把编译指令传递给真正的编译器 clang。&/p&&p&新建一个文件命名为ccache-clang, 内容为下面这段脚本,放到你的项目里&/p&&h4&ccache-clang&/h4&&div class=&highlight&&&pre&&code class=&language-bash&&&span&&/span&&span class=&ch&&#!/bin/sh&/span&
&span class=&k&&if&/span& &span class=&nb&&type&/span& -p ccache &/dev/null 2&&span class=&p&&&&/span&1&span class=&p&&;&/span& &span class=&k&&then&/span&
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_MAXSIZE&/span&&span class=&o&&=&/span&10G
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_CPP2&/span&&span class=&o&&=&/span&&span class=&nb&&true&/span&
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_HARDLINK&/span&&span class=&o&&=&/span&&span class=&nb&&true&/span&
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_SLOPPINESS&/span&&span class=&o&&=&/span&file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
&span class=&c1&&# 指定日志文件路径到桌面,等下排查集成问题有用,集成成功后删除,否则很占磁盘空间&/span&
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_LOGFILE&/span&&span class=&o&&=&/span&&span class=&s1&&'~/Desktop/CCache.log'&/span&
&span class=&nb&&exec&/span& ccache /usr/bin/clang &span class=&s2&&&&/span&&span class=&nv&&$@&/span&&span class=&s2&&&&/span&
&span class=&k&&else&/span&
&span class=&nb&&exec&/span& clang &span class=&s2&&&&/span&&span class=&nv&&$@&/span&&span class=&s2&&&&/span&
&span class=&k&&fi&/span&
&/code&&/pre&&/div&&p&在命令行中,cd 到 ccache-clang 文件的目录,把它的权限改成可执行文件&br&$ chmod 777 ccache-clang&/p&&p&如果你的代码或者是第三方库的代码用到了C++,则把ccache-clang这个文件复制一份,重命名成ccache-clang++。相应的对clang的调用也要改成clang++,否则 CCache 不会应用在 C++ 的代码上。&/p&&h4&ccache-clang++&/h4&&div class=&highlight&&&pre&&code class=&language-bash&&&span&&/span&&span class=&ch&&#!/bin/sh&/span&
&span class=&k&&if&/span& &span class=&nb&&type&/span& -p ccache &/dev/null 2&&span class=&p&&&&/span&1&span class=&p&&;&/span& &span class=&k&&then&/span&
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_MAXSIZE&/span&&span class=&o&&=&/span&10G
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_CPP2&/span&&span class=&o&&=&/span&&span class=&nb&&true&/span&
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_HARDLINK&/span&&span class=&o&&=&/span&&span class=&nb&&true&/span&
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_SLOPPINESS&/span&&span class=&o&&=&/span&file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
&span class=&c1&&# 指定日志文件路径到桌面,等下排查集成问题有用,集成成功后删除,否则很占磁盘空间&/span&
&span class=&nb&&export&/span& &span class=&nv&&CCACHE_LOGFILE&/span&&span class=&o&&=&/span&&span class=&s1&&'~/Desktop/CCache.log'&/span&
&span class=&nb&&exec&/span& ccache /usr/bin/clang++ &span class=&s2&&&&/span&&span class=&nv&&$@&/span&&span class=&s2&&&&/span&
&span class=&k&&else&/span&
&span class=&nb&&exec&/span& clang++ &span class=&s2&&&&/span&&span class=&nv&&$@&/span&&span class=&s2&&&&/span&
&span class=&k&&fi&/span&
&/code&&/pre&&/div&&p&完成后项目中应该有这两个文件&/p&&p&?&/p&&h4&Xcode 项目的调整&/h4&定义CC常量&p&在你项目的构建设置(Build Settings)中,添加一个常量CC,这个值会让 Xcode 在编译时把执行路径的可执行文件当做 C 编译器。&/p&&p&&img src=&/v2-2d7e2c013adea8fc13734a68_b.png& data-rawwidth=&1467& data-rawheight=&251& class=&origin_image zh-lightbox-thumb& width=&1467& data-original=&/v2-2d7e2c013adea8fc13734a68_r.png&&&img src=&/v2-b0844daad9d3f0fd33955_b.png& data-rawwidth=&1222& data-rawheight=&136& class=&origin_image zh-lightbox-thumb& width=&1222& data-original=&/v2-b0844daad9d3f0fd33955_r.png&&?CC常量的值为 $(SRCROOT)/ccache-clang,如果你的脚本不是放在项目根目录,则自行调整路径。如果一运行项目就报错,检查下路径是不是填错了。&/p&&p&关闭 Clang Modules&/p&&p&因为 CCache 不支持 Clang Modules,所以需要把 Enable Modules 的选项关掉。这个问题在 CocoaPods 上如何处理,后面会讲。&/p&&br&&img src=&/v2-b41ba260e8a30c20153b_b.png& data-rawwidth=&1236& data-rawheight=&294& class=&origin_image zh-lightbox-thumb& width=&1236& data-original=&/v2-b41ba260e8a30c20153b_r.png&&关闭了 Enable Modules 后需要作出的调整&p&因为关闭了 Enable Modules,所以必须删除所有的 @import语句,替换为#import的语法&br&例如将 @import UIKit 替换为 #import &UIKit/UIKit.h&。之后,如果你用到了其他的系统框架例如 AVFoundation、CoreLocation等,现在 Xcode 不会再帮你自动引入了,你得要在项目 Target 的 Build Phrase -& Link Binary With Libraries 里面自己手动引入。&/p&&h4&测试效果&/h4&&p&尝试编译一遍,然后在命令行里输入 ccache -s 就能看见类似下面的 ccache 运行情况统计:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&cache directory
/Users/mac/.ccache
primary config
/Users/mac/.ccache/ccache.conf
secondary config
(readonly)
/usr/local/Cellar/ccache/3.3.4_1/etc/ccache.conf
cache hit (direct)
cache hit (preprocessed)
cache miss
cache hit rate
called for link
called for preprocessing
compile failed
preprocessor error
can't use precompiled header
unsupported compiler option
no input file
cleanups performed
files in cache
cache size
max cache size
&/code&&/pre&&/div&&p&如果成功接入,就能看见 cache miss 不为0。因为第一次编译没有缓存,肯定是全 miss 的。接着编译第二遍,如果能看见 cache hit 的数字开始飙升,恭喜你,接入成功了。&/p&&h4&CocoaPods 的处理&/h4&&p&如果你的项目不用 CocoaPods 来做包管理,那你已经完全接入成功了,不用执行下面的操作。&/p&&p&因为 CocoaPods 会单独把第三方库打包成一个 Static Library(或者是Dynamic Framework,如果用了 use_frameworks!选项),所以 CocoaPods 生成的 Static Library 也需要把 Enable Modules 选项给关掉。但是因为 CocoaPods 每次执行 pod update 的时候都会把 Pods 项目重新生成一遍,如果直接在 Xcode 里面修改 Pods 项目里面的 Enable Modules 选项,下次执行pod update的时候又会被改回来。我们需要在 Podfile 里面加入下面的代码,让生成的项目关闭 Enable Modules 选项,同时加入 CC 参数,否则 pod 在编译的时候就无法使用 CCache 加速:&/p&&div class=&highlight&&&pre&&code class=&language-rb&&&span&&/span&&span class=&n&&post_install&/span& &span class=&k&&do&/span& &span class=&o&&|&/span&&span class=&n&&installer_representation&/span&&span class=&o&&|&/span&
&span class=&n&&installer_representation&/span&&span class=&o&&.&/span&&span class=&n&&pods_project&/span&&span class=&o&&.&/span&&span class=&n&&targets&/span&&span class=&o&&.&/span&&span class=&n&&each&/span& &span class=&k&&do&/span& &span class=&o&&|&/span&&span class=&n&&target&/span&&span class=&o&&|&/span&
&span class=&n&&target&/span&&span class=&o&&.&/span&&span class=&n&&build_configurations&/span&&span class=&o&&.&/span&&span class=&n&&each&/span& &span class=&k&&do&/span& &span class=&o&&|&/span&&span class=&n&&config&/span&&span class=&o&&|&/span&
&span class=&c1&&#关闭 Enable Modules&/span&
&span class=&n&&config&/span&&span class=&o&&.&/span&&span class=&n&&build_settings&/span&&span class=&o&&[&/span&&span class=&s1&&'CLANG_ENABLE_MODULES'&/span&&span class=&o&&]&/span& &span class=&o&&=&/span& &span class=&s1&&'NO'&/span&
&span class=&c1&&# 在生成的 Pods 项目文件中加入 CC 参数,路径的值根据你自己的项目来修改&/span&
&span class=&n&&config&/span&&span class=&o&&.&/span&&span class=&n&&build_settings&/span&&span class=&o&&[&/span&&span class=&s1&&'CC'&/span&&span class=&o&&]&/span& &span class=&o&&=&/span& &span class=&s1&&'$(PODS_ROOT)/../ccache-clang'&/span&
&span class=&k&&end&/span&
&span class=&k&&end&/span&
&span class=&k&&end&/span&
&/code&&/pre&&/div&&p&需要注意的是,如果你使用的某个 Pod 引用了系统框架,例如AFNetworking引用了System Configuration,你需要在你自己项目的Build Phrase -& Link Binary With Libraries里面代为引入,否则你编译时可能会收到 Undefined symbols xxx for architecture yyy一类的错误。有点回到了原始时代的感觉,但考虑到编译速度的极大提升,这一点代价可以接受。&/p&&h4&集成问题排查&/h4&&p&重点关注日志文件的输出和ccache -s 命令的统计,如果在日志中看到了 unsupported compiler option -fmodules 这样的字眼,就是你的 Enable Modules 没有关掉了,根据前面的步骤仔细检查。其他问题,参考官方文档的 &a href=&/?target=https%3A//ccache.samba.org/manual.html%23_troubleshooting& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Troubleshooting&i class=&icon-external&&&/i&&/a&。&/p&&h3&进一步的优化&/h3&&h4&移除 Precompiled Header File&/h4&&p&PCH 的内容会被附加在每个文件前面,而 CCache 是根据文件内容的 MD4 摘要来查找缓存的,因此当你修改了 PCH 或者 PCH 引用到的头文件的内容时,会造成全部缓存失效,只能全体重新编译。CCache 在首次编译的时候因为需要更新缓存,会造成编译时间变长,对贝聊的项目来说变长了差不多一倍。因此如果 PCH 或者 PCH 引入的文件被频繁修改的话,缓存就会频繁地 miss,这种情况下还不如不用 CCache。&/p&&p&为了避免以上这种情况,我建议在 PCH 里面尽量少引入头文件,只保留比较少更改的系统框架和第三方类库的头文件。最好是把 PCH 彻底删除,反正苹果现在也不建议使用 PCH 了,Xcode 新建的项目默认都是不带 PCH 的。&/p&&h4&在团队内部共享缓存文件夹&/h4&&p&这个优化方式我尝试过,最终效果不是很好,因此没有采用。CCache 的官方文档中有一段&a href=&/?target=https%3A//ccache.samba.org/manual.html%23_sharing_a_cache& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&关于共享缓存文件夹的说明&i class=&icon-external&&&/i&&/a&,描述了如何修改 CCache 的配置,让编译缓存能够在多台电脑之间公用,理论上只要其中一个人编译过的文件其他人就能直接下载到了,节约了整个团队的时间。因为 Buck 也有类似的机制,我觉得值得尝试一下,便在公司局域网内搭建了一个 &a href=&/?target=https%3A//owncloud.org/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&OwnCloud&i class=&icon-external&&&/i&&/a& 网盘,让大家把自己电脑上的 CCache 缓存目录放上去共享。虽然试验是成功了,但是实际效果并不好。因为同步在多台电脑上大小达到几个G的缓存目录,需要在后台进行很多文件的对比和传输的工作,在编译的同时进行这些操作会耗费不少计算资源,反而会拖慢编译速度。加上移除掉 PCH 后,其实缓存的命中率已经相当可观了,不太需要通过共享缓存来进一步提高缓存命中率,所以我最后放弃了共享缓存这个想法。如果你对缓存命中率还是不满意的话,可以考虑往这个方向尝试一下。&/p&&h3&总结&/h3&&p&通过集成 CCache,我们的项目在 Xcode 里面的打包(在菜单里面选择 Product -& Archive)时间从 11~12分钟减少到了 130 秒,大概有五倍的提升,成果喜人。集成的过程其实很简单,我从开始尝试到集成成功总共就花了两个小时。如果你也被过长的编译时间困扰,建议尝试一下。&/p&
前言贝聊目前开发的两款App分别是贝聊家长版和贝聊老师版,最近因为在快速迭代开发新功能,项目规模急速增长,单个端业务代码约23万行,私有库约6万行,第三方库代码约15万行,单个客户端的代码行数约60万。现在打包一次耗时需要11~12分钟。虽然还远远比不…
&blockquote&&p&作者:林蓝东&/p&
&/blockquote&
&p& 最近的一个手机 QQ 版本发出去后收到比较多关于 CoreMotion 的 crash 上报,案发现场如下:&/p&
&img src=&/v2-fcbe977a1e98_b.png& data-rawwidth=&1420& data-rawheight=&638& class=&origin_image zh-lightbox-thumb& width=&1420& data-original=&/v2-fcbe977a1e98_r.png&&&p& 但是看看这个堆栈发现它完全不按照套路出牌啊!&/p&
&img src=&/v2-c1e4b8ba4164_b.jpg& data-rawwidth=&225& data-rawheight=&208& class=&content_image& width=&225&&&p& 乍一看是挂在 CoreMotion 里面的CLStartStopAdvertisingBeacon函数,看似是 iBeacon 相关的问题,但实际上是具体函数的符号解不出来,注意 CLStartStopAdvertisingBeacon + 175940 这个巨大的偏移量,一般的函数不可能这么大,所以&strong&这个地址对应的肯定是另外的一个函数!&/strong&&/p&
&p& 抛开错误的函数名,看看堆栈的调用顺序,看上去是像是 CoreMotion 在子线程起了一个 Runloop,然后在这个 Runloop 处理来自 IOKit 的回调。&/p&
&p& 再看看 crash 的 Exception Codes: BUS_ADRALN at 0x205d,可以知道这是&strong&访问了一个未对齐的地址&/strong& 0x205d 导致的崩溃;同时留意到上报上来的寄存器状态,这个地址正是当前 pc 和 x8 寄存器的值!:&/p&
&img src=&/v2-c4af665f8c3e_b.png& data-rawwidth=&1572& data-rawheight=&386& class=&origin_image zh-lightbox-thumb& width=&1572& data-original=&/v2-c4af665f8c3e_r.png&&&p& 一般 PC 寄存器保存的是下一条指令的地址,并且要求地址最后的两个比特位是 00 ,这个地址很明显不能满足要求;这种情况通常是因为数据被破坏,导致&strong&读取到的函数指针值异常&/strong&。&/p&
&p& 有了上面几点发现,我们可以到真机上去探一探究竟。这个上报上来的 crash 是发生在安装了 iOS 10.3.1 (14E304 的一台 64 位机器上,所以我们找来一台符合这两个条件的设备;因为这是发生在系统框架里面,满足这两个条件才能&strong&保证 CoreMotion 的二进制内容和 crash 的机器是一致的&/strong&(可以通过 framework 的 UUID 来验证这一点)。&/p&
&p& 在真机上我们要去找到这几个解错的函数名,而我们的依据就是下图中红色框的地址:&/p&
&img src=&/v2-76cfc3f6f7f5c6_b.png& data-rawwidth=&1474& data-rawheight=&454& class=&origin_image zh-lightbox-thumb& width=&1474& data-original=&/v2-76cfc3f6f7f5c6_r.png&&&p& 这些是 crash 所在指令的地址,但这些地址由于 &a href=&/?target=http%3A//Address%2520space%2520layout%2520randomization& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&ASLR&i class=&icon-external&&&/i&&/a&(地址空间配置随机载入) 的原因是不固定的,所以我们不能在自己的机器上直接用这些地址,而是要利用 crash 时 CoreMotion 框架的载入地址来计算出一个相对的偏移量。通常一个 crash 日志上报上来都会带有一个Binary Images信息:&/p&
&img src=&/v2-c31cb6c10ba_b.png& data-rawwidth=&1882& data-rawheight=&296& class=&origin_image zh-lightbox-thumb& width=&1882& data-original=&/v2-c31cb6c10ba_r.png&&&p& 可以看到当时 CoreMotion 的载入起始地址是 0x,然后我们用 crash 堆栈顶部指令的地址 0xab62c 减去它得到一个&strong&偏移量 0x6862c&/strong&( 0x1995ab62c - 0x = 0x6862c)。&/p&
&p& 接下来在真机上编译运行手机QQ,启动后暂停进入 lldb,执行命令:image list 命令可以得到当前 CoreMotion 的载入地址:&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span& [ 36] 1EE3BF50-5BBD-3BB1-B441-D6 0xcb000 /.../Library/Frameworks/CoreMotion.framework/CoreMotion
&/code&&/pre&&/div&
&p& 我们把 0xcb000 加上之前计算出来的偏移量 0x6862c 就得出一个新地址: 0x + 0x6862c = 0xc 这个就是当前机器上对应的地址。有了这个地址我们可以尝试解下真实的函数名:image lookup -a 0xc,不过遗憾的是输出结果并没有什么卵用:&/p&
&img src=&/v2-f20eea0fd896f13380ca7dab29cda720_b.png& data-rawwidth=&1074& data-rawheight=&162& class=&origin_image zh-lightbox-thumb& width=&1074& data-original=&/v2-f20eea0fd896f13380ca7dab29cda720_r.png&&&p& ___lldb_unnamed_symbol2303说明 CoreMotion 把这个符号裁掉了… 不过我们可以在这个地址打个断点 br set -a 0xc,然后跑进去看一下;进入手机QQ的好友动态页面 (QQ空间),发现这个断点被触发了:&/p&
&img src=&/v2-06a2ef9aadaef_b.png& data-rawwidth=&1110& data-rawheight=&902& class=&origin_image zh-lightbox-thumb& width=&1110& data-original=&/v2-06a2ef9aadaef_r.png&&&p& 注意断点位置的上一句 blr x8 :&strong&跳转到 x8 寄存器中的地址,并把 lr 寄存器设置为 pc + 4 的值&/strong&,如果此处 x8 的值出现问题,那么就会出现上报堆栈中的现象: BUS_ADRALN,并且 x8 和 pc 的值都是这个出错的地址。&/p&
&p& 然而到这一步后似乎遇到死胡同,函数符号都被裁剪掉,而且这里的回调都是 C 函数,无法从 selector 获取方法名,操作的也不是 OC 对象,唯一可以确定的是进入手机QQ的 &em&好友动态&/em& 页面时该函数会被调用。通过查看此页面代码,确实会启动一个 CMMotionManager 然后通过回调监听陀螺仪的回调,但是此段代码并非新增功能,之前版本一直稳定工作,检查后没有发现可疑点。所以进一步推测:&strong&有没有其它业务代码也在使用 CMMotionManager ?&/strong&&/p&
&p& 为此,我们查看了上报信息中这些 crash 的发生场景,发现集中发生在两个地方:&/p&
TBStoryViewController 和 MQZoneVideoRecordViewController ,这两个类都是提供摄像功能 ViewController,而且继承自同样的父类,界面展示出来之后确实也会触发之前 crash 的函数;但是找遍这几个类的代码,&strong&没有发现直接使用 CMMotionManager的地方&/strong&,于是推测是&strong&间接使用了 CMMotionManager&/strong&。&/p&
&p& 为了找到谁间接使用了 CMMotionManager ,首先想到的是给所有的 CMMotionManager 方法打上断点,这样一调用就会停住,然后从堆栈上就能看出谁使用了它&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span& (lldb) br set -r &CMMotionManager&
&/code&&/pre&&/div&
&p& 这里使用了 -r 选项来传入一个正则表达式,用于匹配所有 CMMotionManager 的方法,然后打上符号断点。当是最后还是行不通,因为 CMMotionManager 的几乎所有的符号都被裁掉了,所以打不上…. 这时候 &a href=&/?target=http%3A//Frida%%%world-class%2520dynamic%2520instrumentation%2520framework& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Frida&i class=&icon-external&&&/i&&/a& 这个工具就派上用场了,将它提供的 framework 编译到自己的工程里后,我们就可以在命令行监控到所有的 Objective-C 方法调用记录:&/p&
&p& frida-trace -U -f re.frida.Gadget -m &-[CMMotionManager \*]&&/p&
&p& 通过这个方法发现那两个 controller 一旦展示,就会出现包括 -[CMMotionManager isAccelerometerActive] 的几个调用。那么给-[CMMotionManager isAccelerometerActive]打个断点看看谁在使用,符号断点我们打不上,那么我们就直接打到函数地址上,利用运行时 API 取出该方法的 IMP 值:&/p&
&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span& (lldb) po method_getImplementation((Method)class_getInstanceMethod([CMMotionManager class], @selector(isAccelerometerActive)))
(lldb) br set -a 0x2918
&/code&&/pre&&/div&
&p& 运行后果然逮到了,一个业务代码会使用 UIAccelerometer,然后 UIAccelerometer 使用了 CMMotionManager:&/p&
&img src=&/v2-11bfbfb062f11e_b.png& data-rawwidth=&1298& data-rawheight=&542& class=&origin_image zh-lightbox-thumb& width=&1298& data-original=&/v2-11bfbfb062f11e_r.png&&&p& 进一步通过 &a href=&/?target=http%3A//nst/iOS-Runtime-Headers& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&iOSRuntimeHeader&i class=&icon-external&&&/i&&/a& 可以确认 UIAccelerometer有一个 CMMotionManager 作为实例变量:&/p&
&img src=&/v2-d048ae104abddb5d0abb8c_b.png& data-rawwidth=&854& data-rawheight=&370& class=&origin_image zh-lightbox-thumb& width=&854& data-original=&/v2-d048ae104abddb5d0abb8c_r.png&&&p& 看看业务代码,对 UIAccelerometer 的使用也是很简单,似乎没有什么不妥,难道又冤枉了好人?但是仔细看看断点处的堆栈发现一个可疑的地方:调用发生在 Thread 139,而 UIAccelerometer 是一个 UIKit 的类,&strong&一般 UIKit 的方法只能在主线程使用&/strong&!查看官方文档并没有说明 UIAccelerometer 是否是线程安全,所以我们需要验证一下,如果不是,这里可能是一个突破口。&/p&
&p& 查看代码发现是通过 -[UIAccelerometer sharedAccelerometer] 获取一个单例对象进行使用,如果这个类是线程安全的,那么 sharedAccelerometer 的实现也应该是线程的,由于这种单例方法一般实现比较简单,所以不妨查看下汇编代码看看实现:&/p&
&img src=&/v2-81caa23d74d63db9d688_b.png& data-rawwidth=&1358& data-rawheight=&876& class=&origin_image zh-lightbox-thumb& width=&1358& data-original=&/v2-81caa23d74d63db9d688_r.png&&&p& 翻译成ARC代码大概是:&/p&
&img src=&/v2-895930ccd7d64f2feb3d_b.png& data-rawwidth=&880& data-rawheight=&294& class=&origin_image zh-lightbox-thumb& width=&880& data-original=&/v2-895930ccd7d64f2feb3d_r.png&&&p& 可以看到整段代码没有任何锁的保护,如果有两个线程同时获取单例,就可能发生 sharedInstance 变量被重复赋值的情况,而且第二次赋值会将第一次构造的对象进行 release,让该对象野掉,而我们知道 UIAccelerometer 有一个 CMMotionManager 的成员变量,它也会随之一起野掉!&/p&
&p& 同时还发现 -[UIAccelerometer _motionManager] 这个私有方法:&/p&
&img src=&/v2-61d5b9c5f3f3b94e02e8fa_b.png& data-rawwidth=&1404& data-rawheight=&1246& class=&origin_image zh-lightbox-thumb& width=&1404& data-original=&/v2-61d5b9c5f3f3b94e02e8fa_r.png&&&p& 同样用判断是否为空的形式对 _motionManager 变量进行惰性初始化,同样没有加任何锁的保护,如果多个线程同时调用这个方法也会造成 _motionManager 野掉!&/p&
&p& 验证是否在多线程使用很简单了,[UIAccelerometer sharedAccelerometer] 和 [UIAccelerometer _motionManager] 分别打个断点,然后运行:&/p&
&img src=&/v2-443d7fbcffe49edcaa7ad_b.png& data-rawwidth=&1324& data-rawheight=&760& class=&origin_image zh-lightbox-thumb& width=&1324& data-original=&/v2-443d7fbcffe49edcaa7ad_r.png&&&p& 从断点触发的位置可以发现该两个方法会在不同线程进行访问,而且时机非常接近。最后追溯原因,是之前有同学为了避免 UIAccelerometer 在主线程启动造成卡顿,直接将加速剂的开始和借宿操作通过 dispatch_async 放到了一个 global_queue 里面,都放到了一个 global_queue 里面,属于并发队列,UIAccelerometer 的回调又是在主线程,所以造成了上面的问题:快速开关界面造成多线程同时调用 -[UIAccelerometer sharedAccelerometer] !&/p&
&p& 所以,最终的解决方案是将 UIAccelerometer 的操作全部移动回主线程。&/p&
&h2&总结&/h2&
&p& 林子大了什么鸟都有,一个大型的应用总会遇到各种奇葩的 BUG,具体解决的手段可能各有不同,但是有一个 科学方法 很值得参考,通过观察收集一个 crash 上报的细节信息,然后提出假设,验证假设;这个过程中辅助以各种工具和经验,最后通过几个这样的迭代定位出问题所在:&/p&
&img src=&/v2-c345c65e2ce74e5eef8f5_b.png& data-rawwidth=&1416& data-rawheight=&1138& class=&origin_image zh-lightbox-thumb& width=&1416& data-original=&/v2-c345c65e2ce74e5eef8f5_r.png&&&p&更多精彩内容欢迎关注&a href=&/?target=https%3A///& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&bugly&i class=&icon-external&&&/i&&/a&的微信公众账号:&/p&&img src=&/3f2c1b1ff77fcedf3fb54616_b.jpg& class=&content_image&&&p&&a href=&/?target=https%3A///& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&腾讯 Bugly&i class=&icon-external&&&/i&&/a&是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 &a href=&/?target=https%3A///& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Crash&i class=&icon-external&&&/i&&/a& 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!&/p&
作者:林蓝东
最近的一个手机 QQ 版本发出去后收到比较多关于 CoreMotion 的 crash 上报,案发现场如下:
但是看看这个堆栈发现它完全不按照套路出牌啊!
乍一看是挂在 CoreMotion 里面的CLStartStopAdvertisingBeacon函数,看似是 iBeacon 相关的问题…
&p&「家人共享」功能最大的好处之一是,&b&它能让我们免费下载其他成员购买过的 app&/b&。另外还能分享地理位置、共享相册和日历……&br&&/p&&p&简单的设置后,「家人共享」能让你和家人的生活更加方便。接下来 AppSo 将为你带来它的功能介绍(文末附设置指南)。&/p&&blockquote&&p&&strong&AppSo 之前为大家深度剖析过不少 iOS 的原生功能,像是原生输入法、备忘录和提醒事项等。&/strong&(微信号 appsolution 后台回复「苹果全家桶」获取那些你不知道的苹果设备使用技巧。)&/p&&/blockquote&&h3&一、Ta 买了 App,我怎么能用上?&/h3&&p&想要下载家人已经购买过的 app,只需在 App Store 的「更新 - 已购项目 - 家庭购买项目」中找到家庭成员,点进去之后就可以看到该位成员购买过的 app 了。&/p&&img src=&/v2-ddee534b51b1da432eadd_b.png& data-rawwidth=&1131& data-rawheight=&667& class=&origin_image zh-lightbox-thumb& width=&1131& data-original=&/v2-ddee534b51b1da432eadd_r.png&&&p&如果是 Mac App Store,则在「已购项目」中查看。&/p&&img src=&/v2-673e57dc97a0d5c33e6ccc9_b.jpg& data-rawwidth=&1000& data-rawheight=&560& class=&origin_image zh-lightbox-thumb& width=&1000& data-original=&/v2-673e57dc97a0d5c33e6ccc9_r.jpg&&&p&AppSo(微信号 appsolution )提醒,&strong& 如果在 App Store 中直接搜索 app,有可能还是会显示原价,因此我们建议各位尽量在「已购项目」下载应用,避免被错误扣费。&/strong&&/p&&p&但是如果你买的 app 并不想给家人共享组的其它用户看到的话,可以选择隐藏它,这样其它家庭成员在查看你的已购清单的时候就不会看到这个 app 了。&strong&想要隐藏某个 app 只需要在已购项目中左滑选择「隐藏」即可。&/strong&&/p&&p&虽然 &a href=&///?target=https%3A///zh-cn/HT201322& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Apple 官方&i class=&icon-external&&&/i&&/a&指南表示我们可以通过这个方法隐藏,但 AppSo 实测发现部分手机可能无法如此操作。此时&strong&你可以在 iTunes 中隐藏&/strong&:在「已购项目」找到想要隐藏的 app,鼠标指针指向图标直至左上角出现「x」,点击即可隐藏。&/p&&img src=&/v2-6f339de329b029a3497a_b.jpg& data-rawwidth=&976& data-rawheight=&623& class=&origin_image zh-lightbox-thumb& width=&976& data-original=&/v2-6f339de329b029a3497a_r.jpg&&&br&&h3&二、除了共享购买,家人共享还能做很多&/h3&&p&不过帮家人买买买并不是家人共享最重要的地方,在我看来,&strong&家人共享最重要的在于它搭建了一套属于家庭之间的私密空间&/st

我要回帖

更多关于 怎样减少焦虑 的文章

 

随机推荐