1.NSObject对象所占内存大小
@interface Person : NSObject
@property (nonatomic, copy) NSString *p;
@property (nonatomic, assign) short p1;
@end
@interface Student : Person
@property (nonatomic, assign) short s;
@end
该文件用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person.cpp命令翻译成苹果手机arm64架构对应的c/c++代码,查找头文件如下
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
short _p1;
NSString *_p;
};
struct Student_IMPL {
struct Person_IMPL Person_IVARS;
short _s;
};
OC对象的本质是c中的结构体指针。
这里尤其要注意的是Person类中p和p1的顺序,这个会影响到它的子类内存对齐后的大小。
c中有两个函数可以查看内存大小,如下
class_getInstanceSize(Class _Nullable cls) ,获取类对象中成员变量内存对齐之后所占据空间大小。
malloc_size(const void *ptr),获取指针所指向内存的空间大小。
测试代码如下
NSObject *obj = [NSObject new];
Person *p = [Person new];
Student *s = [Student new];
NSLog(@"%zd", class_getInstanceSize(NSObject.class));//8
NSLog(@"%zd", class_getInstanceSize(Person.class));//24
NSLog(@"%zd", class_getInstanceSize(Student.class));//32
NSLog(@"%zd",malloc_size((__bridge const void *)(obj))) ;// 16
NSLog(@"%zd",malloc_size((__bridge const void *)(p))) ; // 32,每隔16个字节递增
NSLog(@"%zd",malloc_size((__bridge const void *)(s))) ; //32,每隔16个字节递增
1.1NSObject内部就只有一个isa指针,当然是8个字节。class_getInstanceSize底层是调用的alignedInstanceSize方法,获取到属性内存对齐后的大小
1.2 对象是通过allocWithZone方法分配内存空间的,追查后发现分配内存空间的函数calloc的参数size获取方法如下
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
size其实也就是class_getInstanceSize获取到的值,只不过做了最小值限制,那么可以得知,大部分情况下,class_getInstanceSize和malloc_size获取到的值应该是一样的,但是实际情况却是后者总是大于等于前者。原因出在c函数calloc上面,这个函数并不是传入多少字节就返回这个字节大小的空间,而是有其自己的分配规则,类似于字节对齐,以最大化/最快速分配内存空间,减少内脆碎片。内存分配空间的选择跟平台和系统架构相关。在ios中,一般的,空间为16的倍数
1.3.p所占空间大小,根据内存对齐,可知是24. 但是s的大小为32确需要注意,因为类的成员变量底层排列方式并不是根据代码顺序,而是遵循
struct A_IMPL {
struct B_IMPL NSObject_IVARS; // B是A的父类
少字节的字段
多字节的字段
};
因为此,所以子类Student的s属性,并不能填充到父类Person的p1字节对齐空间。
1.4. class_getInstanceSize获取属性实际空间大小,为8的整数倍(8是因为内存对齐结构体最大的那个属性,为isa指针)。malloc_size总是大于等于前者,且为16的整数倍。
1.5. 类在编译阶段即翻译为c,子类实例对象中存储了所有父类的所有的成员变量,并确定内部每个字段基于isa指针的偏移。并没有预留空间来存储新增的字段,所以oc中类是不支持动态添加成员变量的。 那么所谓的oc动态添加属性是怎么回事?
动态添加属性方式:https://blog.csdn.net/shengyumojian/article/details/44919695
1.5.1.通过runtime动态添加Ivar,这个只能为动态添加的类添加属性
1.5.2.通过runtime动态添加property,这个新增的成员变量的值也只能存储在事先定义好的对象字段中,并没有在原先类中插入成员变量
1.5.3.通过setValue:forUndefinedKey动态添加键值,这个跟上面那个一样的存储方式
1.5.4.通过objc_setAssociatedObject关联对象,这个是以对象地址为key,值为关联在该对象上面所有key-value的map,
{
"对象的地址": {
"key1" : "value1",
"key2" : "value2",
}
}
这个大Map存储在一个全局的区域,当首次为一个对象p(Person类型)关联属性时,创建该对象地址对应&p的map({&p:{key:value}}),并对p进行标记,当该对象引用计数器为0,检查标记位,存在标记则从这个全局区域删除p对象的关联map。
2.方法查找
@interface Person: NSObject
+ (void)test;
@end
@implementation Person
@end
@interface NSObject (Extension)
@end
@implementation NSObject (Extension)
- (void)test{
NSLog(@"test---%p",self); // 打印的是该方法调用者的地址,test---0x10b439ea8
}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"%p",[Person class]); // 0x10b439ea8
NSLog(@"%p",[NSObject class]);//0x10c3e3ea8
[Person test];
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
调用Person的静态方法test,底层代码是objc_msgsend(objc_getClass(Person),@selctor(test)),具体流程是根据Person类对象的isa找到Person的元类对象,查找内部的方法,不存在,于是根据Person的元类对象的superClass指针找到NSObject的元类对象,查找方法,又不存在,NSObject的元类的superClass指向的是NSObject类对象(这里要注意),发现存在test方法,于是调用。
从上面的例子可以看出,调用[Person test],最终执行的是NSObject的实例方法test方法,调用者为Person,即为消息的最初发送者。
一个对象能够存在相同名字的静态方法和实例方法,是因为这两个方法的存储位置不同,实例方法存储在类对象中,静态方法存储在元类中,各不相干。
3.分类
分类在编译后内存中的结构如下
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;// 实例方法
struct method_list_t *classMethods;//类方法
struct protocol_list_t *protocols;//遵守的协议
struct property_list_t *instanceProperties;//成员变量
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
所有分类中的方法等信息,在运行时由runtime添加到所属的类中。
程序运行时,runtime初始化函数在objc-os.mm中_objc_init -> map_images->map_images_nolock-> _read_images->remethodizeClass->attachCategories->attachLists
后面两个方法的缩略实现如下(只列举了实例方法,其他的类似):
// 举例:Person有两个分类,分别是A, B,依次编译(编译顺序由xcode中Build Phases->Compile Sources顺序决定)
static void attachCategories(Class cls, category_list *cats, bool flush_caches){
bool isMeta = cls->isMetaClass();
// 创建一个数组,元素个数为cls所有分类的个数2
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
int i = cats->count;
// 反向遍历,意味着后面编译的分类B在数组头部
while (i--) {
auto& entry = cats->list[i];
// 获取某个分类的方法列表数组
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
// mlists的结构是:[B的方法列表地址,A的方法列表地址]
mlists[mcount++] = mlist;
}
}
// rw是Person类对象表
auto rw = cls->data();
// 下一个关键方法入口
rw->methods.attachLists(mlists, mcount);
}
// addedLists:[B的方法列表地址,A的方法列表地址]
// addedCount:2
void attachLists(List* const * addedLists, uint32_t addedCount) {
// array()->lists()是Person类中的方法数组,结构是[Person的方法列表地址]
uint32_t oldCount = array()->count; // 1
uint32_t newCount = oldCount + addedCount;// 1 + 2 = 3
// 扩充Person类方法数组的空间,扩容后的结构是[Person的方法列表地址,空位,空位]
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
// 移动后的结构是[空位,空位,Person的方法列表地址]
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
// 移动后的结构是[B的方法列表地址,A的方法列表地址,Person的方法列表地址]
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
分类中的方法,追加到Person中时不会判断该方法是否已经存在,会直接插入到方法列表的头部,方法查找时,依次遍历数组,最先找到的总是分类中的方法,若出现方法重复现象,则原本类中的方法再也不能调用了。
4.+load方法
程序启动后,加载完类信息,分类信息等之后,执行+load方法。源码在objc-os.mm中_objc_init->load_images
void load_images(const char *path __unused, const struct mach_header *mh){
// 将所有类,分类信息分别放到数组loadable_classes,loadable_categories中
prepare_load_methods((const headerType *)mh);
// load方法调用
call_load_methods();
}
void prepare_load_methods(const headerType *mhdr){
size_t count, i;
// 看源码,推测系统所有类在内存中是连续的,该方法是找出所有类存储位置的头指针(猜测)
classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 加载类信息到loadable_classes数组中
schedule_class_load(remapClass(classlist[i]));
}
category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
// 加载分类信息到loadable_categories数组中
add_category_to_loadable_list(cat);
}
}
static void schedule_class_load(Class cls){
if (!cls) return;
// 这个条件,是判断该类是否已经被添加到数组中。
if (cls->data()->flags & RW_LOADED) return;
// 递归,确保父类在子类前面
schedule_class_load(cls->superclass);
// 加载该类信息到数组
add_class_to_loadable_list(cls);
// 做标记,表征该类已经被添加,避免重复添加
cls->setInfo(RW_LOADED);
}
void add_class_to_loadable_list(Class cls){
IMP method;
// method是类的load方法
method = cls->getLoadMethod();
if (!method) return; // 数组扩容
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
// loadable_classes结构: [loadable_category,loadable_category]
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
IMP objc_class::getLoadMethod(){
const method_list_t *mlist;
// mlist是类信息中最初的那个方法列表,后续分类,以及动态添加的方法均不再此内
mlist = ISA()->data()->ro->baseMethods();
if (mlist) {
// 遍历,字符串匹配找到load方法
// 注意,寻找类的load方法不是走的消息发送机制,故不会出现消息覆盖现象
for (const auto& meth : *mlist) {
const char *name = sel_cname(meth.name);
if (0 == strcmp(name, "load")) {
return meth.imp;
}
}
}
return nil;
}
void call_load_methods(void){
call_class_loads(); // 先调用类中的load方法
call_category_loads(); // 在调用分类中的load方法
}
注意点:
4.1. 类的load方法和分类的load方法分别存放在不同的数组之中。
4.2.类的load方法加载时,存在递归操作,确保父类的load方法存储位置在子类的load方法之前
4.3.类的load方法获取不是通过消息机制,而是通过元类信息中的一张初始方法列表获取,该表在类最初加载时就生成,只读,后续分类,以及动态添加的方法均不再此内
4.4.分类load方法获取是通过分类在内存中的属性classMethods类方法列表获取
4.5.load方法调用时,先调用所有类的load方法,再调用分类的load方法
4.6.因为系统执行load方法时不是走消息发送机制,故不存在分类load方法覆盖类load方法的现象,而代码手动调用类的load方法,因为是走消息发送机制,分类load方法会优先执行
5.initialize
// 消息机制,任意方法查找时执行
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver){
// 查找缓存,有缓存则直接返回
// 查找类中的方法时,发现该类的标识位位NO,表名改类尚未初始化,则先执行初始化操作
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst));
}
}
void _class_initialize(Class cls){
Class supercls;
supercls = cls->superclass;
// 递归,检查父类标志位,若父类没有初始化(指的是没有调用过initialize方法),先执行父类的initialize方法并标记
if (supercls && !supercls->isInitialized()) {
_class_initialize(supercls);
}
// 设置标志位,表明该类已经初始化
cls->setInitializing();
// 消息机制,类似调用[类 initialize]
callInitialize(cls);
}
给类发送消息时,检查标识位,若发现尚未初始化则会执行_class_initialize方法,并设置标识位。
_class_initialize方法中,先查找父类有没有初始化,若无则先执行父类initialize方法,再执行自己的initialize方法,并都设置标示位。
因为调用initialize方法走的是消息发送机制,所以分类的initialize方法会覆盖父类的initialize方法。
特例:Person自己实现了initialize方法,子类Student没有实现initialize方法,当依次执行方法[Person alloc]; [Student alloc];时候,会发现Person的initialize方法被执行了2次。
分析:[Person alloc]调用时,会先执行Person的initialize方法一次,并且设置Person标示位为YES。
[Student alloc]调用时,先查找父类Person,发现标示位为YES,忽略父类,然后执行Student的initialize方法,但是Student并未实现initialize,于是通过消息查找流程找到了Person的initialize方法,调用。
由上,可知,类的initialize方法是可能多次被执行的,在开发中要注意。
6.关联对象
常用于给分类的属性实现类似成员变量的特征。
分类是能添加属性的,但是无法自动生成setter和getter,以及生成成员变量。
分类无法生成成员变量的原因分析:
A,分类编译后的struct结构只有属性列表,不存在成员变量列表
B,程序启动时,运行时只将分类的方法列表添加到类的方法列表中,并未合并分类的属性列表到类中
关联对象方法 objc_setAssociatedObject(object, key, value, objc_AssociationPolicy),全局hashTable的结构如下
{
object地址:{key: value},
object2地址:{key: value},
}
该hashTable会根据关联策略objc_AssociationPolicy引用value,若value为对象,但是关联策略为assign时,value可能会被销毁,后若再取出该value,可能会引发坏内存访问崩溃。
hashTable只是使用object的内存地址为key,并未强引用object。对某一个object进行关联对象,则object会有个标志位表明该对象有关联对象,object销毁时会查看该标志位,若为YES,则会清除hashTable中该对象的所有关联数据。
7.block
struct __block_impl {
void *isa; // isa指针,表明是个对象,继承自NSObject
int Flags;
int Reserved;
void *FuncPtr;// 函数指针
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
// 下面这两个函数,要在block内部捕获了对象时候才会存在,用来对捕获的对象进行内存管理。
// copy当block从栈上拷贝到堆上时执行,根据捕获对象的修饰符(strong/weak/unsafe_retained)决定是否对捕获的对象进行强引用(retain)
// dispose当block销毁时,根据....对捕获的对象进行相应的release
// 当捕获的对象使用了__block修饰时,例如捕获了__block int a = 10,因为编译器会将a包装成一个对象,内部也会有这两个方法,copy时对包装类进行强引用,保证包装对象的生命周期
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a; // 捕获的局部变量
// struct的构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
7.1 变量捕获:block访问的全局变量不必捕获,因为block本来就能访问,block内部也没有保留字段。block访问的局部变量必须捕获,因为局部变量作用域问题。block内部有相应的字段保留值,如上面的int a。局部变量的auto(默认)是值捕获,static是指针捕获。auto变量随时可能被销毁,所以不能使用指针捕获,否则当block调用时可能出现坏内存访问。
需要注意的是,局部变量捕获时,所谓的值捕获指的是捕获的内容原本是什么类型就捕获什么类型,例如对auto的int a,捕获的是int 类型,NSObject *p,则捕获的是NSObject *类型。同理,static的int a捕获的是int *类型,static NSObject *p则捕获的是NSObject **类型。
注意如下: 这是定义在一个类中的方法
// 这里的block会捕获self,因为self是一个局部变量
// oc类中的任何方法的前面两个参数都是self和SEL,所以方法中才能访问self和_cmd
- (void)test{
void (^block)(void) = ^{
NSLog(@"%p",self);
};
}
// 这里不是直接捕获的_name的地址,是因为_name是依赖self这个局部变量而存在的,所以捕获的是self
- (void)test2{
void (^block)(void) = ^{
NSLog(@"%@",self->_name);
};
}
7.2 block的类型,也就是isa的指向:block经过clang之后,发现isa指向的都是栈block,这是因为在运行阶段,runtime才会改变block的真正指向,以程序运行时打印为准,例如全局block的继承链NSGlobalBlock: __NSGlobalBlock : NSBlock : NSObject。block分为栈block,堆block,全局block。 block内部不访问auto变量,则是全局block,存储在数据段。 block访问了auto变量,则是栈block,存放在栈中。 栈block执行copy后变为堆block,存储在堆中,对象在堆上面才会启用引用计数进行管理。 栈block作为一个对象,但是在栈中,随时可能被释放,那么这个block调用的时候很容易出现坏内存访问, 在ARC中,运行时在大部分情况下会对栈block进行copy操作,比如block赋值给__strong指针,block作为函数返回值,block在GCD和在系统API中且有usingBlock时。
7.3 block内部改变auto的值
__block int a = 10;
void (^block)(void) = ^{
a = 100;
};
a = 1000;
a;
编译后变成
// __block修饰的auto变量a,编译后被包装变成了一个对象,对象类型是__Block_byref_a_0
__Block_byref_a_0 a = {0,&a, 0, sizeof(__Block_byref_a_0), 10};
void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a, 570425344));
a.__forwarding->a = 1000; // 修改/获取a的值,是取的对象中的a属性
a.__forwarding->a;
__Block_byref_a_0是a的包装类,在编译阶段就进行了,所以a不再是一个int类型的变量,而是一个对象。结构如下
struct __Block_byref_a_0 {
void *__isa; // 0
__Block_byref_a_0 *__forwarding; // 该指针的值为当前对象的地址,指向自身
int __flags; //0
int __size; // 结构体__Block_byref_a_0的大小
int a;// 被包装的具体值,10
};
既然a是一个对象,那么block捕获的就是个对象,那么block执行时就能够修改对象的属性,block执行时代码如下
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // 找到block中包装好的a对象
(a->__forwarding->a) = 100;
}
我们代码中写的a = 100;但是实际上底层修改的是对象中的属性值,当然能成功。以后代码中取值a,取的也是对象中a属性的值。
为什么block内部改变auto值必须使用__block修饰?假若不用__block修饰,且编译器能通过,分析如下:
int a = 10;
void (^block)(void) = ^{
a = 100;
};
block捕获a时是值捕获,即block内部有个属性a等于10。当block执行时,也就是调用指针FuncPtr指向的函数,参数为block自身,类似于
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a;
a = 100;
}
这样改的也只是block内部变量a的值,跟外界代码中的int a无关,无法修改外界a成功。因为这样,所以编译器干脆禁止编译通过。
__Block_byref_num_0 *__forwarding; 这个最初是指向栈中自身的指针,copy操作时会将block从栈拷贝到堆,同时会拷贝__Block_byref_a_0到堆上,并改变栈上面这个__Block_byref_a_0的__forwarding指针,让其指向堆上面的那个__Block_byref_a_0对象,这样,通过a->__forwarding->a取值,则不管当前是栈上的还是堆上的对象,都是改变/获取到堆上那个对象的a值。如上面显示的,a=100使用的是堆上面那个__Block_byref_a_0对象,而a=1000则使用的是栈上面的对象,改变的都是堆上面的值。
7.4 循环引用
ARC:__weak,__unsafe_unretained,__block并执行block后清空__block对象。
__block避免循环引用的优缺点:可控制捕获对象的生命周期,缺点是必须执行block,且在block中将__block变量设置为nil。
MRC:__unsafe_unretained和__block,在MRC下,block捕获的__block变量,当拷贝到堆上的时候,不会对__block对象进行retain操作,也就是说根本不会循环引用
- 方法调用
首先看方法的缓存实现类objc-cache.mm,几个主要的方法如下
struct cache_t { // 缓存类的数据结构
struct bucket_t *_buckets; // hashtable,方法缓存列表
mask_t _mask; // 表的长度 - 1
mask_t _occupied; // 表中实际存储了多少个数据,hashtable的长度一般会大于存储的数据,一旦接近容易触发hash碰撞,要扩容
};
struct bucket_t {// hashtable中元素的结构
private:
cache_key_t _key; // 函数名为key
IMP _imp; // 函数指针
};
// 找出key对应在hashtable中的位置,若出现hash碰撞,采取的算法是将得到的index递增,直到找到合适的位置
bucket_t * cache_t::find(cache_key_t k, id receiver){
bucket_t *b = buckets(); // 获取缓存的hashtable
mask_t m = mask(); // mask,就是表的长度 - 1
mask_t begin = cache_hash(k, m); // hash获取函数,key & mask,得到一个不大于mask的值。也可以采用key % mask
mask_t i = begin;
do {
// b[i].key()结果为0,表明hashtable中该索引位置没有存储内容
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
// 来到这,说明在索引位置处取到了元素,但是该元素不是目标元素,也就是存在hash碰撞,这时,将索引+1,遍历数组直到遍历结束
} while ((i = cache_next(i, m)) != begin);
}
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver){// 将方法缓存
// 如果已经缓存,返回
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {// 缓存尚未初始化,则初始化,初始化hashtable表容量INIT_CACHE_SIZE是1<<2,为4
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
}
else { // hashtable中元素个数大于容量的四分之三,则扩容
cache->expand();
}
// 因为当前方法尚未缓存,这里得到的bucket是key在hashtable中对应位置的指针
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied(); // 实际存储元素个数+1
bucket->set(key, imp);
}
void cache_t::expand(){ // hashtable扩容
uint32_t oldCapacity = capacity();
// 每次扩容量为之前的2倍
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
reallocate(oldCapacity, newCapacity);
}
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity){ // 真正的分配扩容空间,重组
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity); // 重新为hashtable分配内存空间
// 将新分配的hashtable赋值给缓存中的_buckets,可知新的hashtable是空的,以前的缓存失效了
setBucketsAndMask(newBuckets, newCapacity - 1);
// 以下方法是收集旧的hashtable中的所有元素,并且在元素数量达到一定阀值(32*1024字节)时统一销毁
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
方法查找源码在lookUpImpOrForward中,比较简单,直接说明。查找顺序如下,一旦找到方法就将其缓存并直接返回该方法结束查找。
1 isa指针找到当前的类对象
2 类的缓存列表中查找
3 在类的方法列表(是一个二维数组,包括分类的方法)中查找,需要注意的是,二维数组的结构是[动态添加方法1的列表,动态添加方法2的列表,分类1的方法列表,分类2的方法列表,类本身的方法列表],外层数组是按顺序遍历,但是内层的方法列表大多是折半查找,因为方法动态添加/分类方法添加等都会先进行递增排序。
4 通过superclass指针遍历,在父类中查找,直到superclass为nil。在父类中找到的方法,也是缓存在本类中
--- 4.1 在父类中的缓存列表中查找
--- 4.2 遍历父类的方法列表查找
5 没有找到,执行_class_resolveMethod,触发动态消息转发,调用类中的方法resolveInstanceMethod,加载这个方法中可能动态添加的方法到类方法列表中,回到2继续。 第5步在每次触发消息转发流程中只会执行一次。
因为调用resolveInstanceMethod方法后系统不知道用户究竟有没有给类或者父类动态添加了方法,假如添加了,则该方法会处于类的rw表的首位,可以通过再次遍历查找找到。要注意,系统不会去判断resolveInstanceMethod的返回值,就算返回NO也还是会重新走一遍方法查找过程,但是只此一次。
6 将第二部消息转发的实现缓存,这样,再次给对象发送这个消息时就能够通过缓存将这个第二部转发消息所需调用的方法返回,直接触发消息转发
7 返回第二部消息转发的实现
以上所谓的第二部消息转发的实现是汇编写的,网上有人用下面伪代码表示
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8); // sel是第二个参数,指针偏移为8
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// 调用 forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
// 将消息转发给另外一个对象
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// 调用 methodSignatureForSelector 获取方法签名后再调用 forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
// 方法签名,指的是方法的返回值类型,参数类型,如@"v16@0:8"这样,其中数字可以省略
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
// 能来到这里,就不会再报找不到消息错误了
// 一般来到这里,是做异常上报
return;
}
}
// 进行到这里,才会报找不到消息错误
if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
}
需要注意的是类方法也是有消息转发过程的,但是需要将上面的各个消息转发方法以+号开头编写,因为从源码可以看出,根本不会区分类方法还是实例方法,只看receiver和sel。
- 类的结构
struct objc_object {
isa_t isa;
}
// 联合体,共用一块内存,大小为内部最大某个单元的值,这里是8个字节
union isa_t{// isa指针所属的结构体
Class cls;
// isa指针的信息都存储在bits中,通过bits & 下面的ISA_MASK等取出指针中各个位存储的信息值
uintptr_t bits;
// bits & ISA_MASK得到isa指针指向对象的地址,查看下面位域中bits各个位的存储内容,可知bits & ISA_MASK得到的结果后三位一定是0,
// 所以ios中,打印类对象,元类对象的地址,16位显示时,会发现最后一个字符总是0或者8.
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
// 以下是arm64架构下的存储方式,其他如模拟器,mac电脑环境不是这样子的
// 位域,从右往左表示,这里表示的是bits这个64字节中字节的存储内容
struct {
// 是否是一个优化过的pointer,1表示是,指针内部存储了更多信息。64位系统都是1
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1; // 是否曾经有关联对象
uintptr_t has_cxx_dtor : 1;// 是否有c++的析构函数
uintptr_t shiftcls : 33; // 类(元类)对象的地址
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1; // 是否曾经有弱引用
uintptr_t deallocating : 1; // 是否正在释放
// 引用计数器太大,extra_rc存储不下则会存储到一个全局哈希表sidetable中,此时这个标志位为1
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19; // 引用计数器-1的值
};
}
struct objc_class : objc_object {// 类对象的结构,isa在c++父类中
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits;
}
struct class_data_bits_t {
uintptr_t bits; // 指针值,各个位存储的内容在以FAST_做标记的宏定义中,如FAST_ALLOC_MASK
class_rw_t* data() {
// bits& FAST_DATA_MASK,取出指针中指定位的值,得到class_rw_t的地址
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData) {
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
}
struct class_rw_t {// rw表示可读写,ro为只读
uint32_t flags; // 指针值,各个位存储的内容在以RW_做标记的宏定义中,如RW_LOADED
uint32_t version; // 这个runtime版本,isMeta是7,否则是0
const class_ro_t *ro; // 只读表
//二维数组 method_array_t:[method_list_t, method_list_t], method_list_t:[method_t,method_t]
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
}
//只读表,程序加载,类的最初信息(不包括分类信息)就存储在这里,后期不再变动
struct class_ro_t {
uint32_t flags; //指针值,各个位存储的内容在以RO_做标记的宏定义中,如RO_META
uint32_t instanceStart;
uint32_t instanceSize;
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList; // 方法列表,这只是个一维数组
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 成员变量存储在这里,进一步证明为何类无法动态添加成员变量
};
类中的信息,最初都是加载在class_ro_t表中的,这张表只读,不能更改。程序启动运行时将class_ro_t表中的信息加载到类的class_rw_t等地方。
objc-os.mm中的_objc_init->map_images->map_images_nolock->_read_images->realizeClass
static Class realizeClass(Class cls){
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
// 刚开始,类对象中的cls->data()指向的是class_ro_t表,里面存放着类最初的信息(不包含分类等添加进去的内容)
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {// class_rw_t表还不存在
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);// 创建class_rw_t表
rw->ro = ro; // 让class_rw_t表的ro指向最初的class_ro_t表
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw); // 将类对象中的cls->data()指向class_rw_t表,注意,这里修改了指向,最初是指向的class_ro_t表
}
isMeta = ro->flags & RO_META;
rw->version = isMeta ? 7 : 0; // old runtime went up to 6
cls->setInstanceSize(ro->instanceSize);// ro表的大小赋值给cls表的成员变量
// 将ro表中的方法,协议等添加到rw表中,并且将分类信息追加到rw表中
methodizeClass(cls);
return cls;
}
- Method Swizzling
+ (void)load {
Method fromMethod = class_getInstanceMethod([UIViewController class], @selector(viewDidLoad));
Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}
- (void)swizzlingViewDidLoad {
[self swizzlingViewDidLoad];
NSLog(@"222");
}
标准的Method Swizzling如上,为何需要判断class_addMethod?
先看class_getInstanceMethod的实现
Method class_getInstanceMethod(Class cls, SEL sel){
return _class_getMethod(cls, sel);
}
static Method _class_getMethod(Class cls, SEL sel){
return getMethod_nolock(cls, sel);
}
static method_t *getMethod_nolock(Class cls, SEL sel){
method_t *m = nil;
// getMethodNoSuper_nolock是在cls的rw表方法列表中找出sel的实现
// 遍历当前类和父类,直到找到对应的方法实现
while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
cls = cls->superclass;
}
return m;
}
可知class_getInstanceMethod获取到的是当前类或者父类的方法实现。如果直接进行方法交换,可能将父类的方法实现跟当前类的方法实现给交换了。
再来瞧瞧class_addMethod
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types){
return ! addMethod(cls, name, imp, types ?: "", NO);
}
static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace){
IMP result = nil;
method_t *m;
// getMethodNoSuper_nolock,在当前类rw表的方法列表中查找方法
// 注意:只在当前类对象中查找方法,父类不管
if ((m = getMethodNoSuper_nolock(cls, name))) {
// 进入这个条件,说明动态添加的方法在类中已经存在
if (!replace) {
result = m->imp;
} else {
result = _method_setImplementation(cls, m, imp);
}
} else {
// 当前类对象中不存在该方法,添加。
method_list_t *newlist;
newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
newlist->entsizeAndFlags =
(uint32_t)sizeof(method_t) | fixed_up_method_list;
newlist->count = 1;
newlist->first.name = name;
newlist->first.types = strdupIfMutable(types);
newlist->first.imp = imp;
prepareMethodLists(cls, &newlist, 1, NO, NO);
// 往类的rw表中的方法列表methods中添加数据,添加后methods的数据结构是[动态添加方法的method_list_t,分类的method_list_t,类本身的method_list_t]
cls->data()->methods.attachLists(&newlist, 1);
// 清空类中的方法缓存
flushCaches(cls);
result = nil;
}
return result;
}
该方法给类添加方法时,只会判断当前类是否已经存在了该方法,如果有则什么也不做,返回nil。要注意,如果父类存在方法A,而子类不存在,对子类动态添加A是能成功的,这样,给子类再发送A消息时,找到的是子类的A方法。
void method_exchangeImplementations(Method m1, Method m2){
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
flushCaches(nil);
}
注意到,method_exchangeImplementations,class_addMethod,_method_setImplementation等方法中都执行了类方法缓存清空操作flushCaches,这是为了避免原先方法已经在缓存中存在,下次发送消息时找到的是缓存中的旧方法,而不是最新的替换过的方法。至于class_addMethod时也要flushCaches,举个例子,子类缓存了父类的方法A,而子类又动态添加了A方法,为了避免缓存找到旧的父类的A,只能清空缓存。
12 RunLoop
原理详解:https://blog.ibireme.com/2015/05/18/runloop/
struct __CFRunLoop {
pthread_t _pthread; // runloop对应的线程,一一对应
CFMutableSetRef _commonModes;
CFMutableSetRef
CFRunLoopModeRef _currentMode; // 当前runloop运行的mode
CFMutableSetRef _modes; // 所有可用的mode
};
_commonModes:common标记的mode,包括UITrackingRunLoopMode和NSDefaultRunLoopMode
_commonModeItems:运行在_commonModes环境下的item,例如NSTimer添加在NSRunLoopCommonModes中,则该NSTimer会在_commonModeItems内
RunLoop同一时刻只能运行在一个Mode下,切换Mode时,必须先退出当前RunLoop,再重新进入另一个Mode。用observer能观察到,UIScrollView滚动时,RunLoop发出退出NSDefaultRunLoopMode通知,并进入UITrackingRunLoopMode通知。
RunLoop添加内容启动后,线程就不会再销毁,这会导致内存泄露问题,需要在合适时机让RunLoop退出。
RunLoop启动有好几种方式,例如
CFRunLoopRun();
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
需要注意的是,run方法,苹果文档描述如下: run方法相当于
while (1) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]
}
显然无法退出RunLoop,就算通过移除RunLoop中的所有sources,timers,ports也不行。如果想使RunLoop能退出,不能采用[[NSRunLoop currentRunLoop] run]方式,要采用如下方式:
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
根据文档,设计了如下具体实现方式
// 开启runloop
- (void)strat{
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[MJThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
NSRunLoop *rl = [NSRunLoop currentRunLoop];
while (weakSelf && !weakSelf.isStoped) {
// 文档解释:Runs the loop once
// 这个方法只能执行一次runloop循环中那些方法就会退出
// 要想保持runloop不退出,需要while循环来保证
// 线程启动,线程会暂停在这里面,直到被外界唤醒,或者手动退出,函数返回
[rl runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.thread start];
}
// 停止runloop
- (void)stop {
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)stopThread{
self.stopped = YES;
// 停止当前的这一次RunLoop循环,就是让runMode: beforeDate:方法返回
CFRunLoopStop(CFRunLoopGetCurrent());
}
如果是用CFRunLoopRun()方法启动RunLoop,则可直接调用CFRunLoopStop(CFRunLoopGetCurrent())停止,而不必像上面那样设置外部变量self.stopped控制。
原理追查,CFRunLoopRun()的实现是
void CFRunLoopRun(void) {
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
一旦调用CFRunLoopStop,会唤醒runloop,设置返回结果result为kCFRunLoopRunStopped结束当然runloop。
而runMode: beforeDate:的底层是
// 参数returnAfterSourceHandled为YES,意思是当前runloop处理完source事件就会退出
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
可知,oc层面若要用runMode: beforeDate:方法启动runloop,需要自己包装一层while循环。run方法就是苹果给我们添加的包装函数,但是该方法会导致无法退出runloop。
跟runloop相关联的有Timer,performSelecter:afterDelay: (同Timer),AutoreleasePool,页面更新(设置frame,setneeddisplay等会将对应view做标记,注册监听runloop进入休眠时进行页面更新操作),事件响应,NSURLConnection回调,常驻线程等
13 Dealloc流程
源码在NSObject.mm中,dealloc->_objc_rootDealloc->rootDealloc
inline void objc_object::rootDealloc(){
if (isTaggedPointer()) return; // TaggedPointer并非OC对象
// isa是优化过的指针,且没有弱引用,关联对象等等,直接释放
if (fastpath(isa.nonpointer && !isa.weakly_referenced &&
!isa.has_assoc && !isa.has_cxx_dtor &&
!isa.has_sidetable_rc)) {
free(this);
} else {
object_dispose((id)this);
}
}
id object_dispose(id obj){
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
void *objc_destructInstance(id obj) {
if (obj) {
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
if (cxx) object_cxxDestruct(obj); // 有c++析构函数,执行
if (assoc) _object_remove_assocations(obj);// 删除关联表中该对象的关联item
obj->clearDeallocating(); // 清空该对象可能存在的弱引用指针
}
return obj;
}
inline void objc_object::clearDeallocating(){
if (slowpath(!isa.nonpointer)) {// 非优化过的isa指针
sidetable_clearDeallocating(); // 清空weak指针
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
clearDeallocating_slow();
}
}
void objc_object::clearDeallocating_slow(){
SideTable& table = SideTables()[this];
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)this); // 清空weak指针
}
if (isa.has_sidetable_rc) {
table.refcnts.erase(this); // 擦除SideTable中引用计数
}
}
// referent_id:即将被销毁的对象
void weak_clear_no_lock(weak_table_t *weak_table, id referent_id){
objc_object *referent = (objc_object *)referent_id;
// 根据开放定址算法,找出当前对象在weak_table中存储位置地址
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
// weak_entry_t中所有指向该对象的弱指针,该指针是weak_referrer_t类型
weak_referrer_t *referrers = entry->referrers;
size_t count = TABLE_SIZE(entry);
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {// 遍历,清空所有的weak_referrer_t指针
if (*referrer == referent) {
*referrer = nil;
}
}
}
// weak_table表中删除该对象所对应的item
weak_entry_remove(weak_table, entry);
}
对象的弱引用指针,以及引用计数存储如下结构中
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
refcnts:引用计数值,对象isa为普通指针,或者是优化过的isa,但是isa内部存储不下时使用
weak_table:对象所有关联的弱引用指针表
struct weak_table_t {// hash表,采用的是开放定址法处理hash碰撞
weak_entry_t *weak_entries; // 存储的元素是weak_entry_t
size_t num_entries; // 表中实际存储的元素个数
uintptr_t mask; // 表的长度-1,用来做key的hash运算
uintptr_t max_hash_displacement;
};
对象的弱引用指针存储在hash表SideTable中,以对象地址为key,根据hash算法计算出在hash表中的索引,存储元素是当前对象的所有弱引用指针数组。
14 锁
https://juejin.im/post/57f6e9f85bbb50005b126e5f
cpu时间片轮转算法,就是轮流分配cpu给多个线程,线程优先级高,会分配更多的时间或者更多几率。
锁按照线程等待时是否休眠可以分为自旋锁和互斥锁。
自旋锁: 等待线程处于while循环状态,消耗cpu资源,一旦解锁,马上执行后面代码,效率高。ios10过期,因为存在优先级反转问题。就是例如一段代码,低优先级的线程进行加锁,而高优先级的线程处于自旋等待状态,因为优先级高,cpu分配时间片轮多,低优先级的线程可能无法执行完后续代码,导致锁迟迟无法释放。自旋锁只有一个,就是OSSpinLock,已过期
互斥锁: 等待线程处于休眠状态,cpu处于睡眠状态,消耗低,当锁解除线程唤醒时效率较低。除了自旋锁,其他的都是互斥锁。
下面是互斥锁中几个特例
递归锁:允许同一个线程对一把锁重复加锁,通常用在递归方法之中
信号量dispatch_semaphore: 控制一段代码的最大线程访问量。初始值设置为1时就是一个互斥锁。
@synchronized 是对递归锁的封装
锁性能对比,从上到下
os_unfair_lock:ios10才支持,OSSpinLock的替代品
OSSpinLock: ios10过期,自旋锁,性能高
dispatch_semaphore:
pthread_mutex: 通过属性设置为普通锁dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock: pthread_mutex的封装
NSCondition:
pthread_mutex(recursive): 通过属性设置为递归锁
NSRecursiveLock: pthread_mutex(recursive)的封装
NSConditionLock:
@synchronized:
对数据库,IO等,最好实现多读单写,可以采用dispatch_barrier_async或者pthread_rwlock
锁的底层调用方法,可以用汇编调试,查看底层调用方法栈。si是一行一行执行汇编代码指令。再到objc源码中查看
14 autorelease
https://www.jianshu.com/p/677687ffff73
https://www.jianshu.com/p/cc3ee2909457
15 dispatch
dispatch_sync不会产生线程,往当前串行队列添加任务,会产生死锁。死锁原因是串行队列一定要等前面的任务执行完成,后面的任务才能执行,进入相互等待。
dispatch_async只有在主队列不会产生线程,其他的都会产生线程,传入串行队列只会产生一个线程,并发队列会产生多个线程