摘要:如果接下来立刻进行符号化去获取方法名,那么就需要去 __LINKEDIT segment 里查找栈指针地址所对应符号表的符号,特别当你设置的时间隔较小的时候,符号化过程会持续消耗较多的 CPU 资源,从而影响主线程。就比如说,你掌握了 Clang 的知识,那在研究 无侵入的埋点方案 应该如何实现时,你才能可能会想到用 Clang 的 LibTooling 来开发一个独立的工具,专门以静态方式插入埋点的代码。

你好,我是戴铭,专栏上线以来,我收到了很多同学非常用心的反馈,有问题、建议、心得和经验,当然提的问题居多。虽然我未在评论区对每条留言做出回复,但是我对大家提出的问题都一一记录了下来,内容很丰富,我进行了汇总和整理,发布到专栏答疑中,感兴趣的小伙伴可以去看看。

更多热点问题答疑

在这里,我先挑选并展开了 3 个典型问题,希望为你抽丝剥茧,答疑解惑。

动态库加载方式的相关问题

问题:

@五子棋 在看完第 5 篇文章“链接器:符号是怎么绑定到地址上的?”后,关于动态库是否参与链接的问题,通过私信和我反馈了他的观点。

他指出:动态库也是要参与链接的,不然就没法知道函数的标记在哪儿。

解答:

为了帮助大家理解这个问题,我把与之相关的内容,再和你展开一下。

我在第 5 篇文章中,是这么阐述这部分内容的:

Mach-O 文件是编译后的产物,而动态库在运行时才会被链接,并没参与 Mach-O 文件的编译和链接,所以 Mach-O 文件中并没有包含动态库里的符号定义。也就是说,这些符号会显示为“未定义”,但它们的名字和对应的库的路径会被记录下来。运行时通过 dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

细细想来,关于这个问题,更严谨的说法应该是,加载动态库的方式有两种:

  • 一种是,在程序开始运行时通过 dyld 动态加载。通过 dyld 加载的动态库需要在编译时进行链接,链接时会做标记,绑定的地址在加载后再决定。

  • 第二种是,显式运行时链接(Explicit Runtime Linking),即在运行时通过动态链接器提供的 API dlopen 和 dlsym 来加载。这种方式,在编译时是不需要参与链接的。

    不过,通过这种运行时加载动态库的 App,苹果公司是不允许上线 App Store 的,所以只能用于线下调试环节。关于这种方式的适用场景,我在专栏第 6 篇“ App 如何通过注入动态库的方式实现极速编译调试? ”中有举例说明过。

在第 5 篇文章中,我将动态库的这两种加载方式混在一起说了,让你感到些许困惑,所以在这里我特地做个补充说明。

App 启动速度的相关问题

专栏的第 2 篇文章“ App 启动速度怎么做优化与监控? ”中的大部分问题,我都直接在评论区回复了。今天主要和大家聊一下课后作业的实现问题。

问题:

按照今天文中提到的 Time Profiler 工具检查方法耗时的原理,你来动手实现一个方法耗时检查工具吧。

虽然这个问题的思路,我在文章中提到了,但还是有很多同学感觉无从下手。接下来,我们就再一起来看看这个思考题。

解答:

关于实现思路,我在文章中写到:

定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。

我们再一起看一下这个实现思路(我原本未在文中详细展开,是希望多留点思考空间给你)。动手写耗时检查工具时,首先需要开启一个定时器,来定时获取方法调用堆栈。一段时间内方法调用堆栈相同,那么这段时间,就是这个方法调用堆栈的栈顶方法耗时。

这个解题思路里很关键的一步,也是你最容易忽视的一步,就是应该怎么做好获取方法调用堆栈。

callstackSymbols 是一种获取方法调用栈的方法,但是只能获取当前线程的调用栈,为了把对主线程的影响降到最小,获取当前线程调用栈的工作就需要在其他线程去做。

所以, 这个解题思路就需要换成: 使用系统提供的 task_threads 去获取所有线程,使用 thread_info 得到各个线程的详细信息,使用 thread_get_state 方法去获取线程栈里的所有栈指针。

如果接下来立刻进行符号化去获取方法名,那么就需要去 __LINKEDIT segment 里查找栈指针地址所对应符号表的符号,特别当你设置的时间隔较小的时候,符号化过程会持续消耗较多的 CPU 资源,从而影响主线程。

所以,获取到栈指针后,我们可以不用立刻做符号化,而是先使用一个结构体将栈地址记录下来,最后再统一符号化,将对主线程的影响降到最低,这样获取的数据也会更加准确。

我们可以把记录栈地址的结构体设计为通用回溯结构,代码如下:

复制代码

typedefstructSMStackFrame{
   conststructSMStackFrame*constprevious;
   constuintptr_treturn_address;
} SMStackFrame;

在这段代码中, previous 记录的是上一个栈指针的地址。考虑 CPU 性能,记录堆栈的数量也不必很多,取最近几条即可。通过栈基地址指针获取当前栈指针地址的关键代码如下:

复制代码

// 栈地址初始化
SMStackFrame stackFrame = {0};
// 栈基地址指针
constuintptr_t framePointer = smMachStackBasePointerByCPU(&machineContext);
if(framePointer ==0|| smMemCopySafely((void*)framePointer, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) {
   return@"Fail frame pointer";
}
// 下面的 8 表示堆栈数量
for(; i <8; i++) {
// 记录栈地址
    buffer[i] = stackFrame.return_address;
   if(buffer[i] ==0|| stackFrame.previous ==0|| smMemCopySafely(stackFrame.previous, &stackFrame, sizeof(stackFrame)) != KERN_SUCCESS) {
       break;
    }
}

关于 Clang 的相关问题

专栏已经更新的第 7~ 第 10 这 4 篇文章中,都涉及到了 Clang 的知识以及应用。

在第 7 篇“ Clang、Infer 和 OCLint ,我们应该使用谁来做静态分析? ”中,介绍的 3 款静态分析工具都用到了 Clang,而且 Clang 本身也提供了 LibTooling 这种强大的 C++ 接口来方便定制独立的工具。

问题:

Clang 的知识需要投入大量精力才能掌握好,有同学可能会有疑问:”我掌握这些偏底层的知识有什么用呢,好像也解决不了我在现实开发工作中遇到的问题啊?“

解答:

在我看来,你只有掌握了某个方面的知识,在工作中碰到问题时,才能够想到用这个知识去解决问题。如果你都不知道有这么一种方法,又怎么会用它去解决自己的问题呢?

就比如说,你掌握了 Clang 的知识,那在研究 无侵入的埋点方案 应该如何实现时,你才能可能会想到用 Clang 的 LibTooling 来开发一个独立的工具,专门以静态方式插入埋点的代码;只有掌握了 Clang 的知识,当你在面对代码量达到百万行的 App 包瘦身需求时,才会想到通过 Clang 静态分析来开发工具,去检查无用的方法和类。

当你掌握了 Clang 的相关知识后,编译前端的技术也就掌握得差不多了;在理解了编译前端的词法分析和语法分析的套路后,脱离 Clang 的接口完成第 8 篇文章“ 如何利用 Clang 为 App 提质? ”的课后作业,也就没什么难度了。

篇幅有限,关于 3 个 iOS 开发典型问题的答疑就先到这里。我希望通过这三个问题,可以帮你搞明白那些让你困惑的知识点,逐步地建立起自己的知识体系。

出处:极客时间 《iOS 开发高手课》 专栏

点击这里试看或订阅《iOS 开发高手课》

相关文章