iOS8中蘋果用UIAlertController來統(tǒng)一管理alert和actionSheet了粱年,之前的UIAlertView和UIActionSheet已經(jīng)廢棄了钦睡。通常我們要兼容iOS7的時(shí)候逆皮,要這樣:
if([[[UIDevicecurrentDevice] systemVersion] floatValue] <=8.0) {//用UIAlertView或UIActionSheet}else{//用UIAlertController}
而GJAlertController解決了這里的系統(tǒng)版本兼容問題,不需要判斷版本柳譬,直接使用:
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:@"title"message:@"message"preferredStyle:UIAlertControllerStyleAlert];[alertVCaddAction:[UIAlertAction actionWithTitle:@"ok"style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnullaction) {? ? NSLog(@"button ok pressed");}]];[alertVCaddAction:[UIAlertAction actionWithTitle:@"cancel"style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnullaction) {? ? 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)入正題硫椰。
原理概述
簡單來說就是三個(gè)字——黑魔法繁调。
利用這種黑魔法的例子已經(jīng)越來越多,我所知道的最早使用這種方法的是一個(gè)老外在三年為了解決NSUUID而使用的靶草。
我們國內(nèi)團(tuán)隊(duì)開發(fā)的FDStackView是一個(gè)非常好的開源庫蹄胰,已經(jīng)有1500+顆星星了,希望大家多多支持我們國內(nèi)的團(tuán)隊(duì)奕翔,在FDStackView庫中也用到了相同的技術(shù)裕寨,網(wǎng)上有人發(fā)出了分析實(shí)現(xiàn)原理的文章,但分析的很淺派继,而且根本沒有說在點(diǎn)子上宾袜,使得這種黑魔法的魅力并沒有被大家欣賞到,我這里做了一些功課驾窟,把這個(gè)原理詳細(xì)的闡述一下庆猫,以及這里的關(guān)鍵點(diǎn)在哪里。如果中間過程中有什么錯(cuò)誤绅络,還請(qǐng)大家指正月培,謝謝。
下面簡單說一下實(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è)重要前提之前举娩,我們先來簡單說說內(nèi)存。內(nèi)存有好多種,我們最熟悉的有:棧:函數(shù)的實(shí)現(xiàn)就依賴于棧铜涉,函數(shù)中簡單類型的局部變量也都開辟在棧上智玻;堆:我們平時(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 Classobjc_allocateClassPair(Class superclass,constchar*name,size_textraBytes)__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)心看不懂它,我也不懂匯編往扔,這不影響我們分析贩猎,我簡單的解釋一下:
1.__asm是在C、C++源碼中放入?yún)R編代碼(OC是C的超集)萍膛。
2..align是對(duì)指令或數(shù)據(jù)的存放地址進(jìn)行對(duì)齊吭服,有些CPU架構(gòu)要求固定的指令長度,并且存放地址相對(duì)于2的冪指數(shù)圓整蝗罗,否則無法運(yùn)行艇棕,比如arm。有些不要這樣也能運(yùn)行串塑,就是執(zhí)行效率稍微低點(diǎn)沼琉,如i386。
3.64位的對(duì)齊方式是8位(2^3(.align后面的數(shù)))桩匪,32位的對(duì)齊方式是4位(2^2(.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))staticvoidGJAlertControllerPatchEntry(void) {staticdispatch_once_tonceToken;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_X86void*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#endifif(alertController && !*alertController) {? ? ? ? ? ? ? ? Classclass= objc_allocateClassPair([GJAlertControllerclass],"UIAlertController",0);if(class) {? ? ? ? ? ? ? ? ? ? objc_registerClassPair(class);? ? ? ? ? ? ? ? ? ? *alertController =class;? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? });}
大家堅(jiān)持住,這是要分析的最后一段代碼了乳绕。
__attribute__((constructor))staticvoidGJAlertControllerPatchEntry(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_X86void *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) {? ? Classclass=objc_allocateClassPair([GJAlertControllerclass],"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)添加類
Classclass=objc_allocateClassPair([GJAlertControllerclass],"UIAlertController",0);
這決對(duì)是畫龍點(diǎn)睛的一筆沥邻,我們之前用的時(shí)候都是繼承一個(gè)系統(tǒng)類危虱,動(dòng)態(tài)添加一個(gè)自定義的類:
Class person = objc_allocateClassPair([NSObjectclass],"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ì)所有的一切都明白了吧扇苞。
我之所以要寫這篇文章,主要是在欣賞:
Classclass=objc_allocateClassPair([GJAlertControllerclass],"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)該理解為國外友人晌姚,相反,正是他們無私的分享才使得開源具有很大的意義歇竟,給人類進(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)警告的。