低版本中使用高版本出现的类怎么办?

原理概述

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

利用这种黑魔法的例子已经越来越多,我所知道的最早使用这种方法的是一个老外在三年为了解决NSUUID而使用的。

我们国内团队开发的FDStackView是一个非常好的开源库,已经有1500+颗星星了,希望大家多多支持我们国内的团队,在FDStackView库中也用到了相同的技术,网上有人发出了分析实现原理的文章,但分析的很浅,而且根本没有说在点子上,使得这种黑魔法的魅力并没有被大家欣赏到,我这里做了一些功课,把这个原理详细的阐述一下,以及这里的关键点在哪里。如果中间过程中有什么错误,还请大家指正,谢谢。

下面简单说一下实现思路。

1.运行时去判断系统中是否已经存在UIAlertController,如果存在,那就什么都不做,静静的看着UIAlertController装逼,这就是iOS8及其之上版本的情况。

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

详细分析

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

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

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

数据段的内存有些特殊,并不是我们理解的32上的指针是4Byte=32bit,64位上指针是8Byte=64bit,大家这里对数据段先有个概念,一会要用它来解释一些现象。

下面开始讲这个黑魔法能够实现的前提,是很重要的部分。在编译的时候,系统中的每个类都在数据段上有一个标签(形式是这样的:OBJC_CLASS$_ClassName),这个标签你可以理解成key,它的value就是该类的类名,举例:数据段中会有一个key是OBJC_CLASS$_UIAlertController,它对应的value就是UIAlertController的类名,当然也就会有OBJC_CLASS$_UIStackView这个标签,标识着UIStackView这个类。

最重要的一点是:在iOS7中,还没有UIAlertController的时候,这个标签OBJC_CLASS$_UIAlertController已经存在了,只是这个标签对应的value值是nil,因为没有这个类,我们可以认为是苹果在给高版本的这个类站位,就是苹果的这个站位才使得我们有幸用上了这个黑魔法。当然每个后出现的类都是有站位的,比如UIStackView。

if this label is Nil or doesnt exist, the class does not exist and cannot be allocated/used

这是我看到的老外在用该种黑魔法实现UUID的时候其中的一句说明,意思是:如果我们没找找到这个标签,就不能为该申请内存,也就不能使用了。

我对这句话的结论持怀疑态度,但又无法做实验验证,因为“标签站位”在早期版本中就存在了,而要找到“更早期”的版本验证该没有标签是很困难的,因为Xcode已经不能支持对“更早期”的版本的编译了,这段话表述有些混乱,大家还是往后看吧。

下面我们看看runtime里动态添加类的方法:

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

大家看官方对函数的说明可以知道: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位(2^3(.align后面的数)),32位的对齐方式是4位(2^2(.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, [email protected]\n"
          "add  %0, %0, [email protected]" : "=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)) static void GJAlertControllerPatchEntry(void){

}

总的来说上面的代码是一个函数,
__attribute__((constructor))只是用来修饰函数的,它起什么作用呢?这里涉及一个关于__attribute__的黑魔法,有兴趣的人可以看我同事的一篇专门介绍__attribute__的文章

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

if (objc_getClass("UIAlertController")) {
    return;
}

系统中有UIAlertController类的话,直接返回,这个逻辑之前已经提到过了。

    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里,之所以这么麻烦是因为不同架构的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);

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

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

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

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

我之所以要写这篇文章,主要是在欣赏:

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

这段代码的美丽与魅力,表达我对这段代码,及其想到这样使用这段代码的人的敬佩当然其实用其他的runtime函数在这里也也可以做相同的事情,具体看我刚刚发的那个老外的链接。

文/二亮子(简书作者)
原文链接:http://www.jianshu.com/p/55180ade32d1
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

时间: 2024-08-05 11:11:12

低版本中使用高版本出现的类怎么办?的相关文章

高版本->低版本迁移,低版本客户端连接高版本数据库EXP导出报错EXP-00008,ORA-01455,EXP-00000

生产环境: 源数据库:RHEL + Oracle 11.2.0.3 目标数据库:HP-UX + Oracle 10.2.0.4 需求:迁移部分表  11.2.0.3-->10.2.0.4,若迁移范围内的有些表在目标库已经存在,则替换. 本次迁移数据量<1G. 初定方案:低版本的客户端连接到高版本数据库,用低版本导出,低版本导入. 1.采用初定方案,目标数据库所在服务器连接到源数据库,exp导出过程中报错. ZJCRNOPDB 36: sqlplus -version SQL*Plus: Rel

如何让VMware低版本运行VMware高版本创建的虚拟机

如何让VMware低版本运行VMware高版本创建的虚拟机 问题描述: 本机安装的VMware Workstation是10版本,之前VMware Workstation 11版本创建的虚拟机,在运行虚拟机时报错: The configuration file "vmname.vmx" was created by a VMware product that is incompatible with this version of VMware Workstation and cann

技巧:低版本VS打开高版本VS创建的工程

当用低版本VS打开高版本VS创建的工程时,会出现: 方案:将该工程的解决方案文件的后缀由xxx.sln改成了xxx.txt然后,查看其内容如下: Microsoft Visual Studio Solution File, Format Version 11.00# Visual Studio 2010 将相应内容替换,然后再改回sln后缀即可. Microsoft Visual Studio Solution File, Format Version 10.00# Visual Studio

补充:关于如何用低版本vs打开高版本项目存在的一些后续问题

在"如何用低版本vs打开高版本项目"一文中,分享了一个解决方案,但是,出现一个问题:有些解决方案修改参数("如何用低版本vs打开高版本项目"解决方案详解)时,会无法正常打开,这时候新的解决方案出现. 就wpf为例,还是低版本打开高版本问题. 1.找到.csproj,双击打开. 2.重新生成解决方案.sln文件. 3. .sln文件的替换和保存.

iOS开发中的高版本宏检测

在框架开发中,为了适配最低的系统版本,我们需要注意不使用高版本API.但除了个人注意之外,是否可以在编译时由系统提示超出兼容版本的API使用情况呢, 观察了CF_AVAILABLE_IOS宏之后,我们给出了如下解决方案,即更改iOS SDK.具体方案如下: 添加位置: Frameworks-> CoreFoundation -> CFAvalailability.h line 115  添加代码: /* added by Mitty */ #define __NEP_2_0 availabil

如何用低版本vs打开高版本项目

今天就以vs2010打开vs2013项目为例. 第一步:找到你用vs2013创建的项目,里面有个.sln文件. 注:该文件不要急着用vs打开,先用记事本打开.这时候你会发现里面的代码不是你所熟悉的代码,除了前两行其他的都不是重点,至少与这次讲解没关系. 第二步:在第一步的的前提下,对.sln文件修改. 注:①改version后面的数字为11.00,vs2013会显示为12.00; ②注意第二行内容,意思大概是打开工具版本,vs2013会显示2013,这时候,这个数字也要改为2010. 以上所述,

设置低版本VDA注册高版本DDC

获取计算机目录信息获取交付组目录信息更改计算机目录接受7.8版本的VDI注册.更改交付组接受7.8版本的VDI注册. 原文地址:http://blog.51cto.com/kuazhang/2336045

怎么将低版本的CAD转换成高版本

怎么将低版本的CAD转换成高版本?就是在我们日常的工作中,每天最常见的就是CAD格式的图纸,但是有的时候在网上查找的CAD图纸,在进行打开的时候发现打不开,那就是因为CAD文件的版本太低,导致在较高的CAD编辑器中打不开,这个时候我们就需要将CAD版本精装转换,那怎么将低版本的CAD转换成高版本,具体要怎么来进行操作了,下面小编就是用迅捷CAD转换来教大家具体的操作步骤,有兴趣的朋友可以来看一下. 第一步:首先打开自己常用的电脑,如果电脑桌面中没有CAD转换器的,可以在电脑桌面中任意的打开一个浏

MSSQL数据库高版本迁移到低版本

起因是因为客户要把系统从阿里云迁移到本地服务器,阿里云上的数据库版本是MSSQL2016,客户提供的服务器是Server2008R2的,问题就来了,Server2008不支持2016版本,最后只能装的2012版本,那就需要从2016迁移到2012. 过程是一波三折,网上流传选择一下兼容模式,如下图 然后再去备份,不知道别人是怎么成功的或者是他们自己也没有成功,我反正最后还是报错,低版本不支持高版本. 最后没本法只能采用生成架构跟数据的脚本来进行导入了 好吧,好在我们数据量还不是很大,sql文件导