31

3 个 iOS 开发典型问题解答

 5 years ago
source link: https://www.infoq.cn/article/ljYYWc8JjlF8EMrrx_5q?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

更多热点问题答疑

在这里,我先挑选并展开了 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 开发高手课》


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK