一、谈谈OC的反射弧机制
程序可以访问、检测和修改它本身状态或行为的一种能力。OC的反射机制,即在动态运行状态下可以构造任意一个类或对象,知道这个类的所有属性和方法。
二、探索分析App启动时间优化问题
启动慢问题分析:
通过代码工程的设置Product->Scheme->Edit Scheme->Run->Environment Variables
设置:key:DYLD_PRINT_STATISTICS value:1
时间分为两个部分:
main-pre:系统环境布局时间,创建进程,加载解析可执行文件(库加载,堆栈环境配置等等)
main:从main
函数到第一个界面显示时间
启动慢的影响的主要因素:
1、库加载越多,启动越慢
2、Objc
类越多,越慢
3、静态对象全局对象越多,启动越慢
4、Objc
的+load
越多,启动越慢
解决方法:
1、尽量避免过多使用+load
方法,可以使用+initlalize
替代。
在系统第一次使用到这个类的使用,才会使用到他的+(void)initlalize
方法。
2、减少- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
里面的操作,在启动页出现后再进行相关操作。
例如放到子线程中去执行
三、点击 Run 之后发生了什么?
简单来说,点击Run之后App进行编译、汇编、链接、代码签名以及启动执行等操作。
因为某些操作流程及其原理中在其它总结中做了相关的介绍说明。这里主要介绍代码签名以及启动执行。
- 代码签名
每次build
之后,都会发现工程目录下多了一个.app
文件。在.app
目录中,有又一个叫_CodeSignature
的子目录,这是一个plist文件,里面包含了程序的代码签名,你的程序一旦签名,就没有办法更改其中的任何东西,包括资源文件,可执行文件等,iOS系统会检查这个签名。
签名过程本身是由命令行工具codesign
来完成的。如果你在Xcode中build
一个应用,这个应用构建完成之后会自动调用codesign
命令进行签名,这也是link
之后的一个关键步骤。
- 启动
其实在启动过程中,dyld
(动态链接器)起了很重要的作用,进行动态链接,进行符号和地址的一个绑定。
dyld
主要在启动过程中主要做了以下事情:
1、加载所依赖的dylibs
2、Fix-ups:Rebase修正地址偏移,因为OSX和iOS搞了一个叫ASLR
的东西来做地址偏移(随机化)来避免收到攻击
3、Fix-ups:Binding确定Non-LazyPointer
地址,进行符号地址绑定。
4、ObjCruntime初始化:加载所有类
5、Initializers:执行load方法
和__attribute__((constructor))
修饰的函数
四、请简述OC的编译过程。
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime
来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
更多详情的过程:
iOS编译采用Clang
作为编译器前端,LLVM
作为编译器后端,编译器前端负责语法分析、语义分析,生成中间码(LLVMIR)
,在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行;编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。
-
Clang
的编译过程
预处理
预处理器会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换,删除注释,展开头文件,产生.i
文件。
词法分析
预处理完成了以后,开始词法分析,这里会把代码切成一个个Token,比如大小括号、等于号,还有字符串等。
语法分析
语法分析,在Clang
中由Parser
和Sema
两个模块配合完成,验证语法是否正确,根据当前语言的语法,生成语意节点,并将所有节点组合成抽象语法树AST
。
静态分析
一旦编译器把源码生成了抽象语法树,编译器可以对这棵树做分析处理,以找出代码中的错误,比如类型检查:即检查程序中是否有类型错误。例如:如果代码中给某个对象发送了一个消息,编译器会检查这个对象是否实现了这个消息(函数、方法)。此外,Clang
对整个程序还做了其它更高级的一些分析,以确保程序没有错误。
类型检查
一般会把类型检查分为两类:动态的和静态的。动态的在运行时做检查,静态的在编译时做检查。以往,编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。由于只是在运行时做此类检查,所以叫做动态类型。
至于静态类型,是在编译时做检查。当在代码中使用ARC
时,编译器在编译期间,会做许多的类型检查:因为编译器需要知道哪个对象该如何使用。
-
LLVM
的编译过程
目标代码的生成与优化
CodeGen负责将语法树AST
丛顶至下遍历,翻译成LLVMIR
中间码,LLVMIR
中间码编译过程的前端的输出后端的输入。
编译器后端主要包括代码生成器、代码优化器。代码生成器将中间代码转换为目标代码,代码优化器主要是进行一些优化,比如删除多余指令,选择合适寻址方式等,如果开启了Bitcode
苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的Bitcode
去生成。优化中间代码生成输出汇编代码,把之前的.i
文件转换为汇编语言,产生.s
文件
汇编
目标代码需要经过汇编器处理,把汇编语言文件转换为机器码文件,产生.o
文件。
链接
链接又分为静态链接和动态链接。对.o
文件中的对于其他的库的引用的地方进行引用,生成最后的可执行文件(同时也包括多个.o
文件进行link)。
静态链接
在编译链接期间发挥作用,把目标文件和静态库一起链接形成可执行文件。
动态链接
链接过程推迟到运行时再进行。如果多个程序都用到了一个库,那么每个程序都要将其链接到可执行文件中,非常冗余,动态链接的话,多个程序可以共享同一段代码,不需要在磁盘上存多份拷贝,但是动态链接发生在启动或运行时,增加了启动时间,造成一些性能的影响。
静态库不方便升级,必须重新编译,动态库的升级更加方便。
五、深入理解iOS开发中的Bitcode功能
LLVM
是目前苹果采用的编译器工具链,Bitcode
是LLVM
编译器的中间代码的一种编码,LLVM
的前端可以理解为C/C++/OC/Swift
等编程语言,LLVM
的后端可以理解为各个芯片平台上的汇编指令或者可执行机器指令数据,那么,Bitcode
就是位于这两者直接的中间码。LLVM
的编译工作原理是前端负责把项目程序源代码翻译成Bitcode
中间码,然后再根据不同目标机器芯片平台转换为相应的汇编指令以及翻译为机器码。
这样设计就可以让LLVM
成为了一个编译器架构,可以轻而易举的在LLVM
架构之上发明新的语言(前端),以及在LLVM
架构下面支持新的CPU(后端)指令输出,虽然Bitcode
仅仅只是一个中间码不能在任何平台上运行,但是它可以转化为任何被支持的CPU架构,包括现在还没被发明的CPU架构,也就是说现在打开Bitcode
功能提交一个App到应用商店,以后如果苹果新出了一款手机并CPU也是全新设计的,在苹果后台服务器一样可以从这个App的Bitcode
开始编译转化为新CPU上的可执行程序,可供新手机用户下载运行这个App。
更多详情参考深入理解iOS开发中的Bitcode功能
六、探寻OC对象的本质
OC的对象结构都是通过基础C\C++
的结构体实现的。我们通过创建OC文件及对象,并将OC文件转化为C++
文件来探寻OC对象的本质。
转化为C语言其实就是一个结构体
struct NSObject_IMPL {
Class isa;
};
那么这个结构体占多大的内存空间呢,我们发现这个结构体只有一个成员,isa指针,而指针在64位架构中占8个字节。也就是说一个NSObjec对象所占用的内存是8个字节。但是我们发现NSObject
对象中还有很多方法,那这些方法不占用内存空间吗?其实类的方法等也占用内存空间,但是这些方法所占用的存储空间并不在NSObject
对象中。
为了探寻OC对象在内存中如何体现,我们来看下面一段代码
NSObject *objc = [[NSObject alloc] init];
上面一段代码在内存中如何体现的呢?上述一段代码中系统为NSObject
对象分配8个字节的内存空间,用来存放一个成员isa指针。那么isa指针这个变量的地址就是结构体的地址,也就是NSObject
对象的地址。
假设isa的地址为0x100400110
,那么上述代码分配存储空间给NSObject
对象,然后将存储空间的地址赋值给objc指针。objc存储的就是isa的地址。objc指向内存中NSObject
对象地址,即指向内存中的结构体,也就是isa的位置。
七、探寻Class的本质
我们知道不管是类对象还是元类对象,类型都是Class
,class
和mete-class
的底层都是objc_class
结构体的指针,内存中就是结构体。
Class objectClass = [NSObject class];
Class objectMetaClass = object_getClass([NSObject class]);
点击Class来到内部,我们可以发现
typedef struct objc_class *Class;
Class对象其实是一个指向objc_class
结构体的指针。因此我们可以说类对象或元类对象在内存中其实就是objc_class
结构体。
我们来到objc_class
内部,但是OBJC2_UNAVAILABLE;
说明这些代码已经不在使用了。那么目前objc_class
的结构是什么样的呢?通过objc源码中去查找objc_class
结构体的内容。
发现这个结构体继承objc_object
并且结构体内有一些函数,因为这是c++
结构体,在c上做了扩展,因此结构体中可以包含函数。发现objc_object
中有一个isa指针,那么objc_class
继承objc_object
,也就同样拥有一个isa指针。
简单点说,通过Runtime等源码知道,class->objc_class->objc_object
。
更多详情请参考iOS底层原理总结 - 探寻Class的本质