Runtime在iOS開發中的實際應用
執行時的文章一直被同學們熱炒,當然現在面試中也都喜歡問道,當大夥說的頭頭是道時候,可到真正的專案中幾乎侷限只會關聯物件或者MethodSwizzling奉為神劍到處揮砍,開發畢竟不能紙上談兵,實踐出真知,介紹目前在專案中runtime的具體使用,真切希望和各位同學探討。
(個人的使用)
在這裡我還是要推薦下我自己建的iOS開發學習群:680565220,群裡都是學ios開發的,如果你正在學習ios ,小編歡迎你加入,今天分享的這個案例已經上傳到群檔案,大家都是軟體開發黨,不定期分享乾貨(只有iOS軟體開發相關的),包括我自己整理的一份2018最新的iOS進階資料和高階開發教程
使用 runtime 為 UIButton 分類修改響應位置 使得點選範圍擴大https://github.com/stevendinggang/UIButton-HitRect
使用 runtime 為 UIView 新增一個點選手勢,使得所有的繼承 UIView 的控制元件輕鬆新增點選效果
https://github.com/stevendinggang/UIView-tap-runtime
1 關聯物件(AssociatedObject )
Catagory主要為已經存在的類(主要是系統類)擴充套件新的方法,關聯物件是runtime在開發中應用的最廣泛,其主要用於為Catagory的物件增加屬性。
AFNetworking的關聯物件的
Masony的關聯的物件
關於分類的介紹可以檢視美團技術團隊寫的 深入理解Objective-C:Category
1.1 為什麼catagory 無法設定屬性
structobjc_category{char*category_name OBJC2_UNAVAILABLE;char*class_name OBJC2_UNAVAILABLE;structobjc_method_list*instance_methods OBJC2_UNAVAILABLE;structobjc_method_list*class_methods OBJC2_UNAVAILABLE;structobjc_protocol_list*protocols OBJC2_UNAVAILABLE;} OBJC2_UNAVAILABLE;
分類中可以新增例項方法,類方法,甚至可以實現協議,新增屬性,不可以新增成員變數。主要因為方法定義都在objc_class中管理的,不管如何增刪方法,都不影響類例項的記憶體佈局,建立一個物件必然會分配一塊記憶體區域,包含了isa指標和所有的成員變數。假如允許動態修改類成員變數佈局,已經創建出的類例項就不符合類定義了,變成了無效物件。
1.2 相關函式
//為一個例項物件新增一個關聯物件,由於是C函式只能使用C字串,這個key就是關聯物件的名稱,value為具體的關聯物件的值,policy為關聯物件策略,與我們自定義屬性時設定的修飾符類似voidobjc_setAssociatedObject(idobject,constvoid*key, idvalue, objc_AssociationPolicy policy);//通過key和例項物件獲取關聯物件的值idobjc_getAssociatedObject(idobject,constvoid*key);//刪除例項物件的關聯物件voidobjc_removeAssociatedObjects(idobject);
(1)key值
關於前兩個函式中的 key 值是我們需要重點關注的一個點,這個 key 值必須保證是一個物件級別(為什麼是物件級別?看完下面的章節你就會明白了)的唯一常量。一般來說,有以下三種推薦的 key 值:
宣告 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作為 key 值;
宣告 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作為 key 值;
用 selector ,使用 getter 方法的名稱作為 key 值。
static char kAssociatedObjectKey;
objc_getAssociatedObject(self,&kAssociatedObjectKey);
但是還有更簡單的方法, 可以使用selector:
objc_getAssociatedObject(self,@selector(associatedObject));
或者直接使用_cmd: _cmd在Objective-C的方法中表示當前方法的selector, 正如同self表示呼叫當前方法的物件(類)一樣.
objc_getAssociatedObject(self, _cmd);
(2) 關聯規則 objc_AssociationPolicy policy和property使用的修飾符神似,具體含義也與property修飾符相同。
image.png
objc_AssociationPolicymodifier
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICnonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMICnonatomic, copy
OBJC_ASSOCIATION_RETAINatomic, strong
OBJC_ASSOCIATION_COPYatomic, copy
(3)objc_removeAssociatedObjects函式實際運用很少,它會移除一個物件的所有關聯物件,將該物件恢復成“原始”狀態。這樣做就很有可能把別人新增的關聯物件也一併移除,這並不是我們所希望的。所以一般的做法是通過給 objc_setAssociatedObject 函式傳入 nil 來移除某個已有的關聯物件。
1.4 category關聯物件的大體原理
isa 結構體中的標記位 has_assoc 標記為 true,表示當前物件有關聯物件,關聯物件並不是成員變數,關聯物件是由一個全域性雜湊表儲存的鍵值對中的值。
2 物件關係對映(ORM)
通過逆向APP會發現目前物件轉模型這塊目前主要用的是MJExtension和YYModel,老專案一般是MJExtension,新崛起的專案轉到了YYModel上。利用runtime 我們可以實現json資料的直接轉換成物件模型,或者把模型通過對映拼接成晦澀的sql語句,間接實現了物件儲存到sqlite資料庫
MJExtension
YYModel中的YYClassInfo
其中ORM主要涉及到一下方法:
獲取屬性列表
objc_property_t *propertyList = class_copyPropertyList([selfclass], &count);for(unsignedinti=0; i%@", [NSStringstringWithUTF8String:propertyName]);}
獲取成員變數列表
Ivar *ivarList = class_copyIvarList([selfclass], &count);for(unsignedinti; i<count; i++)="" {="" ="" ivar="" myivar="ivarList[i];constchar*ivarName" =="" ivar_getname(myivar);nslog(@"ivar----="">%@", [NSStringstringWithUTF8String:ivarName]);}
獲取方法列表
unsigned int count;Method *methodList = class_copyMethodList([self class],&count);for (inti =0; i < count; i++) {Method method = methodList[i];DebugLog(@"------getRunTimeMethodList: %@",NSStringFromSelector(method_getName(method)));}
3 熱修復(HotfixPatch)
蘋果稽核一直被開發者吐槽的,一是蘋果稽核的嚴格,各種理由反反覆覆被打回去欲哭無淚,二是稽核週期長,在2017年之前蘋果稽核的週期一般都在三天,如果是新應用甚至需要一週以上,如果碰上聖誕節蘋果放假我們這邊是一般都不會提交稽核,於是JSPatch 為代表的熱修復技術被開發者推崇,通過逆向中國市面上有頭有臉的iOS應用,我發現幾乎都使用JSPath或者JSPath的變種。以至於蘋果發郵件禁止使用熱修復時 整個JSPath的Issues被炸鍋了。熱修復主要做的是替換現有的方法,或者增加新方法,需要對訊息傳送和轉發有一定的理解。
3.1 訊息轉發_objc_msgForward
-[*** ***]:unrecognized selector sent to instance 0x*****
這個是ios開發中最常見的crash,當前物件找不到這個方法,實際上蘋果 呼叫doesNotRecognizeSelector方法的時候,是給了我們三次機會的。就是我們常說的訊息轉發,
舉一個栗子,我在工作中專案出現了差錯,本著挽救同志的目的,領導讓我立即馬上提供一次挽回的方法,如果我給力這個危機到此沒了,但是我跪了搞不定,領導就問誰可以解決,這是老王站了出來,如果老王接盤搞定了這個危機那也沒事了,但是老王也沒有解決 領導就會找小李啊或者小張處理,如果大家都沉默無法解決, 那就專案徹底破產啦。oc中訊息轉發差不多就是這樣的。
+(BOOL)resolveInstanceMethod:(SEL)sel// 例項方法+(BOOL)resolveClassMethod:(SEL)sel// 類方法
第一次機會允許使用者在此時為該Class動態新增實現。如果有實現了,則呼叫並返回。
BOOL class_addMethod(Classcls, SELname, IMP imp,constchar *types);
如果仍沒實現,繼續下面的動作。
-(id)forwardingTargetForSelector:(SEL)aSelector
呼叫forwardingTargetForSelector:方法,嘗試找到一個能響應該訊息的物件。如果獲取到,則直接轉發給它。如果返回了nil,繼續下面的動作。
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector-(void)forwardInvocation:(NSInvocation*)anInvocation
通過 -methodSignatureForSelector: 訊息獲得函式的引數和返回值型別。如果 -methodSignatureForSelector: 返回 nil ,Runtime 則會發出 -doesNotRecognizeSelector: 訊息,程式這時也就掛掉了。如果返回了一個函式簽名,Runtime 就會建立一個 NSInvocation 物件併發送 -forwardInvocation: 訊息給目標物件。
NSInvocation 是一個訊息體的封裝,包括selector 以及引數等資訊。因此JSPatch通過NSInvocation來建立訊息
JSPatch
NSInvocation可以實現傳遞多個引數。
- (void)viewDidLoad { [superviewDidLoad];//建立一個函式簽名,這個簽名可以是任意的,但需要注意,簽名函式的引數數量要和呼叫的一致。NSMethodSignature* signature = [[selfclass] instanceMethodSignatureForSelector:@selector(testFun:argb:)];NSLog(@"引數個數%lu---返回引數型別%s",signature.numberOfArguments,signature.methodReturnType);//通過簽名初始化NSInvocation* invocatin = [NSInvocationinvocationWithMethodSignature:signature];NSString*argumentOne [email protected]"First";NSString*argumentTwo [email protected]"Two";//atIndex的下標必須從2開始。原因為:0 1 兩個引數已經被target和selector佔用[invocatin setArgument:&argumentOne atIndex:2]; [invocatin setArgument:&argumentTwo atIndex:3]; [invocatin setTarget:self];//設定target[invocatin setSelector:@selector(testFun:argb:)];//設定selecteor[invocatin invoke];//訊息呼叫}-(NSString*)testFun:(NSString*)argc argb:(NSString*)argb{//實現 [argc stringByAppendingString:argb];NSString* string = argc;NSString* aString;NSString* stringToAppend = argb;NSInvocation* inv = [NSInvocationinvocationWithMethodSignature:[NSStringinstanceMethodSignatureForSelector:@selector(stringByAppendingString:)]]; [inv setTarget: string]; [inv setSelector:@selector(stringByAppendingString:)]; [inv setArgument:&stringToAppend atIndex:2]; [inv retainArguments]; [inv invoke];// 獲取返回值[inv getReturnValue:&aString];returnaString;}
4 私有變數的修改
主要是利用class_copyIvarList獲取當前類的所有屬性,主要為了獲取私有變數然後利用KVC修改物件的屬性。
通過列印UITextField的屬性,獲取到變數名稱為_placeholderLabel,可以修改placeholder字型顏色。
unsignedintoutCount =0; Ivar *ivars = class_copyIvarList([UITextFieldclass], &outCount);for(NSIntegeri =0; i < outCount; ++i) {// 遍歷取出該類成員變數Ivar ivar = *(ivars + i);NSLog(@"\n name = %s \n type = %s", ivar_getName(ivar),ivar_getTypeEncoding(ivar)); }
KVC 修改屬性值
[_textView setValue: [UIColor redColor]forKeyPath:@"_placeholderLabel.textColor"];
一般上面寫法用的很少,儘快替換了方法還是有很多坑等著
一般我們的用法直接KVC 替換系統原有的變數
UITextField*textFiled = [[UITextFieldalloc] initWithFrame:CGRectMake(20,100,100,50)]; [self.view addSubview:textFiled];UILabel*placeholderLabel = [UILabelnew]; placeholderLabel.textColor = [UIColorredColor]; [placeholderLabel sizeToFit]; placeholderLabel.text [email protected]"請輸入密碼"; [textFiled addSubview:placeholderLabel]; [textFiled setValue:placeholderLabel forKey:@"_placeholderLabel"];
5 面向切面程式設計(AOP)
主要利用Method Swizzling 在不破話原有的程式碼,將獨立的功能模組剝離出來,實現程式碼注入。
5.1 Method Swizzling
+ (BOOL)swizzleInstanceMethod:(SEL)originalSel with:(SEL)newSel { Method originalMethod = class_getInstanceMethod(self, originalSel); Method newMethod = class_getInstanceMethod(self, newSel);if(!originalMethod || !newMethod)returnNO; class_addMethod(self, originalSel, class_getMethodImplementation(self, originalSel), method_getTypeEncoding(originalMethod)); class_addMethod(self, newSel, class_getMethodImplementation(self, newSel), method_getTypeEncoding(newMethod)); method_exchangeImplementations(class_getInstanceMethod(self, originalSel), class_getInstanceMethod(self, newSel));returnYES;}+ (BOOL)swizzleClassMethod:(SEL)originalSel with:(SEL)newSel { Classclass= object_getClass(self); Method originalMethod = class_getInstanceMethod(class, originalSel); Method newMethod = class_getInstanceMethod(class, newSel);if(!originalMethod || !newMethod)returnNO; method_exchangeImplementations(originalMethod, newMethod);returnYES;}
最重要的是需要理解selector, method, implementation 三者之間關係:在執行時,類(Class)維護了一個訊息分發列表來解決訊息的正確傳送。每一個訊息列表的入口是一個方法(Method),這個方法映射了一對鍵值對,其中鍵值是這個方法的名字 selector(SEL),值是指向這個方法實現的函式指標 implementation(IMP)。 Method swizzling 修改了類的訊息分發列表使得已經存在的 selector 映射了另一個實現 implementation,同時重新命名了原生方法的實現為一個新的 selector。
NSPipster的Method Swizzling
Method Swizzling需要注意的是:
(1)應該總在+load中執行,+load會在類初始載入時呼叫,和+initialize比較+load能保證在類的初始化過程中被載入。
(2) dispatch_once中執行:swizzling會改變全域性狀態,所以在執行時採取一些預防措施,使用dispatch_once就能夠確保程式碼不管有多少執行緒都只被執行一次。這將成為method swizzling的最佳實踐。
5.2日誌列印 快速熟悉專案。
程式猿是跳槽率偏高的職業,如果去新公司做新專案還好說,一旦需要接手老專案的維護,商業專案可不是我們平常寫的Demo的程式碼量,那程式碼中的邏輯結構瞬間會讓新入職的小夥伴們懵逼,通過通過攔截點選事件,可以快速的熟悉程式碼的邏輯。
image.png
5.3處理通用邏輯
比如在一些介面我們需要使用者登入才能檢視,最笨的辦法實在實在需要的ViewController 新增判斷登入的邏輯。下面這張截圖是從Github的找到的利用AOP處理使用者登入的程式碼,當然這個用繼承基礎類去寫也是不錯的,暫且不要在意寫法的好壞 最起碼我們程式開發提供了新的思路。
處理使用者登入
5.4Crash的防範
OC中容器類在空值nil 和陣列越界都會直接導致我們app 的crash 我們一種處理方式是利用Category增加新方法中判斷值是否為空或者越界,對於新工程我們使用大家約定使用容器的category還好說,
- (id)objectOrNilAtIndex:(NSUInteger)index {returnindex
但是對於老專案 我們難道需要修改所有的容器類方法?我們可以使用切面來修改。
+(void)load{staticdispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class __NSPlaceholderArray = NSClassFromString(@"__NSPlaceholderArray"); [NSArrayswizzleInstance:__NSPlaceholderArrayorigMethod:@selector(initWithObjects:count:)withMethod:@selector(RBSafe_initWithObjects:count:)]; Class __NSArray = NSClassFromString(@"NSArray"); Class __NSArrayI = NSClassFromString(@"__NSArrayI");//陣列有內容obj型別才是__NSArrayIClass __NSSingleObjectArrayI = NSClassFromString(@"__NSSingleObjectArrayI");//iOS10 以上,單個內容型別是__NSArraySingleObjectIClass __NSArray0 = NSClassFromString(@"__NSArray0");//iOS9 以上,沒內容型別是__NSArray0[NSArrayswizzleInstance:__NSArrayorigMethod:@selector(subarrayWithRange:)withMethod:@selector(RBSafe_subarrayWithRange:)]; [NSArrayswizzleInstance:__NSArrayorigMethod:@selector(objectsAtIndexes:)withMethod:@selector(RBSafe_objectsAtIndexes:)]; [NSArrayswizzleInstance:__NSArrayIorigMethod:@selector(objectAtIndex:)withMethod:@selector(RBSafe_NSArrayIobjectAtIndex:)]; [NSArrayswizzleInstance:__NSSingleObjectArrayIorigMethod:@selector(objectAtIndex:)withMethod:@selector(RBSafe_NSSingleObjectArrayIobjectAtIndex:)]; [NSArrayswizzleInstance:__NSArray0origMethod:@selector(objectAtIndex:)withMethod:@selector(RBSafe_NSArray0ObjectAtIndex:)]; });}
當然這種用法 我個人是持中立態度的,因為可以瞬間把我們程式碼所犯的錯誤處理的風平浪靜,但是讓我有一種掩耳盜鈴的感覺,我們的問題和錯誤根源還在的,不斷的錯誤疊加只會讓我們程式碼變得危機重重,同時AOP的crash處理是無痛無感知的,一旦我們運用在第三方的靜態庫實際上我們就會侵入被人工程的程式碼,被人的程式碼被篡改都不知情的,這個需要謹慎使用。
6 逆向開發
逆向開發主要集中在iOS越獄方面,逆向開發可以讓我們在iOS開發中開啟另一扇門,對於大部門開發者來說很少接觸這個領域,我也是在工作中才接觸到iOS的越獄,逆向開發的基礎就是利用Method Swizzling,不管是現在熱門的THEOS還是iOSOpenDev都是Method Swizzling的封裝,點選iOSOpenDev使用的CaptainHook就可以看到都是Method Swizzling 各種方法。
#import#import#define CYCRIPT_PORT 8888CHDeclareClass(AppDelegate);CHDeclareClass(UIApplication);CHOptimizedMethod2(self,void, AppDelegate, application,UIApplication*, application, didFinishLaunchingWithOptions,NSDictionary*, options){ CHSuper2(AppDelegate, application, application, didFinishLaunchingWithOptions, options);NSLog(@"## Start Cycript ##"); CYListenServer(CYCRIPT_PORT);}__attribute__((constructor))staticvoidentry() { CHLoadLateClass(AppDelegate); CHHook2(AppDelegate, application, didFinishLaunchingWithOptions);}
使用 runtime 為 UIButton 分類修改響應位置 使得點選範圍擴大https://github.com/stevendinggang/UIButton-HitRect
使用 runtime 為 UIView 新增一個點選手勢,使得所有的繼承 UIView 的控制元件輕鬆新增點選效果
https://github.com/stevendinggang/UIView-tap-runtime