zoukankan      html  css  js  c++  java
  • 低版本中使用高版本出现的类怎么办?

    原理概述

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

    利用这种黑魔法的例子已经越来越多,我所知道的最早使用这种方法的是一个老外在三年为了解决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
    "
    #if    TARGET_RT_64_BIT
        ".align          3
    "
        "L_OBJC_CLASS_UIAlertController:
    "
        ".quad           _OBJC_CLASS_$_UIAlertController
    "
    #else
        ".align          2
    "
        "_OBJC_CLASS_UIAlertController:
    "
        ".long           _OBJC_CLASS_$_UIAlertController
    "
    #endif
        ".weak_reference _OBJC_CLASS_$_UIAlertController
    "
    );

    这是一段汇编代码,不用担心看不懂它,我也不懂汇编,这不影响我们分析,我简单的解释一下:

    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))
    "
              "movt %0, :upper16:(_OBJC_CLASS_UIAlertController-(LPC0+4))
    "
              "LPC0: add %0, pc" : "=r"(alertController));
    
    #elif TARGET_CPU_ARM64
        __asm("adrp %0, L_OBJC_CLASS_UIAlertController@PAGE
    "
              "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
    "
              "L0: popl %0
    "
              "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))
    "
              "movt %0, :upper16:(_OBJC_CLASS_UIAlertController-(LPC0+4))
    "
              "LPC0: add %0, pc" : "=r"(alertController));
    
    #elif TARGET_CPU_ARM64
        __asm("adrp %0, L_OBJC_CLASS_UIAlertController@PAGE
    "
              "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
    "
              "L0: popl %0
    "
              "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
    著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
  • 相关阅读:
    Knockout应用开发指南 第八章:简单应用举例(2)
    微软ASP.NET站点部署指南(7):生产环境部署
    Knockout应用开发指南 第七章:Mapping插件
    《Microsoft Sql server 2008 Internals》读书笔记第九章Plan Caching and Recompilation(6)
    《Microsoft Sql server 2008 Internals》读书笔记第九章Plan Caching and Recompilation(5)
    《Microsoft Sql server 2008 Internals》读书笔记第九章Plan Caching and Recompilation(3)
    《Microsoft Sql server 2008 Internals》读书笔记第九章Plan Caching and Recompilation(9)
    《Microsoft Sql server 2008 Internals》读书笔记第九章Plan Caching and Recompilation(8)
    Microsoft Visual Studio .NET 2003 引导程序插件下载地址(非官方)
    Vs2010在没有安装SQL Server 2005/2008 Express时如何连接MDF数据文件?
  • 原文地址:https://www.cnblogs.com/yintingting/p/5572632.html
Copyright © 2011-2022 走看看