iOS底層原理 runtime - super、hook、以及簡單應用--(8)

fgyong發表於2019-07-22

上篇已經講過了函式執行的本質是訊息機制,它包含了訊息傳送、訊息動態解析、訊息轉發這三個階段。那麼今天我們再研究一下一些綜合題目和runtime的一些應用。

關鍵字 super

關鍵字super,在呼叫[super init]的時候,super會轉化成結構體__rw_objc_super

struct __rw_objc_super { 
	struct objc_object *object; //訊息接受者
	struct objc_object *superClass; //父類
	__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};
複製程式碼

[super init]使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 Student.m轉化成cpp 開啟cpp大概在底部的位置找到

(Student *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init"))
複製程式碼

簡化之後是

(void *)objc_msgSendSuper((__rw_objc_super){self, class_getSuperclass(objc_getClass("Student"))}, sel_registerName("init"))
複製程式碼

void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ ) 其實是向父類傳送訊息,引數是struct objc_super *super, SEL op, ...,我們原始碼中找到了該函式的實現在objc-msg-arm64.s

ENTRY _objc_msgSendSuper
UNWIND _objc_msgSendSuper, NoFrame
//根據結構體struct __rw_objc_super 
{ 
	//struct objc_object *object; //訊息接受者
	//struct objc_object *superClass; //父類
}佔用空間16位元組,objc_msgSendSuper引數是__rw_objc_super,
//使x0偏移16位元組,就是兩個指標的空間,賦值給p0 和p16
ldp	p0, p16, [x0]		// p0 = self , p16 = superclass

CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

END_ENTRY _objc_msgSendSuper
複製程式碼

selfsuperclass賦值給 p0, p16呼叫CacheLookup NORMAL

.macro CacheLookup //.macro 是一個巨集 使用 _cmd&mask 查詢快取中的方法
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
#if !__LP64__
	and	w11, w11, 0xffff	// p11 = mask
#endif
	and	w12, w1, w11		// x12 = _cmd & mask
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp 命中 呼叫或者返回imp
	
2:	// not hit: p12 = not-hit bucket 沒有命中
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// wrap: p12 = first bucket, w11 = mask
	add	p12, p12, w11, UXTW #(1+PTRSHIFT)
		                        // p12 = buckets + (mask << 1+PTRSHIFT)

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0
	
.endmacro
複製程式碼

彙編比較多,只看到第二行p1 = SEL, p16 = isa,查詢快取是從p16,也就是superclass開始查詢,後邊的都和objc_msgSend一樣。 大致上比較清楚了,super本質上呼叫了objc_msgSendSuperobjc_msgSendSuper是查詢從父類開始查詢方法。

[super init]就是self直接呼叫父類init的方法,但是objc_msgSend接受者是self,假如是[self init]則會產生死迴圈。[super test]則是執行父類的test。 使用Debug Workflow->Always Show Disassemdly發現super其實呼叫了彙編的objc_msgSendSuper2,進入objc_msgSendSuper2 objc-msg-arm64.s 422 行發現和objc_msgSendSuper其實基本一致的

//_objc_msgSendSuper 開始
	ENTRY _objc_msgSendSuper
	UNWIND _objc_msgSendSuper, NoFrame
//x0偏移16位元組,就是兩個指標的空間,賦值給p0 和p16
	ldp	p0, p16, [x0]		// p0 = self , p16 = superclass
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
	END_ENTRY _objc_msgSendSuper  //_objc_msgSendSuper 結束
	
//objc_msgLookupSuper2 開始
	ENTRY _objc_msgSendSuper2 
	UNWIND _objc_msgSendSuper2, NoFrame

	ldp	p0, p16, [x0]		// p0 = real receiver, p16 = class
	//將儲存器地址為x16+8的字資料讀入暫存器p16。
	ldr	p16, [x16, #SUPERCLASS]	// p16 = class->superclass
	CacheLookup NORMAL
	END_ENTRY _objc_msgSendSuper2
複製程式碼

也可以使用LLVM轉化成中間程式碼來檢視,clang -emit-llvm -S FYCat.m檢視關鍵函式

define internal void @"\01-[FYCat forwardInvocation:]"(%1*, i8*, %2*) #1 {
    call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*, %2*)*)(%struct._objc_super* %7, i8* %18, %2* %12)
}
複製程式碼

這是forwardInvocation函式的呼叫程式碼,簡化之後是objc_msgSendSuper2(self,struct._objc_super i8*,%2*),就是objc_msgSendSuper2(self,superclass,@selector(forwardInvocation),anInvocation)

驗證

@interface FYPerson : NSObject
@property (nonatomic,copy) NSString *name;
- (int)age;
-(void)test;
@end

@implementation FYPerson
- (void)test{
    ;    NSLog(@"%s",__func__);
}
- (int)age{
    NSLog(@"%s",__func__);
    return 10;
}
- (NSString *)name{
    return [_name stringByAppendingString:@" eat apple"];
}
@end


@interface FYStudent : FYPerson

@end
@implementation FYStudent
- (void)test{
    [super test]; //執行父類的test
    int age = [super age]; //獲取父類的方法 返回值
    NSLog(@"age is %d",age);
    NSString * name = [self name]; //從父類開始尋找name的值,但返回的是self.name的值
    NSLog(@"%@",name);
}
-(int)age{
    return 12;
}
@end
//輸出
-[FYPerson test]
-[FYPerson age]
age is 10
小李子 eat apple

複製程式碼

test是執行父類的方法,[super age]獲取父類中固定的age, [self name]從父類開始尋找name的值,但返回的是self.name的值。

isMemberOfClass & isKindOfClass

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        printf("%s %s\n",class_getName(tcls),class_getName(cls));
        if (tcls == cls)
        {return YES;}else{
            printf("%s",class_getName(tcls));
        }
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        
        printf(" %s %s\n",class_getName(tcls),class_getName(cls));
        if (tcls == cls) return YES;
    }
    return NO;
}

+ (BOOL)isSubclassOfClass:(Class)cls {
    for (Class tcls = self; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
複製程式碼

- (BOOL)isMemberOfClass- (BOOL)isKindOfClass:(Class)cls比較簡單,都是判斷self.classcls+ (BOOL)isMemberOfClass:(Class)cls是判斷self.class->isa是否和cls相等,+ (BOOL)isKindOfClass:(Class)cls判斷cls->isacls->isa->isa有沒有可能和cls相等?只有基類是,其他的都不是。

驗證 例項方法

Class cls = NSObject.class;
Class pcls = FYPerson.class;
FYPerson *p=[FYPerson new];
NSObject *obj=[NSObject new];
BOOL res11 =[p isKindOfClass:pcls];
BOOL res12 =[p isMemberOfClass:pcls];
BOOL res13 =[obj isKindOfClass:cls];
BOOL res14 =[obj isMemberOfClass:cls];
NSLog(@"instance:%d %d %d %d",res11,res12,res13,res14);
//log
//instance:1 1 1 1
複製程式碼

ppcls的子類,objcls的子類,在明顯不過了。

驗證 類方法


//isKindOfClass cls->isa 和cls/cls->superclass相等嗎?
//元類物件和類物件不相等,但是最後一個元類的isa->superclass是指向NSObject的class 所以res1 = YES;
//cls->isa:元類物件 cls->isa->superclass: NSObject類物件
//cls:類物件
BOOL res1 =[cls isKindOfClass:cls];
//cls->isa 和cls相等嗎? 不相等 cls->isa是元類物件,cls是類物件,不可能相等。
BOOL res2 =[cls isMemberOfClass:cls];
//pcls->isa:person的元類物件 cls->isa->superclass: NSObject元類類物件 ->superclass:NSObject類物件 ->superclass:nil
//pcls:person類物件
BOOL res3 =[pcls isKindOfClass:pcls];
//pcls->isa:person的元類物件
//pcls:person類物件
BOOL res4 =[pcls isMemberOfClass:pcls];
NSLog(@"%d %d %d %d",res1,res2,res3,res4);
結果:
1 0 0 0
複製程式碼

堆疊 物件本質 class本質實戰

網上看到了一個比較有意思的面試題,今天我們就藉此機會分析一下,雖然網上很多博文已經講了,但是好像都不很對,或者沒有講到根本的東西,所以今天再來探討一下究竟。 其實這道題考察了物件在記憶體中的佈局,類和物件的關係,和堆上的記憶體佈局。基礎知識不很牢固的同學可以看一下我歷史的博文obj_msgsend基礎類的本質物件的本質

@interface FYPerson : NSObject
@property (nonatomic,copy) NSString *name;
- (void)print;
@end
@implementation FYPerson
- (void)print{
    NSLog(@"my name is %@",self.name);
}
@end


- (void)viewDidLoad {
    [super viewDidLoad];
    NSObject *fix =[NSObject new]; // 16位元組 0x60000219b030
    id cls  = [FYPerson class];針
    void * obj = &cls; 
    [(__bridge id)obj print];
}
複製程式碼

問題一 能否編譯成功?

當大家看到第二個問題的時候,不傻的話都會回答能編譯成功,否則還問結果乾嘛。我們從之前學的只是來分析一下,呼叫方法成功需要有id selfSEL sel,現在clsobj都在棧區,obj 指標指向cls的記憶體地址,訪問obj相當於直接訪問cls記憶體儲存的值,cls儲存的是Person.class,[obj print] 相當於objc_msgSend(cls,@selector(print)),cls是有print方法的,所以會編譯成功。

輸出什麼?

fix/cls/obj這三個物件都是儲存在棧上,fix/cls/obj地址是連續從高到低的,而且他們地址相差都是8位元組,一個指標大小是8位元組。他們三個地址如下所示:

使用圖來表示fixobj

物件 地址 地址高低
fix 0x7ffeec3df920
cls 0x7ffeec3df918
obj 0x7ffeec3df910

尋找屬性先是尋找isa,然後再在isa地址上+8則是屬性的值,所以根據obj尋找cls地址是0x7ffeec3df918,然後cls地址+8位元組則是_name的地址,cls地址是0x7ffeec3df918,加上8位元組正好是fix的地址0x7ffeec3df920,因為都是指標,所以都是8位元組,所以最後輸出是結果是fix物件的地址的資料。

情況再複雜一點,FYPerson結構改動一下

@interface FYPerson : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *name2;
@property (nonatomic,copy) NSString *name3;
- (void)print;
@end
複製程式碼

則他們的_name_name2_name3則在cls的地址基礎上再向上尋找8*1=8/8*2=16/8*3=24位元組,就是向上尋找第1個,第2個,第3個指向物件的指標。

測試程式碼:

@interface FYPerson : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) NSString *name2;
@property (nonatomic,copy) NSString *name3;
- (void)print;
@end
@implementation FYPerson
- (void)print{
    NSLog(@"name1:%@ name2:%@ name3:%@",self.name1,self.name2,self.name3);
}
@end

//主函式

NSObject *fix =[NSObject new];
FYPerson *fix2 =[FYPerson new];

id cls  = [FYPerson class];
void * obj = &cls; 
[(__bridge id)obj print];//objc_msgSend(self,sel);
NSLog(@"fix:%p fix2:%p cls:%p obj:%p",&fix,&fix2,&cls,&obj);

//log
name1:<FYPerson: 0x6000033a38a0> 
name2:<NSObject: 0x6000031f5380> 
name3:<ViewController: 0x7f8307505580>

fix: 0x7ffeec3d f9 28 
fix2:0x7ffeec3d f9 20 
cls: 0x7ffeec3d f9 18 
obj: 0x7ffeec3d f9 10
複製程式碼

再變形:

- (void)viewDidLoad {
    [super viewDidLoad];
	/*
	 objc_msgSuperSend(self,ViewController,sel)
	 */
NSLog(@"self:%p ViewController.class:%p SEL:%p",self,ViewController.class,@selector(viewDidLoad));
    id cls  = [FYPerson class];//cls 是類指標
    void * obj = &cls; //obj 
    [(__bridge id)obj print];//objc_msgSend(self,sel);
    
 NSLog(@"cls:%p obj:%p",&cls,&obj);
 //log
 
 name1:<ViewController: 0x7fad03e04ea0> 
 name2:ViewController
 
 self:                  0x7fad03e04ea0 
 ViewController.class:  0x10d0edf00 
 SEL:                   0x1117d5687
 
 cls:0x7ffee2b11908 
 obj:0x7ffee2b11900
 
}
複製程式碼

_name1cls地址向上+8位元組,_name2是向上移動16位元組,[super viewDidLoad]本質上是objc_msgSuperSend(self,ViewController.class,sel)selfViewController.classSEL是同一塊連續記憶體,佈局由低到高,看了下圖的記憶體佈局就會頓悟, 結構體如下圖所示:

物件 地址高低
self
ViewController.class
SEL

常用的runtimeAPI

method desc
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) 動態建立一個類(引數:父類,類名,額外的記憶體空間
void objc_registerClassPair(Class cls)) 註冊一個類
void objc_disposeClassPair(Class cls) 銷燬一個類
Class objcect_getClass(id obj) 獲取isa指向的class
Class object_setClass (id obj,Class cls) 設定isa指向的class
BOOL object_isClass(id class) 判斷oc物件是否為Class
BOOL class_isMetaClass(Class cls) 是否是元類
Class class_getSuperclass(Class cls) 獲取父類
Ivar class_getInstanceVariable(Class cls ,const char * name 獲取一個例項變數資訊
Ivar * class_copyIvarList(Class cls,unsigned int * outCount) 拷貝例項變數列表,需要free
void object_setIvar(id obj,Ivar ivar,id value 設定獲取例項變數的值
id object_getIvar(id obj,Ivar ivar) 獲取例項變數的值
BOOL class_addIvar(Class cls,const cahr * name ,size_t size,uint_t alignment,const char * types) 動態新增成員變數(已註冊的類不能動態新增成員變數)
const char * ivar_getName(Ivar v) 獲取變數名字
const char * ivar_getTypeEncoding(Ivar v) 變數的encode
objc_property_t class_getProperty(Class cls,const char* name) 獲取一個屬性
objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount) 拷貝屬性列表
objc_property_t _Nullable class_getProperty(Class _Nullable cls, const char * _Nonnull name) 獲取屬性列表
BOOL class_addProperty(Class _Nullable cls, const char * _Nonnull name,const objc_property_attribute_t * _Nullable attributes,unsigned int attributeCount) 新增屬性
void class_replaceProperty(Class _Nullable cls, const char * _Nonnull name,const objc_property_attribute_t * _Nullable attributes, unsigned int attributeCount) 替換屬性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,unsigned int attributeCount) 動態替換屬性
const char * _Nonnull property_getName(objc_property_t _Nonnull property) 獲取name
const char * _Nullable property_getAttributes(objc_property_t _Nonnull property) 獲取屬性的屬性
IMP imp_implementationWithBlock(id block) 獲取block的IMP
id imp_getBlock(IMP anIMP) 通過imp 獲取block
BOOL imp_removeBlock(IMP anIMP) IMP是否被刪除
... ...

在業務上有些時候需要給系統控制元件的某個屬性賦值,但是系統沒有提供方法,只能靠自己了,那麼我們 獲取class的所有成員變數,可以獲取Ivar檢視是否有該變數,然後可以通過KVC來賦值。


@interface FYCat : NSObject
@property (nonatomic,copy) NSString * name;
@property (nonatomic,assign) int  age;
@end

FYCat *cat=[FYCat new];
unsigned int count = 0;
Ivar *vars= class_copyIvarList(cat.class, &count);
for (int i = 0; i < count; i ++) {
	Ivar item = vars[i];
	const char *name = ivar_getName(item);
	NSLog(@"%s",name);
}
free(vars);

Method *m1= class_copyMethodList(cat.class, &count);
for (int i = 0; i < count; i ++) {
	Method item = m1[i];
	SEL name = method_getName(item);
	printf("method:%s \n",NSStringFromSelector(name).UTF8String);
}
free(m1);
		
//log
_age
_name

method:.cxx_destruct 
method:name 
method:setName: 
method:methodSignatureForSelector: 
method:forwardInvocation: 
method:age 
method:setAge:
複製程式碼

大家常用的一個功能是JsonToModel,那麼我們已經瞭解到了runtime的基礎知識,現在可以自己擼一個JsonToModel了。

@interface NSObject (Json)
+ (instancetype)fy_objectWithJson:(NSDictionary *)json;
@end
@implementation NSObject (Json)
+ (instancetype)fy_objectWithJson:(NSDictionary *)json{
	id obj = [[self alloc]init];
	unsigned int count = 0;
	Ivar *vars= class_copyIvarList(self, &count);
	for (int i = 0; i < count; i ++) {
		Ivar item = vars[i];
		const char *name = ivar_getName(item);
		NSString * nameOC= [NSString stringWithUTF8String:name];
		if (nameOC.length>1) {
			nameOC = [nameOC substringFromIndex:1];
			NSString * value = json[nameOC];
			if ([value isKindOfClass:NSString.class] && value.length) {
				[obj setValue:value forKey:nameOC];
			}else if ([value isKindOfClass:NSArray.class]){
				[obj setValue:value forKey:nameOC];
			}else if ([value isKindOfClass:NSDictionary.class]){
				[obj setValue:value forKey:nameOC];
			}else if ([value isKindOfClass:[NSNull class]] || [value isEqual:nil])
			{
				printf("%s value is nil or null \n",name);
			}else if ([value integerValue] > 0){
				[obj setValue:value forKey:nameOC];
			}else{
				printf("未知錯誤 \n");
			}
		}
	}
	free(vars);
	return obj;
}
@end
複製程式碼

然後自己定義一個字典,來測試一下這段程式碼

@interface FYCat : NSObject
@property (nonatomic,copy) NSString * name;
@property (nonatomic,assign) int  age;

- (void)run;
@end

NSDictionary * info = @{@"age":@"10",@"value":@10,@"name":@"小明"};
		FYCat *cat=[FYCat fy_objectWithJson:info];
//log
age:10 name:小明
複製程式碼

hook鉤子(method_exchangeImplementations)

由於業務需求需要在某些按鈕點選事件進行記錄日誌,那麼我們可以利用鉤子來實現攔截所有button的點選事件。

@implementation UIButton (add)
+ (void)load{
	Method m1= class_getInstanceMethod(self.class, @selector(sendAction:to:forEvent:));
	Method m2= class_getInstanceMethod(self.class, @selector(fy_sendAction:to:forEvent:));
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		method_exchangeImplementations(m1, m2);
	});
}
- (void)fy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{
	NSLog(@"%@ ",NSStringFromSelector(action));
	/*
	 code here
	 */
	 //sel IMP 已經交換過了,所以不會死迴圈
	[self fy_sendAction:action to:target forEvent:event];
}
@end
複製程式碼

可以在code here新增需要處理的程式碼,一般記錄日誌和延遲觸發都可以處理。[self fy_sendAction:action to:target forEvent:event];不會產生死迴圈,原因是在+load中已經將m1m2已經交換過了IMP。我們進入到method_exchangeImplementations內部:

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    mutex_locker_t lock(runtimeLock);

//交換IMP
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;

//重新整理快取
    flushCaches(nil);

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;
};
using MethodListIMP = IMP;
複製程式碼

m1m2交換了IMP,交換的是method_t->imp,然後重新整理快取(清空快取),等下次呼叫IMP則需要在cls->rw->data->method中去尋找。

陣列越界和nil處理

@implementation NSMutableArray (add)
+ (void)load{
	Class cls= NSClassFromString(@"__NSArrayM");
	Method m1= class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
	SEL sel = @selector(fy_insertObject:atIndex:);
	Method m2= class_getInstanceMethod(cls, sel);
	
	Method m3= class_getInstanceMethod(cls, @selector(objectAtIndexedSubscript:));
	Method m4= class_getInstanceMethod(cls, @selector(fy_objectAtIndexedSubscript:));

	static dispatch_once_t onceToken;
	dispatch_once(&onceToken, ^{
		method_exchangeImplementations(m1, m2);
		method_exchangeImplementations(m3, m4);
	});
}

- (void)fy_insertObject:(id)anObject atIndex:(NSUInteger)index{
	if (anObject != nil) {
		[self fy_insertObject:anObject atIndex:index];
	}else{
		printf(" anObject is nil \n");
	}
}
- (id)fy_objectAtIndexedSubscript:(NSUInteger)idx{
	if (self.count > idx) {
		return [self fy_objectAtIndexedSubscript:idx];
	}else{
		printf(" %ld is outof rang \n",(long)idx);
		return nil;
	}
}
@end



NSMutableArray *array=[NSMutableArray array];
id obj = nil;
[array addObject:obj];
array[1];

//log
 anObject is nil 
 1 is outof rang 
複製程式碼

NSMutableArray是類簇,使用工廠模式,NSMutableArray不是陣列例項,而是生產陣列物件的工廠。 真實的陣列物件是__NSArrayM,然後給__NSArrayM鉤子,交換objectAtIndexedSubscript:(NSUInteger)idxinsertObject:(id)anObject atIndex:(NSUInteger)index方法,實現崩潰避免。

字典nil處理

@interface NSMutableDictionary (add)

@end

@implementation NSMutableDictionary (add)
+ (void)load{
    Class cls= NSClassFromString(@"__NSDictionaryM");
    Method m1= class_getInstanceMethod(cls, @selector(setObject:forKey:));
//    __NSDictionaryM
    SEL sel = @selector(fy_setObject:forKey:);
    Method m2= class_getInstanceMethod(cls, sel);
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        method_exchangeImplementations(m1, m2);
    });
}
- (void)fy_setObject:(id)anObject forKey:(id<NSCopying>)aKey{
    if (anObject) {
        [self fy_setObject:anObject forKey:aKey];
    }else{
        NSString * key = (NSString *)aKey;
        printf("key:%s anobj is nil \n",key.UTF8String);
    }
}
@end
複製程式碼

利用類別+load__NSDictionaryM新增方法,然後交換IMP,實現給NSMutableDictionary setObject:Key:的時候進行nil校驗,+load雖然系統啟動的自動呼叫一次的,但是為防止開發者再次呼叫造成IMPSEL混亂,使用dispatch_once進行單次執行。

總結

  1. super本質上是self呼叫函式,不過查詢函式是從sueprclass開始查詢的
  2. +isKandOfClass是判斷self是否是cls的子類,+isMemberOfClass:是判斷self是否和cls相同。
  3. 瞭解+loadCategory是啟動的時候使用執行時編譯的,而且只會載入一次,然後利用objc/runtime.hmethod_exchangeImplementations實現交換兩個函式的IMP,可以實現攔截nil,降低崩潰率。
  4. NSMutableDictionaryNSMutableArray是類簇,先找到他們的類然後再交換該類的函式的IMP

資料參考

資料下載


最怕一生碌碌無為,還安慰自己平凡可貴。

廣告時間

iOS底層原理 runtime - super、hook、以及簡單應用--(8)

相關文章