• iOS RunTime 底层原理探究


    什么是RunTime

    OC是一门动态性比较强的编程语言 跟C,C++等静态语言有很大的不同。

    静态语言:如C语言 编译阶段就要决定调用哪个函数 如果函数未实现就会报错。

    动态语言:编译阶段并不能决定真正调用哪个函数 只要函数声明过 没有实现也不会报错。

    OC之所以被称为动态语言 就是因为它把一些决定性的工作从编译阶段推迟到运行阶段。OC代码的运行不仅需要编译器,还需要运行时系统(Runtime Sytem)来执行编译后的代码。

    RunTime 是一套底层纯C语言的API。OC代码最终都会被编译器编译为运行时代码。然后通过消息机制决定函数调用的方式。这也是OC作为动态语言使用的基础。

    isa详解

    要想学习RunTime 首先要了解它底层的一些常用的数据结构 比如isa指针。

    之前我们总是认为OC中的每个对象都包含着一个isa指针,实例对象的isa指针 指向类对象 类对象的isa指针指向元类对象 元类对象的isa指向基类。

    那么isa中只有这些信息吗,其实我们可以再深入的探究一下的。在arm64的架构中isa指针并不是直接指向类对象 而是要进行一次位运算 本身的isa指针地址 &ISA_MASK 才能得到类对象或者元类对象。在arm64之前isa就是一个普通的指针,存储着Class meta-Class对象的内存地址。arm64架构开始,对isa指针进行了优化,变成了一个共用体结构,还使用位域来存储更多的信息。所以在arm64架构中,我们拿到isa指针地址后 还要进行&ISA_MASK才能得到Class meta-Class对象的地址。

    如果你看RunTime的源码 你会发现objc_object中的isa指针已经变成isa_t这种共用体结构了。里面通过位域技术存储了更多的信息。

    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits; //存放所有的数据 一共64位 下面struct结构体中的属性 写在前面的在低地址位置 也就是位数的最右边
    
    #if SUPPORT_PACKED_ISA
    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
        struct {
            uintptr_t nonpointer       : 1; //占1位 0代表普通指针 代表着isa只存储着Class meta-Class对象的内存地址 1 代表优化过 使用位域存储着更多的信息
            uintptr_t has_assoc        : 1;//是否设置过关联对象 没有释放更快
            uintptr_t has_cxx_dtor     : 1;//是否有C++的析构函数 没有释放的更快
            uintptr_t shiftcls         : 33;//存储着Class meta-Class对象的内存地址信息
            uintptr_t magic            : 6;//用于在调试时分辨对象是否未完成初始化
            uintptr_t weakly_referenced : 1;//是否被弱指针指向过
            uintptr_t deallocating      : 1;//对象是否正在释放
            uintptr_t has_sidetable_rc  : 1;//引用计数是否过大 无法存储在isa中 如果为1 那么引用计数会存储在一个叫SideTable的类的属性中
            uintptr_t extra_rc          : 19;//存储的值是引用计数减1
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
        };
    }

    很显然位域技术可以用更小的内存 存储更多的信息 比如BOOL值 一般存储一个BOOL值需要一个字节 但是如果使用位域技术 一个自己 0000 0000 用每一位代表一个二进制的信息 一个字节就可以存储8个BOOL值的信息了。

    Class的结构

    struct objc_class : objc_object {
        // Class ISA;
        objc_class;
        Class superclass;
        cache_t cache;             // 方法缓存
        class_data_bits_t bits;    // 用于获取具体的类信息 &FAST_DATA_MASK 得到 class_rw_t
    }
    
    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; //指向了另一张表 ro_t 只读表
    
        method_array_t methods; //方法列表 二维数组 method_array_t 装着 method_list_t 里面装着method_t
        property_array_t properties; //属性信息 二维数组
        protocol_array_t protocols; //协议信息 二维数组
    
        Class firstSubclass;
        Class nextSiblingClass;
    
        char *demangledName;
    }
    
    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; //方法信息 一维数组 method_list_t 装着method_t 
        protocol_list_t * baseProtocols;
        const ivar_list_t * ivars; //属性信息
    
        const uint8_t * weakIvarLayout;
        property_list_t *baseProperties;
    
        method_list_t *baseMethods() const {
            return baseMethodList;
        }
    };

    上面就是objc_class的结构了 可以清晰的看到 存储了isa指针 属性信息 方法信息 协议信息 成员变量等重要信息。

    class_rw_t 里面的methods properties protocols 是二维数组 是可读可写的 包含了类的初始内容(初始化时候已有的属性 协议 方法) 分类的内容。

    class_ro_t 里面的baseMethodList baseProtocols ivars baseProperties是一维数组 是只读的 包含了类的初始内容

    为什么class_rw_t 和 class_ro_t 都存储了类的初始信息呢 是不是感觉有点浪费呢?还记得我们OC中的分类吗,一个类初始的方法 协议 成员变量 属性等信息其实是存储在class_ro_t中的,但是在运行阶段RunTime系统会把class_ro_t存储的方法 属性 协议等信息和分类中的方法 协议 属性等信息 一并合并到class_rw_t中,并且分类的方法靠前。所以class_rw_t存储的原始信息是这样来的。class_rw_t一开始是不存在的 在运行的时候合并的时候 创建出来的。一开始bits是指向class_ro_t的 我们设置了class_rw_t后 才指向class_rw_t的。

    method_t

    struct method_t {
        SEL name; //函数名 SEL代表方法或者函数名字 一般叫做选择器
        const char *types; //编码(返回值类型 参数类型) 是个字符串 根据encode指令编写的 比如 v代表void @代表id类型 :代表SEL类型
        IMP imp; // 指向函数的指针 IMP代表函数的具体实现
    
        struct SortBySELAddress :
            public std::binary_function<const method_t&,
                                        const method_t&, bool>
        {
            bool operator() (const method_t& lhs,
                             const method_t& rhs)
            { return lhs.name < rhs.name; }
        };
    };

    不同类中如果有相同的方法名 他们的选择器是相同的 即SEL相同 可通过@selector() 或者 sel_registerName()获取

     cache_t 方法缓存

    cache 用散列表来缓存曾经调用过的方法 可以提高方法的查找速度.

    我们都知道当向对象发送一个消息的时候,对象会通过自己的isa指针找到类对象或者元类对象存储的方法列表中找到并实现,这个需要遍历方法列表寻找,如果在类对象或者元类对象的方法列表中找不到该方法,类对象和元类对象还会通过自己的superClass指针到自己的父类对像或者父类元类对象的方法列表中遍历寻找,直到找到该方法的实现。如果我们常用的方法每次都这样寻找会很麻烦。所以苹果对我们对象每次调用多的方法 都缓存到类对象或者元类对象的cache中。这样每次调用方法,会先通过isa指针找到类对象或者元类对象的cache列表中查找,如果找到直接调用。找不到,在走以上过程,找到了就缓存到cache列表中。极大的提高了效率

     缓存cache_t的底层结构

    struct cache_t {
        struct bucket_t *_buckets; //散列表
        mask_t _mask; //散列表的长度-1
        mask_t _occupied;//已经缓存的方法数量
    }
    
    struct bucket_t {
    private:
        cache_key_t _key; //SEL 作为key
        IMP _imp; //函数的内存地址
    }

    散列表为什么比较快呢,散列表可以避免遍历直接找到目标。

    原理是这样的。存储的时候 通过一定的规则 得到一个索引 那么我们就将存储的内容放到这个索引对应的位置。取出的时候可以按照规则直接得到索引,迅速找到。

    方法缓存的索引规则其实是通过@selector('方法名') & _mask = 数组中的索引,得到这个索引后直接将该方法封装成bucket_t存储到该索引位置。取出时根据相同的规则得到索引,直接取值。

    如果@selector('方法名') & _mask 得到索引值已经存储了东西 那么会存储到@selector('方法名') & (_mask -1)的位置。取出的时候也是如果发现key和调用的方法名不对,那么会@selector('方法名') & (_mask -1)得到一个新的索引值重新取。以此类推

    如果索引之间有间距 直接填充NULL 所以是空间换时间 散列表就是哈希表

    有了Cache_t这个结构体那么消息转发机制 就变成了这样先通过isa找到类对象或者元类对象 然后在起cache的方法列表中查找方法,如果找不到,再遍历其存储的方法列表查找,如果找到,调用并缓存起来,找不到通过superClass找到父类对象或者父元类对象 先在其cache的方法列表中查询,找到调用并缓存到自己的类或者元类。找不到在从其存储的方法列表中查询 直到找到调用并缓存到自己类或者元类的cache列表中。

    objc_msgSend() 消息机制

    MJPerson *person = [[MJPerson alloc] init];
    [person personTest];
    // objc_msgSend(person, @selector(personTest));
    // 消息接收者(receiver):person
    // 消息名称:personTest

    OC方法的调用 消息机制 给方法调用者 发送消息

    objc_msgSend的执行流程可以分为3大阶段

    1.消息发送 2.动态方法解析 3 消息转发

    消息发送阶段就是我们上面讲的消息寻找流程 如果能找到就调用 如果不能就进入第二个阶段 允许我们动态的创建方法 如果这个阶段我们没做任何事情 那么就进入到第三个阶段 消息转发 可能会找其他对象去调用者饿高方法 如果这三个流程都走了 还是没找到方法 那就报找不到方法的错误了。

    消息发送阶段我们已经讲的很清楚了。下面我们来讲一下动态解析阶段

    1.先判断是否曾经有过动态解析 如果有 直接进入消息转发阶段

    2.如果没有 调用+resolveInstanceMethod或者+resolveClassMethod方法 我们可以动态的添加实现 标记为已经动态解析 然后再次进入消息发送阶段

    #import "Person.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        Person *p = [[Person alloc] init];
        [p test];
        [Person classMethod];
    }
    
    @interface Person : NSObject
    
    - (void)test;
    
    +(void) classMethod;
    
    @end
    
    #import "Person.h"
    #import <objc/runtime.h>
    
    @implementation Person
    
    //如果消息发送阶段没有找到方法 就会走到动态解析阶段 会调用这个方法
    //我们有机会在这个方法里 动态添加方法的实现
    //如果我们已经动态添加了方法 又回回到第一阶段 消息发送阶段
    +(BOOL)resolveInstanceMethod:(SEL)sel {
        //方案一 如果方法没被实现 回调用我们写的other方法
        if (sel == @selector((test))) {
            Method otherMethod = class_getInstanceMethod(self, @selector(other));
            //相当于放到class_rw_t即类存储的方法列表里面了 所以再次回到消息发送阶段会从类存储的方法列表里找到
            class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
            //代表已经实现了动态解析
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    //如果调用的类方法没被实现 可在这个方法里面动态实现
    + (BOOL)resolveClassMethod:(SEL)sel {
        if (sel == @selector((classMethod))) {
            Method classMethod = class_getClassMethod(self, @selector(classOtherMethod));
            //注意传参事元类对象
            class_addMethod(object_getClass(self), sel, method_getImplementation(classMethod), method_getTypeEncoding(classMethod));
            return YES;
        }
        return [super resolveClassMethod:sel];
    }
    
    - (void) other {
        NSLog(@"%s",__func__);
    }
    
    + (void) classOtherMethod {
        NSLog(@"%s",__func__);
    }
    
    @end

    如果第二阶段 我们也没做什么,那么就会进入消息转发阶段。将消息转发给别人。就是自己没能力处理 看看别人是否有能力处理。

    调用forwradingTargetForSelector:返回一个对象 让这个对象接收这个消息 走这个对象的消息发送阶段

    #import "Person.h"
    #import "Student.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        Person *p = [[Person alloc] init];
        [p test];
        [Person classMethod];
        Student *s = [[Student alloc] init];
        [s test];
    }
    
    #import "Student.h"
    #import "Person.h"
    
    @implementation Student
    
    //消息转发阶段
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(test)) {
            //给Person对象 发送test消息
            return [[Person alloc] init];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    @end

    如果消息转发阶段我们没有实现forwardingTargetForSelector:方法 或者该方法返回nil 系统还是给我们提供了一个流程来处理这个问题

    会调用 methodSignatureForSelector:返回这个方法的签名  这些信息会包装到NSInvocation对象中 然后调用 forwardInvocation:方法 我们可以修改NSInvocation的调用对象 让另外一个对象调用这个方法。 达到和实现forwardingTargetForSelector:方法一样的效果

    #import "Student.h"
    #import "Person.h"
    
    @implementation Student
    
    //消息转发阶段
    //- (id)forwardingTargetForSelector:(SEL)aSelector {
    //    if (aSelector == @selector(test)) {
    //        //给Person对象 发送test消息
    //        return [[Person alloc] init];
    //    }
    //    return [super forwardingTargetForSelector:aSelector];
    //}
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(test)) {
            return nil;
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    //如果没有实现forwardingTargetForSelector 或者forwardingTargetForSelector 返回nil 会调用这个方法 获取方法签名 然后调用forwardInvocation:方法
    //方法签名 返回值类型 参数类型
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if (aSelector == @selector(test)) {
            // v void 返回值为空 16:所有参数的大小 @0 id类型的参数从0开始 :SEL类型的参数 从第8位开始   返回nil 不会调用forwardingInvocation:方法 就报错了
            return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    //NSInvocation封装了一个方法调用 包括 方法的调用者 方法 方法参数
    //anInvocation.target; 方法调用者
    //anInvocation.selector;//方法
    //anInvocation getArgument: atIndex: 参数
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
    //    anInvocation.target = [[Person alloc] init];
    //    //调用函数
    //    [anInvocation invoke];
        [anInvocation invokeWithTarget:[[Person alloc] init]];
    }
    
    @end

    如果我们实现methodSignatureForSelector:和forwardInvocation:方法 仅仅能达到和forwardingTargetForSelector:一样的效果,那么下面的也太麻烦了。是不是下面的方法还能实现一些不同的效果呢。我们可以看一下 事实是,只要我们进入到forwardingInvocation:方法 我们可以做任何事情 我们甚至可以不给出转发者 只打印一下也是可以的。

    - (void)forwardInvocation:(NSInvocation *)anInvocation {
    //    anInvocation.target = [[Person alloc] init];
    //    //调用函数
    //    [anInvocation invoke];
    //    [anInvocation invokeWithTarget:[[Person alloc] init]];
        //不给出转发者 只打印一下
        NSLog(@"哈哈哈");
    }

    相当于我们调用test 方法 实现的是forwardInvocation:里面的内容。类方法也有消息转发机制 只要把消息转发机制的方法变成类方法就行了 意思就是-变为+号。因为消息转发机制的三个方法 都是用消息接收着直接调用的。如果你传的是实例对象 那就是实例方法 你传的是个类对象 那就是类方法。

    super 关键字

    struct objc_super {
        __unsafe_unretained _Nonnull id receiver; // 消息接收者
        __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
    };

    在arm64中 objc_super 的构成是一个消息接收者 和 一个消息接收者的父类

    - (void)run {
        // super调用的receiver仍然是MJStudent对象
        // 但是调用的方法 先从父类的cache找 然后从父类的method_list中找
        [super run];
    //    struct objc_super arg = {self, [MJPerson class]};
    //    objc_msgSendSuper(arg, @selector(run));
    //    NSLog(@"MJStudet.......");
    }

    可以看到虽然调用父类的run方法,但是从objc_msgSendSuper(arg, @selector(run));可以看到消息接收者仍然是子类 只不过执行消息发送的时候是从父类开始的。
     [super message]的底层实现
     1.消息接收者仍然是子类对象
     2.从父类开始查找方法的实现

    - (instancetype)init
    {
        if (self = [super init]) {
            NSLog(@"[self class] = %@", [self class]); // MJStudent
            NSLog(@"[self superclass] = %@", [self superclass]); // MJPerson
    
            NSLog(@"--------------------------------");
    
            // objc_msgSendSuper({self, [MJPerson class]}, @selector(class));
            NSLog(@"[super class] = %@", [super class]); // MJStudent
            NSLog(@"[super superclass] = %@", [super superclass]); // MJPerson
        }
        return self;
    }

    结果为什么是这样呢?其实我们都知道 class方法,是在基类(NSObject)实现的。所以上面的代码调用的其实都是一个方法。接收者都是self 而class 和 superClass的实现是这样的

    - (Class)class
    {
        return object_getClass(self);
    }
    
    - (Class)superclass
    {
        return class_getSuperclass(object_getClass(self));
    }

    所以上面的结局也都是可以理解的了。

    RunTime相关的API

    #import "ViewController.h"
    #import <objc/runtime.h>
    #import "MJPerson.h"
    #import "MJCar.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        //动态创建一个类
        Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0);
        //添加成员变量 第一个参数 为谁添加 第二个 成员变量的名字 第三个 成员变量的大小 第四个对齐方式 一般传1 第五个成员变量的类型
        class_addIvar(newClass, "_age", 4, 1, @encode(int));
        class_addIvar(newClass, "_weight", 4, 1, @encode(int));
        //注册类 如果要添加成员变量和方法 要在这个方法之前调用 因为类管理的成员变量信息是只读的class_ro_t中 注册之后就不能更改了
        //也就是说 已有的类不能再动态的添加成员变量了 但是方法可以 方法是存储在类中class_rw_t中的可读可写
        objc_registerClassPair(newClass);
        //这个dog 就属于MJDog这个类了
        id dog = [[newClass alloc] init];
        [dog setValue:@10 forKey:@"_age"];
        [dog setValue:@20 forKey:@"_weight"];
        NSLog(@"%@",[dog class]); //MJDog
        NSLog(@"%zd and %@",class_getInstanceSize(newClass),[dog valueForKey:@"_age"]); //16 isa 8 _age 4 _weight 4
    }
    
    - (void)test {
        MJPerson *person = [[MJPerson alloc] init];
        //获取类对象
        NSLog(@"%p and %p",object_getClass(person),[person class]);
        //获取元类对象
        NSLog(@"%p",object_getClass([person class]));
        [person run];
        //设置类对象指向的isa
        object_setClass(person, [MJCar class]);
        [person run];
        
        //判断一个OC对象是否为calss
        object_isClass(person);
        NSLog(@"%d and %d and %d",object_isClass(person),object_isClass([MJPerson class]),object_isClass(object_getClass([MJPerson class])));
        //是否为一个元类
        class_isMetaClass(object_getClass([MJPerson class]));
    }
    
    
    @end

    获取和设置成员变量

    //获取成员变量
    - (void) getIvar {
        Ivar ageIvar = class_getInstanceVariable([MJCar class], "_age");
        NSLog(@"%s %s",ivar_getName(ageIvar),ivar_getTypeEncoding(ageIvar));
        //设置或者获取成员变量的值
        MJCar *car = [[MJCar alloc] init];
        Ivar name = class_getInstanceVariable([MJCar class], "_name");
        object_setIvar(car, name, @"123");
        NSLog(@"%@",object_getIvar(car, name));
    }

    获取成员变量列表 和 获取属性列表

    - (void)getIvarList {
        unsigned int count;
        Ivar *ivars = class_copyIvarList([MJCar class], &count);
        for (NSInteger i = 0; i < count ; i ++ ) {
            Ivar ivar = ivars[i];
            const char *cname = ivar_getName(ivar);
            NSString *name = [NSString stringWithCString:cname encoding:NSUTF8StringEncoding];
            NSLog(@"%@",name);
        }
        free(ivars);
        
        //获取属性列表
        unsigned int number;
        objc_property_t *propertys = class_copyPropertyList([MJCar class], &number);
        for (NSInteger i = 0; i < number; i ++) {
            objc_property_t property = propertys[i];
            const char *cname = property_getName(property);
            NSString *name = [NSString stringWithCString:cname encoding:NSUTF8StringEncoding];
            NSLog(@"%@",name);
            //属性的特性
            unsigned int attrCount = 0;
            objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
            for (unsigned int j = 0; j < attrCount; j ++) {
                objc_property_attribute_t attr = attrs[j];
                const char * name = attr.name;
                const char * value = attr.value;
                NSLog(@"属性的描述:%s 值:%s", name, value);
            }
        }
        free(propertys);
    }

    讲一下OC的消息机制

    OC中的方法调用其实都是转成了objc_msgSend函数的调用,给接收者(方法调用者)发送了一条消息(selector)

    objc_msgSend底层有三大阶段 消息发送 动态解析 消息转发阶段

    消息发送:

    1.判断接收者是否为nil 如果为nil 直接返回

    2.实例对象调用方法 先通过isa找到类对象 然后在类对象的缓存方法列表中查询 如果找到直接调用 找不到 再从类对象存储的方法列表中遍历查找 找到调用 并且缓存到类对象的cache列表中。找不到 通过superClass指针,找到父类对象 重复上述过程。类对象调用方法,先通过isa找到元类对象 然后在元类对象的缓存方法列表中查询 如果找到直接调用 找不到 再从元类对象存储的方法列表中遍历查找 找到调用 并且缓存到元类对象的cache列表中。如果还找不到 通过superClass指针 找到找到父元类对象 重复上述过程。

    动态解析

    如果上面的消息发送阶段到了基类仍没有找到方法实现 就会到达动态解析阶段 这个阶段会调用两个方法resolveInstanceMethod:(实例对象调用) 或者 resolveClassMethod:(类对象调用) 在这两个方法中 系统允许我们动态的添加一些方法的实现(存储到class_rw_t中)。如果实现了该方法 会标记为已经动态实现过 会再走一遍消息发送流程。事实上只要你重写了这两个方法 都会被标记为已经实现了动态解析。

    消息转发

    如果动态解析阶段 我们还是没做什么事情 那么就会进入到消息转发阶段 这个阶段我们可以指定一个其他的对象 来接收这个消息。我们可以实现forwardingTargetForSelector:来指定对象实现 如果没有指定对象 那么系统会调用methodSignatureForSelector:来获取一个方法签名(如果返回nil 就报错找不到方法)  如果methodSigntureForSelector:没有返回nil 那么就调用 forwardInvocation:方法 这个方法我们可以做任何处理

    什么是RunTime?平时项目中有用过吗?

    OC 是一门动态语言,相比C或者C++编译完成后就已经确定代码的结果,OC可以在运行的时候动态的改变类的实现 添加属性 修改方法的实现。这一切都是基于运行时的机制。

    RunTIme就是C语言封装的一套底层的API 封装了很多动态性相关的函数。

    平时我们写的一些代码 底层都是转换成了RunTImeAPI进行调用。

    1.给分类添加属性 关联对象

    2.遍历类的所有成员变量,然后访问私有变量 或者实现字典转模型 归档接档

    3.交换方法的实现(一般是交换系统的方法实现,可以实现在调用系统方法的同时,实现自己的一些逻辑)

    4.利用消息转发机制 解决一些方法找不到的问题

    @dynamic 告诉编译器 不用生成getter和setter方法 也不会自动生成成员变量 等到运行时再添加方法的实现

    @synthesize 关键字 可以自动生成getter和setter方法 并且生成一个_xxx的成员变量

  • 相关阅读:
    安全测试的概述和用例设计
    性能测试(四)常见调优
    性能测试(三)常见的性能测试缺陷
    Jmeter(七)六种参数化的方式
    Jmeter(六)所有的断言
    接口测试的问题解答
    ES学习
    flutter 之BottomNavigationBar属性
    flutter StaggeredGridView.countBuilder 上方取消空白
    flutter升级、回退到指定版本---mac版
  • 原文地址:https://www.cnblogs.com/huanying2000/p/13961581.html
Copyright © 2020-2023  润新知