歡迎閱讀iOS探索系列(按序閱讀食用效果更加)c++
本文涉及的面試題以下:程序員
runtime
是由C
和C++
、彙編
實現的一套API,爲OC語言加入了面向對象、運行時的功能運行時(runtime)
將數據類型的肯定由編譯時推遲到了運行時runtime
是Objective-C
的幕後⼯做者如類結構中的ro
和rw
屬性面試
ro(read-only)
在編譯時已經肯定rw(read-write)
在運行時才肯定,所以可使用runtime
進行修改方法的本質是發送消息objc_msgSend
,即尋找IMP
的過程數組
發送消息會有如下⼏個流程:緩存
objc_msgSend
查找緩存cache_t
是否有imp實現lookUpImpOrForward
遞歸查找當前類和父類的rw
中methodlist
的方法resolveInstanceMethod
和resolveClassMethod
來動態方法決議——實現消息動態處理CoreFoundation
來觸發消息轉發流程,forwardingTargetForSelector
實現快速轉發,由其餘對象來實現處理方法methodSignatureForSelector
獲取到方法的簽名,生成對應的invocation
;再經過forwardInvocation
來進行處理遇到這種問題先要解釋二者分別是什麼?再解釋二者的關係sass
SEL
是方法編號,也是方法名,在dyld加載鏡像到內存時,經過_read_image
方法加載到內存的表中了bash
IMP
是函數實現指針,找IMP
就是找函數實現的過程架構
SEL
和IMP
的關係就能夠解釋爲:app
SEL
就至關於書本的⽬錄標題IMP
就是書本的⻚碼函數
就是具體頁碼對應的內容好比咱們想在《程序員的自我修養——連接、裝載與庫》一書中找到「動態連接」(SEL)
,確定會翻到179頁(IMP)
,179頁會開始講述具體內容(函數實現)
ide
不能。
ro
,而ro
是在編譯時就已經肯定了的rw
中的方法或者能夠經過關聯對象的方式來添加屬性這題對runtime-API要求程度比較高
/** *建立類 * *superClass: 父類,傳Nil會建立一個新的根類 *name: 類名 *extraBytes: 額外的內存空間,通常傳0 *return:返回新類,建立失敗返回Nil,若是類名已經存在,則建立失敗 */
Class FXPerson = objc_allocateClassPair([NSObject class], "LGPerson", 0);
複製代碼
/** *添加成員變量 *這個函數只能在objc_allocateClassPair和objc_registerClassPair以前調用。不支持向現有類添加一個實例變量 *這個類不能是元類,不支持在元類中添加一個實例變量 *實例變量的最小對齊爲1 << align,實例變量的最小對齊依賴於ivar的類型和機器架構。對於任何指針類型的變量,請經過log2(sizeof(pointer_type)) * *cls 往哪一個類添加 *name 添加的名字 *size 大小 *alignment 對齊處理方式 *types 簽名 */
class_addIvar(FXPerson, "fxName", sizeof(NSString *), log2(sizeof(NSString *)), "@");
複製代碼
/** *往內存註冊類 * * cls 要註冊的類 */
objc_registerClassPair(FXPerson);
複製代碼
/** *往類裏面添加屬性 * *cls 要添加屬性的類 *name 屬性名字 *attributes 屬性的屬性數組。 *attriCount 屬性中屬性的數量。 */
class_addProperty(targetClass, propertyName, attrs, 4);
複製代碼
/** *往類裏面添加方法 * *cls 要添加方法的類 *sel 方法編號 *imp 函數實現指針 *types 簽名 */
class_addMethod(FXPerson, @selector(setHobby), (IMP)fxSetter, "v@:@");
複製代碼
// hobby的setter-IMP
void fxSetter(NSString *value) {
printf("%s/n",__func__);
}
// hobby的getter-IMP
NSString *fxHobby() {
return @"iOS";
}
// 添加屬性變量的封裝方法
void fx_class_addProperty(Class targetClass, const char *propertyName) {
objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
objc_property_attribute_t backingivar = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String }; //variable name
objc_property_attribute_t attrs[] = {type, ownership0, ownership, backingivar};
class_addProperty(targetClass, propertyName, attrs, 4);
}
// 打印屬性變量的封裝方法
void fx_printerProperty(Class targetClass){
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 動態建立類
Class FXPerson = objc_allocateClassPair([NSObject class], "FXPerson", 0);
// 添加成員變量
class_addIvar(FXPerson, "name", sizeof(NSString *), log2(sizeof(NSString *)), "@");
// 註冊到內存
objc_registerClassPair(FXPerson);
// 添加屬性變量
fx_class_addProperty(FXPerson, "hobby");
fx_printerProperty(FXPerson);
// 添加方法(爲屬性方法添加setter、getter方法)
class_addMethod(FXPerson, @selector(setHobby:), (IMP)fxSetter, "v@:@");
class_addMethod(FXPerson, @selector(hobby), (IMP)fxHobby, "@@:");
// 開始使用
id person = [FXPerson alloc];
[person setValue:@"Felix" forKey:@"name"];
NSLog(@"FXPerson的名字是:%@ 愛好是:%@", [person valueForKey:@"name"], [person valueForKey:@"hobby"]);
}
return 0;
}
複製代碼
<objc/runtime.h>
class_addIvar
必須在objc_registerClassPair
前,由於註冊到內存時ro
已經肯定了,不能再往ivars
添加(同第四個面試題)class_addProperty
能夠在註冊內存先後,由於是往rw
中添加的class_addProperty
中「屬性的屬性」——nonatomic/copy
是根據屬性的類型變化而變化的class_addProperty
不會自動生成setter和getter
方法,所以直接調用KVC
會崩潰不僅能夠經過KVC
打印來檢驗,也能夠下斷點查看ro、rw
的結構來檢驗
實則是爲了解決分類建立屬性的問題
Category
在runtime
中是用一個結構體表示的:
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;
...
};
複製代碼
裏面雖然能夠添加屬性變量,可是這些properties
並不會自動生成Ivar
,也就是不會有 @synthesize
的做用,dyld加載期間,這些分類會被加載並patch到相應的類中。這是一個動態過程,Ivar
不能動態添加
手動實現setter、getter方法,關聯對象
- (void)setName:(NSString *)name {
/** 參數一:id object : 給哪一個對象添加屬性,這裏要給本身添加屬性,用self。 參數二:void * == id key : 屬性名,根據key獲取關聯對象的屬性的值,在objc_getAssociatedObject中經過次key得到屬性的值並返回。 參數三:id value : 關聯的值,也就是set方法傳入的值給屬性去保存。 參數四:objc_AssociationPolicy policy : 策略,屬性以什麼形式保存。 */
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
/** 參數一:id object : 獲取哪一個對象裏面的關聯的屬性。 參數二:void * == id key : 什麼屬性,與objc_setAssociatedObject中的key相對應,即經過key值取出value。 */
return objc_getAssociatedObject(self, @"name");
}
複製代碼
objc_setAssociatedObject
分析蘋果設計接口時每每會加個中間層——即便底層實現邏輯發生變化也不會影響到對外接口
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
複製代碼
跟進去看看_object_set_associative_reference
實現
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
assert(object);
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// retain the new value (if any) outside the lock.
// 在鎖以外保留新值(若是有)。
ObjcAssociation old_association(0, nil);
// acquireValue會對retain和copy進行操做,
id new_value = value ? acquireValue(value, policy) : nil;
{
// 關聯對象的管理類
AssociationsManager manager;
// 獲取關聯的 HashMap -> 存儲當前關聯對象
AssociationsHashMap &associations(manager.associations());
// 對當前的對象的地址作按位去反操做 - 就是 HashMap 的key (哈希函數)
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
// 獲取 AssociationsHashMap 的迭代器 - (對象的) 進行遍歷
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
// 根據key去獲取關聯屬性的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
// 替換設置新值
j->second = ObjcAssociation(policy, new_value);
} else {
// 到最後了 - 直接設置新值
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
// 若是AssociationsHashMap從沒有對象的關聯信息表,
// 那麼就建立一個map並經過傳入的key把value存進去
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
// 若是傳入的value是nil,而且以前使用相同的key存儲過關聯對象,
// 那麼就把這個關聯的value移除(這也是爲何傳入nil對象可以把對象的關聯value移除)
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
// 最後把以前使用傳入的這個key存儲的關聯的value釋放(OBJC_ASSOCIATION_SETTER_RETAIN策略存儲的)
if (old_association.hasValue()) ReleaseValue()(old_association);
}
複製代碼
ObjcAssociation old_association(0, nil)
處理傳進來的值獲得new_value
AssociationsManager
,而後拿到hashmap總表AssociationsHashMap
DISGUISE(object)
對關聯對象的地址進行取反操做獲得哈希表對應的下標index
new_value
爲空(即對屬性賦值爲nil)就直接找到相應的表進行刪除new_value
不爲空,就拿到總表的迭代器經過拿到的下標index進行遍歷查找;若是找到管理對象的關聯屬性哈希map表,而後再經過key去遍歷取值
仍是不明白就LLVM斷點調試唄
objc_getAssociatedObject
分析id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
複製代碼
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
// 關聯對象的管理類
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 生成假裝地址。處理參數 object 地址
disguised_ptr_t disguised_object = DISGUISE(object);
// 全部對象的額迭代器
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
// 內部對象的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 找到 - 把值和策略讀取出來
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
// OBJC_ASSOCIATION_GETTER_RETAIN - 就會持有一下
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
複製代碼
objc_getAssociatedObject
是objc_setAssociatedObject
的逆過程
當面試官問你weak置空原理是什麼,你可能只知道weak怎麼用殊不知道怎麼答吧
在weak
一行打下斷點運行項目
int main(int argc, const char * argv[]) {
@autoreleasepool {
FXPerson *person = [[FXPerson alloc] init];
id __weak person = object;
}
return 0;
}
複製代碼
Xcode菜單欄Debug->Debug Workflow->Always show Disassembly
打上勾查看彙編——彙編代碼會來到libobjc庫
的objc_initWeak
①objc_initWeak
location
:表示__weak指針
的地址(咱們研究的就是__weak指針
指向的內容怎麼置爲nil)newObj
:所引用的對象,即例子中的person
id objc_initWeak(id *location, id newObj) {
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
複製代碼
②storeWeak
HaveOld
:weak指針以前是否已經指向了一個弱引用HaveNew
:weak指針是否須要指向一個新引用CrashIfDeallocating
:若是被弱引用的對象正在析構,此時再弱引用該對象,是否應該crashstoreWeak最主要的兩個邏輯點(源碼太長,這裏不貼了)
因爲是第一次調用,因此走
haveNew
分支——獲取到的是新的散列表SideTable,主要執行了weak_register_no_lock方法來進行插入
③weak_register_no_lock
isTaggedPointer
和deallocating
條件判斷weak_table
中的weak_entry_t
哈希數組中取出對應的weak_entry_t
weak_entry_t
不存在,則會新建一個並插入referrer
經過函數append_referrer
插入到對應的weak_entry_t
引用數組④append_referrer
找到弱引用對象的對應的weak_entry
哈希數組中插入
因爲弱引用在析構dealloc時自動置空,因此查看dealloc的底層實現並LLVM調試
_objc_rootDealloc
->rootDealloc
rootDealloc
->object_dispose
object_dispose
->objc_destructInstance
objc_destructInstance
->clearDeallocating
clearDeallocating
->sidetable_clearDeallocating
sidetable_clearDeallocating3
->table.refcnts.erase(it)
(非本人作圖)具體可查閱iOS底層學習 - 內存管理之weak原理探究
對
method swizzing
不瞭解的能夠閱讀iOS逆向 代碼注入+Hook
在平常開發中,再好的程序員都會犯錯,好比數組越界
NSArray *array = @[@"F", @"e", @"l", @"i", @"x"];
NSLog(@"%@", array[5]);
NSLog(@"%@", [array objectAtIndex:5]);
複製代碼
所以爲了不數組越界這種問題,大神們開始玩起了黑魔法——method swizzing
NSArray分類
runtime
頭文件——<objc/runtime.h>
+load
利用黑魔法交換方法#import "NSArray+FX.h"
#import <objc/runtime.h>
@implementation NSArray (FX)
+ (void)load {
// 交換objectAtIndex方法
Method oriMethod1 = class_getInstanceMethod(self, @selector(objectAtIndex:));
Method swiMethod1 = class_getInstanceMethod(self, @selector(fx_objectAtIndex:));
method_exchangeImplementations(oriMethod1, swiMethod1);
// 交換取下標方法
Method oriMethod2 = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
Method swiMethod2 = class_getInstanceMethod(self, @selector(fx_objectAtIndexedSubscript:));
method_exchangeImplementations(oriMethod2, swiMethod2);
}
- (void)fx_objectAtIndex:(NSInteger)index {
if (index > self.count - 1) {
NSLog(@"objectAtIndex————————數組越界");
return;
}
return [self fx_objectAtIndex:index];
}
- (void)fx_objectAtIndexedSubscript:(NSInteger)index {
if (index > self.count - 1) {
NSLog(@"取下標————————數組越界");
return;
}
return [self fx_objectAtIndexedSubscript:index];
}
@end
複製代碼
然而程序仍是無情的崩了...
NSNumber、NSArray、NSDictionary
等這些類都是
類簇(Class Clusters)
,一個
NSArray
的實現可能由多個類組成。因此若是想對
NSArray
進行方法交換,必須獲取到其真身進行方法交換,直接對NSArray進行操做是無效的
如下是NSArray
和NSDictionary
本類的類名
這樣就好辦了,可使用runtime
取出本類
黑魔法最好寫成單例,避免屢次交換
好比說添加了[NSArray load]
代碼,方法實現又交換回去了致使了崩潰
將+load
方法改寫成單例(雖然不常見,但也要避免)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 交換objectAtIndex方法
Method oriMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method swiMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndex:));
method_exchangeImplementations(oriMethod1, swiMethod1);
// 交換取下標方法
Method oriMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
Method swiMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndexedSubscript:));
method_exchangeImplementations(oriMethod2, swiMethod2);
});
}
複製代碼
①子類交換父類實現的方法
FXPerson類
中有-doInstance
方法,子類FXSon類
沒有重寫FXSon類
新建分類作了方法交換,新方法中調用舊方法FXPerson類
、FXSon類
調用-doInstance
子類打印出結果,而父類調用卻崩潰了,爲何會這樣呢?
由於FXSon類
交換方法時取得doInstance
先在本類搜索方法,再往父類裏查找,在FXFather
中找到了方法實現就把它跟新方法進行交換了。但是新方法是在FXSon分類
中的,FXFather
找不到imp就unrecognized selector sent to instance 0x600002334250
因此這種狀況下應該只交換子類的方法,不動父類的方法
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
複製代碼
class_addMethod
給FXSon類
添加方法(class_addMethod
不會取代本類中已存在的實現,只會覆蓋本類中繼承父類的方法實現)
swiMethod
的實現和方法類型@selector(fx_doInstance)
添加方法class_addMethod
把新方法實現放到舊方法名中,此刻調用doInstance就是調用fx_doInstance
,可是調用fx_doInstance
會崩潰didAddMethod
判斷是否添加成功
class_replaceMethod
替換方法method_exchangeImplementations
交換方法class_replaceMethod
用doInstance
方法實現替換掉fx_doInstance
中的方法實現②FXPerson類
只寫了方法聲明,沒有方法實現,卻作了方法交換——會形成死循環
doInstance
方法中添加了新的方法實現fx_doInstance
方法中想用舊的方法實現替代以前的方法實現,但是找不到doInstance
實現,致使class_replaceMethod
無效->在fx_doInstance
中調用fx_doInstance
就會死循環所以改變代碼邏輯以下
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
if (!oriMethod) {
class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
NSLog(@"方法未實現");
}));
}
BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
複製代碼
doInstance
就是調用fx_doInstance
fx_doInstance
方法內部仍是調用本身,用block修改fx_doInstance
的實現,就能夠斷開死循環了,method_exchangeImplementations
交換失敗使用Method Swizzling有如下注意事項:
+load
方法中交換方法單例
保證只交換一次這是一份作好封裝的Method Swizzling
交換方法
+ (void)FXMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) NSLog(@"傳入的交換類不能爲空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
NSLog(@"方法未實現");
}));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
}
複製代碼
如今iOS面試都喜歡問些底層的問題,這能夠很是直觀的看出你對runtime的理解,並且在知識點上繼續推敲、挖坑,當你答不上來時只能任人宰割——壓低薪資或不錄用。因此仍是要在平時多加練習,只要懂了原理就能觸類旁通了,即使面試的時候不能打的十全十美,也能給面試官留下個好印象