objc源码:opensource.apple.com/source/objc…
一些命令:
- oc文件转c++命令:xcrun -sdk -iphoneos clang -arch arm64 -rewrite-objc xxx.m
- 命令行生成LLVM中间代码(IR): clang -emit-llvm -S xxx.m
OC对象本质
通过点击Class进入内部,我们可以发现Class对象是一个指向objc_class结构体的指针。
我们首先看一下objc_class结构体:
struct objc_class {
Class isa;
Class super_class;
cache_t cache; //方法缓存
class_data_bits_t bits;//用于获取具体的类信息class_rw_t *data = bits & FAST_DATA_MASK
}
注:
class_rw_t是个保存类信息的结构体,如下
struct class_rw_t {
uint32_t flags;
uint16_t version;
uint16_t witness;
const class_ro_t *ro; //存储编译时当前类已经确定的属性、方法、协议的信息
method_array_t methods; //方法列表
property_array_t properties; //属性列表
protocol_array_t protocols; //协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
注:
class_ro_t是个保存编译时类已确定的信息的结构体,如下
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //instance对象占用的内存空间
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name; //类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; //成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}
1. 一个NSObject对象占用多少内存?
占用一个指针变量所占用的大小(64bit,8个字节,32bit,4个字节)
系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)
2. 对象的isa指针指向哪里?
instance对象的isa指向class对象。
class对象的isa指向meta-class对象。
meta-class对象的isa指向基类的meta-class对象
3. OC的类信息存放在哪里?
对象方法、属性、成员变量、协议信息,存放在class对象中
类方法,存放在meta-class对象中
(因为这些编译器只需编译一次就可以知道类里面有什么东西了)
----------------------------------
成员变量的具体值,存放在instance对象里
4. class对象superclass的指针作用,meta-class对象的superclass指针的作用
@interface Person: NSObject
- (void)run;
+ (void)personClassMethod;
@end
Class Student: Person
@end
Student *student = [[Student alloc] init];
[student run];
[Student personClassMethod];
调用方法其实就是给对象发消息
class对象superclass指针: 实例方法是存在类对象里的,首先先通过student实例对象的isa找到student自己的类对象,然后通过Sutdent类对象的superclass指针找到Person类对象,然后就调起Person类对象里面的run方法
meta-class: 首先类方法时存放在元类对象里的,当Student的class要调用Person的类方法时,首先通过Student的class的isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法实现进行调用
5. 简述OC方法的调用过程
oc是动态语言,在运行时方法会被动态转为消息发送,即:objc_msgSend(receiver, selector)。在方法调用的时候,其实就是给某个对象发送消息。
1.runtime会根据对象的isa找到对象所属的类,然后在类的方法列表里寻找,如果找到相符的方法的话,就调用这个方法的实现。
2.如果在当前所属类找不到的话,就通过superclass指针向父类的方法列表中寻找方法并调用。
3.如果在最顶层的父类(一般是NSObject)中找不到的话,程序会抛出异常unrecognized selector sent to xxx,
KVO
1. iOS用什么方式实现一个对象的KVO?(KVO本质、原理是什么?)
- 利用runtime的API(
objc_allocateClassPair和objc_registerClassPair)动态生成一个子类,并且让instance的isa指向这个全新的子类(NSKVONotifying_XXX) - 当修改instance对象的属性时,会调用Foundation的
_NSSetXXXValueAndNotify函数
而
_NSSetXXXValueAndNotify函数里会调用:
1. willChangeValueForKey:
2. 父类原来的setter方法
3. didChangeValueForKey:
内部会触发监听器(Observer)的监听方法(observeValueForKeyPath: ofObject:(id)object change: context:)
KVO本质其实就是重写了set方法。
KVO的原理其实就是把addObserver的instance的isa指向了NSKVONotifying_XXX的类,而这个类里面重写了set方法的实现。
2. 如何手动触发KVO?
手动调用
willChangeValueForKey:和didChangeValueForKey:
3. 直接修改成员变量会触发KVO吗?
不会。因为只接修改成员变量(_age = 2)不会触发set方法。
Category分析
1. Category的加载处理过程。
1.通过Runtime加载某个类的所有分类数据。
2.把所有Category的方法、熟悉、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面。
3.将合并后的分类数据(方法、熟悉、协议)插入到类原来数据的前面。
注:也就是说调用方法时默认会找到最新添加的Category的方法调用。(可以通过xcode的compile sources修改编译顺序)
2.分类的实现原理。
Category在编译后会生成一个
category_t对象,有多个分类的时候就会有多个category_t类型的对象,在编译阶段分类里面的东西跟类的东西是分开的,在运行的时候,分类的东西会合并到类(如:对象方法等)或者元类(如:类方法)里面去。
分类编译成struct category_t,里面存储着分类的对象方法,类方法,属性,协议信息。结构如下:
struct _category_t {
const char *name; //分类名称,如:Person
struct _class_t *cls; // 0
const struct _method_list_t *instance_methods; //对象方法列表
const struct _method_list_t *class_methods; //类方法列表
const struct _protocol_list_t *protocols; //协议列表
const struct _prop_list_t *properties; //属性列表
};
+load, +initialize
1. +load, +initialize方法的区别是什么?
- 调用方式
+load是根据函数地址直接调用。(*load_method)(cls, @selector(load))
+initialize是通过objc_msgsend调用。- 调用时刻
+load是runtime加载类、分类的时候调用(只会调用1次)
+initialize是类第一次接受到消息的时候调用,每一个类只会initialize一次(父类的initialize可能会被调用多次,因为当给子类发消息时,如果子类没有实现+initialize,isa在元类中找不到方法,然后会通过superclass指针找到父类的+initialize)。
2. +load, +initialize方法的区别是什么?
- +load
(1) 先调用类的+load:先编译的类,优先调用+load,调用子类的+load之前会先调用父类的+load。
(2) 再调用分类的+load:先编译的分类,优先调用+load。- +initialize (1)先初始化父类
(2)再初始化子类(可能最终调用的是父类的initialize方法)
Block
首先要知道的内容:
栈:内存会自动销毁的(执行完当前方法之后)
堆:需要手动申请和释放内存(release、free())
auto变量:auto int a,auto是在OC语法中默认省略了的
Block有3种类型: __NSGlobleBlock__、__NSStackBlock、__NSMallocBlock__。
疑问:
1.怎么判断block是哪种类型?
| 类型 | 环境 |
|---|---|
__NSGlobleBlock__ |
没有访问auto变量 |
__NSStackBlock__ |
访问了auto变量 |
__NSMallocBlock__ |
__NSStackBlock__调用了copy |
2. __block的内存管理
当
block在栈上时,并不会对__block变量产生强引用。
当block被copy到堆时,会调用block内部的copy函数,然后copy函数内部会调用_Block_object_assign函数,而_Block_object_assign函数会对__block变量形成强引用(retain)
3. __weak与__unsafe_unretained
__weak: 不会产生强引用,指向的对象销毁时会自动让指针置为nil。
__unsafe_unretained: 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,访问的时候会出现野指针
4. 解决block的循环引用问题---ARC
5. block的本质是什么?
block本质上也是一个OC对象,内部封装了函数调用以及函数调用环境(函数调用地址、变量)。
6.__block修饰符的作用是什么?
__block修饰的变量会被编译器包装成一个__Block_byre_xx_0对象,可以用于解决block内部无法修改auto变量值的问题。
7. block的属性修饰词为什么用copy?使用block有哪些注意点?
block一旦没有进行copy操作,就不会在堆上。因为只有在堆上的时候才能控制block的生命周期,才能进行内存管理
Runtime
1.讲一下消息机制。objc_msgSend流程。
源码阅读顺序:
OC中的方法调用其实就是
objc_msgSend的调用,它会给receiver(方法调用者)发送一条消息(selector方法名),objc_msgSend底层有三个阶段:
消息发送->动态方法解析->消息转发
**消息发送阶段流程**
执行`objc_msgSend`后
1. 首先会判断`receiver`是否为`nil`,
2. (否)然后会通过`isa`指针找到类对象`(reveiverClass)`再从中的`cache`中查找方法,
3. (没找到)然后会去`class_rw_t`中查找方法,
4. (没找到)然后会通过`receiverClass`的`superclass`指针找到`superClass`再从`cache`中查找
5. (没找到)再去这个`superClass`的`class_rw_t`中查找方法
一直重复4跟5的步骤,找到了方法就结束查找并将方法缓存到`receiverClass`的`cache`中。
假如在最上层的`superClass`还没找到方法,就会走下一个流程:
**动态方法解析流程**
1. 首先会判断是否有过动态解析, 有的话会直接走**消息转发流程**
2. (否)会调用`+resolveInstanceMethod`或`+resolveClassMethod`方法来动态解析方法(这方法的实现里面可以通过`class_addMethod`添加方法)
3. 标记已经动态解析(这里为了如果动态解析完成后,再走回消息发送流程还是找不到方法实现,就继续走动态分析解析流程的判断1)
动态解析过后会重新走 >> **消息发送流程**(如果还是找不到方法实现,就会走**消息转发流程**)
**消息转发流程**
当上面两个流程都找不到方法的话,进入消息转发流程,首先会调用
1. `forwardingTargetForSelector:`
返回值不为nil的话,
会调用`objc_msgSend(返回值, SEL)`,让别人进行调用
返回值为nil的话,
2. 会调用`methodSignatureForSelector`方法返回方法签名,
返回值不为nil的话,
会调用`forwardInvocation:`的方法,里面可以随便处理想做的事情
返回值为`nil`的话,
会调用`doesNotRecognizeSelector`方法(就会在控制台报方法找不到的错误)
2. 什么是runtime?项目中哪里用到?
首先OC是一门动态性比较强的编程语言,它允许很多操作推迟到程序运行时再进行,OC的动态性是由
Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数,平时我们编写的OC代码都是转换成RuntimeAPI进行调用。
具体应用:
1.利用关联对象(AsscoiatedObject)在分类添加属性。
2.遍历类的所有成员变量(class_copyIvarList), 字典转模型里用到。
3.交换方法实现(method_exchangeImplementations), 页面统计、拦截Button点击事件(sendAction:to:ForEvent:)时用到。
4.利用消息转发机制解决方法找不到的异常问题。
sendAction:to:ForEvent::点击Button时会先触发这个方法再触发selector,可以在这个方法里面处理要不要触发selector方法
3. isa
从arm64架构开始,isa经过优化变成了一个(
union)共用体结构,它的8个字节不仅仅存放Class, Meta-Class地址值,还存放着更多的信息。
//arm64
union isa_t {
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
};
};
Runloop
1. Runloop与线程
- 每条线程都有唯一的一个与之对应的
Runloop对象Runloop保存在一个全局的Dictionary里,线程为key,Runloop为value- 线程刚创建时并没有
Runloop对象,Runloop在第一次获取 它时创建Runloop会在线程结束时销毁- 主线程的
Runloop已经自动创建,子线程默认没有开启Runloop
2. Runloop的运行逻辑
Source0:
触摸事件处理
performSelector:onThread:
Source1:
基于port的线程间通信
系统事件捕捉
Timers:
NStimer
performSelector:withObject:afterDelay:
Observers:
用于监听Runloop的状态
UI刷新
AutoreleasePool
流程:
1.通知Observers: 进入loop
2.通知Observers: 即将处理Timers
3.通知Observers:即将处理Sources
4.处理Blocks
5.处理Source0(可能会再次处理Blocks)
6.如果存在Source1,就跳转到第8步
7.通知Observers: 开始休眠(等待消息唤醒)
8.通知Observers: 结束休眠(被某个消息唤醒)
---或(1)处理Timer
---或(2)处理GCD Async To MainQueue
---或(3)处理Source1
9.处理Blocks
10.根据前面的执行结果,决定如何操作
---(1)回到第二部
---(2)退出loop
11.通知Observers: 退出loop
3. performSelecter:afterDelay: 本质原理
本质是会内部会创建一个定时器,并添加到当前线程的
Runloop中,如果当前线程没有启动Runloop(子线程默认没有启动Runloop),该方法会失效。
4. Runloop 基本作用
1.保持程序的持续运行
2.处理APP中的各种事件(比如触摸事件、定时器事件等)
3.节省CPU资源,提高性能:该做事时做事,该休息时休息 ...
多线程
常见多线程方案:
1. 多线程中容易混淆的术语:同步/异步,并发/串行
1.同步和异步主要影响: 能不能开启新线程
同步:在当前线程中执行任务,不具备开启新线程能力
异步:在新的线程中执行任务,不具备开启新线程能力
2.并发和串行主要影响:任务的执行方式
并发:多个任务并发(同时)执行
串行:一个任务执行完后,再执行下一个
使用sync函数往当前串行队列中添加任务,会卡住当前串行队列(产生死锁)
2. 线程同步的方案(性能从高到低)
os_unfair_lock(ios10),
OSSpinLock(10之前)(自旋锁),
dispatch_semaphore,
pthread_mutex(互斥锁), dispatch_queue(DISPATCH_QUEUE_SERIAL),
NSLock,
NSCondition,
pthread_mutex(rescursive)(递归锁),
NSRecursiveLock(递归锁),
NSConditionLock,
@synchronized
自旋锁:等待锁的线程会处于忙等状态,一直占用CPU资源
互斥锁:等待锁的线程会处于休眠状态
文件读写:
1.用pthread_rwlock可以解决读写安全的问题。
同一时间,只能有1个线程进行写的操作
同一时间,允许有多个线程进行读的操作
同一时间,不允许既有写,又有读的操作。
2.用`dispatch_barrier_async`
dispatch_async(queue, ^{
//读
});
dispatch_barrier_async(queue, ^{
//写
})
内存管理
内存布局从低到高:代码段->数据段->堆->栈
1. 深拷贝和浅拷贝
拷贝本质:产生一份副本对象,跟源对象互不影响
深拷贝:内容拷贝,会产生新对象
浅拷贝:指针拷贝,不产生新对象
1. oc对象的内存管理
在iOS中,是使用引用计数来管理OC对象的内存的
一个新创建的OC对象引用计数默认为1,当引用计数减为0后,OC对象就会销毁,释放占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
2. weak指针的实现原理
它会将我们这些弱引用存到一个哈希表里边,到时候对象销毁的时候,就会取出当前对象对应的弱引用表,把弱引用表里面的弱引用都清除掉
3. Runloop与Autorelease
ios在主线程的Runloop中注册了2个Observer,
第一个Observer监听了kcfRunloopEntry事件,调用objc_autoreleasePush()。
第二个Observer
监听了kCFRunloopBefreWaiting休眠事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePush()监听了kCFRunloopBefreExit退出事件,会调用objc_autoreleasePoolPop()
性能优化
1. 卡顿优化
① CPU
- 尽量用轻量级的对象,比如用不到事件处理的地方,考虑使用
CALayer取代UIView- 不要频繁地调用
UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改- 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
Autolayout会比直接设置frame消耗更多的CPU资源- 图片size最好跟
UIImageView的size保持一致- 控制一下线程的最大并发数
- 尽量把耗时的操作放到子线程,例如:文本处理(尺寸计算、绘制),图片处理(解码、绘制)。
② GPU
- 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
- GPU能处理的最大纹理尺寸为
4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸- 尽量减少视图数量和层次
- 减少透明的视图(alpha<1),不透明就设置
opaque为YES- 尽量避免出现离屏渲染, 例如:光栅化(Ratersize),遮罩(mask),圆角(cornerRadius,尽量考虑通过
CoreGraphic绘制裁剪圆角),阴影(shadowXXX,如果设置了layer.shadowPath就不会产生离屏渲染)
卡顿原因:
平时我们所说的卡顿,主要是因为在主线程执行了比较耗时的操作
卡顿检测:
可以添加Observer到主线程Runloop中,通过监听Runloop状态切换的耗时(即监听休眠结束以后,处理source1到source0中间的时间),以达到监控卡顿的目的
2. APP冷启动过程
dyld(动态链接器) -> runtime -> main:启动三大阶段。
APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库。
并由runtime负责加载成objc定义的结构
所有初始化工作结束后,dyld就会调用main函数。
3. APP启动优化
按照不同的阶段:
dyld阶段:
1.减少动态库、合并一些动态库(定期清理不必要动态库)
2.减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类和分类)
3.减少C++虚函数数量
4.Swift尽量使用struct
...
runtime阶段:
用+initialize方法和dispatch_once取代所有__attribute__((constructor))、C++静态构造器、Objc的+load。
...
main阶段:
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishlaunching方法中
4. 安装包瘦身
安装包(IPA)主要由可执行文件、资源组成。
资源(图片、音频、视频等):
1.采取无损压缩
2.去除无用资源,可用LSUnusedResources查找
...
可执行文件瘦身
去掉异常支持:Enable C++ Exceptions、Enable Objective-C Exceptions设为NO,Other C Flags添加-fno-exceptions.
利用AppCode检测未使用的代码,菜单 -> Code -> Inspect Code.
或编写LLVM插件检测未使用的代码、重复代码.
...
LinkMap
生成LinkMap文件,可以查看可执行文件的具体组成。
Build Setting -> Link Map -> Write Link Map File => Yes,生成的LinkMap文件路径修改放到桌面。可用工具LinkMap解析
架构
1. MVC/MVVM/MVP
苹果官方MVC:
优点:model、view可重复利用
缺点:controller过于臃肿
变种后MVC:
优点:对controller进行了瘦身,将view内部细节封装了起来,外面不知道view的具体实现
缺点:view依赖于Model
2. 三层架构
界面层、业务层、数据层。
我们平时所说的MVC/MVVM等等都是我们界面层的架构。
未完,待续。
本文供本人用来回忆所用,知识点理解没写完整,具体理解自己看