被面试官问的android问题难倒了怎么办,抖音品质建设 - iOS启动优化,2024年Android开发陷入饱和

  • iOS 12 及以下:root viewController 的 viewDidAppear

  • iOS 13+:applicationDidBecomeActive

Apple 官方的统计方式是第一个 CA::Transaction::commit,但对应的实现在系统框架内部,抖音的方式已经非常接近这个点了。

分阶段


只有一个启动耗时的埋点在排查线上问题的时候显然是不够的,可以通过分阶段和单点埋****点结合,下面是这是目前抖音的监控方案:

+load、initializer 的调用顺序和链接顺序有关,链接顺序默认按照 CocoaPod 的 Pod 命名升序排列,所以取一个命名为 AAA 开头既可以让某个 +load、initializer 第一个被执行。

无侵入监控


公司的 APM 团队提供了一种无侵入的启动监控方案,方案将启动流程拆分成几个粒度比较粗的与业务无关的阶段:进程创建,最早的 +load,didFinishLuanching 开始和首屏首次绘制完成。

前三个时间点无侵入获取较为简单

  • 进程创建:通过 sysctl 系统调用拿到进程创建的时间戳

  • 最早的 +load:和上面的分阶段监控一样,通过 AAA 为前缀命名 Pod,让 +load 第一个被执行

  • didFinishLaunching:监控 SDK 初始化一般在启动的很早期,用监控 SDK 的初始化时间作为 didFinishLaunching 的时间

首屏渲染完成时间我们希望和 MetricKit 对齐,即获取到 CA::Transaction::commit()方法被调用的时间。

通过 Runloop 源码分析和线下调试,我们发现 CA::Transaction::commit()CFRunLoopPerformBlockkCFRunLoopBeforeTimers 这三个时机的顺序从早到晚依次是:

可以通过在 didFinishLaunch 中向 Runloop 注册 block 或者 BeforeTimer 的 Observer 来获取上图中两个时间点的回调,代码如下:

//注册block

CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];

CFRunLoopPerformBlock(mainRunloop,NSDefaultRunLoopMode,^(){

NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];

NSLog(@“runloop block launch end:%f”,stamp);

});

//注册kCFRunLoopBeforeTimers回调

CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];

CFRunLoopActivity activities = kCFRunLoopAllActivities;

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

if (activity == kCFRunLoopBeforeTimers) {

NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];

NSLog(@“runloop beforetimers launch end:%f”,stamp);

CFRunLoopRemoveObserver(mainRunloop, observer, kCFRunLoopCommonModes);

}

});

CFRunLoopAddObserver(mainRunloop, observer, kCFRunLoopCommonModes);

经过实测,我们最后选择的无侵入获取首屏渲染方案是:

  1. iOS13(含)以上的系统采用 runloop 中注册一个 kCFRunLoopBeforeTimers 的回调获取到的 App 首屏渲染完成的时机更准确。

  2. iOS13 以下的系统采用 CFRunLoopPerformBlock 方法注入 block 获取到的 App 首屏渲染完成的时机更准确。

监控周期


App 的生命周期可以分为三个阶段:研发,灰度和线上,不同阶段监控的目的和方式都不一样。

研发阶段

研发阶段的监控主要目的是防止劣化,对应着会有线下的自动化监控,通过实际的启动性能测试来尽早地发现和解决问题,抖音的线下自动化监控流程图如下:

由定时任务触发,先 release 模式下打包,接着跑一次自动化测试,测试完毕后会上报测试结果,方便通过看板来跟踪整体的变化趋势。

如果发现有劣化,会先发一条报警信息,接着通过二分查找的方式找到对应的劣化 MR,然后自动跑火焰图和 Instrument 来辅助定位问题。

那么如何保证测试的结果是稳定可靠的呢?

答案就是控制变量:

  • 关闭 iCloud & 不登录 AppleID & 飞行模式

  • 风扇降温,且用 MFI 认证数据线

  • 重启手机和开始下一次测试之前静置一段时间

  • 多次测量取平均值 & 计算方差

  • Mock 启动过程中的 AB 变量

实践下来,我们发现 iPhone 8 的稳定性最好,其次是 iPhone X,iPhone 6 的稳定性很差。

除了自动化测试,在研发流程上还可以加一些准入,来防止启动劣化,这些准入包括

  • 新增动态库

  • 新增 +load 和静态初始化

  • 新增启动任务 Code Review

不建议做细粒度的 Code Review,除非对相关业务很了解,否则一般肉眼看不出会不会有劣化。

线上 & 灰度

灰度和线上的策略是相似的,主要看的是大盘数据和配置报警,大盘监控和报警和公司的基建有很大关系,如果没有对应基建 Xcode MetricKit 本身也可以看到启动耗时:打开 Xcode -> Window -> Origanizer -> Launch Time

大盘数据本身是统计学的,会有些统计学的规律:

  • 发版本的前几天启动速度比较慢,这是因为 iOS 13 后更新 App 的第一次启动要创建启动闭包,这个过程是比较慢的

  • 新版本发布会导致老版本 pct50 变慢,因为性能差的设备升级速度慢,导致老版本性能差设备比例变高

  • 采样率调整会影响 pct50,比如某些地区的 iPhone 6 比例较高,如果这些地区采样率提高会导致大盘性能差的设备比例提高。

基于这些背景,我们一般会通过控制变量的方式:拆地区,机型,版本,有时候甚至要根据时间看启动耗时的趋势。

工具

==

完成了监控之后,我们需要找到一些可以优化的点,就需要用到工具。主要包括两大类:Instrument 和自研

Time Profiler


Time Profiler 是大家日常性能分析中用的比较多的工具,通常会选择一个时间段,然后聚合分析调用栈的耗时。但Time Profiler 其实只适合粗粒度的分析,为什么这么说呢?我们来看下它的实现原理:

默认 Time Profiler 会 1ms 采样一次,只采集在运行线程的调用栈,最后以统计学的方式汇总。比如下图中的 5 次采样中,method3 都没有采样到,所以最后聚合到的栈里就看不到 method3。所以 Time Profiler 中的看到的时间,并不是代码实际执行的时间,而是栈在采样统计中出现的时间。

Time Profiler 支持一些额外的配置,如果统计出来的时间和实际的时间相差比较多,可以尝试开启

  • High Frequency,降低采样的时间间隔

  • Record Kernel Callstacks,记录内核的调用栈

  • Record Waiting Thread,记录被 block 的线程

System Trace


既然 Time Profiler 支持粗粒度的分析,那么有没有什么精细化的分析工具呢?答案就是 System Trace。

既然要精细化分析,那么我们就需要标记出一小段时间,可以用 Point of interest 来标记。除此之外,System Trace 分析虚拟内存和线程状态都很管用:

  • Virtual Memory:主要关注 Page In这个事件,因为启动路径上有很多次 Page In,且相对耗时

  • Thread State:主要关注挂起和抢占两个状态,记住主线程不是一直在运行的

  • System Load 线程有优先级,高优先级的线程不应该超过系统核心数量

os_signpost


os_signpost 是 iOS 12 推出的用于在 instruments 里标记时间段的 API,性能非常高,可以认为对启动无影响。结合最开始讲的分阶段监控,我们可以在 Instrument 把启动划分成多个阶段,和其他模板一起分析具体问题:

结合 swizzle,os_signpost 可以发挥出意想不到的效果,比如 hook 所有的 load 方法,来分析对应耗时,又比如 hook UIImage 对应方法,来统计启动路径上用到的图片加载耗时。

其他 Instrument 模板


除了这些,还有几个模板是比较常用的:

  • Static Initializer:分析 C++ 静态初始化

  • App Launch:Xcode 11 后新出的模板,可以认为是 Time Profiler + System Trace

  • Custom Instrument:自定义 Instrument,最简单是用 os_signpost 作为模板的数据源,自己做一些简单的定制化展示,具体可参考 WWDC 的相关 Session。

火焰图


火焰图用来分析时间相关的性能瓶颈非常有用,可以直接把业务代码的耗时绘制出来。此外,火焰图可以自动化生成然后 diff,所以可以用于自动化归因。

火焰图有两种常见实现方式

  • hook objc_msgSend

  • 编译期插桩

本质上都是在方法的开始和末尾打两个点,就知道这个方法的耗时,然后转换成 Chrome 的标准的 json 格式就可以分析了。注意就算用 mmap 来写文件,仍然会有一些误差,所以找到的问题并不一定是问题,需要二次确认。

最佳实践

====

整体思路


优化的整体思路其实就四步:

  1. 掉启动项,最直接

  2. 如果不能删除,尝试延迟,延迟包括第一次访问以及启动结束后找个合适的时间预热

  3. 不能延迟的可以尝试并发,利用好多核多线程

  4. 如果并发也不行,可以尝试让代码执行更快

这块会以 Main 函数做分界线,看下 Main 函数前后的优化方案;接着介绍如何优化 Page In;最后讲解一些非常规的优化方案,这些方案对架构的要求比较高

Main 之前


Main 函数之前的启动流程如下:

  • 加载 dyld

  • 创建启动闭包(更新 App/重启手机需要)

  • 加载动态库

  • Bind & Rebase & Runtime 初始化

  • +load 和静态初始化

动态库

减少动态库数量可以加减少启动闭包创建和加载动态库阶段的耗时,官方建议动态库数量小于 6 个。

推荐的方式是动态库转静态库,因为还能额外减少包大小。另外一个方式是合并动态库,但实践下来可操作性不大。最后一点要提的是,不要链接那些用不到的库(包括系统),因为会拖慢创建闭包的速度。

下线代码

下线代码可以减少 Rebase & Bind & Runtime 初始化的耗时。那么如何找到用不到的代码,然后把这些代码下线呢?可以分为静态扫描和线上统计两种方式

最简单的静态扫描是基于 AppCode,但是项目大了之后 AppCode 的索引速度非常慢,另外的一种静态扫描是基于 Mach-O 的:

  • _objc_selrefs_objc_classrefs 存储了引用到的 sel 和 class

  • __objc_classlist 存储了所有的 sel 和 class

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

总结

其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

Android大厂面试真题全套解析

2017-2020字节跳动Android面试真题解析PDF
然而Android架构学习进阶是一条漫长而艰苦的道路,不能靠一时激情,更不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

片转存中…(img-jFaPRCEi-1712541494959)]

[外链图片转存中…(img-Qfas1huD-1712541494959)]
然而Android架构学习进阶是一条漫长而艰苦的道路,不能靠一时激情,更不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。所以:贵在坚持!

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

  • 10
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值