Bo2SS

Bo2SS

1 基础篇(中)

今天要分享的是:基础篇(调试测试和发布阶段)。

image

06 | 通过注入动态库实现极速编译调试?#

虽然我们可以通过将部分代码先编译成二进制集成到工程里,来避免每次都全量编译来加快编译速度,但每次编译还是需要重启 App,再走一遍调试流程。

那么原生代码怎样实现动态极速调试呢?我们先看看有哪些工具实现了动态调试

1)Swift Playground,任何代码修改都能实时反馈出来;

2)Flutter Hot Reload,Flutter 会在点击 VSCode 调试栏的 reload 按钮后,查看自上次编译后改动过的代码,重新编译涉及到的代码库,还包括主库及其相关联库。这些重新编译过的库都会转换成内核文件发到 Dart VM 里,Dart VM 重新加载新的内核文件,加载后使 Flutter framework 触发所有 Widgets 和 Render Objects 进行重建、重布局、重绘。

那么,Cocoa 框架想要达到极速调试应该要怎么做呢?

Injection for Xcode#

Injection 工具可以动态地将 Swift 或 Objective-C 的代码在已运行的程序中执行,以加快调试速度,同时保证程序不用重启。

工作原理:Injection 会监听源代码文件的变化,如果文件被改动了,Injection Server 就会执行 rebuildClass 重新进行编译、打包成动态库,也就是 .dylib 文件,然后使用 writeSting 方法通过 Socket 通知运行的 App。原理示意图如下:

Injection 的工作原理示意图 ——《极客时间》

07 | OCLint 、Clang 和 Infer ,静态分析工具比较#

首先介绍三个常用的复杂度指标,静态分析工具可以借助它们来分析代码是否需要优化和重构。

  1. 圈复杂度,指的是遍历一个模块时的复杂度,这个复杂度是由分支语句比如 if、for,还有运算符比如 &&、|| 等等共同确定的。一般来说,圈复杂度在 11 以上时就非常高了,这时需要考虑重构,不然就会因为测试用例的数量过高而难以维护。
  2. NPath 度,指一个方法所有可能执行的路径数量。一般高于 200 就需要考虑降低复杂度了。
  3. NCSS 度,指不包含注释的源码行数。NCSS 度过大表示方法或类做的事情太多,影响代码的维护性和可读性,应该拆分或重构。一般方法行数不过百,类的行数不过千。

再提前申明使用静态分析工具的两大缺陷:

  1. 需要耗费更长的时间。相比于编译过程,静态分析本身就包含了编译最耗时的 IO 和语法分析阶段,当发现深层次程序错误时,还会对当前分析的方法、参数、变量去和整个工程相关代码一起做分析。
  2. 只能检查出那些专门设计好的、可查找的错误。对于特定类型的错误分析,还需要开发者靠自己的能力写一些插件并添加进去。

下面正式比较 3 款常用静态分析工具:

  • OCLint 是基于 Clang Tooling 开发的静态分析工具。
  • Clang 静态分析器基于 C++ 开发的开源工具,是 Clang 项目的一部分,构建在 Clang 和 LLVM 之上。
  • Infer 是基于 OCaml 语言编写的 Facebook 开源静态分析工具。
静态分析工具优缺点
静态分析工具优点缺点
OCLint检查规则多、定制性强可定制度过高,易用性较差
Clang 静态分析器与 Xcode 的集成度高,支持命令行检查规则少,检查粒度较粗
Infer效率高,支持增量分析,也可小范围分析可定制性中等

综合来看,Infer 在准确性、性能效率、规则、扩展性、易用性整体度上的把握是做得最好的,值得一试。

参考资料

08 | 如何利用 Clang 为 App 提质?#

除了之前提到的 Clang 静态分析工具,基于 Clang 还可以开发出保障 App 质量的系统平台,比如CodeChecker,具备了代码增量分析、代码可视化、代码质量报告生成等能力,或者在线网页代码导航工具,比如 Mozilla 开发的 DXR,方便在便携设备上去操作、分析问题。

什么是 Clang?#

我们先看看iOS 开发的完整编译流程图

image

其中,左侧黑块 Clang 是 C、C++、Objective-C 的编译前端, Swift 有自己的编译前端 SIL optimizer。

Clang 是基于 C++ 开发的,其源码质量非常高,有很多值得学习的地方,比如说目录清晰、功能解耦做得很好、分类清晰方便组合和复用、代码风格统一而且规范、注释量大便于阅读等。并且,它不光工程代码量巨大,而且工具也非常多,但相互间的关系复杂,好在 Clang 提供了一个易用性很高的黑盒 Driver,封装了前端命令和工具链命令,大大提升了其易用性。

Clang 做了哪些事?#

1)对代码进行词法分析,切分成 Token。

Token 类型分为 4 类:

  • 关键字:语法中的关键字,比如 if、else、while、for 等;
  • 标识符:变量名;
  • 字面量:值、数字、字符串;
  • 特殊符号:加减乘除等符号。

2)再进行语法分析,将 Token 组合成语义生成节点,从而构成抽象语法树(AST)。

节点主要分成 Type 类型、Decl 声明、Stmt 陈述这三种,通过扩展这三类节点,就能够表现出无限的代码形态。

Clang 提供了什么能力?#

Clang 为一些需要分析代码语法、语义信息的工具提供了基础设施: LibClang、Clang Plugin 和 LibTooling。

LibClang#

LibClang 可以访问 Clang 上层高级抽象的能力,比如获取所有 Token、遍历语法树、代码补全等。由于 API 很稳定,Clang 版本更新对其影响不大。但是,LibClang 并不能完全访问到 Clang AST 信息。

Clang Plugins#

Clang Plugins 是在运行时由编译器加载的动态库,可以让你在 AST 上做些操作,并集成到编译中,成为编译的一部分。

LibTooling#

LibTooling 是一个 C++ 接口,通过 LibTooling 能够编写独立运行的语法检查和代码重构工具。

与 LibClang 相比,它的接口没有那么稳定,需关注 AST 的 API 升级,也无法开箱即用;与 Clang Plugins 相比,它无法影响编译过程。但是,它能够完全控制 Clang AST ,并可以独立运行。

相关资料:

Tutorial for building tools using LibTooling and LibASTMatchers(使用 LibTooling 构建一个语言转换工具)

09 | 无侵入的埋点方案如何实现?#

在 iOS 开发中,埋点可以解决两大类问题:1)了解用户使用 App 的行为,2)降低线上问题的分析难度。

常见的埋点方式主要包括三种:

1)代码埋点:手写代码来埋点。特点是埋点精确,方便调试,但开发和维护的工作量较大。

2)可视化埋点:将埋点增加和修改的工作可视化。特点是提升埋点体验。

3)无埋点:更确切地说是 “全埋点”,埋点代码不会出现在业务代码中。特点是容易管理和维护,但只能针对通用的埋点需求。

可视化埋点和无埋点,都属于无侵入的埋点方案,那么该如何实现呢?

运行时方法替换#

在 iOS 开发中最常见的三种埋点,就是对页面进入次数、页面停留时间、点击事件的埋点。

对于这三种常见情况,我们都可以通过运行时方法替换技术来插入埋点代码,以实现无侵入的埋点方法:

1)先写一个运行时方法替换的类 SMHook,加上替换的方法 hookClass:fromSelector

2)根据埋点类型确定要替换的方法和标识符,在 +load () 方法里使用 SMHook 进行方法替换。

下面为无侵入埋点 - 运行时方法替换信息对照表,仅供参考:

无侵入埋点-运行时方法替换信息
埋点类型替换方法标识符
页面进入次数、页面停留时间UIViewController 的生命周期NSStringFromClass([self class])
UITableView(特例)setDelegateNSStringFromClass([self class])
点击事件点击事件的方法NSStringFromSelector(action) + NSStringFromClass([target class])
手势事件initWithTarget:action:NSStringFromSelector(action) + NSStringFromClass([target class])

事件唯一标识#

一个视图下相同 UIButton 的不同实例,仅仅通过 “action 选择器名”+“视图类名” 的组合还不能够区分开。这时,我们就需要有一个唯一标识来区分不同的事件。

每个子视图在父视图中都会有自己的索引,可以结合它生成事件唯一标识。特例:UITableViewCell 可以通过 indexPath 来确定每个 Cell 的唯一性;UIAlertController 可以通过内容来确定它的唯一标识...

但是,事件唯一标识的准确性难以保障(如视图层级在运行时被更改,因需求迭代页面更新频繁),所以通过运行时方法替换进行无侵入埋点方案,一般只是用于一些功能和视图稳定的地方

思考:是否可以使用 Clang AST 的接口,在构建时遍历 AST,从而将所需要的埋点代码加进去。

10 | 包大小:从资源和代码层面瘦身#

苹果对 iOS App 大小有严格限制:下载大小(200 MB)超限会阻碍用户在蜂窝网络下载 App ,影响新用户转化;可执行文件 text 段大小(iOS 7-:80MB,iOS 7 - 8:60MB,iOS 9+:500MB)超限将导致 App 审核被拒,影响 App 上架;另外,App 包体积过大,还会影响用户升级率。

所以,控制包大小至关重要。接下来,介绍一些常见的包大小瘦身方法。

官方 App Thinning#

App Thinning 是由苹果公司推出的一项改善 App 下载进程的新技术,主要解决用户下载 App 耗费过高流量,以及占用 iOS 设备过大存储空间的问题。

两个瘦身点

  1. 图片资源尺寸。根据 iOS 设备屏幕尺寸来匹配不同尺寸的资源, 比如,iPhone 6 只会下载 2x 分辨率的图片资源,iPhone 6plus 则只会下载 3x 分辨率的图片资源。
  2. 芯片指令集架构文件。用户下载时就只会下载一个适合自己设备的芯片指令集架构文件,因为 App 也会有 32 位、64 位不同芯片架构的优化版本。

三种瘦身方式

  1. App Slicing :在你向 iTunes Connect 上传 App 后,对 App 做切割,创建不同的变体,以适用不同的设备。
  2. On-Demand Resources :主要是为游戏多关卡场景服务,根据用户的关卡进度下载随后几个关卡的资源,并且已经过关的资源也会被删掉,这样可以减少初装 App 的包大小。
  3. Bitcode :针对特定设备进行包大小优化,优化不明显。

如何使用呢

这里的大部分工作都是由 Xcode 和 App Store 帮你完成的~

你只需要通过 Xcode 添加 xcassets 目录(File > New File > Asset Catalog),然后将 2x 分辨率和 3x 分辨率的图片添加进来。

芯片指令集架构文件按照默认设置, App Store 会根据设备创建不同的变体,每个变体里只有当前设备需要的指令集文件。

无用图片资源#

推荐开源工具: LSUnusedResources ,可以通过直接添加规则来处理,相当于帮你做了以下 4 步:

  1. 通过 find 命令获取项目中的所有图片资源文件;
  2. 通过 find 命令和正则匹配找出源码中使用到的资源名;
  3. 上述两者取差集,得到的就是无用资源了;
  4. 最后,可以通过系统类 NSFileManger 来删除无用资源。

图片资源压缩#

推荐的压缩方案:转成 WebP 格式(Google 开源项目;压缩率高,支持有损和无损两种压缩模式;支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够而出现毛边)。

相关的压缩工具cwebp(Google,命令行)、iSparta(腾讯,GUI)。

需权衡的地方:显示 WebP 图片需要使用 libwebp 进行解析(libwebp Demo),其在 CPU 消耗和解码时间上会比 PNG 高两倍,这又是空间和时间上的取舍了。

作者建议

1)图片大小超过 100KB,考虑使用 WebP ;

2)否则,可以使用 TinyPng (网页工具)或者 ImageOptim (GUI 工具)进行图片压缩。

代码瘦身#

App 的安装包主要是由资源和可执行文件组成的。

那么聊完了资源瘦身,接下来就是对可执行文件进行瘦身,也就是找到并删除无用代码的过程,思路类似找无用图片资源的 4 步走。

1)首先,通过 LinkMap 找出方法和类的全集;

获取 LinkMap :将工程文件 > Targets > Build Setting 里的 Write Link Map File 设置为 Yes,然后指定 Path to Link Map File 的路径,即可在每次编译后得到 LinkMap 文件。

LinkMap 文件的组成如下图所示:

LinkMap 文件主要组成 ——《极客时间》

  • Object File:代码工程的所有文件;
  • Section:代码段在生成的 Mach-O 文件里的偏移位置和大小;
  • Symbols:每个方法、类、block,以及它们的大小。

2)然后,找到使用过的方法和类;3)取二者的差集得到无用代码;4)最后,由人工确认无用代码可删除后,进行删除即可。这些过程共有 3 种方式选择:

A. 使用 MachOView 软件查看 Mach-O 文件。

在 Section 信息中,__objc_selrefs 里是被调用过的方法,__objc_classrefs 里是被调用过的类,__objc_superrefs 里是调用过 super 的类。

但是,这些不包括运行时动态调用的方法(因为 Objective-C 的动态性),还需要二次确认

B. 直接使用 AppCode 软件找到无用代码。

前提:工程代码量没有达到百万行。

通过 AppCode > Code > Inspect Code 分析完后,Unused code 里展示了所有的无用代码。

但是,也有一些情况会被误判为无用,也需要人工二次确认,如:

  • performSelector 方式调用的方法,如 [self performSelector:@selector (xxx)];
  • 在子类中使用的父类方法;
  • 运行时声明的类,如 NSClassFromString 调用的类、[[self class] xxx] 这样不指定类名被使用的类、使用 registerClass 的 UITableView 自定义的 Cell;
  • 通过点的方式使用的属性;
  • JSONModel 里定义的未使用的协议...

C. 运行时检查类是否真正被使用过。

通过方式 1 和 2 找到并删除了无用代码,但包里可能还存在无用代码,比如在执行静态检查时被用到的代码,在线上可能连它们的入口都没有了,更不用说被用户用到。

ObjC 的 runtime 源码里,有一个判断类是否初始化过的函数 isInitialized,并且它返回的结果会保存到元类(class_rw_t 结构体 flags 信息的第 1<<29 位)。

具体编写运行时无用类检查工具时,我们通过这个是否初始化的信息:

  • 先在线下测试环节检查所有类,查出没有初始化的类;
  • 然后在上线后对没有初始化的类,进行多版本观察;
  • 确认真正没有用到的类后,删掉。

11 | 热点问题答疑(一):基础模块问题答疑#

动态库加载方式#

有两种

  1. 在程序开始运行时通过 dyld 动态加载。通过 dyld 加载的动态库,需要在编译时进行链接,链接时会做标记,绑定的地址在加载后再决定。
  2. 显式运行时链接,即在运行时通过动态链接器提供的 API dlopen 和 dlsym 来加载。这种方式,在编译时是不需要参与链接的。 不过,通过这种运行时加载远程动态库的 App,苹果公司是不允许上线 App Store 的,所以只能用于线下调试环节。

App 启动速度的相关问题#

学习汇编学到什么程度合适

  • 如果工作不涉及逆向和安全领域,能够看懂汇编代码就非常不错了;
  • 如果你想学汇编语言的话,多动手去编写和调试代码,同时结合使用 Xcode 工具。

参考资料:

Mike Ash 的 “Dissecting objc_msgSend on ARM64”,剖析 objc_msgSend 源码,详细讲述了里面的 ARM64 汇编代码。

额外推荐:

王垠的博客

只有掌握了某个方面的知识,才能在碰到问题时想到用这个知识去解决问题,所以至少先了解这些知识吧~

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。