美文网首页面试题
Runtime相关问题

Runtime相关问题

作者: 6ffd6634d577 | 来源:发表于2020-03-30 21:20 被阅读0次
    1. 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)

    objc4-779源码

    首先,关于NSObject,objc_class 和 objc_object 三者之间的关系,我们可以用下面的图来更清晰的了解:


    image.png
    一 NSObject

    NSObject是OC 中的基类,除了NSProxy其他都继承自NSObject

    @interface NSObject <NSObject> {
    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
        Class isa  OBJC_ISA_AVAILABILITY;
    #pragma clang diagnostic pop
    }
    
    二 对象结构体 objc_object

    在运行时,类的对象被定义为objc_object类型,就是对象结构体,在OC 中每一个对象都是一个结构体,结构体都包含了一个isa成员变量。根据isa的定义可以知道,类型为isa_t类型的

    struct objc_object {
    private:
        isa_t isa;
    
    public:
        // ISA() assumes this is NOT a tagged pointer object
        Class ISA();
        // getIsa() allows this to be a tagged pointer object
        Class getIsa();
    
        // 省略其余方法
        ...
    }
    

    isa_t 的定义是什么?
    是一个union的结构对象,类似于C++结构体,其内部可以定义为成员变量和函数,是一个联合类型,其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间。

    三 类结构体 objc_class

    类也是一个对象,类的结构体objc_class 是继承自objc_object的,具备对象的所有特征

    iOS中不管类对象还是元类对象类型都是Class,而Class的结构则是objc_class结构体

    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    
    //注意,有些人看到的objc_class结构体定义不一样,有OBJC2_UNAVAILABLE 的注释,在OC 2.0中,
    //这种关于之前objc_class的定义已经废弃掉了,可以在 objc-runtime-new.h 看OC 2.0之后的,如下:
    
    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // 方法缓存  formerly cache pointer and vtable 
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() const {
            return bits.data();
        }
        void setData(class_rw_t *newData) {
            bits.setData(newData);
        }
        ...//此处省略
    }
    
    struct class_data_bits_t {
        friend objc_class;
    
        // Values are the FAST_ flags above.
        uintptr_t bits;
        private:
        ...
     
        public:
        class_rw_t* data() {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
        void setData(class_rw_t *newData)
        {
            assert(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
            // Set during realization or construction only. No locking needed.
            // Use a store-release fence because there may be concurrent
            // readers of data and data's contents.
            uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
            atomic_thread_fence(memory_order_release);
            bits = newBits;
        }
        ...
    

    cache_t cache

    #if __LP64__
    typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    #else
    typedef uint16_t mask_t;
    #endif
    
    struct cache_t {
        struct bucket_t *_buckets;
        mask_t _mask;
        mask_t _occupied;
    
    public:
        struct bucket_t *buckets();
        mask_t mask();
        mask_t occupied();
        void incrementOccupied();
        void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
        void initializeToEmpty();
    
        mask_t capacity();
        bool isConstantEmptyCache();
        bool canBeFreed();
    
        static size_t bytesForCapacity(uint32_t cap);
        static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    
        void expand();
        void reallocate(mask_t oldCapacity, mask_t newCapacity);
        struct bucket_t * find(cache_key_t key, id receiver);
    
        static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
    };
    
    image.png

    根据源码,我们可以知道 cache_t 中存储了一个 bucket_t 的结构体,和两个unsigned int的变量。
    mask:分配用来缓存bucket的总数。
    occupied:表明目前实际占用的缓存bucket的个数。

    bucket_t 的结构体中存储了一个unsigned long和一个IMP。IMP是一个函数指针,指向了一个方法的具体实现。

    cache_t 中的bucket_t *_buckets其实就是一个散列表,用来存储Method的链表。

    Cache 的作用主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

    class_data_bits_t bits

    存储类的方法、属性、遵循的协议等信息的地方

    class_data_bits_t bits有一个成员uintptr_t bits, 可以理解为一个‘复合指针’。什么意思呢,就是bits不仅包含了指针,同时包含了Class的各种异或flag,来说明Class的属性。把这些信息复合在一起,仅用一个uint指针bits来表示。当需要取出这些信息时,需要用对应的以FAST_前缀开头的flag掩码bits按位与操作。

    例如,我们需要取出Classs的核心信息class_rw_t, 则需要调用方法:

        class_rw_t* data() {
            return (class_rw_t *)(bits & FAST_DATA_MASK);
        }
    
    image.png image.png
    2. 为什么要设计metaclass

    类对象(class object)中包含了类的实例变量,实例方法的定义,
    元类对象(metaclass object)中包括了类对象的方法,也就是类方法(也就是C++中的静态方法)的定义。

    那么可不可以把元类干掉,在类中把实例方法和类方法存在两个不同的数组中?

    __class_lookupMethodAndLoadCache3
    
    IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
    {
        return lookUpImpOrForward(cls, sel, obj, 
                                  YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
    }
    
    `lookUpImpOrForward`用于查找方法
    1、首先会再一次的从类中寻找需要调用方法的缓存,如果能命中缓存直接返回该方法的实现,如果不能命中则继续往下走。
    2、从类的方法列表中寻找该方法,如果能从列表中找到方法则对方法进行缓存并返回该方法的实现,如果找不到该方法则继续往下走。
    3、从父类的缓存寻找该方法,如果父类缓存能命中则将方法缓存至当前调用方法的类中(注意这里不是存进父类),如果缓存未命中则遍历父类的方法列表,之后操作如同第2步,未能命中则继续走第3步直到寻找到基类。
    4、如果到基类依然没有找到该方法则触发动态方法解析流程。=
    5、还是找不到就触发消息转发流程
    

    答:行是肯定可行的,但是在lookUpImpOrForward执行的时候就得标注上传入的cls到底是实例对象还是类对象,这也就意味着在查找方法的缓存时同样也需要判断cls到底是个啥。

    从OC的消息机制分析了元类存在的意义,元类的存在巧妙的简化了实例方法和类方法的调用流程,大大提升了消息发送的效率

    metaclass代表的是类对象的对象,它存储了类的类方法,它的目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则。

    具体可以参考这篇文章

    3. class_copyIvarList & class_copyPropertyList区别

    class_copyIvarList

    获取 类对象 中的所有实例变量信息,从 class_ro_t 中获取

    class_copyPropertyList

    获取 类对象 中的属性信息, class_rw_tproperties,先后输出了 category / extension/ baseClass 的属性,而且仅输出 当前的类 的属性信息,而不会向上去找 superClass 中定义的属性。

    Q1: class_ro_t 中的 baseProperties 呢?
    Q2: class_rw_t 中的 properties 包含了所有属性,那何时注入进去的呢?
    An: methodizeClass 方法,会把类里面的属性,协议,方法都加载进来

        property_list_t *proplist = ro->baseProperties;
        if (proplist) {
            rw->properties.attachLists(&proplist, 1);
        }
    
    4. class_rw_t 和 class_ro_t 的区别
    image.png
    class_rw_t 表示read write,class_ro_t 表示 read only
    
    struct class_rw_t {
        // Be warned that Symbolication knows the layout of this structure.
        uint32_t flags;
        uint32_t version;
    
        const class_ro_t *ro;
    
        method_array_t methods;
        property_array_t properties;
        protocol_array_t protocols;
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;
    
    #if SUPPORT_INDEXED_ISA
        uint32_t index;
    #endif
    }
    
    struct class_ro_t {
        uint32_t flags;
        uint32_t instanceStart;
        uint32_t instanceSize;
    #ifdef __LP64__
        uint32_t reserved;
    #endif
    
        const uint8_t * ivarLayout;
    
        const char * name;
        method_list_t * baseMethodList;
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars;
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    };
    

    每个类都对应有一个class_ro_t结构体和一个class_rw_t结构体。在编译期间class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址

    区别在于:class_ro_t存放的是编译期间就确定的属性、方法和遵守协议;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容

    OC对象中存储的属性、方法、遵循的协议数据其实被存储在这两块儿内存区域的,而我们通过runtime动态修改类的方法时,是修改在class_rw_t区域中存储的方法列表。

    参考这篇文章
    更加详细的分析,请看@Draveness 的这篇文章深入解析 ObjC 中方法的结构

    5. category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序

    objc_init ->... -> realizeClass -> methodizeClass(用于Attach categories)-> attachCategories -> attachLists

    在运行时调用 realizeClass方法,会做以下3件事情:

    1. class_data_bits_t调用 data方法,将结果从 class_rw_t强制转换为 class_ro_t指针
    2. 初始化一个 class_rw_t结构体
    3. 设置结构体 ro的值以及 flag

    最后调用methodizeClass方法,把分类里面的属性,协议,方法都加载进来。
    关键就是在methodizeClass 方法实现中

    category的加载是在运行时发生的,加载过程是,把category的实例方法、属性、协议添加到类对象上。把category的类方法、属性、协议添加到metaclass上。

    category的load方法执行顺序是根据类的编译顺序决定的,即:xcode中的Build Phases中的Compile Sources中的文件从上到下的顺序加载的。

    category并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category添加的methodA会排在原有类的methodA的前面,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category里的对应方法

    6、category & extension区别,能给NSObject添加Extension吗,结果如何?

    category:分类

    • 给类添加新的方法
    • 不能给类添加成员变量
    • 通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和* setter方法,同时也不能生成带下划线的成员属性
    • 是运行期决定的

    注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。

    extension:扩展

    • 可以给类添加成员变量,但是是私有的
    • 可以給类添加方法,但是是私有的
    • 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
    • 伴随着类的产生而产生,也随着类的消失而消失
    • 必须有类的源码才可以给类添加extension,所以对于系统一些类,如nsstring,就无法添加类扩展

    不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension。

    8. 在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么

    OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, ...)方法进行调用,这个方法第一个参数是一个消息接收者对象,runtime通过这个对象的isa指针找到这个对象的类对象,从类对象中的cache中查找是否存在SEL对应的IMP,若不存在,则会在 method_list中查找,如果还是没找到,则会到supper_class中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)进行消息转发。

    9. IMP、SEL、Method的区别和使用场景
    typedef struct objc_selector *SEL
    

    SEL: 方法选择器,虽然 SELobjc_selector 结构体指针,但实际上它只是一个 C 字符串,方法的名称

    typedef id (*IMP)(id, SEL, …)
    

    IMP-函数指针,指向实际执行函数体

    typedef struct objc_method *Method
    
    /// Method
    struct objc_method {
        SEL method_name; 
        char *method_types;
        IMP method_imp;
    };
    

    方法名 method_name 类型为 SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同。
    方法类型 method_types 是个 char 指针,其实存储着方法的参数类型返回值类型,即是 Type Encoding 编码。
    method_imp 指向方法的实现,本质上是一个函数的指针,就是前面讲到的 Implementation。

    使用场景:
    实现类的swizzle的时候会用到,通过class_getInstanceMethod(class, SEL)来获取类的方法Method,其中用到了SEL作为方法名
    调用method_exchangeImplementations(Method1, Method2)进行方法交换

    我们还可以给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types),该方法需要我们传递一个方法的实现函数IMP,例如:

    static void funcName(id receiver, SEL cmd, 方法参数...) {
       // 方法具体的实现   
    }
    

    函数第一个参数:方法接收者,第二个参数:调用的方法名SEL,方法对应的参数,这个顺序是固定的。

    10、load、initialize方法的区别什么?在继承关系中他们有什么区别

    load:当类被装载的时候被调用,只调用一次

    • 调用方式并不是采用runtime的objc_msgSend方式调用的,而是直接采用函数的内存地址直接调用的
    • 多个类的load调用顺序,是依赖于compile sources中的文件顺序决定的,根据文件从上到下的顺序调用
    • 子类和父类同时实现load的方法时,父类的方法先被调用
    • 本类与category的调用顺序是,优先调用本类的(注意:category是在最后被装载的)
    • 多个category,每个load都会被调用(这也是load的调用方式不是采用objc_msgSend的方式调用的),同样按照compile sources中的顺序调用的
    • load是被动调用的,在类装载时调用的,不需要手动触发调用

    initialize:当类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次

    • 调用方式是通过runtime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕
    • 子类和父类同时实现initialize,父类的先被调用,然后调用子类
    • 本类与category同时实现initialize,category覆盖本类的方法,只调用category的initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的)
    • initialize是主动调用的,只有当类第一次被用到的时候才会触发

    参考这篇文章

    11. _objc_msgForward函数是做什么的,直接调用它将会发生什么?

    _objc_msgForward是一个函数指针(和 IMP 的类型一样),是用于消息转发的:
    当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发

    也就是说_objc_msgForward在进行消息转发的过程中会涉及以下这几个方法:

    resolveInstanceMethod:方法 (或 resolveClassMethod:)。
    forwardingTargetForSelector:方法
    methodSignatureForSelector:方法
    forwardInvocation:方法
    doesNotRecognizeSelector: 方法

    12. 通过runtime动态创建一个类

    要创建一个新类,

    1. 首先调用objc_allocateClassPair
    2. 然后使用class_addMethodclass_addIvar等函数设置类的属性。
    3. 完成构建类后调用objc_registerClassPair
    /** 
     * 创建一个新类和元类.
     * 
     * @param superclass 这个类是新创建的类的父类,可以传入Nil去创建一个新根类.
     * @param name 这个字符串是类的名字(例:"NSObject")
     * @param extraBytes 一般传入0 
     * @return 新的类,如果返回的是Nil,那么就是这个类创建失败了(例:创建的是"NSObject"类,然而这个类已经存在了)
     */
    objc_allocateClassPair(
    Class _Nullable superclass, 
    const char * _Nonnull name,  
    size_t extraBytes
    ) 
    

    objc_allocateClassPair只返回一个值:Class。那么pair的另一半在哪里呢?
    是的,估计你已经猜到了这个另一半就是meta-class

    /** 
     * 注册使用`objc_allocateClassPair`方法创建的类
     * 
     * @param cls 需要注册的类(不能为Nil)
     */ 
    objc_registerClassPair(Class _Nonnull cls) 
    

    代码样例

    /// 创建一个元类
    Class class = objc_allocateClassPair([NSObject class], "Person", 0);
    /// 添加方法
    //class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)
    /// 添加属性
    //class_addIvar(<#Class  _Nullable __unsafe_unretained cls#>, <#const char * _Nonnull name#>, <#size_t size#>, <#uint8_t alignment#>, <#const char * _Nullable types#>)
    /// 注册元类
    objc_registerClassPair(class);
    
    13. object_getClass 和 object_setClass

    1. Class object_getClass(id obj)

        //返回对象的isa指针
         eg:参数为实例对象返回类对象
         eg:参数为类对象返回元类对象
         eg:参数为元类对象返回根元类对象
         eg:参数为根元类对象返回自身
    
        Class object_getClass(id obj)
    
       // 根据字符串返回类对象
        Class _Nullable objc_getClass(const char * _Nonnull name)
    
        // 创建一个NSObject对象obj,然后获取obj的类
        NSObject *obj = [[NSObject alloc] init]; // 1
        Class objClass = object_getClass(obj); // 2
        NSLog(@"%@", NSStringFromClass(objClass)); // 3
        
        Class nsobjectClass = object_getClass([NSObject class]); // 4
        NSLog(@"%@", NSStringFromClass(nsobjectClass)); // 5
        
        BOOL isMeta1 = class_isMetaClass(objClass); // 6
        BOOL isMeta2 = class_isMetaClass(nsobjectClass); // 7
        NSLog(@"isMeta1:%i, isMeta2:%i", isMeta1, isMeta2); // 8
    
    //打印结果
    2020-04-03 18:08:48.875921+0800 ImplementKVO[81240:4259330] NSObject
    2020-04-03 18:08:48.876057+0800 ImplementKVO[81240:4259330] NSObject
    2020-04-03 18:08:48.876155+0800 ImplementKVO[81240:4259330] isMeta1:0, isMeta2:1
    

    说明:class_isMetaClass函数的作用是判断传入的类是不是元类,经过判断是不是元类可看出区别,因此可得出结论:
    object_getClass函数的参数传一个类的实例时,返回的是该实例的类对象
    参数传时,返回的是该类的元类

        Class class1 = [obj class];
        Class class2 = [[NSObject class] class];
    
        NSLog(@"%@", NSStringFromClass(class1));
        NSLog(@"%@", NSStringFromClass(class2));
        NSLog(@"%i, %i", class_isMetaClass(class1), class_isMetaClass(class2));
    
    2020-04-03 18:17:05.175123+0800 ImplementKVO[81544:4270627] NSObject
    2020-04-03 18:17:05.175261+0800 ImplementKVO[81544:4270627] NSObject
    2020-04-03 18:17:05.175363+0800 ImplementKVO[81544:4270627] 0, 0
    

    通过打印结果可以看出,class方法的调用者是一个实例时,获取到的是该实例的类,此时和object_getClass函数作用相同;而调用者是一个(比如[NSObject class])时,获取到的并不是该类的元类,此时和object_getClass函数的作用不同.

    //[xxx class]方法内部实现
    
    + (Class)class {
    return self;
    }
    
    - (Class)class {
    return object_getClass(self);
    }
    

    2. Class object_setClass(id obj, Class cls)

    /**
    将一个对象设置为别的类类型,返回原来的Class ,将一个对象的isa指针指向设置的Class
     * Sets the class of an object.
     * 
     * @param obj The object to modify.
     * @param cls A class object.
     * 
     * @return The previous value of \e object's class, or \c Nil if \e object is \c nil.
     */
    
    Class _Nullable  object_setClass(id _Nullable obj, Class _Nonnull cls) 
    
    
        // 分别创建一个可变数组对象mutArray和不可变数组对象array
        NSMutableArray *mutArray = [NSMutableArray arrayWithObjects:@"a", @"b", nil]; // 1
        NSArray *array = @[@"c", @"d"]; // 2
    
        // 获取这两个对象mutArray和array的类并打印
        Class mutArrayClassBefore = object_getClass(mutArray); // 3
        Class arrayClassBefore = object_getClass(array); // 4
        NSLog(@"%@ -- %@", NSStringFromClass(mutArrayClassBefore), NSStringFromClass(arrayClassBefore)); // 5
    
        Class setclass = object_setClass(mutArray, arrayClassBefore); // 6
        NSLog(@"%@", NSStringFromClass(setclass)); // 7
    
        Class mutArrayClassNow = object_getClass(mutArray); // 8
        Class arrayClassNow = object_getClass(array); // 9
    
        NSLog(@"%@ -- %@", NSStringFromClass(mutArrayClassNow), NSStringFromClass(arrayClassNow)); // 10
    
    2020-04-03 18:27:43.569594+0800 ImplementKVO[81775:4277728] __NSArrayM -- __NSArrayI
    2020-04-03 18:27:43.569724+0800 ImplementKVO[81775:4277728] __NSArrayM
    2020-04-03 18:27:43.569822+0800 ImplementKVO[81775:4277728] __NSArrayI -- __NSArrayI
    

    从打印结果可以看出用object_setClass函数将可变数组对象mutArray的类设置为它的父类是可以的,此时再用mutArray调用NSMutableArray的方法会导致程序crash,如

    [mutArray addObject:@"e"]; // 11
    

    crash信息为经典的:

    2020-04-03 18:31:04.101045+0800 ImplementKVO[81860:4280357] -[__NSArrayI addObject:]: unrecognized selector sent to instance 0x6000000fec40
    

    总结

    • 使用object_setClass后会使对象的isa的值指向新的Class
    • 使用object_setClass对象的内存布局不会发生变化。
    • 使用object_setClass不能访问超过原对象申请的内存区域,否则程序会crash
    14. Method Swizzle注意事项
    1. 对于一般的Class.推荐在load方法中交换, 系统的类,可以通过Category添加方法交换
    2. 避免交换父类方法(先class_addMethod,判断是否成功)
    3. 交换方法应在+load方法
    4. 交换方法应该放到dispatch_once中执行
    5. 交换的分类方法应该添加自定义前缀,避免冲突
    6. 交换的分类方法应调用原实现

    注意:如果是交换不同类的方法,并且在方法中访问了类的属性,会造成Crash,更安全的做法,Runtime 还提供了另一种 Swizzle 函数 method_setImplementation。
    method_setImplementation 可以让我们提供一个新的函数来代替我们要替换的方法。 而不是将两个方法的实现做交换。 这样就不会造成 method_exchangeImplementations 的潜在对已有实现的副作用了。
    method_setImplementation 接受两个参数,第一个还是我们要替换的方法, 而第二个参数是一个 IMP 类型的。

    Objc 黑科技 - Method Swizzle 的一些注意事项

    例如:统计VC加载次数并打印

    • UIViewController+Logging.m
    #import "UIViewController+Logging.h"
    #import <objc/runtime.h>
    
    @implementation UIViewController (Logging)
    
    + (void)load
    {
        swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
    }
    
    - (void)swizzled_viewDidAppear:(BOOL)animated
    {
      //交换前需要先调用系统的方法初始化调用[self xxx]的系统方法的时候.需要把xxx改成xx_xxx(你交换的方法)
      //这里是因为你的方法已经和系统的方法交换了,调用你的方法其实是调用的系统方法,调用系统方法的话就调用的是你的方法,
      //然后就会产生循环调用.
        // call original implementation
        [self swizzled_viewDidAppear:animated];
        
        // Logging
        NSLog(@"%@", NSStringFromClass([self class]));
    }
    
    void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
    {
        // the method might not exist in the class, but in its superclass
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // class_addMethod will fail if original method already exists
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        // the method doesn’t exist and we just added one
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }
        else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
    }
    
    

    参考文章

    15. iOS 中内省的几个方法有哪些?内部实现原理是什么

    反射机制

    16. 怎么防止UI控件短时间多次激活事件?

    需求

    当前项目写好的按钮,还没有全局地控制他们短时间内不可连续点击(也许有过零星地在某些网络请求接口之前做过一些控制)。现在来了新需求:本APP所有的按钮1秒内不可连续点击。你怎么做?一个个改?这种低效率低维护度肯定是不妥的。

    方案

    给按钮添加分类,并添加一个点击事件间隔的属性,执行点击事件的时候判断一下是否时间到了,如果时间不到,那么拦截点击事件。
    怎么拦截点击事件呢?其实点击事件在runtime里面是发送消息,我们可以把要发送的消息的SEL 和自己写的SEL交换一下,然后在自己写的SEL里面判断是否执行点击事件。

    实践

    UIButton是UIControl的子类,因而根据UIControl新建一个分类即可

    • UIControl+Limit.m
    #import "UIControl+Limit.h"
    #import <objc/runtime.h>
    
    static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval";
    static const char *UIControl_ignoreEvent="UIControl_ignoreEvent";
    
    @implementation UIControl (Limit)
    
    #pragma mark - acceptEventInterval
    - (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval
    {
        objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    -(NSTimeInterval)acceptEventInterval {
        return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue];
    }
    
    #pragma mark - ignoreEvent
    -(void)setIgnoreEvent:(BOOL)ignoreEvent{
        objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
    }
    
    -(BOOL)ignoreEvent{
        return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue];
    }
    
    #pragma mark - Swizzling
    +(void)load {
        Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
        Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:));
        method_exchangeImplementations(a, b);//交换方法
    }
    
    - (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event
    {
        if(self.ignoreEvent){
            NSLog(@"btnAction is intercepted");
            return;
        }
        if(self.acceptEventInterval>0){
            self.ignoreEvent=YES;
            //延迟执行
            [self performSelector:@selector(setIgnoreEventWithNo)  withObject:nil afterDelay:self.acceptEventInterval];
        }
        //调用原方法实现
        [self swizzled_sendAction:action to:target forEvent:event];
    }
    
    -(void)setIgnoreEventWithNo{
        self.ignoreEvent=NO;
    }
    
    @end
    
    
    • ViewController.m
    -(void)setupSubViews{
        
        UIButton *btn = [UIButton new];
        btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)];
        [btn setTitle:@"btnTest"forState:UIControlStateNormal];
        [btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal];
        btn.acceptEventInterval = 3;
        [self.view addSubview:btn];
        [btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];
    }
    
    - (void)btnAction{
        NSLog(@"btnAction is executed");
    }
    
    //日志打印
    2020-04-06 21:07:44.244251+0800 Runtime[5230:326947] btnAction is executed
    2020-04-06 21:07:45.077352+0800 Runtime[5230:326947] btnAction is intercepted
    2020-04-06 21:07:45.871633+0800 Runtime[5230:326947] btnAction is intercepted
    2020-04-06 21:07:46.844650+0800 Runtime[5230:326947] btnAction is intercepted
    2020-04-06 21:07:47.719857+0800 Runtime[5230:326947] btnAction is executed
    
    17. 防奔溃处理

    崩溃拦截

    18. AOP(切面编程)

    参考总结

    相关文章

      网友评论

        本文标题:Runtime相关问题

        本文链接:https://www.haomeiwen.com/subject/gshpuhtx.html