2024年Android最全阿里技术分享:APP启动提速方法总结,2024Android研发必问高级面试题

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

Android 通过系统提供的资源拦截Api即可实现加载拦截,拦截后根据请求的url识别资源类型,命中后设置对应的mimeType、encoding、fileStream即可。

三 下载速度

App 安装前的下载速度也直接影响到了用户从选择你的 App 到使用的体验,如果下载大小过大,用户没有耐心等待,可能就放弃了你的 App,4G5G 环境下超 200MB 会弹窗提示是否继续下载,严重影响转化率。

因此还对下载大小做了优化,将 __TEXT 字段迁移到自定义段,使得 iPhone X 以前机器的下载大小减少了50M,几乎少了1/3的大小,这招之所以对 iPhone X 以前机器管用的原因是因为先前机器是按照先加密再压缩,压缩率低,而之后机器改变了策略因此下载大小就会大幅减少。Michael Eisel 这篇博客《One Quick Way to Drastically Reduce your iOS App’s Download Size》[2] 提出了这套方案,此方案已经线上验证,你可以立刻应用到自己应用中,提高老机器下载速度。

Michael Eisel 还用 Swift 包装了 simdjson[3] 写了个库 ZippyJSONDecoder[4] 比系统自带 JSONDecoder 快三倍。人类对速度的追求是没有止境的,最近 YY 大神 ibireme 也在写 JSON 库 YYJSON[5] 速度比 simdjson 还快。Michael 还写个了提速构建的自制链接器 zld[6],项目说明还描述了如何开发定制自己的链接器。还有主线程阻塞(ANR)检测的 swift 类 ANRChecker[7],还有通过 hook 方式记录系统错误日志的例子[8]展示如何通过截获自动布局错误,函数是 UIViewAlertForUnsatisfiableConstraints ,malloc 问题替换函数为 malloc_error_break 即可。Michael 的这些性能问题处理手段非常实用,真是个宝藏男孩。

通过每月新增激活量、浏览到新增激活转换率、下载到激活转换率、转换率受体积因素影响占比、每个用户获取成本,使用公式计算能够得到每月成本收益,把你们公司对应具体参数数值套到公式中,算出来后你会发现如果降低了50多MB,每月就会有非常大的收益。

对于 Android 来说,很多功能是可以放在云端按需下载使用,后面的方向是重云轻端,云端一体,打通云端链路。

下载和安装完成后,就要分析 App 开始启动时如何做优化了,我接下来跟你说说 Android 启动 so 库加载如何做监控和优化。

四 Android so 库加载优化

1 编译阶段 - 静态分析优化

依托自动化构建平台,通过构建配置实现对源码模块的灵活配置,进行定制化编译。

-ffunction-sections -fdata-sections // 实现按需加载

-fvisibility=hidden -fvisibility-inlines-hidden // 实现符号隐藏

这样可以避免无用模块的引入,效果如下图:

2 运行阶段 - hook分析优化

Android Linker 调用流程如下:

注意,find_library 加载成功后返回 soinfo 对象指针,然后调用其 call_constructors 来调用 so 的 init_array。call_constructors 调用 call_array,其内部循环调用 call_funtion 来访问 init_array 数组的调用。

高德 Android 小伙伴们基于 frida-gum[9] 的 hook 引擎开发了线下性能监控工具,可以 hook c++ 库,支持 macos、android、ios,针对 so 的全局构造时间和链接时间进行 hook,对关键 so 加载的关键节点耗时进行分析。dlopen 相关 hook 监控点如下:

static target_func_t android_funcs_22[] = {

{“__dl_dlopen”, 0, (void *)my_dlopen},

{“__dl_ZL12find_libraryPKciPK12android_dlextinfo”, 0, (void *)my_find_library},

{“__dl_ZN6soinfo16CallConstructorsEv”, 0, (void *)my_soinfo_CallConstructors},

{“__dl_ZN6soinfo9CallArrayEPKcPPFvvEjb”, 0, (void *)my_soinfo_CallArray}

};

static target_func_t android_funcs_28[] = {

{“__dl_Z9do_dlopenPKciPK17android_dlextinfoPKv”, 0, (void *)my_do_dlopen_28},

{“__dl_Z14find_librariesP19android_namespace_tP6soinfoPKPKcjPS2_PNSt3__16vectorIS2_NS8_9a”},

{“__dl_ZN6soinfo17call_constructorsEv”, 0, (void *)my_soinfo_CallConstructors},

{“_dl_ZL10call_arrayIPFviPPcS1_EEvPKcPT_jbS5”, 0, (void *)my_call_array_28<constructor_func>},

{“__dl_ZN6soinfo10link_imageERK10LinkListIS_19SoinfoListAllocatorES4_PK17android_dlextin”},

{“__dl_g_argc”, 0, 0},

{“__dl_g_argv”, 0, 0},

{“__dl_g_envp”, 0, 0}

};

Android 版本不同对应 hook 方法有所不同,要注意当 so 有其他外部链接依赖时,针对 dlopen 的监控数据,不只包括自身部分,也包括依赖的 so 部分。在这种情况下,so 加载顺序也会产生很大的影响。

JNI_OnLoad 的 hook 监控代码如下:

#ifdef ABTOR_ANDROID

jint my_JNI_ONLoad(JavaVM* vm, void* reserved) {

asl::HookEngine::HoolContext *ctx = asl::HookEngine::getHookContext();

uint64_t start = PerfUtils::getTickTime();

jint res = asl::CastFuncPtr(my_JNI_OnLoad, ctx->org_func)(vm, reserved);

int duration = (int)(PerfUtils::getTickTime() - start);

LibLoaderMonitorImpl monitor = (LibLoaderMonitorImpl)LibLoaderMonitor::getInstance();

monitor->addOnloadInfo(ctx->user_data, duration);

return res;

}

#endif

如上代码所示,linker 的 dlopen 完成加载,然后调用 dlsym 来调用目标 so 的 JNI_OnLoad,完成 JNI 涉及的初始化操作。

加载 so 需要注意并行出现 loadLibrary0 锁的问题,这样会让多线程发生等锁现象。可以减少并发加载,但不能简单把整个加载过程放到串行任务里,这样耗时可能会更长,并且没法充分利用资源。比较好的做法是,将耗时少的串行起来同时并行耗时长的 so 加载。

至此完成了 so 的初始化和链接的监控。

说完 Android,那么 iOS 的加载是怎样的,如何优化呢?我接着跟你说。

五 App 加载

dyld_start 之前做了什么,dyld_start 是谁调用的,通过查看 xnu 的源码[10]可以理出,当 App 点击后会通过_mac_execve 函数 fork 进程,加载解析 Mach-O 文件,调用 exec_activate_image() 开始激活 image 的过程。先根据 image 类型来选择 imgact,开始 load_machfile,这个过程会先解析 Mach-O,解析后依据其中的 LoadCommand 启动 dyld。最后使用 activate_exec_state() 处理结构信息,thread_setentrypoint() 设置 entry_point App的入口点。

_dyld_start 之后要少些动态库,因为链接耗时;少些 +load、C 的 constructor 函数和 C++ 静态对象,因为这些会在启动阶段执行,多了就会影响启动时间。因此,没有用的代码就需要定期清理和线上监控。通过元类中flag的方式进行监控然后定期清理。

六 iOS 主线程方法调用时长检测

+load 方法时间统计,使用运行时 swizzling 的方式,将统计代码放到链接顺序的最前面即可。静态初始化函数在 DATA 的 mod_init_func 区,先把里面原始函数地址保存,前后加上自定义函数记录时间。

在 Linux上 有 strace 工具,还有库跟踪工具 ltrace,OSX 有包装了 dtrace 的 instruments 和 dtruss 工具,不过在某些场景需求下不好用。objc_msgSend 实际上会通过在类对象中查找选择器到函数的映射来重定向执行到实现函数。一旦它找到了目标函数,它就会简单地跳转到那里,而不必重新调整参数寄存器。这就是为什么我把它称为路由机制,而不是消息传递。Objective-C 的一个方法被调用时,堆栈和寄存器是为 objc_msgSend 调用配置的,objc_msgSend 路由执行。objc_msgSend 会在类对象中查找函数表对应定向到的函数,找到目标函数就跳转,参数寄存器不会重新调整。

因此可以在这里 hook 住做统一处理。hook objc_msgSend 还可以获取启动方法列表,用于二进制重排方案中所需要的 AppOrderFiles,不过 AppOrderFiles 还可以通过 Clang SanitizerCoverage 获得,具体可以看 Michael Eisel 这个宝藏男孩这篇博客《Improving App Performance with Order Files》[11] 的介绍。

objc_msgSend 可以通过 fishhook 指定到你定义的 hook 方法中,也可以使用创建跳转 page 的方式来 hook。做法是先用 mmap 分配一个跳转的 page,这个内存后面会用来执行原函数,使用特殊指令集将CPU重定向到内存的任意位置。创建一个内联汇编函数用来放置跳转的地址,利用 C 编译器自动复制跳转 page 的结构,指向 hook 的函数,之前把指令复制到跳转 page 中。ARM64 是一个 RISC 架构,需要根据指令种类检查分支指令。可以在 _objc_msgSend[12] 里找到 b 指令的检查。相关代码如下:

ENTRY _objc_msgSend

MESSENGER_START

cmp x0, #0 // nil check and tagged pointer check

b.le LNilOrTagged // (MSB tagged pointer looks negative)

ldr x13, [x0] // x13 = isa

and x9, x13, #ISA_MASK // x9 = class

检查通过就可以用这个指针读取偏移量,并修改指向跳转地址,跳转page完成,hook 函数就可以被调用了。

接下来看下 hook _objc_msgSend 的函数,这个我在以前博客《深入剖析 iOS 性能优化》[13] 写过,不过多赘述,只做点补充说明。从这里的源码[14]可以看实现,其中的attribute((naked)) 表示无参数准备和栈初始化, asm 表示其后面是汇编代码,volatile 是让后面的指令避免被编译优化到缓存寄存器中和改变指令顺序,volatile 使其修饰变量被访问时都会在共享内存里重新读取,变量值变化时也能写到共享内存中,这样不同线程看到的变量都是一个值。如果你发现不加 volatile 也没有问题,你可以把编译优化选项调到更优试试。stp表示操作两个寄存器,中括号部分表示压栈存入sp偏移地址,!符号表合并了压栈指令。

save() 的作用是把传递参数寄存器入栈保存,call(b, value)用来跳到指定函数地址,call(blr, &before_objc_msgSend) 是调用原 _objc_msgSend 之前指定执行函数,call(blr, orig_objc_msgSend) 是调用 objc_msgSend 函数,call(blr, &after_objc_msgSend) 是调用原 _objc_msgSend 之后指定执行函数。before_objc_msgSend 和 after_objc_msgSend 分别记录时间,差值就是方法调用执行的时长。

调用之间通过 save() 保存参数,通过 load() 来读取参数。call 的第一个参数是blr,blr 是指跳转到寄存器地址后会返回,由于 blr 会改变 lr 寄存器X30的值,影响 ret 跳到原方法调用方地址,崩溃堆栈找方法调研栈也依赖 lr 在栈上记录的地址,所以需要在 call() 之前对 lr 进行保存,call() 都调用完后再进行恢复。跳转到hook函数,hook函数可以执行我们自定义的事情,完成后恢复CPU状态。

七 进入主图后的优化

进入主图后,用户就可以点击按钮进入不同功能了,是否能够快速响应按钮点击操作也是启动体验感知很重要的事情。按钮点击的两个事件 didTouchUp 和 didTouchDown 之间也会有延时,因此可以在 didTouchDown 时在主线程先 async 初始化下一个 VC,把初始化提前完成,这样做可以提高50ms-100ms的速度,甚至更多,具体收益依赖当前主线程繁忙情况和下一个页面 viewDidLoad 等初始化方法里的耗时,启动阶段主线程一定不会闲,即使点击后主线程阻塞,使用 async 也能保证下一个页面的初始化不会停。

八 线程调度和任务编排

1 整体思路

对于任务编排有种打法,就是先把所有任务滞后,然后再看哪个是启动开始必须要加载的。效果立竿见影,很快就能看到最好的结果,后面就是反复斟酌,严格把关谁才是必要的启动任务了。

启动阶段的任务,先理出相关依赖关系,在框架中进行配置,有依赖的任务有序执行,无依赖独立任务可以在非密集任务执行期串行分组,组内并发执行。

这里需要注意的是Android 的 SharedPreferences 文件加载导致的 ContextImpl 锁竞争,一种解法是合并文件,不过后期维护成本会高,另一种是使用串行任务加载。你可能会疑惑,我没怎么用锁,那是不是就不会有锁等待的问题了。其实不然,比如在 iOS中,dispatch_once 里有 dispatch_atomic_barrier 方法,此方法就有锁的作用,因此锁其实存在各个 API 之下,如不用工具去做检查,有时还真不容易发现这些问题。

有 IO 操作的任务除了锁等待问题,还有效率方面也需要特别注意,比如 iOS 的 Fundation 库使用的是 NSData writeToFile:atomically: 方法,此方法会调用系统提供的 fsync 函数将文件描述符 fd 里修改的数据强写到磁盘里,fsync 相比较与 fcntl 效率高但写入物理磁盘会有等待,可能会在系统异常时出现写入顺序错乱的情况。系统提供的 write() 和 mmap() 函数都会用到内核页缓存,是否写入磁盘不由调用返回是否成功决定,另外 c 的标准库的读写 API fread 和 fwrite 还会在系统内核页缓存同步对应由保存了缓冲区基地址的 FILE 结构体的内部缓冲区。因此启动阶段 IO 操作方法需要综合做效率、准确和重要性三方面因素的权衡考虑,再进行有 IO 操作的任务编排。

针对初始化耗时的库,比如埋点库,可以延后初始化,先将所需要的数据存储到内存中,待到埋点库初始化时再进行记录。对一些主图上业务网络可以延后请求,比如闪屏、消息盒子、主图天气、限行控件数据请求、开放图层数据、Wi-Fi信息上报请求等。

2 多线程共享数据的问题

并发任务编排缺少一个统一的异步编程模型,并发通信共享数据方式的手段,比如代理和通知会让处理到处飞,闭包这种匿名函数排查问题不方便,而且回调中套回调前期设计后期维护和理解很困难,调试、性能测试也乱。这些通过回调来处理异步,不光复杂难控,还有静态条件、依赖关系、执行顺序这样的额外复杂度,为了解决这些额外复杂度,还需要使用更多的复杂机制来保证线程安全,比如使用低效的 mutex、超高复杂度的读写锁、双重检查锁定、底层原子操作或信号量的方式来保护数据,需要保证数据是正确锁住的,不然会有内存问题,锁粒度要定还要注意避免死锁。

并发线程通信一般都会使用 libdispatch(GCD)这样的共享数据方式来处理,也就异步再回调的方式。libdispatch 的 async 策略是把任务的 block 放到队列链表,使用时会在底层的线程池里找可用线程,有就直接用,没有就新建一个线程(参看 libdispatch[15] 源码,监控线程池 workqueue.c,队列调度 queue.c),使用这样的策略来减少线程创建。当并发任务多时,比如启动期间,即使线程没爆,但 CPU 在各个线程切换处理任务时也是会有时间开销的,每次切换线程,CPU 都需要执行调度程序增加调度成本和增加 CPU 使用率,并且还容易出现多线程竞争问题。单次线程切换看起来不长,但整个启动,切换频率高的话,整体时间就会增大。

多线程的问题以及处理方式,带来了开发和排查问题的复杂性,以及出现问题机率的提高,资源和功能云化也有类似的问题,云化和本地的耦合依赖、云化之间的关系处理、版本兼容问题会带来更复杂的开发以及测试挑战,还有问题排查的复杂度。这些都需要去做权衡,对基础建设方案提出了更高的要求,对容错回滚的响应速度也有更高的要求。

这里有个 book[16] 专门来说并行编程难的,并告诉你该怎么做。这里有篇文章[17]列出了苹果公司 libdispatch 的维护者 Pierre Habouzit 关于 libdispatch 的讨论邮件。

说了一堆共享数据方式的问题,没有体感,下面我说个最近碰到的多线程问题,你也看看排查有多费劲。

3 一个具体多线程问题排查思路

问题是工程引入一个系统库,暂叫 A 库,出现的问题现象是 CoreMotion 不回调,网络请求无法执行,除了全局并发队列会 pending block 外主线程和其它队列工作正常。

第一阶段,排查思路看是否跟我们工程相关,首先看是不是各个系统都有此问题,发现 iOS14 和 iOS13 都有问题。然后把A库放到一个纯净 Demo 工程中,发现没有出问题了。基于上面两种情况,推测只有将A库引入我们工程才会出现问题。在纯净 Demo 工程中,A库使用时 CPU 会占用60%-80%,集成到我们工程后涨到100%,所以下个阶段排查方向就是性能。

第二阶段的打法是看是否是由性能引起的问题。先在纯净工程中创建大量线程,直到线程打满,然后进行大量浮点运算使 CPU 到100%,但是没法复现,任务通过 libdispatch 到全局并发队列能正常工作。

怎么在 Demo 里看到出线程已爆满了呢?

libdispatch 可以使用线程数是有上限的,在 libdispatch 的源码[18]里可以看到 libdispatch 的队列初始化时使用 pthread 线程池相关代码:

#if DISPATCH_USE_PTHREAD_POOL

static inline void

_dispatch_root_queue_init_pthread_pool(dispatch_queue_global_t dq,

int pool_size, dispatch_priority_t pri)

{

dispatch_pthread_root_queue_context_t pqc = dq->do_ctxt;

int thread_pool_size = DISPATCH_WORKQ_MAX_PTHREAD_COUNT;

if (!(pri & DISPATCH_PRIORITY_FLAG_OVERCOMMIT)) {

thread_pool_size = (int32_t)dispatch_hw_config(active_cpus);

}

if (pool_size && pool_size < thread_pool_size) thread_pool_size = pool_size;

… // 省略不相关代码

}

如上面代码所示,dispatch_hw_config 会用 dispatch_source 来监控逻辑 CPU、物理 CPU、激活 CPU 的情况计算出线程池最大线程数量,如果当前状态是 DISPATCH_PRIORITY_FLAG_OVERCOMMIT,也就是会出现 overcommit 队列时,线程池最大线程数就按照 DISPATCH_WORKQ_MAX_PTHREAD_COUNT 这个宏定义的数量来,这个宏对应的值是255。因此通过查看是否出现 overcommit 队列可以看出线程池是否已满。

什么时候 libdispatch 会创建一个新线程?

当 libdispatch 要执行队列里 block 时会去检查是否有可用的线程,发现有可用线程时,在可用线程去执行 block,如果没有,通过 pthread_create 新建一个线程,在上面执行,函数关键代码如下:

static void

_dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor)

{

// 如果状态是overcommit,那么就继续添加到pending

bool overcommit = dq->dq_priority & DISPATCH_PRIORITY_FLAG_OVERCOMMIT;

if (overcommit) {

os_atomic_add2o(dq, dgq_pending, remaining, relaxed);

} else {

if (!os_atomic_cmpxchg2o(dq, dgq_pending, 0, remaining, relaxed)) {

_dispatch_root_queue_debug("worker thread request still pending for "

“global queue: %p”, dq);

return;

}

}

t_count = os_atomic_load2o(dq, dgq_thread_pool_size, ordered);

do {

can_request = t_count < floor ? 0 : t_count - floor;

// 是否有可用

if (remaining > can_request) {

_dispatch_root_queue_debug(“pthread pool reducing request from %d to %d”,

remaining, can_request);

os_atomic_sub2o(dq, dgq_pending, remaining - can_request, relaxed);

remaining = can_request;

}

// 线程满

if (remaining == 0) {

_dispatch_root_queue_debug("pthread pool is full for root queue: "

“%p”, dq);

return;

}

} while (!os_atomic_cmpxchgvw2o(dq, dgq_thread_pool_size, t_count,

t_count - remaining, &t_count, acquire));

do {

_dispatch_retain(dq); // 在 _dispatch_worker_thread 里取任务并执行

while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {

if (r != EAGAIN) {

(void)dispatch_assume_zero®;

}

_dispatch_temporary_resource_shortage();

}

} while (–remaining);

}

如上面代码所示,can_request 表示可用线程数,通过当前最大可用线程数减去已用线程数获得,赋给 remaining后,用来判断线程是否满和控制线程创建。dispatch_worker_thread 会取任务并执行。

当 libdispatch 使用的线程池中线程过多,并且有 pending 标记,当等待超时,也就是 libdispatch 里 DISPATCH_CONTENTION_USLEEP_MAX 宏定义的时间后,也会触发创建一个新的待处理线程。libdispatch 对应函数关键代码如下:

static bool

DISPATCH_ROOT_QUEUE_CONTENDED_WAIT(dispatch_queue_global_t dq,

int (*predicate)(dispatch_queue_global_t dq))

{

bool pending = false;

do {

if (!pending) {

// 添加pending标记

(void)os_atomic_inc2o(dq, dgq_pending, relaxed);

pending = true;

}

_dispatch_contention_usleep(sleep_time);

sleep_time *= 2;

} while (sleep_time < DISPATCH_CONTENTION_USLEEP_MAX);

if (pending) {

(void)os_atomic_dec2o(dq, dgq_pending, relaxed);

}

if (status == DISPATCH_ROOT_QUEUE_DRAIN_WAIT) {

_dispatch_root_queue_poke(dq, 1, 0); // 创建新线程

}

return status == DISPATCH_ROOT_QUEUE_DRAIN_READY;

}

如上所示,在创建新的待处理线程后,会退出当前线程,负载没了就会去用新建的线程。

接下来使用 Instruments 进行分析 Trace 文件,发现启动阶段立刻开始使用A库的话,CPU 会突然上升,如果使用 A 库稍晚些,CPU 使用率就是稳定正常的。这说明在第一个阶段性能相关结论只是偶现情况才会出现,出问题时,并没有出现系统资源紧张的情况,可以得出并不是性能问题的结论。那么下一个阶段只能从A库的使用和排查我们工程其它功能的问题。

第三个阶段的思路是使用功能二分排查法,先排出 A 库使用问题,做法是在使用最简单的 A 库初始化一个页面在首屏也会复现问题。

我们的功能主要分为渲染、引擎、网络库、基础功能、业务几个部分。将渲染、引擎、网络库拉出来建个Demo,发现这个 Demo 不会出现问题。那么有问题的就可能在基础功能、业务上。

先去掉的功能模块有 CoreMotion、网络、日志模块、定时任务(埋点上传),依然复现。接下来去掉队列里的 libdispatch 任务,队列里的任务主要是由 Operation 和 libdispatch 两种方式放入。其中 Operation 最后是使用 libdispatch 将任务 block 放入队列,期间会做优先级和并发数的判断。对于 libdispatch 可以 Hook 住可以把任务 block 放到队列的 libdispatch 方法,有 dispatch_async、dispatch_after、dispatch_barrier_async、dispatch_apply 这些方法。任务直接返回,还是有问题。

推测验证基础能力和业务对出现问题队列有影响,instruments 只能分析线程,无法分析队列,因此需要写工具分析队列情况。

接下来进入第四个阶段。

先 hook 时截获任务 block 使用的 libdispatch 方法、执行队列名、优先级、做唯一标识的入队时间、当前队列的任务数、还有执行堆栈的信息。通过截获的内容按照时间线看,当出现全局并发队列 pending block 数量堆积时,新的使用 libdispatch 加入的部分任务可以得到执行,也有没执行的,都执行了也会有问题。

然后去掉 Operation 的任务:通过日志还能发现 Operation 调用 libdispatch 的任务直接 hook libdispatch 的方法是获取不到的,可能是 Operation 调用方法有变化。另外在无法执行任务的线程上新建的 libdispatch 任务也无法执行,无法执行的 Operation 任务达到所设置的 maxConcurrentOperationCount,对应的 OperationQueue 就会在 Operation 的队列里 pending。由此可以推断出,在局并发队列 pending 的 block 包含了直接使用 libdispatch 的和 Operation 的任务,pending 的任务。因此还需要 hook 住 Operation,过滤掉所有添加到 Operation Queue 的任务,但结果还是复现问题。

此时很崩溃,本来做好了一个一个下掉功能的准备(成本高),这时,有同学发现前阶段两个不对的结论。

这个阶段定为第五阶段。

第一个不对的结论是经 QA 同学长时间多轮测试,只在14.2及以上系统版本有问题,由于只有这个版本才开始有此问题,推断可能是系统 bug;第二个不对的是只有渲染、引擎、网络库的 Demo 再次检查,可复现问题,因此可以针对这个 Demo 进行进一步二分排查。

于是,咱们针对两个先前错误结论,再次出发,同步进行验证。对 Demo 排除了网络库依然复现,后排除引擎还是复现,同时使用了自己的示例工程在iOS14.2上复现了问题,和第一阶段纯净Demo的区别是往全局并发队列里方式,官方 Demo 是 Operation,我们的是 libdispatch。

因此得出结论是苹果系统升级问题,原因可能在 OperationQueue,问题重现后,不再运行其中的 operation。14.3beta 版还没有解决。五个阶段总结如下图所示:

那么看下 Operation 实现,分析下系统 bug 原因。

ApportableFoundation[19] 里有Operation 的开源实现 NSOperation.m[20],相比较 GNUstep[21] 和 Cocotron[22] 更完善,可以看到 Operation 如何在 _schedulerRun 函数里通过 libdispatch 的 async 方法将 operation 的任务放到队列执行。

Swift 源码[23]里的fundation也有实现 Operation[24],我们看看 _schedule 函数的关键代码:

internal func _schedule() {

// 按优先级顺序执行

for prio in Operation.QueuePriority.priorities {

while let operation = op?.takeUnretainedValue() {

let next = operation.__nextPriorityOperation

if Operation.__NSOperationState.enqueued == operation._state && operation._fetchCachedIsReady(&retest) {

if let previous = prev?.takeUnretainedValue() {

previous.__nextPriorityOperation = next

} else {

_setFirstPriorityOperation(prio, next)

}

if __mainQ {

queue = DispatchQueue.main

} else {

queue = __dispatch_queue ?? _synthesizeBackingQueue()

}

if let schedule = operation.__schedule {

if operation is _BarrierOperation {

queue.async(flags: .barrier, execute: {

schedule.perform()

})

} else {

queue.async(execute: schedule)

}

}

op = next

文末

那么对于想坚持程序员这行的真的就一点希望都没有吗?
其实不然,在互联网的大浪淘沙之下,留下的永远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
Android希望=技能+面试

  • 技能
  • 面试技巧+面试题

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

ion {

queue.async(flags: .barrier, execute: {

schedule.perform()

})

} else {

queue.async(execute: schedule)

}

}

op = next

文末

那么对于想坚持程序员这行的真的就一点希望都没有吗?
其实不然,在互联网的大浪淘沙之下,留下的永远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
Android希望=技能+面试

  • 技能
    [外链图片转存中…(img-P6y5d1ve-1714888896894)]
  • 面试技巧+面试题
    [外链图片转存中…(img-jki7labV-1714888896895)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值