引言
我在github上寫了一個(gè)GJAlertController的開源庫,是為了解決在iOS8以下的系統(tǒng)中使用UIAlertController的問題,結(jié)果收到了100多個(gè)星星,讓我受寵若驚,感謝各位的支持,也感謝我的同事"芋頭"幫我在微博上轉(zhuǎn)發(fā),下面詳細(xì)說明一下實(shí)現(xiàn)原理。
iOS8中蘋果用UIAlertController來統(tǒng)一管理alert和actionSheet了,之前的UIAlertView和UIActionSheet已經(jīng)廢棄了。通常我們要兼容iOS7的時(shí)候,要這樣:
if ([[[UIDevice currentDevice] systemVersion] floatValue] <= 8.0) {
//用UIAlertView或UIActionSheet
} else {
//用UIAlertController
}
而GJAlertController解決了這里的系統(tǒng)版本兼容問題,不需要判斷版本,直接使用:
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];
[alertVC addAction:[UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"button ok pressed");
}]];
[alertVC addAction:[UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"button cancel pressed);
}]];
[alertVC addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"請(qǐng)輸入用戶名";
}];
[self presentViewController:alertVC animated:YES completion:nil];
如果創(chuàng)建的時(shí)候PreferredStyle傳入U(xiǎn)IAlertControllerStyleActionSheet,則顯示actionSheet。
大家可能很好奇,UIAlertController明明是iOS8才引入的,怎么能夠在iOS8以下的系統(tǒng)中跑呢?下面進(jìn)入正題。
原理概述
簡(jiǎn)單來說就是三個(gè)字——黑魔法。
利用這種黑魔法的例子已經(jīng)越來越多,我所知道的最早使用這種方法的是一個(gè)老外在三年為了解決NSUUID而使用的。
我們國(guó)內(nèi)團(tuán)隊(duì)開發(fā)的FDStackView是一個(gè)非常好的開源庫,已經(jīng)有1500+顆星星了,希望大家多多支持我們國(guó)內(nèi)的團(tuán)隊(duì),在FDStackView庫中也用到了相同的技術(shù),網(wǎng)上有人發(fā)出了分析實(shí)現(xiàn)原理的文章,但分析的很淺,而且根本沒有說在點(diǎn)子上,使得這種黑魔法的魅力并沒有被大家欣賞到,我這里做了一些功課,把這個(gè)原理詳細(xì)的闡述一下,以及這里的關(guān)鍵點(diǎn)在哪里。如果中間過程中有什么錯(cuò)誤,還請(qǐng)大家指正,謝謝。
下面簡(jiǎn)單說一下實(shí)現(xiàn)思路。
1.運(yùn)行時(shí)去判斷系統(tǒng)中是否已經(jīng)存在UIAlertController,如果存在,那就什么都不做,靜靜的看著UIAlertController裝逼,這就是iOS8及其之上版本的情況。
2.如果系統(tǒng)中沒有UIAlertController類,我們?cè)谶\(yùn)行時(shí)中做一些“手腳”,讓我們的GJAlertController在低版本中去完成這個(gè)問題。這一步是精華所在,下面分析代碼的時(shí)候回詳細(xì)說明
詳細(xì)分析
實(shí)現(xiàn)的代碼本身其實(shí)并不重要,下面先講最重要的一個(gè)東西,它是這種黑魔法能夠得以實(shí)現(xiàn)的前提。
在揭示這個(gè)重要前提之前,我們先來簡(jiǎn)單說說內(nèi)存。內(nèi)存有好多種,我們最熟悉的有:棧:函數(shù)的實(shí)現(xiàn)就依賴于棧,函數(shù)中簡(jiǎn)單類型的局部變量也都開辟在棧上;堆:我們平時(shí)用的Object都是開辟在堆上的;數(shù)據(jù)段:這個(gè)對(duì)我們相對(duì)陌生,但是其實(shí)靜態(tài)字符串就是存在數(shù)據(jù)段的eg:
NSString *testStr = @"hello world";
NSLog(@"testStr:%p", testStr);
testStr:0xb4338 //32位的機(jī)器上
testStr:0x106326580 //64位的機(jī)器上
數(shù)據(jù)段的內(nèi)存有些特殊,并不是我們理解的32上的指針是4Byte=32bit,64位上指針是8Byte=64bit,大家這里對(duì)數(shù)據(jù)段先有個(gè)概念,一會(huì)要用它來解釋一些現(xiàn)象。
下面開始講這個(gè)黑魔法能夠?qū)崿F(xiàn)的前提,是很重要的部分。在編譯的時(shí)候,系統(tǒng)中的每個(gè)類都在數(shù)據(jù)段上有一個(gè)標(biāo)簽(形式是這樣的:OBJC_CLASS$_ClassName),這個(gè)標(biāo)簽?zāi)憧梢岳斫獬蒶ey,它的value就是該類的類名,舉例:數(shù)據(jù)段中會(huì)有一個(gè)key是OBJC_CLASS$_UIAlertController,它對(duì)應(yīng)的value就是UIAlertController的類名,當(dāng)然也就會(huì)有OBJC_CLASS$_UIStackView這個(gè)標(biāo)簽,標(biāo)識(shí)著UIStackView這個(gè)類。
最重要的一點(diǎn)是:在iOS7中,還沒有UIAlertController的時(shí)候,這個(gè)標(biāo)簽OBJC_CLASS$_UIAlertController已經(jīng)存在了,只是這個(gè)標(biāo)簽對(duì)應(yīng)的value值是nil,因?yàn)闆]有這個(gè)類,我們可以認(rèn)為是蘋果在給高版本的這個(gè)類站位,就是蘋果的這個(gè)站位才使得我們有幸用上了這個(gè)黑魔法。當(dāng)然每個(gè)后出現(xiàn)的類都是有站位的,比如UIStackView。
if this label is Nil or doesnt exist, the class does not exist and cannot be allocated/used
這是我看到的老外在用該種黑魔法實(shí)現(xiàn)UUID的時(shí)候其中的一句說明,意思是:如果我們沒找找到這個(gè)標(biāo)簽,就不能為該申請(qǐng)內(nèi)存,也就不能使用了。
我對(duì)這句話的結(jié)論持懷疑態(tài)度,但又無法做實(shí)驗(yàn)驗(yàn)證,因?yàn)椤皹?biāo)簽站位”在早期版本中就存在了,而要找到“更早期”的版本驗(yàn)證該沒有標(biāo)簽是很困難的,因?yàn)閄code已經(jīng)不能支持對(duì)“更早期”的版本的編譯了,這段話表述有些混亂,大家還是往后看吧。
下面我們看看runtime里動(dòng)態(tài)添加類的方法:
Creates a new class and metaclass.
@param superclass The class to use as the new class's superclass, or \c Nil to create a new root class.
@param name The string to use as the new class's name. The string will be copied.
@param extraBytes The number of bytes to allocate for indexed ivars at the end of
the class and metaclass objects. This should usually be \c 0.@return The new class, or Nil if the class could not be created (for example, the desired name is already in use).
OBJC_EXPORT Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
大家看官方對(duì)函數(shù)的說明可以知道:superClass 是你要添加的類的父類,name是你要添加的類的名字,extraBytes一般傳0,它會(huì)返回一個(gè)新類,如果名字被占用了會(huì)返回Nil。
由此要說明的兩個(gè)重要結(jié)論:
1.如果OBJC_CLASS$_ClassName標(biāo)簽存在,但是對(duì)應(yīng)的類不存在(相當(dāng)于有key,但是value是nil)此時(shí)動(dòng)態(tài)添加類是可以成功的。
2.如果OBJC_CLASS$_ClassName標(biāo)簽和對(duì)應(yīng)的類都有的話,此時(shí)動(dòng)態(tài)添加類是不成功的,返回nil。
我們黑魔法的實(shí)現(xiàn)思路就是基于這兩個(gè)重要結(jié)論,下面我們具體看代碼。
代碼講解
__asm(
".section __DATA,__objc_classrefs,regular,no_dead_strip\n"
#if TARGET_RT_64_BIT
".align 3\n"
"L_OBJC_CLASS_UIAlertController:\n"
".quad _OBJC_CLASS_$_UIAlertController\n"
#else
".align 2\n"
"_OBJC_CLASS_UIAlertController:\n"
".long _OBJC_CLASS_$_UIAlertController\n"
#endif
".weak_reference _OBJC_CLASS_$_UIAlertController\n"
);
這是一段匯編代碼,不用擔(dān)心看不懂它,我也不懂匯編,這不影響我們分析,我簡(jiǎn)單的解釋一下:
1.__asm是在C、C++源碼中放入?yún)R編代碼(OC是C的超集)。
2..align是對(duì)指令或數(shù)據(jù)的存放地址進(jìn)行對(duì)齊,有些CPU架構(gòu)要求固定的指令長(zhǎng)度,并且存放地址相對(duì)于2的冪指數(shù)圓整,否則無法運(yùn)行,比如arm。有些不要這樣也能運(yùn)行,就是執(zhí)行效率稍微低點(diǎn),如i386。
3.64位的對(duì)齊方式是8位(23(.align后面的數(shù))),32位的對(duì)齊方式是4位(22(.align后面的數(shù)))。對(duì)齊只對(duì)緊挨著它的那條語句起作用,既,L_OBJC_CLASS_UIAlertController或_OBJC_CLASS_UIAlertController。
4..quad聲明一組數(shù)占64位,.long聲明一組數(shù)占32位
5..secton 后是指定參數(shù)用的,上述匯編的大體意思是在數(shù)據(jù)段(就是我們之前提到的數(shù)據(jù)段)找到OBJC_CLASS$_UIAlertController標(biāo)簽并利用.quad、.long聲明的一組數(shù)來存放它,取名為:_OBJC_CLASS_UIAlertController。
這是一段枯燥又非重點(diǎn)的代碼,如果大家心情不好直接忽略掉就可以了。
__attribute__((constructor)) static void GJAlertControllerPatchEntry(void) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@autoreleasepool {
// >= iOS8.
if (objc_getClass("UIAlertController")) {
return;
}
Class *alertController = NULL;
#if TARGET_CPU_ARM
__asm("movw %0, :lower16:(_OBJC_CLASS_UIAlertController-(LPC0+4))\n"
"movt %0, :upper16:(_OBJC_CLASS_UIAlertController-(LPC0+4))\n"
"LPC0: add %0, pc" : "=r"(alertController));
#elif TARGET_CPU_ARM64
__asm("adrp %0, L_OBJC_CLASS_UIAlertController@PAGE\n"
"add %0, %0, L_OBJC_CLASS_UIAlertController@PAGEOFF" : "=r"(alertController));
#elif TARGET_CPU_X86_64
__asm("leaq L_OBJC_CLASS_UIAlertController(%%rip), %0" : "=r"(alertController));
#elif TARGET_CPU_X86
void *pc = NULL;
__asm("calll L0\n"
"L0: popl %0\n"
"leal _OBJC_CLASS_UIAlertController-L0(%0), %1" : "=r"(pc), "=r"(alertController));
#else
#error Unsupported CPU
#endif
if (alertController && !*alertController) {
Class class = objc_allocateClassPair([GJAlertController class], "UIAlertController", 0);
if (class) {
objc_registerClassPair(class);
*alertController = class;
}
}
}
});
}
大家堅(jiān)持住,這是要分析的最后一段代碼了。
__attribute__((constructor)) static void GJAlertControllerPatchEntry(void){
}
總的來說上面的代碼是一個(gè)函數(shù),
__attribute__((constructor))只是用來修飾函數(shù)的,它起什么作用呢?這里涉及一個(gè)關(guān)于__attribute__的黑魔法,有興趣的人可以看我同事的一篇專門介紹__attribute__的文章。
__attribute__((constructor))修飾的函數(shù)會(huì)在main函數(shù)之前執(zhí)行,這是我們的最好時(shí)機(jī),有了runtime環(huán)境,但是main函數(shù)還沒有執(zhí)行,一切都“來得及”。
if (objc_getClass("UIAlertController")) {
return;
}
系統(tǒng)中有UIAlertController類的話,直接返回,這個(gè)邏輯之前已經(jīng)提到過了。
Class *alertController = NULL;
#if TARGET_CPU_ARM
__asm("movw %0, :lower16:(_OBJC_CLASS_UIAlertController-(LPC0+4))\n"
"movt %0, :upper16:(_OBJC_CLASS_UIAlertController-(LPC0+4))\n"
"LPC0: add %0, pc" : "=r"(alertController));
#elif TARGET_CPU_ARM64
__asm("adrp %0, L_OBJC_CLASS_UIAlertController@PAGE\n"
"add %0, %0, L_OBJC_CLASS_UIAlertController@PAGEOFF" : "=r"(alertController));
#elif TARGET_CPU_X86_64
__asm("leaq L_OBJC_CLASS_UIAlertController(%%rip), %0" : "=r"(alertController));
#elif TARGET_CPU_X86
void *pc = NULL;
__asm("calll L0\n"
"L0: popl %0\n"
"leal _OBJC_CLASS_UIAlertController-L0(%0), %1" : "=r"(pc), "=r"(alertController));
#else
#error Unsupported CPU
#endif
這段匯編大家直接忽略,意思就是把之前_OBJC_CLASS_UIAlertController中的值拿出來放到alertController里,之所以這么麻煩是因?yàn)椴煌軜?gòu)的CPU運(yùn)行的指令集不同,例如,32位就要這樣弄:MOVW 把16位立即數(shù)放到寄存器的底16位,高16位清0
MOVT 把16位立即數(shù)放到寄存器的高16位,低16位不影響。
if (alertController && !*alertController) {
Class class = objc_allocateClassPair([GJAlertController class], "UIAlertController", 0);
if (class) {
objc_registerClassPair(class);
*alertController = class;
}
}
如果alertController存在,證明OBJC_CLASS$_UIAlertController標(biāo)簽存在,即key存在,*alertController不存在,證明當(dāng)前系統(tǒng)中沒有這個(gè)類,即value不存在。這正是我們之前說的情況,如果我們此時(shí)打印alertController的地址,會(huì)發(fā)現(xiàn),它的位數(shù)和上面數(shù)據(jù)段中的一樣而不是32位或64位,也再次印證了標(biāo)簽在數(shù)據(jù)段上。
此時(shí)執(zhí)行最重要的一句代碼——?jiǎng)討B(tài)添加類
Class class = objc_allocateClassPair([GJAlertController class], "UIAlertController", 0);
這決對(duì)是畫龍點(diǎn)睛的一筆,我們之前用的時(shí)候都是繼承一個(gè)系統(tǒng)類,動(dòng)態(tài)添加一個(gè)自定義的類:
Class person = objc_allocateClassPair([NSObject class], "Person", 0);
這里正好相反,這里是在判斷了沒有系統(tǒng)類的時(shí)候,添加一個(gè)系統(tǒng)類,繼承自我們的類:GJAlertController,也就是說,在低版本中,沒有UIAlerController,我們動(dòng)態(tài)添加這個(gè)類,讓他繼承GJAlertController,我們?cè)贕JAlertController中,實(shí)現(xiàn)一套與系統(tǒng)UIAlertController一模一樣的API給人造成的錯(cuò)覺好像是在低版本中也能使用UIAlertController,其實(shí)只是一個(gè)魔術(shù)。
我們?cè)诘桶姹鞠率褂玫腢IAlertController是我們動(dòng)態(tài)添加的,它什么也沒有做,直接繼承了GJAlertController,而GJAlertController聲明并實(shí)現(xiàn)了和系統(tǒng)UIAlertController一模一樣的一套API。我們的GJAlertController根本不是一個(gè)VC是一個(gè)NSObject,只是自己用UIAlertView和UIActionSheet封裝成了UIAlertController的API罷了,到這里你應(yīng)該對(duì)所有的一切都明白了吧。
我之所以要寫這篇文章,主要是在欣賞:
Class class = objc_allocateClassPair([GJAlertController class], "UIAlertController", 0);
這段代碼的美麗與魅力,表達(dá)我對(duì)這段代碼,及其想到這樣使用這段代碼的人的敬佩當(dāng)然其實(shí)用其他的runtime函數(shù)在這里也也可以做相同的事情,具體看我剛剛發(fā)的那個(gè)老外的鏈接。
幾點(diǎn)說明:
1.為什么要使用匯編?
因?yàn)樵趯ふ覕?shù)據(jù)段上OBJC_CLASS$_ClassName標(biāo)簽的時(shí)候不支持C、C++、OC等高級(jí)語言,只能用匯編。
2.代碼中出現(xiàn)的OBJC_CLASS$_UIAlertController與_OBJC_CLASS_UIAlertController有什么關(guān)系?
沒有任何關(guān)系,OBJC_CLASS$_UIAlertController這個(gè)是系統(tǒng)中類標(biāo)簽的格式,必須是這樣子才可以,而_OBJC_CLASS_UIAlertController只是一個(gè)參數(shù)名,你叫hellworld也可以(已經(jīng)測(cè)試過可以),大家不要被它倆弄暈了,_OBJC_CLASS_UIAlertController這個(gè)寫法只是約定俗稱的寫法,就像我們?cè)贕CD中用到的onceToken一樣,沒多大意義。
3.文中我用了老外一詞,給人的感覺像是帶有些許輕蔑和嘲諷的口氣,我這里正式聲明,其實(shí)并沒有此意,我們應(yīng)該理解為國(guó)外友人,相反,正是他們無私的分享才使得開源具有很大的意義,給人類進(jìn)步做出了巨大貢獻(xiàn),在這里也對(duì)一切做出分享、開源的朋友們加以感謝。
后記
這里可能我表達(dá)不是很清晰,我們使用GJAlertController來使得UIAlertController兼容低版本其實(shí)是基于Xcode的baseSDK的,就是Xcode編譯的SDK,我們要使用UIAlertController,那我們編譯的SDK肯定要大于等于iOS8.0,否則都沒有UIAlertController這個(gè)類,當(dāng)我們baseSDK的版本大于等于iOS8的時(shí)候能夠確定有這個(gè)類,我們基于這個(gè)baseSDK打包出來的app如果運(yùn)行在低版本中的時(shí)候就是有代表UIAlertController這個(gè)類的標(biāo)簽,但是沒有值,也就是沒有類,因?yàn)槭堑陀趇OS8的系統(tǒng),這時(shí)我們才執(zhí)行上面說的邏輯,正式由于基于baseSDK,所以編譯時(shí)不會(huì)報(bào)警告的。