低版本中使用高版本中出现的类”之技术实现原理详解

引言

看到GJAlertController ,FDStackView开源库,都使用一些黑魔法来解决高低版本不同方法的引用问题,以iOS8中苹果用UIAlertController为例,一般实用区分版本来处理

if ([[[UIDevice currentDevice] systemVersion] floatValue] <= 8.0) {
  //用UIAlertView或UIActionSheet
} else {
  //用UIAlertController
}

而GJAlertController解决了这里的系统版本兼容问题,不需要判断版本,直接使用:

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 = @"请输入用户名";
}];

[self presentViewController:alertVC animated:YES completion:nil];

原理概述

简单来说就是三个字——黑魔法。

利用这种黑魔法的例子已经越来越多,而最早使用这种方法的是一个老外在三年为了解决NSUUID而使用的。
实现思路:

1.运行时去判断系统中是否已经存在UIAlertController,如果存在,那就什么都不做.

2.如果系统中没有UIAlertController类,我们在运行时中做一些“手脚”,让我们的GJAlertController在低版本中去完成这个问题。这一步是精华所在,下面分析代码的时候回详细说明

详细分析

实现的代码本身其实并不重要,下面先讲最重要的一个东西,它是这种黑魔法能够得以实现的前提。

在揭示这个重要前提之前,我们先来简单说说内存。内存有好多种,我们最熟悉的有:栈:函数的实现就依赖于栈,函数中简单类型的局部变量也都开辟在栈上;堆:我们平时用的Object都是开辟在堆上的;数据段:这个对我们相对陌生,但是其实静态字符串就是存在数据段的eg:

NSString *testStr = @"hello world";
NSLog(@"testStr:%p", testStr);
testStr:0xb4338 //32位的机器上
testStr:0x106326580 //64位的机器上

在编译的时候,系统中的每个类都在数据段上有一个标签(形式是这样的:OBJC_CLASS$_ClassName),这个标签你可以理解成key,它的value就是该类的类名,举例:数据段中会有一个key是OBJC_CLASS$_UIAlertController,它对应的value就是UIAlertController的类名,当然也就会有OBJC_CLASS$_UIStackView这个标签,标识着UIStackView这个类。

最重要的一点是:在iOS7中,还没有UIAlertController的时候,这个标签OBJC_CLASS$_UIAlertController已经存在了,只是这个标签对应的value值是nil,因为没有这个类,我们可以认为是苹果在给高版本的这个类站位
runtime创建类

OBJC_EXPORT Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

superClass 是你要添加的类的父类,name是你要添加的类的名字,extraBytes一般传0,它会返回一个新类,如果名字被占用了会返回Nil。

由此要说明的两个重要结论:

1.如果OBJC_CLASS$_ClassName标签存在,但是对应的类不存在(相当于有key,但是value是nil)此时动态添加类是可以成功的。

2.如果OBJC_CLASS$_ClassName标签和对应的类都有的话,此时动态添加类是不成功的,返回nil。

__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"
);

1.__asm是在C、C++源码中放入汇编代码(OC是C的超集)。

2..align是对指令或数据的存放地址进行对齐,有些CPU架构要求固定的指令长度,并且存放地址相对于2的幂指数圆整,否则无法运行,比如arm。有些不要这样也能运行,就是执行效率稍微低点,如i386。

3.64位的对齐方式是8位(23(.align后面的数)),32位的对齐方式是4位(22(.align后面的数))。对齐只对紧挨着它的那条语句起作用,既,L_OBJC_CLASS_UIAlertController或_OBJC_CLASS_UIAlertController。

4..quad声明一组数占64位,.long声明一组数占32位

5..secton 后是指定参数用的,上述汇编的大体意思是在数据段(就是我们之前提到的数据段)找到OBJC_CLASS$_UIAlertController标签并利用.quad、.long声明的一组数来存放它,取名为:_OBJC_CLASS_UIAlertController。

__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;
                }
            }
        }
    });
}

attribute((constructor))修饰的函数会在main函数之前执行,这是我们的最好时机,有了runtime环境,但是main函数还没有执行,一切都“来得及”。

_OBJC_CLASS_UIAlertController中的值拿出来放到alertController里,之所以这么麻烦是因为不同架构的CPU运行的指令集不同,例如,32位就要这样弄:MOVW 把16位立即数放到寄存器的底16位,高16位清0
MOVT 把16位立即数放到寄存器的高16位,低16位不影响。

if (alertController && !*alertController) {
    Class class = objc_allocateClassPair([GJAlertController class], "UIAlertController", 0);
    if (class) {
        objc_registerClassPair(class);
        *alertController = class;
    }
}

如果alertController存在,证明OBJC_CLASS$_UIAlertController标签存在,即key存在,*alertController不存在,证明当前系统中没有这个类,即value不存在。这正是我们之前说的情况,如果我们此时打印alertController的地址,会发现,它的位数和上面数据段中的一样而不是32位或64位,也再次印证了标签在数据段上。

此时执行最重要的一句代码——动态添加类

Class class = objc_allocateClassPair([GJAlertController class], "UIAlertController", 0);

这里正好相反,这里是在判断了没有系统类的时候,添加一个系统类,继承自我们的类:GJAlertController,也就是说,在低版本中,没有UIAlerController,我们动态添加这个类,让他继承GJAlertController,我们在GJAlertController中,实现一套与系统UIAlertController一模一样的API给人造成的错觉好像是在低版本中也能使用UIAlertController,其实只是一个魔术。

Class person = objc_allocateClassPair([NSObject class], "Person", 0);

这决对是画龙点睛的一笔,我们之前用的时候都是继承一个系统类,动态添加一个自定义的类:

我们在低版本下使用的UIAlertController是我们动态添加的,它什么也没有做,直接继承了GJAlertController,而GJAlertController声明并实现了和系统UIAlertController一模一样的一套API。我们的GJAlertController根本不是一个VC是一个NSObject,只是自己用UIAlertView和UIActionSheet封装成了UIAlertController的API罢了,到这里你应该对所有的一切都明白了吧。

几点说明:

1.为什么要使用汇编?

因为在寻找数据段上OBJC_CLASS$_ClassName标签的时候不支持C、C++、OC等高级语言,只能用汇编。

2.代码中出现的OBJC_CLASS$_UIAlertController与_OBJC_CLASS_UIAlertController有什么关系?

没有任何关系,OBJC_CLASS$_UIAlertController这个是系统中类标签的格式,必须是这样子才可以,而_OBJC_CLASS_UIAlertController只是一个参数名,你叫hellworld也可以(已经测试过可以),大家不要被它俩弄晕了,_OBJC_CLASS_UIAlertController这个写法只是约定俗称的写法,就像我们在GCD中用到的onceToken一样,没多大意义。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,875评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,569评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,475评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,459评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,537评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,563评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,580评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,326评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,773评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,086评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,252评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,921评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,566评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,190评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,435评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,129评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,125评论 2 352

推荐阅读更多精彩内容