# Runtime > 做很多技术项目或者业务项目、再或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写、结合一些场景和源码分析来系统化学习。 > > 2个问题: > > - OC 的消息机制是什么样的? > - OC 中 super 底层是什么?[super superclass]、[self superclass] 打印结果是什么 > > 带着问题学习本文 ## 动态语言 静态语言:在编译阶段确定了变量数据类型、函数地址等,无法动态修改。 动态语言:只有在运行的时候才可以决定变量属于什么类型、方法真正的地址。 OC 是一门动态性语言,Runtime 是实现 OC 语言动态的 API。 ```objectivec @interface Person : NSObject { NSString *_name; } @property (nonatomic, strong) NSString *hobby; @end malloc_size((__bridge const void *)(p)) // 24 isa占8字节 + _name 指针占8字节 + hobby 指针占8字节 = 24 class_getInstanceSize(p.class) // 32 ,系统内存对齐 ``` QA:为什么系统是由16字节对齐的? 成员变量占用 8 字节对齐,每个对象的第一个都是 isa 指针,必须要占用8字节。举例一个极端 case,假设 n 个对象,其中 m 个对象没有成员变量,只有 isa 指针占用8字节,其中的 n-m个对象既有 isa 指针,又有成员变量。每个类交错排列,那么 CPU 在访问对象的时候会耗费大量时间去识别具体的对象。很多时候会取舍,这个 case 就是时间换空间。以16字节对齐,会加快访问速度(参考链表和数组的设计) 接下去深入探索下 runtime。 ## 使用较少内存存储属性值 ### 位运算的设计 假设给 Person 对象设置高、富、帅3个属性,如果要用最少的内存,该怎么设计呢? 先看几组位运算的特点: ```shell // 如何把最右边的0变为1,其他位不变???与 00000001 按位或即可 0010 1000 0000 0001 ----------- 0010 1001 // 如何把最右边的1变位0,其他位不变???与 11111110 按位与即可。也就是将(0000 0001)取反,再按位与即可 0010 0001 1111 1110 ----------- 0010 1110 ``` 也就是说,我们可以根据位运算的特点,可以更改某个位的数据,可以随意更改为0或者1 Person 类存在3个 BOOL 属性: - 用一个 char 来存储 tall、rich、handsome 3个属性的值,用3个位的0、1表示 BOOL - 从最右到左的3位代表高富帅3个布尔值。只有高则表示为:`0000 0001`,只有富则表示为 `0000 0010` ,只有帅则表示为 `0000 0100` - 对高的 getter 问题转换为对一个字节数据的特定位取值问题。该 case,判断高的 BOOL 值变为,对最后一位的取值。可以与 `0000 0001` 按位与,然后转换为 BOOL 即可 - 对高的 setter ,则演变为对一个字节数据的特点位存值的问题。需要区分真假,如果为真,则可以与 `0000 0001` 按位或运算,或运算,一真为真。如果是假,则与 `1111 1110` 按位与即可,`1111 1110` 也就是 `0000 0001` 取反,也就是 `~0000 0001` 上 Demo ### 结构体位域 位运算方案虽然实现了使用较少内存存储了 Person 的3个 BOOL 属性值。但是后续增加属性不够灵活,需要关心位运算,不具备较好拓展性 新方案采用:**结构体的位域能力**,限定单个成员变量所占用的内存。代码如下: ### 共用体 虽然上述方式都可以实现存储 Person 类3个属性的目的,但是还有第三种方案,参考 iOS 系统设计,采用 Union 实现。代码如下 分析: ```c++ union { char bits; struct { char tall : 1; char rich : 1; char handsome : 1; }; } _infoUnit; ``` Union 里面的内容等价于下面的内容,因为 struct 使用了位域限制了成员变量的大小,所以占用3个空间的结构体还是小于等于 char 所占用的空间。 ```c++ union { char bits; } _infoUnit; ``` 但是增加下面的结构体,是为了增加代码的可读性,内存无负向影响。 ### 位运算设计 API 系统很多 API 都有位或运算。比如 KVO 中的 options,可以传递 `NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld` ,那么系统是如何知道到底传递了哪几个值?搞清楚这个问题,我们就也可以设计位运算这样的 API。 先看看:按位或运算 ```shell 0b0000 0001 // 1 0b0000 0010 // 2 0b0000 0100 // 4 ------------ 0b0000 0111 // 7 ``` 可以看到上面3个数,按位或之后的结果为 `0b0000 0111` 按位与运算。 ```shell 0b0000 0111 0b0000 0001 ----------- 0b0000 0001 0b0000 0111 0b0000 0010 ----------- 0b0000 0010 0b0000 0111 0b0000 0100 ----------- 0b0000 0100 0b0000 0111 0b0000 1000 ----------- 0b0000 0000 ``` 我们发现上面3个数**按位或之后的数字,分别与每个数按位与,得到的结果就是数据本身**。 与一个不是3个数之一的数按位与,得到的结果为`0b0000 0000`。利用这个特性我们可以判断传递来的参数是不是包含了某个值 有了上面的铺垫,就可以更好的查看 runtime 中 isa 的定义了。 ## Runtime 源码阅读 ### isa 本质 - 在 arm64 架构之前,isa 就是一个普通的指针,存储着 Class 或 Meta-Class 对象的内存地址。 - 在 arm64 之后,对 isa 进行了优化,变成了一个共用体(union)结构,还使用了位域技术来存储更多的信息(位运算) ```c++ union isa_t { // ... uintptr_t bits; struct { ISA_BITFIELD; // defined in isa.h }; // ... }; ``` 跳转到 `isa.h` 中查看 isa 里结构体的定义(arm64 系统为例,源码是 objc4-838.1 版本),笔者将无用的代码删除了,isa 的 union 内容如下: ```c++ union isa_t { 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 unused : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 19 }; }; ``` struct 内部的成员变量可以指定占用内存位数, `uintptr_t nonpointer : 1` 代表占用1个字节,是结构体里面的 **位域** - nonpointer:0,代表普通的指针,存储着 class、meta-class 对象的内存地址;1,代表优化过,使用位域存储更多的信息 - has_assoc:是否有设置过关联对象,如果没有,释放时会更快 - has_cxx_dtor:是否有 c++ 的析构函数(`.cxx_destruct`),如果没有,释放时会更快 - shiftcls:存储着 class、meta-class 对象的内存地址信息 - magic:用于在调试时分辨对象是否未完成初始化 - weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快 - unused(deallocating):对象是否正在释放 - extra_rc:里面存储的值是引用计数器减 1(刚创建出的对象,查看这个信息位 0,因为存储着 -1 之后的引用计数) - has_sidetable_rc:引用计数器是否过大无法存储在 isa 中;如果为1,那么引用计数会存储在一个叫 SideTable 的类的属性中 上面说的更快,是如何得出结论的? 查看 objc4 源代码看到对象执行销毁函数的时候会判断对象是否有关联对象、析构函数,有的话分别调用析构函数、移除关联对象等逻辑。 ```c++ /*********************************************************************** * objc_destructInstance * Destroys an instance without freeing memory. * Calls C++ destructors. * Calls ARC ivar cleanup. * Removes associative references. * Returns `obj`. Does nothing if `obj` is nil. **********************************************************************/ void *objc_destructInstance(id obj) { if (obj) { // Read all of the flags at once for performance. bool cxx = obj->hasCxxDtor(); bool assoc = obj->hasAssociatedObjects(); // This order is important. // 存在析构函数则执行析构函数 if (cxx) object_cxxDestruct(obj); // 存在关联对象,则移除关联对象 if (assoc) _object_remove_assocations(obj); // 执行销毁逻辑 obj->clearDeallocating(); } return obj; } ``` isa 在 arm64 之后必须通过 `ISA_MASK` 去查询 class(类对象、元类对象) 真正的地址 `0x0000000ffffffff8ULL` 用程序员模式打开计算器 其中,结构体中的数据存放大体是下面的结构: extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer 知道结构体可以指定存储大小这个功能后,可以看到 `isa_t` 联合体与 `ISA_MASK` 按位与之后的地址,其实就是类真实的地址信息(可能是类对象、也有可能是元类对象) 如果要找出下面中间的 `1010` 如何实现?按位与即可,且要找的位置补充位1,其他位置为0 ```shell 0b0010 1000 0b0011 1100 ----------- 0b0010 1000 ``` 结论:**根据按位与的效果。`ISA_MASK` 的后3位都是0,所以我们找到的类地址二进制表示时后3位一定为0** 我们可以验证下 ```objectivec Person *p = [[Person alloc] init]; NSLog(@"%p", [p class]); // 0x1000081d8 NSLog(@"%p", object_getClass([Person class])); // 0x100008200 NSLog(@"%p", object_getClass([NSObject class])); // 0x7ff84cb29fe0 NSLog(@"%p", object_getClass([NSString class])); // 0x7ff84c9dcc28 ``` 为什么有的结尾是8?16进制的8转为二进制,`0x1000` 关于这部分的调试,需要在真机上运行,真机上 arm64,拷贝对象地址到系统自带的运算器(程序员模式),查看64位地址。按照下面的顺序一一查看 `extra_rc、has_sidetable_rc、deallocating、weakly_referenced、magic、shiftcls、has_has_cxx_dtor、assoc、nonpointer` 所以可以根据 isa 信息查看对象是否创建过关联对象、有没有设置弱引用、 QA:如何理解 isa 指针? isa 指针,在 arm64 之前,isa 指针就是普通的对象,存储着类对象或元类对象的地址值。 从 arm64 开始,采用共用体 union 的形式,共64位,存储了很多信息,其中33位用来存储 class、meta-class 对象的内存地址信息,其他的31位用来存储其他信息,比如 `has_assoc` 占1位,其代表是否有设置过关联对象,如果没有,释放时会更快。 有类对象、为什么设计元类对象? 复用消息机制。比如 `[Person new]`。元类对象:isa、元类方法。 `objc_msgSend` 设计初衷就是为了消息发送很快。假如没有元类,则类方法也存储在类对象的方法信息中,则可能需要加额外的字段来标记某个方法是类方法还是对象方法。遍历或者寻找会比较慢。所以引入元类(单一职责),设计元类的目的就是为了提高 `objc_msgSend` 的效率。 ### 类对象 Class 的结构 查看 objc4 源代码看看 ```c struct objc_object { isa_t isa; } struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags }; ``` 结构体继承于 `objc_object` 等同于下面代码 ```c struct objc_class : objc_object { isa_t isa; Class superclass; cache_t cache; // 方法缓存 class_data_bits_t bits; // 用于获取具体的类信息 }; ``` ```c struct class_rw_t { // Be warned that Symbolication knows the layout of this structure. uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; // 方法列表 property_array_t properties; // 属性列表 protocol_array_t protocols; // 协议列表 Class firstSubclass; Class nextSiblingClass; char *demangledName; }; struct class_data_bits_t { // Values are the FAST_ flags above. uintptr_t bits; public: class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); } } ``` 可以看到 `objc_class` 获取 bits 里的真实数据需要经过按位与 `FAST_DATA_MASK` ```c 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; method_list_t *baseMethods() const { return baseMethodList; } }; ``` 具体关系整理如下图 源码解读: - 元类对象可以看成是特殊的类对象,数据类型都是 `Class`,所以大部分数据结构都一样,区别在于某些值是否有值 - `class_rw_t`:里面的 methods、properties、protocols 是数组(`method_array_t` -> `method_list_t` -> `method_t`),是可读可写的,包含了类的初始内容、分类的内容。 比如访问 method 的过程 ```objectivec // 新版 const method_array_t methods() const { auto v = get_ro_or_rwe(); if (v.is()) { return v.get(&ro_or_rw_ext)->methods; } else { return method_array_t{v.get(&ro_or_rw_ext)->baseMethods}; } } ``` - `class_ro_t` 里面的 baseMethodList、baseProtocols、ivars、baseProperties 是一维数组,是只读的,包含了类的(原始信息)初始内容 QA: 1. `method_array_t` 中存储了 `method_list_t`,`method_list_t` 的元素为 `method_list_t`, 为什么不直接称它为二维数组? 严格来说,不是二维数组,只不过是 array 里添加的对象也是 array,且各个数组不等长,也不会补空。 2. 为什么需要设计为这样的结构? 调用方法,比如调用 load 是 runtime 加载的时候找到方法地址直接调用的,普通方法走的是消息机制。首先判断是对象方法还是类方法,然后根据 isa 找类对象(对象方法)和元类对象(对象方法)信息中先从方法缓存中查找方法是否有缓存(方法缓存查找的过程是:先根据方法的 SEL,SEL 本质上是一个指向方法名的 C 字符串指针,将其转换为 uintptr_t,然后和方法缓存哈希表的 MASK 进行按位与,MASK 值初始为 `1<isInitialized()) return; // Make sure the entry wasn't added to the cache by some other thread // before we grabbed the cacheUpdateLock. if (cache_getImp(cls, sel)) return; cache_t *cache = getCache(cls); cache_key_t key = getKey(sel); // Use the cache as-is if it is less than 3/4 full mask_t newOccupied = cache->occupied() + 1; mask_t capacity = cache->capacity(); if (cache->isConstantEmptyCache()) { // Cache is read-only. Replace it. // 此处传入的第二个参数为 INIT_CACHE_SIZE cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); } else if (newOccupied <= capacity / 4 * 3) { // Cache is less than 3/4 full. Use it as-is. } else { // Cache is too full. Expand it. cache->expand(); } // Scan for the first unused slot and insert there. // There is guaranteed to be an empty slot because the // minimum size is 4 and we resized at 3/4 full. bucket_t *bucket = cache->find(key, receiver); if (bucket->key() == 0) cache->incrementOccupied(); bucket->set(key, imp); } void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { bool freeOld = canBeFreed(); bucket_t *oldBuckets = buckets(); bucket_t *newBuckets = allocateBuckets(newCapacity); // Cache's old contents are not propagated. // This is thought to save cache memory at the cost of extra cache fills. // fixme re-measure this assert(newCapacity > 0); assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1); // 设置默认的 Mask,为 1<filetype == MH_EXECUTE) { // Size some data structures based on main executable's size #if __OBJC2__ // If dyld3 optimized the main executable, then there shouldn't // be any selrefs needed in the dynamic map so we can just init // to a 0 sized map if ( !hi->hasPreoptimizedSelectors() ) { size_t count; _getObjc2SelectorRefs(hi, &count); selrefCount += count; _getObjc2MessageRefs(hi, &count); selrefCount += count; } #else _getObjcSelectorRefs(hi, &selrefCount); #endif #if SUPPORT_GC_COMPAT // Halt if this is a GC app. if (shouldRejectGCApp(hi)) { _objc_fatal_with_reason (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, OS_REASON_FLAG_CONSISTENT_FAILURE, "Objective-C garbage collection " "is no longer supported."); } #endif } hList[hCount++] = hi; if (PrintImages) { _objc_inform("IMAGES: loading image for %s%s%s%s%s\n", hi->fname(), mhdr->filetype == MH_BUNDLE ? " (bundle)" : "", hi->info()->isReplacement() ? " (replacement)" : "", hi->info()->hasCategoryClassProperties() ? " (has class properties)" : "", hi->info()->optimizedByDyld()?" (preoptimized)":""); } } } // Perform one-time runtime initialization that must be deferred until // the executable itself is found. This needs to be done before // further initialization. // (The executable may not be present in this infoList if the // executable does not contain Objective-C code but Objective-C // is dynamically loaded later. if (firstTime) { sel_init(selrefCount); arr_init(); #if SUPPORT_GC_COMPAT // Reject any GC images linked to the main executable. // We already rejected the app itself above. // Images loaded after launch will be rejected by dyld. for (uint32_t i = 0; i < hCount; i++) { auto hi = hList[i]; auto mh = hi->mhdr(); if (mh->filetype != MH_EXECUTE && shouldRejectGCImage(mh)) { _objc_fatal_with_reason (OBJC_EXIT_REASON_GC_NOT_SUPPORTED, OS_REASON_FLAG_CONSISTENT_FAILURE, "%s requires Objective-C garbage collection " "which is no longer supported.", hi->fname()); } } #endif #if TARGET_OS_OSX // Disable +initialize fork safety if the app is too old (< 10.13). // Disable +initialize fork safety if the app has a // __DATA,__objc_fork_ok section. // if (!dyld_program_sdk_at_least(dyld_platform_version_macOS_10_13)) { // DisableInitializeForkSafety = true; // if (PrintInitializing) { // _objc_inform("INITIALIZE: disabling +initialize fork " // "safety enforcement because the app is " // "too old.)"); // } // } for (uint32_t i = 0; i < hCount; i++) { auto hi = hList[i]; auto mh = hi->mhdr(); if (mh->filetype != MH_EXECUTE) continue; unsigned long size; if (getsectiondata(hi->mhdr(), "__DATA", "__objc_fork_ok", &size)) { DisableInitializeForkSafety = true; if (PrintInitializing) { _objc_inform("INITIALIZE: disabling +initialize fork " "safety enforcement because the app has " "a __DATA,__objc_fork_ok section"); } } break; // assume only one MH_EXECUTE image } #endif } if (hCount > 0) { _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses); } firstTime = NO; // Call image load funcs after everything is set up. for (auto func : loadImageFuncs) { for (uint32_t i = 0; i < mhCount; i++) { func(mhdrs[i]); } } } ``` 最后会调用 ```c++ void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) { header_info *hi; uint32_t hIndex; size_t count; size_t i; Class *resolvedFutureClasses = nil; size_t resolvedFutureClassCount = 0; static bool doneOnce; bool launchTime = NO; TimeLogger ts(PrintImageTimes); runtimeLock.assertLocked(); #define EACH_HEADER \ hIndex = 0; \ hIndex < hCount && (hi = hList[hIndex]); \ hIndex++ if (!doneOnce) { doneOnce = YES; launchTime = YES; #if SUPPORT_NONPOINTER_ISA // Disable non-pointer isa under some conditions. # if SUPPORT_INDEXED_ISA // Disable nonpointer isa if any image contains old Swift code for (EACH_HEADER) { if (hi->info()->containsSwift() && hi->info()->swiftUnstableVersion() < objc_image_info::SwiftVersion3) { DisableNonpointerIsa = true; if (PrintRawIsa) { _objc_inform("RAW ISA: disabling non-pointer isa because " "the app or a framework contains Swift code " "older than Swift 3.0"); } break; } } # endif # if TARGET_OS_OSX // Disable non-pointer isa if the app is too old // (linked before OS X 10.11) // Note: we must check for macOS, because Catalyst and Almond apps // return false for a Mac SDK check! rdar://78225780 // if (dyld_get_active_platform() == PLATFORM_MACOS && !dyld_program_sdk_at_least(dyld_platform_version_macOS_10_11)) { // DisableNonpointerIsa = true; // if (PrintRawIsa) { // _objc_inform("RAW ISA: disabling non-pointer isa because " // "the app is too old."); // } // } // Disable non-pointer isa if the app has a __DATA,__objc_rawisa section // New apps that load old extensions may need this. for (EACH_HEADER) { if (hi->mhdr()->filetype != MH_EXECUTE) continue; unsigned long size; if (getsectiondata(hi->mhdr(), "__DATA", "__objc_rawisa", &size)) { DisableNonpointerIsa = true; if (PrintRawIsa) { _objc_inform("RAW ISA: disabling non-pointer isa because " "the app has a __DATA,__objc_rawisa section"); } } break; // assume only one MH_EXECUTE image } # endif #endif if (DisableTaggedPointers) { disableTaggedPointers(); } initializeTaggedPointerObfuscator(); if (PrintConnecting) { _objc_inform("CLASS: found %d classes during launch", totalClasses); } // namedClasses // Preoptimized classes don't go in this table. // 4/3 is NXMapTable's load factor int namedClassesSize = (isPreoptimized() ? unoptimizedTotalClasses : totalClasses) * 4 / 3; gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize); ts.log("IMAGE TIMES: first time tasks"); } // Fix up @selector references // Note this has to be before anyone uses a method list, as relative method // lists point to selRefs, and assume they are already fixed up (uniqued). static size_t UnfixedSelectors; { mutex_locker_t lock(selLock); for (EACH_HEADER) { if (hi->hasPreoptimizedSelectors()) continue; bool isBundle = hi->isBundle(); SEL *sels = _getObjc2SelectorRefs(hi, &count); UnfixedSelectors += count; for (i = 0; i < count; i++) { const char *name = sel_cname(sels[i]); SEL sel = sel_registerNameNoLock(name, isBundle); if (sels[i] != sel) { sels[i] = sel; } } } } ts.log("IMAGE TIMES: fix up selector references"); // Discover classes. Fix up unresolved future classes. Mark bundle classes. bool hasDyldRoots = dyld_shared_cache_some_image_overridden(); for (EACH_HEADER) { if (! mustReadClasses(hi, hasDyldRoots)) { // Image is sufficiently optimized that we need not call readClass() continue; } classref_t const *classlist = _getObjc2ClassList(hi, &count); bool headerIsBundle = hi->isBundle(); bool headerIsPreoptimized = hi->hasPreoptimizedClasses(); for (i = 0; i < count; i++) { Class cls = (Class)classlist[i]; Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized); if (newCls != cls && newCls) { // Class was moved but not deleted. Currently this occurs // only when the new class resolved a future class. // Non-lazily realize the class below. resolvedFutureClasses = (Class *) realloc(resolvedFutureClasses, (resolvedFutureClassCount+1) * sizeof(Class)); resolvedFutureClasses[resolvedFutureClassCount++] = newCls; } } } ts.log("IMAGE TIMES: discover classes"); // Fix up remapped classes // Class list and nonlazy class list remain unremapped. // Class refs and super refs are remapped for message dispatching. if (!noClassesRemapped()) { for (EACH_HEADER) { Class *classrefs = _getObjc2ClassRefs(hi, &count); for (i = 0; i < count; i++) { remapClassRef(&classrefs[i]); } // fixme why doesn't test future1 catch the absence of this? classrefs = _getObjc2SuperRefs(hi, &count); for (i = 0; i < count; i++) { remapClassRef(&classrefs[i]); } } } ts.log("IMAGE TIMES: remap classes"); #if SUPPORT_FIXUP // Fix up old objc_msgSend_fixup call sites for (EACH_HEADER) { message_ref_t *refs = _getObjc2MessageRefs(hi, &count); if (count == 0) continue; if (PrintVtables) { _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch " "call sites in %s", count, hi->fname()); } for (i = 0; i < count; i++) { fixupMessageRef(refs+i); } } ts.log("IMAGE TIMES: fix up objc_msgSend_fixup"); #endif // Discover protocols. Fix up protocol refs. for (EACH_HEADER) { extern objc_class OBJC_CLASS_$_Protocol; Class cls = (Class)&OBJC_CLASS_$_Protocol; ASSERT(cls); NXMapTable *protocol_map = protocols(); bool isPreoptimized = hi->hasPreoptimizedProtocols(); // Skip reading protocols if this is an image from the shared cache // and we support roots // Note, after launch we do need to walk the protocol as the protocol // in the shared cache is marked with isCanonical() and that may not // be true if some non-shared cache binary was chosen as the canonical // definition if (launchTime && isPreoptimized) { if (PrintProtocols) { _objc_inform("PROTOCOLS: Skipping reading protocols in image: %s", hi->fname()); } continue; } bool isBundle = hi->isBundle(); protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count); for (i = 0; i < count; i++) { readProtocol(protolist[i], cls, protocol_map, isPreoptimized, isBundle); } } ts.log("IMAGE TIMES: discover protocols"); // Fix up @protocol references // Preoptimized images may have the right // answer already but we don't know for sure. for (EACH_HEADER) { // At launch time, we know preoptimized image refs are pointing at the // shared cache definition of a protocol. We can skip the check on // launch, but have to visit @protocol refs for shared cache images // loaded later. if (launchTime && hi->isPreoptimized()) continue; protocol_t **protolist = _getObjc2ProtocolRefs(hi, &count); for (i = 0; i < count; i++) { remapProtocolRef(&protolist[i]); } } ts.log("IMAGE TIMES: fix up @protocol references"); // Discover categories. Only do this after the initial category // attachment has been done. For categories present at startup, // discovery is deferred until the first load_images call after // the call to _dyld_objc_notify_register completes. rdar://problem/53119145 if (didInitialAttachCategories) { for (EACH_HEADER) { load_categories_nolock(hi); } } ts.log("IMAGE TIMES: discover categories"); // Category discovery MUST BE Late to avoid potential races // when other threads call the new category code before // this thread finishes its fixups. // +load handled by prepare_load_methods() // Realize non-lazy classes (for +load methods and static instances) for (EACH_HEADER) { classref_t const *classlist = hi->nlclslist(&count); for (i = 0; i < count; i++) { Class cls = remapClass(classlist[i]); if (!cls) continue; addClassTableEntry(cls); if (cls->isSwiftStable()) { if (cls->swiftMetadataInitializer()) { _objc_fatal("Swift class %s with a metadata initializer " "is not allowed to be non-lazy", cls->nameForLogging()); } // fixme also disallow relocatable classes // We can't disallow all Swift classes because of // classes like Swift.__EmptyArrayStorage } realizeClassWithoutSwift(cls, nil); } } ts.log("IMAGE TIMES: realize non-lazy classes"); // Realize newly-resolved future classes, in case CF manipulates them if (resolvedFutureClasses) { for (i = 0; i < resolvedFutureClassCount; i++) { Class cls = resolvedFutureClasses[i]; if (cls->isSwiftStable()) { _objc_fatal("Swift class is not allowed to be future"); } realizeClassWithoutSwift(cls, nil); cls->setInstancesRequireRawIsaRecursively(false/*inherited*/); } free(resolvedFutureClasses); } ts.log("IMAGE TIMES: realize future classes"); if (DebugNonFragileIvars) { realizeAllClasses(); } // Print preoptimization statistics if (PrintPreopt) { static unsigned int PreoptTotalMethodLists; static unsigned int PreoptOptimizedMethodLists; static unsigned int PreoptTotalClasses; static unsigned int PreoptOptimizedClasses; for (EACH_HEADER) { if (hi->hasPreoptimizedSelectors()) { _objc_inform("PREOPTIMIZATION: honoring preoptimized selectors " "in %s", hi->fname()); } else if (hi->info()->optimizedByDyld()) { _objc_inform("PREOPTIMIZATION: IGNORING preoptimized selectors " "in %s", hi->fname()); } classref_t const *classlist = _getObjc2ClassList(hi, &count); for (i = 0; i < count; i++) { Class cls = remapClass(classlist[i]); if (!cls) continue; PreoptTotalClasses++; if (hi->hasPreoptimizedClasses()) { PreoptOptimizedClasses++; } const method_list_t *mlist; if ((mlist = cls->bits.safe_ro()->baseMethods)) { PreoptTotalMethodLists++; if (mlist->isFixedUp()) { PreoptOptimizedMethodLists++; } } if ((mlist = cls->ISA()->bits.safe_ro()->baseMethods)) { PreoptTotalMethodLists++; if (mlist->isFixedUp()) { PreoptOptimizedMethodLists++; } } } } _objc_inform("PREOPTIMIZATION: %zu selector references not " "pre-optimized", UnfixedSelectors); _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) method lists pre-sorted", PreoptOptimizedMethodLists, PreoptTotalMethodLists, PreoptTotalMethodLists ? 100.0*PreoptOptimizedMethodLists/PreoptTotalMethodLists : 0.0); _objc_inform("PREOPTIMIZATION: %u/%u (%.3g%%) classes pre-registered", PreoptOptimizedClasses, PreoptTotalClasses, PreoptTotalClasses ? 100.0*PreoptOptimizedClasses/PreoptTotalClasses : 0.0); _objc_inform("PREOPTIMIZATION: %zu protocol references not " "pre-optimized", UnfixedProtocolReferences); } #undef EACH_HEADER } ``` 核心相关代码 ```c++ /*********************************************************************** * realizeClassWithoutSwift * Performs first-time initialization on class cls, * including allocating its read-write data. * Does not perform any Swift-side initialization. * Returns the real class structure for the class. * Locking: runtimeLock must be write-locked by the caller **********************************************************************/ static Class realizeClassWithoutSwift(Class cls, Class previously) { // ... class_rw_t *rw; Class supercls; Class metacls; auto ro = (const class_ro_t *)cls->data(); auto isMeta = ro->flags & RO_META; if (ro->flags & RO_FUTURE) { // This was a future class. rw data is already allocated. rw = cls->data(); ro = cls->data()->ro(); ASSERT(!isMeta); cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE); } else { // Normal class. Allocate writeable class data. rw = objc::zalloc(); rw->set_ro(ro); rw->flags = RW_REALIZED|RW_REALIZING|isMeta; cls->setData(rw); } cls->cache.initializeToEmptyOrPreoptimizedInDisguise(); // ... } struct class_rw_ext_t { DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t) class_ro_t_authed_ptr ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; char *demangledName; uint32_t version; }; ``` 查看 objc4 源码,runtime 启动步骤可以知道: - `struct objc_class` 结构体中的 `bits` 一开始指向的是 `class_ro_t` 结构体,然后经过 runtime 的 `realizeClassWithoutSwift` 方法,将 `bits` 指向一个新创建的 `class_rw_t` 结构体,新创建的结构体中的成员变量 `ro` 指向类原本的 `class_ro_t`。也就是说 runtime 会对类自身信息和 Category 信息进行组合。 - `class_ro_t` 在编译时期生成的,`class_rw_t` 是在运行时期生成的。 那么什么是 `class_rw_ext_t`首先明确 2。个概念 - clean memory:加载后不会被修改。当系统内存紧张时,可以从内存中移除,需要时可以再次加载 - dirty memory:加载后会被修改,一直处于内存中 runtime 初始化的时候,遇到一个类,则会利用类的 `class_ro_t` 中的基础信息(methods、properties、protocols)来创建 `class_rw_t` 对象。`class_rw_t` 设计的目的就是为了 runtime 所需(Category 增加属性、协议、动态增加方法等),但是实际上写了很多的类,只有少部分类才需要 runtime 能力。所以 Apple 为了内存优化,在 iOS 14 对 `class_rw_t` 拆分出 `class_rw_ext_t`,用来存储 Methods、Protocols、Properties 信息,只有在使用的时候才创建,节省更多内存。 ## Runtime - 方法 ### Method_t `method_t` 是对方法、函数的封装 ```c++ struct method_t { // ... struct big { SEL name; // 函数名、方法名 const char *types; // 编码(返回值类型、参数类型) MethodListIMP imp; // 指向函数的指针(函数地址,给方法下断点的话,汇编模式的第一条指令的地址就是函数地址) }; struct small { RelativePointer name; RelativePointer types; RelativePointer imp; }; // ... } ``` `IMP` 代表函数的具体实现 ```c typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); ``` `SEL` 代表方法、函数名,一般叫做选择器,底层结构跟 `char *` 类似。本质上是一个指向方法名 C 字符串的指针。 ```c typedef struct objc_selector *SEL; ``` - 可以通过 `@selector()` 和 `sel_registerName()` 获得 - 可以通过 `sel_getName()` 和 `NSStringFromSelector()` 转成字符串 - 不同类中相同名字的方法,所对应的方法选择器是相同的。也就是 SEL 不具备唯一性,方法命名需要规范,否则 runtime 调用起来就会发生不符合预期的行为。 `types` 包含了函数返回值、参数编码的字符串。`返回值|参数1|参数2| ... | 参数n` ### Type Encoding iOS 中提供了一个叫做 `@encode` 的指令,可以将具体的类型表示成字符串编码 ```objective-c NSLog(@"%s", @encode(int)); NSLog(@"%s", @encode(id)); NSLog(@"%s", @encode(void)); NSLog(@"%s", @encode(SEL)); NSLog(@"%s", @encode(Person)); // console i @ v : {Person=#} ``` 可以对照下面的表格进行查看: ![](./../assets/runtime-method-encoding.png) ```objectivec - (int)calcuate:(int)baseHeight heigith:(float)height; ``` 比如这个方法的 `type encoding` 为 `i24@0:8i16f20` 解读下,上面的方法其实携带了2个基础参数,`(id)self _cmd:(SEL)_cmd` : - `i` 代表方法返回值为 int - `24` 代表参数共占24个字节大小。4个参数分别为 id 类型的 `self`、`SEL` 类型的 `_cmd`, int 类型的 age、float 类型的 height。8+8+4+4 共24个字节(id、SEL 都为指针,长度为8) - `@` 代表第一个参数为 id 类型,从第0个字节开始,即 self - `:`代表第二个参数为 SEL,从第8个字节开始 - `i` 代表第三个参数为 int,从第16个字节开始,占4个字节 - `f` 代表第四个参数为 float,从第20个字节开始 ### 方法缓存 `Class` 内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度 - 用于快速查找方法执行函数 - 是可增量拓展的哈希表结构 - 是**局部性原理**的最佳应用(一般调用方法的时候,并不会每个方法都调用,一般来说可能会调用某个类的某几个方法,这几个方法每调用过1次,就缓存起来,下次再去调用的时候,就省去了 runtime 中方法查找的流程,提高效率) ```c++ /// Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; struct objc_class : objc_object { // Class ISA; // ... Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; } ``` 调用方法的本质,比如说对象方法,先根据对象的 isa 找到类对象,在类对象的 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 methods 方法数组(Array 中的元素是方法 Array),假设某个类的方法调用 `[Worker live]`,需要通过 superclass 查找8次,每次都是在方法的“二维数组”里遍历。某个逻辑就需要调用 `[Worker live]`6次,每次需要8次二维数组的遍历,可想而知,效率低下。 所以为了方便,给类设置了**方法缓存**。比如调用 Worker 对象的 live 方法,通过 isa 找到元类对象,元类对象中不存在,则通过 superclass 找父类的元类对象, 不断找,假设经历了8次 superclass,最后在 Person 类中找到了,则将 Person 类中的 eat 方法缓存在 Worker 的 `cache_t` 类型的 cache 中。 所以完整结构为:先根据对象的 isa 找到类对象,在类对象的 cache 列表中查找方法实现,如果找不到,则去 `method_list_t` 类型的 methods 方法数组(Array 中的元素是方法 Array)中(类的Category1、类的 Category2... 类自身的方法)查找方法,找不到则调用 superclass 查找父类的 cache 中查找,找到则调用方法,同时将父类 cache 缓存中的方法,在子类的 cache 中缓存一边。父类 cache 没找到,则在 methods 方法数组(Array 中的元素是方法 Array)查找,找到则调用,同时在子类 cache 中缓存一份。父类 methods 方法数组(Array 中的元素是方法 Array)没找到则继续调用 superclass,依次类推 ```c struct cache_t { struct bucket_t *_buckets; // 散列表 -> | bucket_t |bucket_t |bucket_t |bucket_t |... mask_t _mask; // 散列表的长度 -1 mask_t _occupied; // 已经缓存的方法数量 } ``` ```c struct bucket_t { cache_key_t _key; // SEL 作为 key IMP _imp; // 函数的内存地址 } ``` 方法缓存查找原理,就是利用散列表(哈希表)查找。涉及:空间换时间,哈希表拓容策略,哈希碰撞算法。 objc4 源码 `objc-cache.mm` ```c++ typedef uintptr_t cache_key_t; // 根据 SEL 计算方法缓存 key 就说根据 SEL(方法名 C 字符串的指针)转换为一个用于存储指针的整数值(这个类型是一个无符号整数类型,uintptr_t 提供了一种方式来将指针转换为一个整数,以及将整数转换回指针) cache_key_t getKey(SEL sel) { assert(sel); return (cache_key_t)sel; } static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { cacheUpdateLock.assertLocked(); // Never cache before +initialize is done if (!cls->isInitialized()) return; // Make sure the entry wasn't added to the cache by some other thread // before we grabbed the cacheUpdateLock. if (cache_getImp(cls, sel)) return; // 获取方法缓存 cache_t cache_t *cache = getCache(cls); // 根据 SEL 计算方法缓存 key cache_key_t key = getKey(sel); // Use the cache as-is if it is less than 3/4 full mask_t newOccupied = cache->occupied() + 1; mask_t capacity = cache->capacity(); if (cache->isConstantEmptyCache()) { // Cache is read-only. Replace it. cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); } else if (newOccupied <= capacity / 4 * 3) { // Cache is less than 3/4 full. Use it as-is. } else { // Cache is too full. Expand it. cache->expand(); } // Scan for the first unused slot and insert there. // There is guaranteed to be an empty slot because the // minimum size is 4 and we resized at 3/4 full. bucket_t *bucket = cache->find(key, receiver); if (bucket->key() == 0) cache->incrementOccupied(); bucket->set(key, imp); } bucket_t * cache_t::find(cache_key_t k, id receiver) { assert(k != 0); bucket_t *b = buckets(); mask_t m = mask(); mask_t begin = cache_hash(k, m); // 该方法实现就是 k & mask,按位与 mask_t i = begin; do { if (b[i].key() == 0 || b[i].key() == k) { return &b[i]; } } while ((i = cache_next(i, m)) != begin); // hack Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)k, cls); } static inline mask_t cache_next(mask_t i, mask_t mask) { return i ? i-1 : mask; } ``` 可以看到在查找方法缓存的时候: - 首先根据缓存 key,利用 `cache_hash` 方法计算出一个 begin,赋值给 i - 然后利用 i 在方法缓存 `cache_t` 的 `buckets` 中查找,假设 key 相等,则直接返回对应的方法 - 如果没有找到则执行 `cache_next` 方法,该方法则会判断 i 是不是等于0,不等于0则自减1,等于0则设置为 mask 的值 - mask 值,在设计上等于 `buckets` 散列表的长度减1 散列表长度不够了,则会哈希拓容,此时之前存储的方法缓存则会被释放,执行 `cache_collect_free` ```c void cache_t::expand() { cacheUpdateLock.assertLocked(); uint32_t oldCapacity = capacity(); // 扩容,容量变为原来的2倍大小 uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; if ((uint32_t)(mask_t)newCapacity != newCapacity) { // mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); } void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { bool freeOld = canBeFreed(); bucket_t *oldBuckets = buckets(); bucket_t *newBuckets = allocateBuckets(newCapacity); // Cache's old contents are not propagated. // This is thought to save cache memory at the cost of extra cache fills. // fixme re-measure this assert(newCapacity > 0); assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1); setBucketsAndMask(newBuckets, newCapacity - 1); if (freeOld) { // 释放之前的方法缓存 cache_collect_free(oldBuckets, oldCapacity); cache_collect(false); } } ``` 哈希查找元素核心是一个求 key 的过程,Java 中是求余,iOS 中是按位与 `key & mask`。 ```c static inline mask_t cache_hash(cache_key_t key, mask_t mask) { return (mask_t)(key & mask); } ``` 空间换时间的一个实现。且按位与的特点,`(key & mask)` 的结果一定比 mask 值小。 查找类的方法缓存 Demo ```objective-c GoodStudent *goodStudent = [[GoodStudent alloc] init]; mock_objc_class *personClass = (__bridge mock_objc_class *)[GoodStudent class]; [goodStudent goodStudentSay]; // 断点1 [goodStudent studentSay]; // 断点2 [goodStudent personSay]; // 断点3 NSLog(@""); ``` 流程: 断点1的地方可以看到 `mock_objc_class` 结构体 `cache` 的 `_occupied` 为1,`_mask` 为3,初始化哈希表长度为4 在断点1的地方,`_occupied` 为1则代表只有 init 方法被缓存,本行代码执行完,`_occupied` 为2. 在断点2的地方,`_occupied` 为2则代表只有 init、goodStudentSay 方法被缓存。本行代码执行完,`_occupied` 为3 在断点3的地方,`_occupied` 为3则代表只有 init 、goodStudentSay 、studentSay方法被缓存。本行代码执行完,`_occupied` 为1,且 `_mask` 为7。 奇了怪了,为什么 `_occupied`为1,且`_mask` 为7? 因为哈希表长度为4,缓存3个方法后,到第4个方法需要缓存的时候会执行哈希表拓容,缓存会失效。拓容策略为乘以2 即 `uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;` 所以长度为8,mask 为`长度-1` ,则为7,第4个方法刚好被缓存下来,`_occupied` 为1。 ```c void cache_t::expand() { cacheUpdateLock.assertLocked(); uint32_t oldCapacity = capacity(); uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; if ((uint32_t)(mask_t)newCapacity != newCapacity) { // mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } reallocate(oldCapacity, newCapacity); } ``` 继续运行 在断点4的地方,`_occupied` 为1则代表只有 personSay方法被缓存。本行代码执行完,`_occupied` 为2,且 `_mask` 为7。 在断点5的地方,`_occupied` 为2则代表只有 personSay、goodStudentSay 方法被缓存。本行代码执行完,`_occupied` 为3,且 `_mask` 为7。 在断点6的地方,`_occupied` 为3则代表只有 personSay、goodStudentSay、studentSay 方法被缓存, `_mask` 为7。 #### 如何根据方法散列表查找某个方法 ```objectivec GoodStudent *goodStudent = [[GoodStudent alloc] init]; mock_objc_class *personClass = (__bridge mock_objc_class *)[GoodStudent class]; [goodStudent goodStudentSay]; [goodStudent studentSay]; [goodStudent personSay]; cache_t cache = personClass->cache; bucket_t *buckets = cache._buckets; // for (int i = 0; i <= cache._mask; i++) { // bucket_t bucket = buckets[i]; // NSLog(@"%s -- %p", (SEL)bucket._key, bucket._imp); // } bucket_t goodStudentSayBucket = buckets[(uintptr_t)@selector(personSay) & cache._mask]; NSLog(@"%s %p", goodStudentSayBucket._key, goodStudentSayBucket._imp); ``` 原理就是根据类对象结构体找到 cache 结构体,cache 结构体内部的 `_buckets` 是一个方法散列表,查看源代码,根据散列表的哈希寻找策略 `(key & mask)` 找到哈希索引,然后找到方法对象 bucket,其中寻找方法索引的 key 就是方法 selector。 ```c static inline mask_t cache_hash(cache_key_t key, mask_t mask) { return (mask_t)(key & mask); } ``` `[student live]`当调用对象方法的时候。会根据对象的 isa 指针,找到类对象,然后在类对象的方法缓存中查找有没有方法实现 - 如果找到了则立马执行 - 如果缓存中没有方法实现,则会在 class_rw_t 的 methods 里面查找有没有方法实现 - 如果找到了则将方法更新到方法缓存中去,然后立马执行(查找的过程则是根据 SEL 的方法名 C 字符串指针地址值与 _mask 按位与运算,然后在方法缓存的哈希表中查找 比如 `cache.buckets[@selector(methodName) & _mask]` ) - 如果此时还是找不到。则根据类对象的 superclass 指针查找父类的类对象。到父类的类对象这一步,也先从方法缓存中查找有没有方法缓存: - 如果找到了,则立马执行,同时将方法缓存到子类自己的缓存中去。下次调用,则直接在子类自己的方法缓存中查找即可 - 如果没找到,则继续在 class_rw_t 的 methods 中查找方法实现。 1. 如果找到了,则更新到子类类对象的方法缓存中,然后执行方法 2. 如果没找到,则继续沿着父类类对象的 superclass 指针,继续往上找,查找流程和上面的步骤类似 - 如果一直找到根类,还是找不到则开始走消息机制... ## Runtime - objc_msgSend ```c++ Person *p = [[Person alloc] init]; [p eat]; objc_msgSend(p, sel_registerName("eat")); [Person sayHi]; objc_msgSend(object_getClass("Person"), sel_registerName("sayHi")); ``` oc 方法(对象方法、类方法)调用本质就是 `objc_msgSend` `objc_msgSend` 可以分为3个阶段: - 消息发送 - 动态方法解析 - 消息转发 这3个阶段,还是没调用成功则抛出错误:`unrecognized selector sent to instance 0x600000ad0260` 查看源码 `objc-msg-arm64.s` ```shell ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame MESSENGER_START     // x0 寄存器代表消息接受者,receiver。objc_msgSend(person, sel_registerName("eat")) 的 person cmp x0, #0 // nil check and tagged pointer check // b 代表指令跳转。le 代表 小于等于。<=0则跳转到 LNilOrTagged b.le LNilOrTagged // (MSB tagged pointer looks negative) ldr x13, [x0] // x13 = isa // ldr 代表加载指令。这里的意思是将 x0 寄存器信息写入到 x13中 and x16, x13, #ISA_MASK // x16 = class // 这里就是将 x13 与 ISA_MASK 按位与,然后得到真实的 isa 信息,然后写入到 x16 中 LGetIsaDone: CacheLookup NORMAL // calls imp or objc_msgSend_uncached // 这里执行 objc_msgSend_uncached 逻辑,CacheLookup 是一个汇编宏,看下面的说明 LNilOrTagged: // 判断为 nil 则跳转到 LReturnZero b.eq LReturnZero // nil check // tagged mov x10, #0xf000000000000000 cmp x0, x10 b.hs LExtTag adrp x10, _objc_debug_taggedpointer_classes@PAGE add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF ubfx x11, x0, #60, #4 ldr x16, [x10, x11, LSL #3] b LGetIsaDone LExtTag: // ext tagged adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF ubfx x11, x0, #52, #8 ldr x16, [x10, x11, LSL #3] b LGetIsaDone LReturnZero: // x0 is already zero mov x1, #0 movi d0, #0 movi d1, #0 movi d2, #0 movi d3, #0 MESSENGER_END_NIL // 汇编中 ret 代表 return ret END_ENTRY _objc_msgSend .macro CacheLookup // 汇编宏,可以看到根据 (SEL & mask) 来寻找真正的方法地址 // x1 = SEL, x16 = isa ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask and w12, w1, w11 // x12 = _cmd & mask add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4) // 可以看到 cache.buckets[_cmd & mask] == SEL 就是在做方法缓存判断逻辑,看看有没有命中方法缓存 ldp x9, x17, [x12] // {x9, x17} = *bucket 1: cmp x9, x1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 2: // not hit: x12 = not-hit bucket CheckMiss $0 // miss if bucket->sel == 0 cmp x12, x10 // wrap if bucket == buckets b.eq 3f ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket b 1b // loop 3: // wrap: x12 = first bucket, w11 = mask add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4) // Clone scanning loop to miss instead of hang when cache is corrupt. // The slow path may detect any corruption and halt later. ldp x9, x17, [x12] // {x9, x17} = *bucket 1: cmp x9, x1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp // cachehit 就是命中缓存,则调用或者 return imp 2: // not hit: x12 = not-hit bucket // checkmiss,就是缓存没命中,也就是方法查找失败,则走 checkMiss 逻辑,具体看下面 CheckMiss $0 // miss if bucket->sel == 0,NORMAL cmp x12, x10 // wrap if bucket == buckets b.eq 3f ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket b 1b // loop 3: // double wrap JumpMiss $0 .endmacro // CheckMiss 汇编宏,上面传入了参数 Normal,内部走 __objc_msgSend_uncached 流程。不同参数,走不同的方法调用逻辑 .macro CheckMiss // miss if bucket->sel == 0 .if $0 == GETIMP cbz x9, LGetImpMiss .elseif $0 == NORMAL cbz x9, __objc_msgSend_uncached .elseif $0 == LOOKUP cbz x9, __objc_msgLookup_uncached .else .abort oops .endif .endmacro // __objc_msgSend_uncached 内部其实走 MethodTableLookup 逻辑 STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band x16 is the class to search MethodTableLookup br x17 END_ENTRY __objc_msgSend_uncached // MethodTableLookup 是一个汇编宏,内部指令跳转到 __class_lookupMethodAndLoadCache3。 .macro MethodTableLookup // push frame stp fp, lr, [sp, #-16]! mov fp, sp // save parameter registers: x0..x8, q0..q7 sub sp, sp, #(10*8 + 8*16) stp q0, q1, [sp, #(0*16)] stp q2, q3, [sp, #(2*16)] stp q4, q5, [sp, #(4*16)] stp q6, q7, [sp, #(6*16)] stp x0, x1, [sp, #(8*16+0*8)] stp x2, x3, [sp, #(8*16+2*8)] stp x4, x5, [sp, #(8*16+4*8)] stp x6, x7, [sp, #(8*16+6*8)] str x8, [sp, #(8*16+8*8)] // receiver and selector already in x0 and x1 mov x2, x16 bl __class_lookupMethodAndLoadCache3 // imp in x0 mov x17, x0 // restore registers and return ldp q0, q1, [sp, #(0*16)] ldp q2, q3, [sp, #(2*16)] ldp q4, q5, [sp, #(4*16)] ldp q6, q7, [sp, #(6*16)] ldp x0, x1, [sp, #(8*16+0*8)] ldp x2, x3, [sp, #(8*16+2*8)] ldp x4, x5, [sp, #(8*16+4*8)] ldp x6, x7, [sp, #(8*16+6*8)] ldr x8, [sp, #(8*16+8*8)] mov sp, fp ldp fp, lr, [sp], #16 .endmacro ``` Tips:c 方法在汇编中使用的时候,需要在方法名前加 `_` 。所以在汇编中某个方法为 `_xxx`,则在其他地方查找实现,需要去掉 `_`。 此时 `__class_lookupMethodAndLoadCache3` 在汇编中没有实现,则去掉一个 `_` 按照 `_class_lookupMethodAndLoadCache3` 在非汇编代码中查找 ```c IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); } IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { IMP imp = nil; bool triedResolver = NO; runtimeLock.assertUnlocked(); // Optimistic cache lookup // 首先从方法缓存中查找,如果找到则 return imp,也就是把方法地址返回给 bl 指令 if (cache) { imp = cache_getImp(cls, sel); if (imp) return imp; } // runtimeLock is held during isRealized and isInitialized checking // to prevent races against concurrent realization. // runtimeLock is held during method search to make // method-lookup + cache-fill atomic with respect to method addition. // Otherwise, a category could be added but ignored indefinitely because // the cache was re-filled with the old value after the cache flush on // behalf of the category. runtimeLock.read(); if (!cls->isRealized()) { // Drop the read-lock and acquire the write-lock. // realizeClass() checks isRealized() again to prevent // a race while the lock is down. runtimeLock.unlockRead(); runtimeLock.write(); realizeClass(cls); runtimeLock.unlockWrite(); runtimeLock.read(); } if (initialize && !cls->isInitialized()) { runtimeLock.unlockRead(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.read(); // If sel == initialize, _class_initialize will send +initialize and // then the messenger will send +initialize again after this // procedure finishes. Of course, if this is not being called // from the messenger then it won't happen. 2778172 } retry: runtimeLock.assertReading(); // 这里也先从方法缓存中查找,目的是前面查找后,后续又通过 runtime 的方式增加了方法缓存 // Try this class's cache. imp = cache_getImp(cls, sel); if (imp) goto done; // 找到方法则执行 done 里面的逻辑,done 里面也是将 IMP 的地址返回出去 // Try this class's method lists. { // 通过 getMethodNoSuper_nolock 方法查找 cls 中有没有 SEL 方法 Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } } // Try superclass caches and method lists. { unsigned attempts = unreasonableClassCount(); for (Class curClass = cls->superclass; curClass != nil; curClass = curClass->superclass) { // Halt if there is a cycle in the superclass chain. if (--attempts == 0) { _objc_fatal("Memory corruption in class list."); } // Superclass cache. imp = cache_getImp(curClass, sel); if (imp) { if (imp != (IMP)_objc_msgForward_impcache) { // Found the method in a superclass. Cache it in this class. log_and_fill_cache(cls, imp, sel, inst, curClass); goto done; } else { // Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method // resolver for this class first. break; } } // Superclass method list. Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; } } } // No implementation found. Try method resolver once. if (resolver && !triedResolver) { runtimeLock.unlockRead(); _class_resolveMethod(cls, sel, inst); runtimeLock.read(); // Don't cache the result; we don't hold the lock so it may have // changed already. Re-do the search from scratch instead. triedResolver = YES; goto retry; } // No implementation found, and method resolver didn't help. // Use forwarding. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); done: runtimeLock.unlockRead(); return imp; } ``` ### 消息发送阶段 上面的代码走到 `getMethodNoSuper_nolock` 寻找类里的方法 ```c static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) { runtimeLock.assertLocked(); assert(cls->isRealized()); // fixme nil cls? // fixme nil sel? // 这里根据类结构体找到 data(),然后找到 methods (Array 数组,数组元素是方法 Array) /* data() 其实就是 class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); } */ for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) { method_t *m = search_method_list(*mlists, sel); if (m) return m; } return nil; } static method_t *search_method_list(const method_list_t *mlist, SEL sel) { int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t); // 排好序则调用 `findMethodInSortedMethodList`,其内部是二分查找实现。 if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) { return findMethodInSortedMethodList(sel, mlist); } else { // 没排序则线性查找,顺序依次遍历查找 // Linear search of unsorted method list for (auto& meth : *mlist) { if (meth.name == sel) return &meth; } } #if DEBUG // sanity-check negative results if (mlist->isFixedUp()) { for (auto& meth : *mlist) { if (meth.name == sel) { _objc_fatal("linear search worked when binary search did not"); } } } #endif return nil; } static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list) { assert(list); const method_t * const first = &list->first; const method_t *base = first; const method_t *probe; uintptr_t keyValue = (uintptr_t)key; uint32_t count; for (count = list->count; count != 0; count >>= 1) { probe = base + (count >> 1); uintptr_t probeValue = (uintptr_t)probe->name; if (keyValue == probeValue) { // `probe` is a match. // Rewind looking for the *first* occurrence of this value. // This is required for correct category overrides. while (probe > first && keyValue == (uintptr_t)probe[-1].name) { probe--; } return (method_t *)probe; } if (keyValue > probeValue) { base = probe + 1; count--; } } return nil; } ``` `cls->data()->methods.beginLists` 这里根据类结构体调用到 data() 方法,获取到 `class_rw_t` ```c class_rw_t *data() { return bits.data(); } ``` 然后通过 `class_rw_t` 找到 methods (Array 数组,数组元素是方法 Array)。内部调用 `search_method_list` 方法。 `search_method_list` 方法内部判断方法数组是否排好序 - 排好序则调用 `findMethodInSortedMethodList`,其内部是二分查找实现。 - 没排序,则线性查找 (Linear search of unsorted method list) `getMethodNoSuper_nolock` 执行完则会将方法写入到当前类对象的缓存中。 ```c static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) { #if SUPPORT_MESSAGE_LOGGING if (objcMsgLogEnabled) { bool cacheIt = logMessageSend(implementer->isMetaClass(), cls->nameForLogging(), implementer->nameForLogging(), sel); if (!cacheIt) return; } #endif cache_fill (cls, sel, imp, receiver); // 继续走 cache_fill 逻辑 } void cache_fill(Class cls, SEL sel, IMP imp, id receiver) { #if !DEBUG_TASK_THREADS mutex_locker_t lock(cacheUpdateLock); cache_fill_nolock(cls, sel, imp, receiver); // 继续走 cache_fill_nolock 逻辑 #else _collecting_in_critical(); return; #endif } static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { cacheUpdateLock.assertLocked(); // Never cache before +initialize is done if (!cls->isInitialized()) return; // Make sure the entry wasn't added to the cache by some other thread // before we grabbed the cacheUpdateLock. if (cache_getImp(cls, sel)) return; // 根据类对象/类的元类对象获得方法缓存 cache_t cache_t *cache = getCache(cls); //根据方法 key 调用 getKey 方法,获取方法缓存哈希表的 key cache_key_t key = getKey(sel); // Use the cache as-is if it is less than 3/4 full mask_t newOccupied = cache->occupied() + 1; mask_t capacity = cache->capacity(); if (cache->isConstantEmptyCache()) { // Cache is read-only. Replace it. cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); } else if (newOccupied <= capacity / 4 * 3) { // Cache is less than 3/4 full. Use it as-is. } else { // Cache is too full. Expand it. cache->expand(); } // Scan for the first unused slot and insert there. // There is guaranteed to be an empty slot because the // minimum size is 4 and we resized at 3/4 full. // 根据方法 key 和 receiver 查找方法缓存哈希表 bucket bucket_t *bucket = cache->find(key, receiver); if (bucket->key() == 0) cache->incrementOccupied(); // 将方法缓存 key,方法地址 IMP 保存到方法缓存哈希 bucket 中 bucket->set(key, imp); } ``` 摘出 `lookUpImpOrForward` 方法中的一段代码 ```c // Try this class's cache. imp = cache_getImp(cls, sel); if (imp) goto done; // Try this class's method lists. { Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } } // Try superclass caches and method lists. ``` 如果代码没有找到,则不会 `goto` 到 `done`,开始走父类缓存查找逻辑 ```c // Try superclass caches and method lists. { unsigned attempts = unreasonableClassCount(); // for 循环不断查找,找当前类的父类,父类的父类...直到当前类为 nil(一轮查找后当前类 curClass = curClass->superclass)。 for (Class curClass = cls->superclass; curClass != nil; curClass = curClass->superclass) { // Halt if there is a cycle in the superclass chain. if (--attempts == 0) { _objc_fatal("Memory corruption in class list."); } // Superclass cache. // 先在父类的方法缓存中查找(根据 sel & mask,得到方法缓存哈希中的 key)`cache_getImp` ,找到则将方法写入到自身类的方法缓存中去 `log_and_fill_cache(cls, imp, sel, inst, curClass);` imp = cache_getImp(curClass, sel); if (imp) { if (imp != (IMP)_objc_msgForward_impcache) { // Found the method in a superclass. Cache it in this class. // 找到则将 IMP 写入到当前类 cls 的方法缓存中去,比如调用 [student personSay] 方法,从父类的方法列表找到 personSay,则将 personSay 的 IMP 写入到 student 类的方法缓存中去 log_and_fill_cache(cls, imp, sel, inst, curClass); goto done; } else { // Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method // resolver for this class first. break; } } // Superclass method list. // 如果在父类的方法缓存中没找到,则调用 `getMethodNoSuper_nolock` 父类的 方法数组(Array 元素为方法数组),按照排序好和没排序好分别走二分查找和线性查找。 Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { // 如果找到则继续填充到当前类的方法缓存中去 log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; } } } ``` 上面的流程是整个 `objc_msgSend` 的消息发送阶段的整个流程。可以用下图表示 注意: 方法缓存,也叫快速映射表(fast map),即使是快速执行路径(fast path),还是不如“静态绑定的函数调用操作”(statically bound function call)那样迅速,但大多数情况下这不是性能瓶颈。如果真的很在意,可以用纯 c 函数去实现。 ### 动态方法解析阶段 接着查看源码 ```c IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { //... // No implementation found. Try method resolver once. if (resolver && !triedResolver) { runtimeLock.unlockRead(); _class_resolveMethod(cls, sel, inst); runtimeLock.read(); // Don't cache the result; we don't hold the lock so it may have // changed already. Re-do the search from scratch instead. triedResolver = YES; goto retry; } // ... } void _class_resolveMethod(Class cls, SEL sel, id inst) { if (! cls->isMetaClass()) { // try [cls resolveInstanceMethod:sel] _class_resolveInstanceMethod(cls, sel, inst); } else { // try [nonMetaClass resolveClassMethod:sel] // and [cls resolveInstanceMethod:sel] _class_resolveClassMethod(cls, sel, inst); if (!lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { _class_resolveInstanceMethod(cls, sel, inst); } } } ``` 判断当前类没有走过动态方法解析阶段,则走动态方法解析阶段,调用 `_class_resolveMethod` 方法。 内部会判断但前类是不是元类对象、还是类对象走不同逻辑。 类对象走 `_class_resolveInstanceMethod` 逻辑 ```c static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst) { if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { // Resolver not implemented. return; } BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); // Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveInstanceMethod adds to self a.k.a. cls IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn't add anything? _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); } } } ``` 核心是 `bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);` ,也就是动态方法解析阶段,如果是类对象,则会调用类方法 `+ (BOOL)resolveInstanceMethod:(SEL)sel` 方法。 元类对象走 `_class_resolveClassMethod` 逻辑 ```c static void _class_resolveClassMethod(Class cls, SEL sel, id inst) { assert(cls->isMetaClass()); if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { // Resolver not implemented. return; } BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; bool resolved = msg(_class_getNonMetaClass(cls, inst), SEL_resolveClassMethod, sel); // Cache the result (good or bad) so the resolver doesn't fire next time. // +resolveClassMethod adds to self->ISA() a.k.a. cls IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn't add anything? _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); } } } ``` 其实就是调用 `bool resolved = msg(_class_getNonMetaClass(cls, inst), SEL_resolveClassMethod, sel);` 最后还是走到了 `goto retry;` 继续走完整的消息发送流程(因为添加了方法,所以会按照方法查找再去执行的逻辑) 完整流程如下 QA: 方法动态解析后,为什么还要 `goto retry`? 因为 API 设计阶段只是增加了口子,预留了能力,让开发者可以给类增加对象方法、类方法进行方法的动态解析。 如果我们实现了 `+ (BOOL)resolveInstanceMethod:(SEL)sel` 方法或者实现了 `+ (BOOL)resolveClassMethod:(SEL)sel`, 则根据函数返回值,拿到 YES 标记为已解析,则执行 `goto retry`。此时在一开始查找的时候就可以找到 IMP,此时 `goto done` 里面返回函数地址到汇编代码中。 如果没有实现动态解析方法,则执行 `goto retry` 的时候,一开始找不到 IMP,且在动态方法解析的 if 条件判断不成立 `if(resolver & !triedResolver)` 则不会继续动态方法解析,开始执行下面的逻辑,进入消息转发阶段 `_objc_msgForward_impcache` 上述动态消息解析后,会缓存起来吗? 第一次消息动态解析后,只是将方法增加到 class 或者 meta-class 的 class_rw_t 中。然后会继续执行 `goto retry`,在 `gto retry` 的流程中,会先在 class 的方法缓存中查找有没有缓存,如果没有缓存,则会在 class 的 class_rw_t 中查找方法,找到并执行,同时还会吧方法保存到方法缓存中去。以便后续再次调用方法的时候更加高效。 所以这个“会”缓存起来,只不过缓存的时机不是同步的,而是再次调用 `goto retry` 的时候,发现没有缓存,则在 class_rw_t 找查找到后,再缓存起来 #### 动态消息解析 Demo ##### 实例方法 ```objectivec Person *person = [[Person alloc] init]; [person makeLiving]; ``` 调用不存在方法则报错 `***** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person makeLiving]: unrecognized makeLiving sent to instance 0x101b2d900'**` 因为调用对象不存在的方法,所以会 Crash 知道 `objc_msgSend` 的流程,我们尝试给它修正下 方法1,增加一个兜底方法,然后利用 `class_addMethod` 动态增加方法实现 ```objective-c struct method_t { SEL sel; char *types; IMP imp; }; - (void)mockMakeLiving { NSLog(@"方法动态解析拦截第一阶段"); } + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(makeLiving)) { struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(mockMakeLiving)); class_addMethod(self, sel, method->imp, method->types); return YES; } return [super resolveInstanceMethod:sel]; } ``` 方法2,不用自定义的 struct method_t 结构体,直接用 Method 去承接,只不过在传递 IMP、encoding 的时候用 runtime api 获取即可 ```objective-c Method method = class_getInstanceMethod(self, @selector(mockMakeLiving)); class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method)); ``` 方法3,也可以添加 c 语言方法 c 函数即函数地址,只不过传递给 class_addMethod 的时候需要转换为函数参数加 `(IMP)cfuntionResolver`,手动添加方法签名。 ```objective-c void cfuntionResolver(id self, SEL _cmd) { NSLog(@"cfuntionResolver %@ %@", self, NSStringFromSelector(_cmd)); } + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(eat)) { // 对象方法,存在于对象上。 class_addMethod(self, sel, (IMP)cfuntionResolver, "v16@0:8"); return YES; } return [super resolveInstanceMethod:sel]; } ``` ##### 类方法动态解析 也可以给类方法做动态方法解析。需要注意的是类方法 - 调用 `-(BOOL)resolveClassMethod:(SEL)sel` - `class_addMethod` 方法中的第一个参数,需要加到类的元类对象中,所以是 `object_getClass` ```objectivec [Person drink]; void mockDrink (id self, SEL _cmd) { NSLog(@"假喝水"); } + (BOOL)resolveClassMethod:(SEL)sel { if (sel == @selector(drink)) { // 类方法,存在于元类对象上。 class_addMethod(object_getClass(self), sel, (IMP)mockDrink, "v16@0:8"); return YES; } return [super resolveClassMethod:sel]; } ``` ### 消息转发阶段 能走到消息转发,说明 1. 类自身没有该方法(`objc_msgSend` 的消息发送) 2. `objc_msgSend` 动态方法解析失败或者没有做 说明:**类自身(自身方法缓存、自身没有方法实现、自身也没有动态增加方法)和父类没有可以处理该消息的能力,此时应该将该消息转发给其他对象**。 查看 objc4 的源码 ```c IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { //... // No implementation found, and method resolver didn't help. // Use forwarding. imp = (IMP)_objc_msgForward_impcache; cache_fill(cls, sel, imp, inst); // ... } ``` 继续查找 `_objc_msgForward_impcache` ```shell STATIC_ENTRY __objc_msgForward_impcache MESSENGER_START nop MESSENGER_END_SLOW // No stret specialization. b __objc_msgForward END_ENTRY __objc_msgForward_impcache ENTRY __objc_msgForward adrp x17, __objc_forward_handler@PAGE ldr x17, [x17, __objc_forward_handler@PAGEOFF] br x1 END_ENTRY __objc_msgForward ``` 查找 `__objc_forward_handler` 没有找到,可以猜想是一个 c 方法,去掉最前面的 `_`,按照 `_objc_forward_handler` 查找得到 ```c __attribute__((noreturn)) void objc_defaultForwardHandler(id self, SEL sel) { _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p " "(no message forward handler is installed)", class_isMetaClass(object_getClass(self)) ? '+' : '-', object_getClassName(self), sel_getName(sel), self); } void *_objc_forward_handler = (void*)objc_defaultForwardHandler; ``` 消息转发的代码是不开源的,查找资料找到一份靠谱的 `__forwarding `方法实现 为什么是 `__forwarding__` 方法。我们可以根据 Xcode 崩溃窥探一二 ![](./../assets/runtime-forwardingFailed.png) ```c int __forwarding__(void *frameStackPointer, int isStret) { id receiver = *(id *)frameStackPointer; SEL sel = *(SEL *)(frameStackPointer + 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:))) { NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel]; if (methodSignature && class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) { NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; [receiver forwardInvocation:invocation]; void *returnValue = NULL; [invocation getReturnValue:&value]; return returnValue; } } if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) { [receiver doesNotRecognizeSelector:sel]; } // The point of no return. kill(getpid(), 9); } ``` 具体地址可以参考 [__frowarding](../assets/__forwarding__clean.c) 完整流程如下 #### 方法签名的获取 方法1: 自己根据方法的返回值类型,方法2个基础参数参数:`id self`、`SEL _cdm`,其他参数类型按照 Encoding 自己拼。 类似 `v16@0:8` 方法2 :根据某个类的对象,去调用 `methodSignatureForSelector ` 方法获取。 `[[[Person alloc] init] methodSignatureForSelector:@selector(makeLiving)];` 方法1自己拼的缺点是不够灵活,修改原始方法,需要在方法签名处修改方法的参数、返回值信息,还需手动计算。效率低。如果某个类实现了方法,则可以通过 `[class methodSignatureForSelector:@selector(***)]` 的方式获取方法签名。 ```objectivec - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(drink)) { return [[[PersonHelper alloc] init] methodSignatureForSelector:@selector(makeLiving:)]; } return [super methodSignatureForSelector:aSelector]; } ``` #### 消息转发 Demo ##### 对象方法消息转发 Demo Person 类不存在对象方法 makeliving ,PersonHelper 类存在。 调用对象不存在的方法,则会抛出错误。同时在 Runtime 动态消息解析阶段,`resolveInstanceMethod` 没有处理对象方法,所以会报错 方法1:因为动态消息解析没有处理,则会开始走消息转发阶段。消息转发首先会调用 `- (id)forwardingTargetForSelector:(SEL)aSelector` 方法。(如果是对象方法则调用 `- (id)forwardingTargetForSelector:(SEL)aSelector`,如果是类方法则调用 `+ (id)forwardingTargetForSelector:(SEL)aSelector`) 方法2:如果消息转发里,`forwardingTargetForSelector` 返回了 nil,则开始调用方法签名 `methodSignatureForSelector` 方法和 `forwardInvocation` 方法 ```objective-c - (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(makeLiving)) { // return [[PersonHelper alloc] init]; return nil; } return [super forwardingTargetForSelector:aSelector]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { // anInvocation 是包含了函数调用的所有信息 // anInvocation.target; 调用者 // anInvocation.selector; 方法名 // anInvocation getArgument: atIndex:; 方法参数 // anInvocation.target = [[PersonHelper alloc] init]; // [anInvocation invoke]; [anInvocation invokeWithTarget:[[PersonHelper alloc] init] ]; } ``` 注意:`methodSignatureForSelector` 如果返回 nil,则 `forwardInvocation` 不会执行 上述方式不够优雅,针对方法签名的获取太被动,方法改变了,方法签名处需要调整,很麻烦。 ##### 类方法消息转发 Demo 上述是实例方法找不到的情况,我们也可以给类方法,增加消息转发的处理。 - 基本上的处理上对象方法转发所用到的方法,前面的 `-` 变为 `+` 即可。 - 某些关于方法签名或者转发的对象换为类对象即可 ```objective-c + (id)forwardingTargetForSelector:(SEL)aSelector { return [PersonHelper class]; } + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [[PersonHelper class] methodSignatureForSelector:aSelector]; } + (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation invokeWithTarget:[PersonHelper class]]; } ``` ### OC 消息机制是什么样的? OC 中方法调用的本质就是利用 runtime 发消息,发消息也给 receiver(方法调用者)发消息(selector 方法名),就是 `objc_msgSend` 的工作流程,具体分为3个阶段: - 消息发送:主要是从当前类、当前类的父类...中查找 - 动态方法解析 - 消息转发 ### 消息传递流程 1. 先判断是否命中缓存,利用 sel 和 _mask,计算出哈希值,然后在 bucket_list 中查找方法缓存,找到则调用。没有找对则继续查找 2. 没有找到缓存,则根据对象的 isa 在类对象的方法列表中,进行方法查找 - 对于已经排序好的列表,采用二分查找算法,查找方法对应执行函数 - 对于没有排序好的列表,采用一般遍历查找方法对应执行函数 3. 如果在当前类的方法列表中没有找对方法实现,则根据类对象的 superclass 在父类的类对象的方法列表中继续查找 先根据 superclass 找到父类,然后在父类中也按照上面3点进行递归查找,即: - 先判断能否命中方法缓存,根据 sel 和 _mask 计算哈希,然后判断是否命中 - 没有命中,则在父类的类对象方法列表中查找。对于已经排序好的列表,采用二分查找算法,查找方法对应执行函数;对于没有排序好的列表,采用一般遍历查找方法对应执行函数 - 还是没有命中,则对父类的父类,继续上述流程 ## Super 底层原理 ```objectivec @interface Person: NSObject @end @implementation Person @end @implementation Student - (instancetype)init { if (self = [super init]) { NSLog(@"%@", [self class]); // Student NSLog(@"%@", [self superclass]); // Person NSLog(@"%@", [super class]); // Student NSLog(@"%@", [super superclass]); // Person } return self; } @end ``` 后面2个的打印似乎不符合预期?转成 c++ 代码看看 ```c static instancetype _I_Student_init(Student * self, SEL _cmd) { if (self = ((Student *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init"))) { NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass"))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_z5_ksvb7q252lbdfg78236t7tt00000gn_T_Student_91af5b_mi_3, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("superclass"))); } return self; } ``` `[super class]` 这句代码底层实现为 `objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"));` `__rw_objc_super` 是什么? ```c struct objc_super { __unsafe_unretained _Nonnull id receiver; __unsafe_unretained _Nonnull Class super_class; }; ``` `objc_msgSendSuper` 如下 ```c /** * Sends a message with a simple return value to the superclass of an instance of a class. * * @param super A pointer to an \c objc_super data structure. Pass values identifying the * context the message was sent to, including the instance of the class that is to receive the * message and the superclass at which to start searching for the method implementation. * @param op A pointer of type SEL. Pass the selector of the method that will handle the message. * @param ... * A variable argument list containing the arguments to the method. * * @return The return value of the method identified by \e op. * * @see objc_msgSend */ objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...) ``` 所以 `objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("class"));` 等同于下面代码 ```c struct objc_super arg = {self, class_getSuperclass(self)}; objc_msgSendSuper(arg, sel_registerName("class")) ``` 也就是说 `[super class]` 中, super 调用的 receiver 还是 self。比如 `[super class]` 等价于 `objc_msgSendSuper({self, class_getSuperClass(objc_getClass("Student"))}, @selector(class))` 调用 `class` 方法,其实是在基类对象 NSObject 中的。因为是类方法,所以先从 Student 的父类 Person 类的元类对象中查找有没有 `class` 方法实现。发现没有,则继续根据 Person 类 superclass 指针,找到 Person 的父类的元类对象的类方法列表中中查找,没找到,则继续向上找,最后找到 NSObject 对象的元类对象的类方法列表中找到了,因为方法调用者是 self,所以获取 class 得到的就是当前类,即 Student。 结构体的目的是为了在类对象查找的过程中,**直接从当前类的父类中查找,而不是本类**(比如 Student 类的 [super init] 会直接从 Person 的类对象中查找 init,找不到则通过 `superclass` 向上查找) 大致推测系统的 class、superclass 方法实现如下 ```objective-c @implementation NSObject - (Class)class{ return object_getClass(self); } - (Class)superclass { // 先获取类对象,然后获取类对象的 superclass return class_getSuperclass(object_getClass(self)); } @end ``` `class` 方法是在 NSObject 类对象的方法列表中的。所以 `[self class]` 等价于 `objc_msgSend(self, sel_registerName("class"))` `[super class]` 等价于 `objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("class"))` 其实2个方法本质上消息 receiver 都是 self,也就是当前的 Student,所以打印都是 Student 结论:`[super message]` 有2个特征 - super 消息的调用者还是 self - 方法查找是根据当前 self 的父类开始查找 通过将代码转为 c++ 发现,super 调用本质就是 `objc_msgSendSuper`,实际不然 我们对 iOS 项目`[super viewDidLoad]` 下符号断点,发现`objc_msgSendSuper2` ![](./../assets/runtime-super.png) 查看 objc4 源代码发现是一段汇编实现。 ```shell ENTRY _objc_msgSendSuper2 UNWIND _objc_msgSendSuper2, NoFrame MESSENGER_START ldp x0, x16, [x0] // x0 = real receiver, x16 = class ldr x16, [x16, #SUPERCLASS] // x16 = class->superclass CacheLookup NORMAL END_ENTRY _objc_msgSendSuper2 ``` 所以 `super viewDidLoad`本质上就是 ```objectivec struct objc_super arg = { self, [UIViewController class] }; objc_msgSendSuper2(arg, sel_registerName("viewDidLoad")); ``` objc_msgSendSuper2 和 objc_msgSendSuper 区别在于第二个参数 objc_msgSendSuper2 底层源码(汇编代码 objc-msg-arm64.s 422 行)会将第二个参数找到父类,然后进行方法缓存查找 objc_msgSendSuper 直接从第二个参数查找方法。 总结:clang 转 c++ 可以窥探系统实现,可以作为研究参考。super 本质上就是 `objc_msgSendSuper2`,传递2个参数,第一个参数为结构体,第二个参数是sel。 为什么转为 c++ 和真正实现不一样?思考下 源代码变为机器码之前,会经过 LLVM 编译器转换为中间代码(Intermediate Representation),最后转为汇编、机器码 我们来验证下 super 在中间码上是什么 ```shell clang -emit-llvm -S Student.m ``` llvm 中间码如下,可以看到确实内部是 `objc_msgSendSuper2` ```shell ; Function Attrs: noinline optnone ssp uwtable define internal void @"\01-[Student sayHi]"(%0* %0, i8* %1) #1 { %3 = alloca %0*, align 8 %4 = alloca i8*, align 8 %5 = alloca %struct._objc_super, align 8 store %0* %0, %0** %3, align 8 store i8* %1, i8** %4, align 8 %6 = load %0*, %0** %3, align 8 %7 = bitcast %0* %6 to i8* %8 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 0 store i8* %7, i8** %8, align 8 %9 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8 %10 = bitcast %struct._class_t* %9 to i8* %11 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %5, i32 0, i32 1 store i8* %10, i8** %11, align 8 %12 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_.6, align 8, !invariant.load !12 call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*)*)(%struct._objc_super* %5, i8* %12) notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_.8 to i8*)) ret void } ``` 指令介绍 ```shell @ - 全局变量 % - 局部变量 alloca - 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存 i32 - 32位4字节的整数 align - 对齐 load - 读出,store 写入 icmp - 两个整数值比较,返回布尔值 br - 选择分支,根据条件来转向label,不根据条件跳转的话类似 goto label - 代码标签 call - 调用函数 ``` 也可以在 Xcode 上可视化面板操作,路径为:菜单栏 `Product -> Perform Action -> Assemble "ViewController.m"` ## 消息发送的边界情况 - objc_msgSend_stret: 如果待发送的消息要返回结构体,那么可交给此函数完成。只有当 CPU 的寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息。如果是返回值无法容纳于 CPU 寄存器中(比如结构体太大了),那么就由另一个函数执行派发,此时函数会通过栈上的某个变量来处理消息所返回的结构体 - objc_msgSend_fpret:如果消息返回的是浮点数,那么可交给此函数处理。在某些架构的 CPU 中调用函数时,需要对浮点数寄存器做特殊处理,通常采用的 objc_msgSend 在这种情况下不合适,这个函数是为了处理 x86 等架构 CPU 中某些特殊的情况的 - objc_msgSendSuper: 要给超类发消息,就交给此函数处理。 ## 编译器优化之尾调用优化 如果函数的最后一行是调用另一个函数,那么就可以采用“尾调用优化”技术。编译器会生成跳转至另一函数所需的指令码,而不会向调用堆栈中推入新的栈帧。 只有当函数的最后一个操作仅仅是调用其他函数而不会将其返回值另作他用时,才能执行尾调用优化。这项优化对 objc_msgSend 很有用。如果不这么做,那么每次调用 OC 方法之前,都需要为调用 objc_msgSend 函数准备栈帧。而且很容易出现 stack overflow 的问题。 ## isKindOfClass、isMemberOfClass Demo1 上面的打印有没有有疑惑的?2个判断都是调用对象方法的 `isMemberOfClass` 、`isKindOfClass` 由于 objc4 是开源的,查看 `object.mm` ```c - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; } ``` `isMemberOfClass` 判断当前对象是不是传递进来的对象 `isKindOfClass` 内部是一个 for 循环,第一次循环先拿当前类的类对象,判断是不是和传递进来的对象一样,一样则 return YES,否则先给 tlcs 赋值当前类的父类,然后走第二次判断,直到 cls 不存在(NSObject 的父类为 nil)。所以 `isKindOfClass` 其实判断的是当前类是传递进来的类,或者传递进来类的子类 Demo2 下面面2个判断都是调用类方法的 `isMemberOfClass` 、`isKindOfClass` ```c + (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } ``` 可以看到 `+(BOOL)isMemberOfClass:(Class)cls` 方法内部就是对当前类获取类对象,然后与传递进来的 cls 判断是否相等。由于是 `[Student isMemberOfClass:[Student class]])` `Student` 类调用类方法 `+isMemberOfClass` 所以类对象的类对象也就是元类对象,cls 参数也就是 `[Student class]` 是一个类对象,元类对象等于类对象吗?显然不是 想让判断成立,可以改为 `[Student isMemberOfClass:object_getClass([Student class])]` 或者 `[[Student **class**] isMemberOfClass:object_getClass([Student class])]` `+(BOOL)isKindOfClass:(Class)cls` 同理分析。作用是当前类的元类,是否是右边传入对象的元类或者元类的子类。 QA: 1. `NSLog(@"%d", [Student isKindOfClass:[Student class]]); ` 为什么会输出0? 因为 `isKindOfClass` 底层是 for 循环对传入的 self 使用 `object_getClass((id)self)` 获取类对象,因为传入的已经是类对象,所以 `object_getClass((id)self)` 内部得到的是元类对象。右边是传入的类对象。等号左边的 Student 的元类对象,不等于等号右边的类对象。所以为 false,输出 0. 如何更改使得输出1?`[Student isKindOfClass:object_getClass([Student class])]`。右边也是元类对象,则为 True 输出1. 2. `NSLog(@"%hhd", [[Student class] isKindOfClass:[NSObject class]]); ` 为什么输出1? 看右边的部分,调用 `isKindOfClass` 方法,本质上就是 Student 类的类对象,也就是 Student 元类,和传入的右边 `[NSObject class]` 判断是否想通过 第一次 for 循环当然不同,所以不能 return,会将 `tcls ` 走步长改变逻辑 `tcls = tcls->superclass`,也就是找到当前 Student 元类对象的父类。 第二次 for 循环也一样不相等,Person 元类不等于 `[NSObject class]` 继续向上,直到 tcls = NSObject。此时还是不等,这时候 tcls  走步长改变逻辑,`tcls = tcls->superclass` NSObject 元类的 superclass 还是 NSObject。所以 for 循环内部的判断编委 ` [NSObject class] == [NSObject class]`,return YES。 **tips:基类的元类对象指向基类的类对象。** ```c + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } ``` ![](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/class-isa-superclass.png) 综合练习: ## Runtime 刁钻题 能否向编译后的类,添加实例变量?class_ro_t 不可以添加。但可以向动态创建的类添加。 > 这道题目设计:super 调用的本质、函数栈空间向下增长、runtime 消息调用本质(isa)、访问对象的成员变量(找到 isa,约过前面的8字节,按照成员变量的大小,去找成员) > > 因为实例对象里存的就是:isa + 各个成员变量的值 ```objective-c @interface Person : NSObject @property (nonatomic, copy) NSString *name; - (void)sayHi; @end @implementation Person - (void)sayHi { NSLog(@"hi,my name is %@", self->_name); } @end - (void)viewDidLoad { [super viewDidLoad]; NSString *temp = @"杭城小刘"; id obj = [Person class]; void *p = &obj; [(__bridge id)p sayHi]; } ``` 程序运行什么结果? 为什么会方法调用成功?为什么 name 打印出为 @"杭城小刘" 我们来分析下: 1.**方法调用本质就是寻找 isa 进行消息发送** ```objective-c Person *person = [[Person alloc] init]; [person sayHi]; ``` `[[Person alloc] init]`在内存中分配一块内存,然后 isa 指向这块内存,然后 person 指针,指向结构体,结构体的第一个成员。 2.**栈空间数据内存向下生长。第一个变量地址高,其次降低。且每个变量的内存地址是连续的。** 这个流程其实和上面的代码一样的。所以可以正常调用 ```c void test () { long long a = 4; // 0x7ff7bfeff2d8 long long b = 5; // 0x7ff7bfeff2d0 long long c = 6; // 0x7ff7bfeff2c8 NSLog(@"%p %p %p", &a, &b, &c); } ``` 方法内的变量存储在栈上,堆向上增长,栈向下增长。 ![](./../assets/runtime-isa-demo.png) 3.**实例对象的本质就是一个结构体,存储所有成员变量(isa 是一个特殊成员变量,其他的成员变量,这里就是 _name),`sayHi` 方法内部的 self 就是 obj,找成员变量的本质就是找内存地址的过程(此时就是偏移8个字节)** 上面代码可以类比类调用方法的流程。 obj 指针指向 Person 这块内存,给类对象发送 `sayHi` 消息也就是通过 obj 指针找到 isa,恰好 obj 指针指向的地址就是类对象的类结构体的地址,结构体成员变量第一个就是 isa 指针,结构体的其他成员变量就是类的其他属性,这里也就是 `_name`,所以我们给自定义的指针 `void *p` 调用 sayHi 方法,系统 runtime 在打印 name 的时候,会在 p 附近(下8个字节,因为 isa 是指针,长度为8)找 `_name` 属性,此时也就找到了 temp 字符串。 ```c struct Person_IMPL { Class isa; // 8字节 NSString *_name; // 8字节 } ``` 再看一个变体1 再看一个变体2 搞懂的小伙伴不迷惑了。没搞懂其实就是没搞懂**栈地址由高到低,向下生长** 和 `super` 调用的本质。 再强调一句,根据指针寻找成员变量 _name 的过程其实就是根据内存偏移找对象的过程。在变体2中,isa 地址就是 class 的地址,所以按照`地址 +8` 的策略,其实前一个局部变量。 `[super viewDidLoad];` 本质就是 `objc_msgSendSuper({self, class_getSuperclass(self)}, sel_registerName("viewDidLoad"))` ```c struct objc_super arg = {self, class_getSuperclass(objc_getClass("ViewController"))}; objc_msgSendSuper(arg, sel_registerName("viewDidLoad")); ``` 所以此时的“前一个局部变量” 也就是结构体 `objc_super` 类型的 arg。arg 是一个结构体,结构体第一个成员变量就是 self,所以“前一个局部变量” 也就是 self(ViewController) ![](./../assets/runtime-super-isa-demo.png) 可能会疑问,知道了 super 调用本质,知道了会产生一个局部变量的结构体,但是结构体里面2个成员变量,找属性的时候,isa 下的8个字节会命中哪个? 结构体 `objc_super` 存在2个成员变量,self 是第一个,`class_getSuperclass(objc_getClass("ViewController"))` 是第二个,self 地址更低。 画图如下,有助于理解 ## 应用场景 ### 统计 App 中未响应的方法。给 NSObject 添加分类 `NSObject+ExceptionHunter.m`,利用 NSProxy 实现 ```objectivec #import "NSObject+ExceptionHunter.h" #import @implementation NSObject (ExceptionHunter) - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([self respondsToSelector:aSelector]) { return [self methodSignatureForSelector:aSelector]; } return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { // 收集上报 NSLog(@"%@方法未调用成功", NSStringFromSelector(anInvocation.selector)); } @end ``` ### 修改类的 isa 类似 KVO 的实现,就是更改 isa。 `object_setClass` 实现 ```objectivec Person *p = [Person new]; object_setClass(p, [Student class]); ``` ![](./../assets/runtime-changeisa-demo.png) ### 动态创建类 `objc_allocateClassPair`、`objc_registerClassPair` 成对存在 动态创建类、添加属性、方法 ```objectivec void study (id self, SEL _cmd) { NSLog(@"在学习了"); } void createClass (void) { Class newClass = objc_allocateClassPair([NSObject class], "GoodStudent", 0); class_addIvar(newClass, "_score", 4, 1, "i"); class_addIvar(newClass, "_height", 4, 1, "i"); class_addMethod(newClass, @selector(study), (IMP)study, "v16@0:8"); objc_registerClassPair(newClass); id student = [[newClass alloc] init]; [student setValue:@100 forKey:@"_score"]; [student setValue:@177 forKey:@"_height"]; [student performSelector:@selector(study)]; NSLog(@"%@ %@", [student valueForKey:@"_score"], [student valueForKey:@"_height"]); } ``` ![](./../assets/runtime-dynamicCreateClass-demo.png) runtime 中 copy、create 等出来的内存,不使用的时候需要手动释放`objc_disposeClassPair(newClass>)` ### 访问成员变量信息 ```objectivec void ivarInfo (void) { Ivar nameIvar = class_getInstanceVariable([Person class], "_name"); NSLog(@"%s %s", ivar_getName(nameIvar), ivar_getTypeEncoding(nameIvar)); //_name @"NSString" // 设置、获取成员变量 Person *p = [[Person alloc] init]; Ivar ageIvar = class_getInstanceVariable([Person class], "_age"); object_setIvar(p, ageIvar, (__bridge id)(void *)27); NSLog(@"%d", p.age); } ``` runtime 设置值 api `object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value)` 第三个参数要求为 id 类型,但是我们给 int 类型的属性设置值,怎么办?可以将27这个数字的地址传进去,同时需要类型转换为 id `(__bridge id)(void *)27)` KVC 可以根据具体的值,去取出 NSNumber ,然后调用 intValue `[p setValue:@27 forKey:@"_age"];` ### 访问对象的所有成员变量信息 ```objectivec @property (nonatomic, strong) NSString *name; @property (nonatomic, assign) int age; @end unsigned int count; // 数组指针 Ivar *properties = class_copyIvarList([Person class], &count); for (int i =0 ; i 不够健壮体现在: - 没有考虑对象继承的情况,比如 Student 继承自 Person,那么 `[Student modelWithJSON:dict]` 转换就存在问题 - 假设服务器返回的 json 有9个字段,本地对象有8个字段,怎么处理? - 服务器给的有名字为 id 的数据,OC 对象的属性名不能叫 id,如何处理? 具体可以参考 YYModel ### 替换方法实现 注意 - 类似 NSMutableArray 的时候,+load 方法进行方法替换的时候需要注意类簇的存在,比如 `__NSArrayM` - 方法交换一般写在类的 `+load` 方法中,且为了防止出问题,比如别人手动调用 load,代码需要加 `dispatch_once` ```objectivec void studentSayHi (void) { NSLog(@"Student say hi"); } void changeMethodImpl (void){ class_replaceMethod([Person class], @selector(sayHi), (IMP)studentSayHi, "v16@0:8"); Person *p = [[Person alloc] init]; [p sayHi]; } // Student say hi ``` 上述代码可以换一种写法 ```objectivec class_replaceMethod([Person class], @selector(sayHi), imp_implementationWithBlock(^{ NSLog(@"Student say hi"); }), "v16@0:8"); Person *p = [[Person alloc] init]; [p sayHi]; ``` `imp_implementationWithBlock(id _Nonnull block)` 该方法将方法实现替换为包装好的 block ```objectivec Person *p = [[Person alloc] init]; Method sleep = class_getInstanceMethod([Person class], @selector(sleep)); Method sayHi = class_getInstanceMethod([Person class], @selector(sayHi)); method_exchangeImplementations(sleep, sayHi); [p sayHi]; // 人生无常,抓紧睡觉 [p sleep]; // Person sayHi ``` runtime 方法交换的本质,就是交换类对象、元类对象的 class_rw_t 的 `method_array_t>` 里的 method_t 的 IMP。 ### 无痕埋点 对 App 内所有的按钮点击事件进行监听并上报。发现 UIButton 继承自 UIControl,所以添加分类,在 load 方法内,替换方法实现。UIControl 存在方法 `sendAction:to:forEvent:` ```objectivec @implementation UIControl (Monitor) + (void)load { Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:)); Method method2 = class_getInstanceMethod(self, @selector(lbp_sendAction:to:forEvent:)); method_exchangeImplementations(method1, method2); } - (void)lbp_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action)); // 调用系统原来的实现 [self mj_sendAction:action to:target forEvent:event]; // [target performSelector:action]; } @end ``` 为了对业务代码无影响,在 hook 代码内部又要调用回去,所以需要调用原来的方法,此时因为交换方法实现,所以原来的方法应该是 `lbp_sendAction:to:forEvent:` `method_exchangeImplementations` 方法实现交换了,系统会清空缓存,调用 `flushCaches` 方法,内部调用 `cache_erase_nolock` 来清空方法缓存。 ```c void method_exchangeImplementations(Method m1, Method m2) { if (!m1 || !m2) return; rwlock_writer_t lock(runtimeLock); IMP m1_imp = m1->imp; m1->imp = m2->imp; m2->imp = m1_imp; // RR/AWZ updates are slow because class is unknown // Cache updates are slow because class is unknown // fixme build list of classes whose Methods are known externally? flushCaches(nil); updateCustomRR_AWZ(nil, m1); updateCustomRR_AWZ(nil, m2); } static void flushCaches(Class cls) { runtimeLock.assertWriting(); mutex_locker_t lock(cacheUpdateLock); if (cls) { foreach_realized_class_and_subclass(cls, ^(Class c){ cache_erase_nolock(c); }); } else { foreach_realized_class_and_metaclass(^(Class c){ cache_erase_nolock(c); }); } } ``` ### 安全气垫 安全气垫就是对代码运行过程中出错的方法进行兜住,比如数组越界等。ROI 和带来的一些业务异常问题就见仁见智了。实现手段就是 runtime hook 然后更改方法实现。做一些安全判断。和网易大白的作者交流过,发现安全气垫的副作用,比如一个商品价格运算后异常,会 crash 这时候起码 crash 不能交易下单了。但是用了安全气垫,好处是不会 crash,缺点是给一个有问题的值,要么价格为0,要么为空。用户可以正常下单,这时候产生资损了。电商业务虾带来的业务异常问题比稳定性异常问题更严重。电商公司一般会给商家有资损保障,赔付率。 ## 总结 OC 是一门动态性很强的编程语言,允许很多操作推迟到程序运行时决定。OC 动态性其实就是由 Runtime 来实现的,Runtime 是一套 c 语言 api,封装了很多动态性相关函数。平时写的 oc 代码,底层大多都是转换为 Runtime api 进行调用的。 - 关联对象 - 遍历类的所有成员变量(可以访问私有变量,比如修改 UITextFiled 的 placeholder 颜色、字典转模型、自动归档解档) - 交换方法实现 - 扩大点击区域 - 利用消息转发机制,解决消息找不到的问题 - 无痕埋点 - 热修复