本篇是是上篇的續(xù)作,請先看上篇。
http://www.reibang.com/p/0eb7238326f5
上一篇博客介紹了如何使用內(nèi)聯(lián)匯編給任意方法添加AOP切面,然后給了一個實現(xiàn),文末曾提到最多僅支持6個顯式(self,_cmd會占用前兩個寄存器)非浮點的參數(shù)和8個浮點參數(shù)梧喷。雖然絕大部分情況下都是少于6個參數(shù)的,但是超過6個情況還是時有發(fā)生。以上只是其一铺敌,其二是對包含匿名參數(shù)的函數(shù)的支持汇歹,匿名參數(shù)都是存儲在棧上的,比如:stringWithFormat:偿凭,第一個參數(shù)以后的那些匿名參數(shù)产弹,因此還是需要提供對更多參數(shù)的支持。
如果對ARM64參數(shù)傳遞規(guī)則了解的就知道弯囊,大于8個參數(shù)會通過棧來傳遞痰哨。我大致畫一個圖就容易理解了。
當前sp在最底下匾嘱,x29到sp范圍是當前調(diào)用函數(shù)所需要的所有暫存的空間斤斧,x29往上16個Byte保存的是上一次x29,30,當前函數(shù)調(diào)用完后恢復(fù)x29,30使用霎烙。再往上是上一次sp的地址撬讽,其存儲的就是額外的參數(shù)。所以當前函數(shù)在調(diào)用的時候會去該位置(x29+0x10)獲取額外的參數(shù)悬垃。
了解了這個再看看遇到的情況游昼,原始調(diào)用函數(shù)A調(diào)用函數(shù)B在加入Swizzle的情況變成了A->Swizzle->...->B,所以此時函數(shù)B到同樣的位置拿到的數(shù)據(jù)就不正確了尝蠕。怎么辦呢烘豌?一種是讓B到A存參數(shù)的位置讀取,這明顯行不通看彼,畢竟到了運行時廊佩,B代碼已經(jīng)編譯完成,是固定的靖榕,正常方法無法改變标锄,Swizzle之后B無法知道A存參數(shù)的位置。第二種將A的放置的參數(shù)再拷貝一份到B需要的位置序矩,換句話說就是建立一個偽棧。大致的思路是有了跋破,但可行性呢簸淀?
眾所周知sp寄存器的重要性,我們在函數(shù)中定義的臨時變量一般都需要sp+偏移量來讀取寫入毒返,一旦隨意改變租幕,后果可想而知,所以我們必須在偽棧調(diào)用完后立即還原sp指針拧簸,而且存參數(shù)的位置也需要傳遞過來劲绪。解決了這個問題,還有第二個問題就是棧上到底存了多少參數(shù),不需要知道其如何存儲贾富,只需要知道其大小歉眷。
NSMethodSignature的frameLength方法可以獲取總共參數(shù)大小,然后我從二進制源碼中發(fā)現(xiàn)frameLength-0xe0才是棧參大小颤枪。NSMethodSignature可以根據(jù)簽名字符串構(gòu)建汗捡,字符串可以從Method中獲取,但頻繁創(chuàng)建NSMethodSignature對象還是開銷較大畏纲,這里我將其和Class扇住,selector關(guān)聯(lián)起來緩存。OK盗胀,原理大致如此艘蹋。
ZWFrameLength
先實現(xiàn)frameLength獲取
/* 0xe0是基礎(chǔ)大小,其中包含9個寄存器共0x48票灰,8浮點寄存器共0x80女阀,還有0x18是額外信息,比如frameLength,
超過0xe0的部分為棧參數(shù)大小
*/
int ZWFrameLength(void **sp) {
id obj = (__bridge id)(*sp);
SEL sel = *(sp + 1);
Class class = object_getClass(obj);
if (!class || !sel) return 0xe0;
[_ZWLock lock];
NSMutableDictionary *methodSigns = _ZWAllSigns[NSStringFromClass(class)];
[_ZWLock unlock];
NSString *selName = class_isMetaClass(class) ? ZWGetMetaSelName(sel) : NSStringFromSelector(sel);
NSMethodSignature *sign = methodSigns[selName];
if (sign) {
return (int)[sign frameLength];
}
Method method = class_isMetaClass(class) ? class_getClassMethod(class, sel) : class_getInstanceMethod(class, sel);
const char *type = method_getTypeEncoding(method);
sign = [NSMethodSignature signatureWithObjCTypes:type];
[_ZWLock lock];
if (!methodSigns) {
_ZWAllSigns[NSStringFromClass(class)] = [NSMutableDictionary dictionaryWithObject:sign forKey:selName];
} else {
methodSigns[selName] = sign;
}
[_ZWLock unlock];
return (int)[sign frameLength];
}
函數(shù)實現(xiàn)還是比較簡單的米间,class_getInstanceMethod調(diào)用强品,NSMethodSignature對象創(chuàng)建還是有一定開銷的,特別是frameLength會被頻繁調(diào)用屈糊,所以還是需要緩存一下的榛。這里type作為Key來緩存性能應(yīng)該也不差,特別是內(nèi)存開銷小很多逻锐,type一致夫晌,sign也就一致。但有一點需要注意昧诱,class_getInstanceMethod在類定義的方法少的時候開銷不大晓淀,開銷較大的情況是Swizzle了大量父類的方法,特別是蘋果提供的父類方法較多盏档,所以如果使用這種方式Swizzle需要指明方法實現(xiàn)所在的類凶掰,可以減少循環(huán)。我這里空間換時間蜈亩,按class和selector來映射懦窘。lock前后使用了兩次,主要是為了減少臨界區(qū)的大小稚配,可以提高效率畅涂。
構(gòu)造偽棧
接下來講調(diào)用時候如何構(gòu)造偽棧和注意事項
void ZWAopInvocation(void **sp, NSDictionary *Invocation, ZWInvocationOption option) {
id obj = (__bridge id)(*sp);
SEL sel = *(sp + 1);
if (!obj || !sel) return;
NSInteger count = ZWGetInvocationCount(Invocation, obj, sel);
__autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(option)];
NSInteger frameLenth = ZWFrameLength(sp) - 0xe0;
for (int i = 0; i < count; ++i) {
ZWGetAopImp(Invocation, obj, sel, i);
asm volatile("cbz x0, LZW_20181107");
asm volatile("mov x17, x0");
asm volatile("ldr x14, %0": "=m"(arr));
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x13, %0": "=m"(frameLenth));
asm volatile("cbz x13, LZW_20181110");
asm volatile("add x12, x11, 0xc0");
asm volatile("sub sp, sp, x13");//增長sp
asm volatile("bl _ZWCopyParams");
asm volatile("LZW_20181110:");
asm volatile("bl _ZWLoadParams");
asm volatile("mov x1, x14");
asm volatile("blr x17");
asm volatile("sub sp, x29, 0x1e0");//恢復(fù)sp
asm volatile("LZW_20181107:");
}
}
相比較于上篇博客的版本,函數(shù)上半部分是一樣的道川,這里需要調(diào)用frameLength獲取棧幀大小午衰,這里使用NSInteger存儲立宜,其和寄存器大小一致,方便操作臊岸。
新增代碼讀取frameLength-0xe0的值到x13橙数,x12=x11(sp的值)+0xc0。其中0xc0=0xb0+0x10扇单,而0xb0為ZWGlobalOCSwizzle中棧的大小商模。如果x13=0,則直接跳到LZW_20181110蜘澜,否則順序執(zhí)行施流。
接下來就是比較危險的操作sub sp, sp, x13
,將sp增長x13的長度鄙信,新增的空間就是我之前說的偽棧瞪醋,此時,在sp未恢復(fù)之前装诡,之前定義的C變量frameLenth银受,sp,arr等全部失效了鸦采,所以不要再使用這些變量了宾巍,當然實在是需要可以強行通過x29+偏移量來讀取,只不過比較麻煩渔伯,需要知道這些臨時變量的具體位置顶霞,再計算相對于x29的偏移量。
跳轉(zhuǎn)ZWCopyParams锣吼,其作用是將x12指向的棧中參數(shù)全部復(fù)制到偽棧上选浑。
不管是否調(diào)用ZWCopyParams都需要調(diào)用ZWLoadParams,加載所有的寄存器參數(shù)玄叠。
blr x17
跳轉(zhuǎn)x17古徒,調(diào)用函數(shù)。
恢復(fù)sp读恃,這個時候x29寄存器還是有效的隧膘, 所有可以根據(jù)它恢復(fù)sp。
注意
恢復(fù)sp時寺惫,偏移量0x1e0怎么來的疹吃?將代碼全部匯編,去_ZWAopInvocation入口處找肌蜻,看其開始將sp調(diào)整的大小互墓,需要注意的是編譯器開啟優(yōu)化后這個值可能會改變必尼,比如我這里Xcode10.1開啟Os級優(yōu)化會變成0xa0蒋搜,不同的版本的編譯器篡撵,不同的優(yōu)化等級可能該值都不一樣,這個是我比較頭疼的地方豆挽,該函數(shù)稍有改動就需要處理這個問題育谬,所以這個庫最好在指定的環(huán)境下編譯成.a再使用(不開啟優(yōu)化生成.a庫性能也不差,另外不知道為什么函數(shù)級別的優(yōu)化關(guān)閉選項__attribute__((optnone))
無效帮哈,不然可以關(guān)閉Xcode對該函數(shù)的優(yōu)化)膛檀,實在不行可以通過Debug和Release環(huán)境來區(qū)分該值。我再想想看有沒有別的辦法解決這個問題娘侍,人不能被問題憋死咖刃,不要撞死胡同,總是有各種招可以解決的憾筏。
ZWCopyParams
OS_ALWAYS_INLINE void ZWCopyParams(void) {
//x12=原始棧參數(shù)地址嚎杨,x13=frameLength-0x0e0
asm volatile("mov x15, sp");
asm volatile("LZW_20181108:");
asm volatile("cbz x13, LZW_20181109");
asm volatile("ldr x0, [x12]");
asm volatile("str x0, [x15]");
asm volatile("add x15, x15, #0x8");
asm volatile("add x12, x12, #0x8");
asm volatile("sub x13, x13, #0x8");
asm volatile("cbnz x13, LZW_20181108");
asm volatile("LZW_20181109:");
}
本函數(shù)接收x12,x13作為參數(shù)氧腰,將x12指向的棧中參數(shù)依次復(fù)制到偽棧枫浙,比較簡單就不具體講解了。
這里需要注意的是古拴,寄存器的選擇箩帚。這里我使用的是x9到x15這些寄存器,這些屬于易失性寄存器黄痪,簡單來說就是臨時暫存紧帕,很容易被修改,不可靠满力。使用它們的好處是不用考慮將其之前存的數(shù)據(jù)轉(zhuǎn)存到內(nèi)容焕参,使用完后一般不需要恢復(fù)原數(shù)據(jù)。
x16-x17是調(diào)用暫存的油额,比如我這里就將函數(shù)入口存入x17叠纷,然后通過blr命令跳轉(zhuǎn)的。x18是平臺保留寄存器潦嘶,一般用不上涩嚣。
函數(shù)A{
返回值C = 函數(shù)B//存儲在x19
函數(shù)D
函數(shù)E(C)//mov x0, x19
}
函數(shù)B{}
函數(shù)D{
使用x19之前需要將x19存儲在內(nèi)存中,之后再恢復(fù)
}
函數(shù)E(C){}
x19-x28是調(diào)用上下文暫存數(shù)據(jù)的掂僵,其是非易失性航厚,具體來講就是在函數(shù)若干范圍內(nèi)的,例如:我在函數(shù)A開始處調(diào)用了函數(shù)B獲得了一個返回值C锰蓬,函數(shù)A實現(xiàn)代碼體接下來需要多次使用C幔睬,就可以將C存在x19-x28中,存在棧上也是可以的芹扭,但效率較差麻顶。然而如果在函數(shù)A的某處調(diào)用了函數(shù)D赦抖,D中也使用x19-x28中相同的寄存器,這就需要先暫存之前數(shù)據(jù)辅肾,之后再恢復(fù)队萤,比較麻煩。
對于OC這類的動態(tài)調(diào)用語言矫钓,調(diào)用關(guān)系不固定的情況下要尔,最好不要使用x0-x8,q0-q7之外的寄存器傳參新娜。我這里之所以使用x11赵辕,x12,x13這些易失性寄存器傳參是因為調(diào)用關(guān)系簡單且固定概龄,調(diào)用上下文也不會有其他操作來破壞其內(nèi)容(什么系統(tǒng)中斷啥的匆帚,就不用我們操心了,其會保存上下文并恢復(fù)的)旁钧,同時省下暫存原數(shù)據(jù)的操作吸重。
ZWInvocation
void ZWInvocation(void **sp) {
__autoreleasing id obj;
SEL sel;
void *obj_p = &obj;
void *sel_p = &sel;
NSInteger frameLenth = ZWFrameLength(sp)- 0xe0;
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x10, %0": "=m"(obj_p));
asm volatile("ldr x0, [x11]");
asm volatile("str x0, [x10]");
asm volatile("ldr x10, %0": "=m"(sel_p));
asm volatile("ldr x0, [x11, #0x8]");
asm volatile("str x0, [x10]");
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x0, [x11]");
asm volatile("ldr x1, [x11, #0x8]");
asm volatile("bl _ZWGetOriginImp");
asm volatile("cbnz x0, LZW_20181105");
__autoreleasing NSArray *arr = @[obj, [NSValue valueWithPointer:sel], @(ZWInvocationOptionReplace)];
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x0, [x11]");
asm volatile("ldr x1, [x11, #0x8]");
asm volatile("bl _ZWGetCurrentImp");
asm volatile("cbz x0, LZW_20181106");
asm volatile("mov x17, x0");
asm volatile("ldr x14, %0": "=m"(arr));
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x13, %0": "=m"(frameLenth));
asm volatile("cbz x13, LZW_20181111");
asm volatile("add x12, x11, 0xc0");//0xb0 + 0x10
asm volatile("sub sp, sp, x13");
asm volatile("bl _ZWCopyParams");
asm volatile("LZW_20181111:");
asm volatile("bl _ZWLoadParams");
asm volatile("mov x1, x14");
asm volatile("blr x17");
asm volatile("sub sp, x29, 0x70");
asm volatile("b LZW_20181106");
asm volatile("LZW_20181105:");
asm volatile("mov x17, x0");
asm volatile("ldr x11, %0": "=m"(sp));
asm volatile("ldr x13, %0": "=m"(frameLenth));
asm volatile("cbz x13, LZW_20181112");
asm volatile("add x12, x11, 0xc0");
asm volatile("sub sp, sp, x13");//增長sp
asm volatile("bl _ZWCopyParams");
asm volatile("LZW_20181112:");
asm volatile("bl _ZWLoadParams");
asm volatile("blr x17");
asm volatile("sub sp, x29, 0x70");//恢復(fù)sp
asm volatile("LZW_20181106:");
}
本函數(shù)與ZWAopInvocation改動類似,這里就不作具體講解了歪今。
給出測試
- (void)viewDidLoad {
ZWAddAop(self, @selector(aMethod2::::::::), ZWInvocationOptionAfter, ^(NSArray *info, NSString *str ,NSString *a2 ,NSString *a3 ,NSString *a4 ,NSString *a5 ,NSString *a6 ,NSString *a7 ,NSString *a8){
NSLog(@"after2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
});
ZWAddAop(self, @selector(aMethod2::::::::), ZWInvocationOptionReplace | ZWInvocationOptionOnly, ^(NSArray *info, NSString *str,NSString *a2 ,NSString *a3 ,NSString *a4 ,NSString *a5 ,NSString *a6 ,NSString *a7 ,NSString *a8){
NSLog(@"replace2 | after2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
});
ZWAddAop(self, @selector(aMethod4::::::::), ZWInvocationOptionAfter, ^int (NSArray *info,NSInteger str, NSInteger a2, NSInteger a3, NSInteger a4, NSInteger a5, NSInteger a6, NSInteger a7, NSInteger a8){
NSLog(@"after43: %ld %ld %ld %ld %ld",str, a5, a6, a7, a8);
return 11034;
});
[self aMethod2:@"test str" :@"this is a test" :@"this is a test":@"this is a test":@"this is a test":@"this is a test":@"this is a test a7":@"this is a test a8"];
[self aMethod4:1 :2 :3 :4 :5 :6 :7 :8];
}
- (void)aMethod2:(NSString *)str :(NSString *)a2 :(NSString *)a3 :(NSString *)a4 :(NSString *)a5 :(NSString *)a6 :(NSString *)a7 :(NSString *)a8 {
NSLog(@"method2: %@\n%@\n%@\n%@\n%@", str, a5, a6, a7, a8);
}
- (void)aMethod4:(NSInteger)str :(NSInteger)a2 :(NSInteger)a3 :(NSInteger)a4 :(NSInteger)a5 :(NSInteger)a6 :(NSInteger)a7 :(NSInteger)a8 {
NSLog(@"method4: %ld %ld %ld %ld %ld",str, a5, a6, a7, a8);
}
2018-11-23 14:54:56.943007+0800 DEMO[12814:3984030] replace2 | after2: test str
this is a test
this is a test
this is a test a7
this is a test a8
2018-11-23 14:54:56.943074+0800 DEMO[12814:3984030] after2: test str
this is a test
this is a test
this is a test a7
this is a test a8
2018-11-23 14:54:56.943584+0800 DEMO[12814:3984030] method4: 1 5 6 7 8
2018-11-23 14:54:56.943607+0800 DEMO[12814:3984030] after43: 1 5 6 7 8
總結(jié)
之所以費力的做AOP嚎幸,是因為這貨確實好用。例如:debug寄猩,日志輸出嫉晶,代碼優(yōu)化,非侵入式埋點田篇,網(wǎng)絡(luò)接口記錄等等替废。之后如果有時間我會將代碼再優(yōu)化,增加些必要功能泊柬,提高其效率和可靠性椎镣。
最后聊幾句:使用內(nèi)聯(lián)匯編,可以結(jié)合高級語言和低級語言兩者的優(yōu)點兽赁,低級語言完成高級語言無法完成的工作状答,提高效率,高級語言則減少開發(fā)難度刀崖,從這個角度看確實是很棒惊科。
呵呵,一切聽上去美好的故事總是有曲折可怖的過程亮钦。兩者混合使用馆截,各自的缺點也就糅在一起了。匯編書寫易出錯蜂莉,C/OC會生成額外的復(fù)雜操作蜡娶,稍微操作不當堪唐,程序就Crash了,一臉懵逼翎蹈,而且錯誤難以排查,如果要增加功能男公,修改代碼也比較麻煩荤堪,很多東西都是寫死的(不寫死就意味著更多的工作量,更復(fù)雜的代碼邏輯枢赔,同時內(nèi)聯(lián)匯編不支持匯編宏澄阳,當然宏還是支持的,這倒是可以簡化不少代碼)踏拜。所以一般只有在必須使用的時候才使用碎赢,比如極致的效率優(yōu)化,完成后不怎么修改的庫速梗。隨隨便便嵌入?yún)R編肮塞,就是自己給自己挖坑,當然要是有特殊用途??????姻锁,比如吹牛逼枕赵,整人啥的,就多多益善了位隶。因此內(nèi)聯(lián)匯編謹慎使用拷窜,書寫的時候要遵循兩者的機制,如果不知道C/OC背地里偷偷干了什么就將其匯編排查涧黄,這可以解決很多問題篮昧。
性能高度優(yōu)化版
Github源碼地址