iOS进阶之详解Runtime运行时机制

runtime简介

runtime简称运行时,OC是运行时机制,也就是在运行时才做一些处理,是一套比较底层的纯C语言API, 属于1个C语言库, 包含了很多底层的C语言API。

在我们平时编写的OC代码中, 程序运行过程时, 最终都是转成了runtime的C语言代码。例如:C语言在编译的时候就知道要调用哪个方法函数,而OC在编译的时候并不知道要调用哪个方法函数,而是推迟到运行的时候才知道调用的方法函数名称,来找到对应的方法函数进行调用。

runtime消息传递流程

在OC语言中,任何类的定义都是对象。类和类的实例(对象)没有任何本质上的区别,任何对象都有isa指针。

一、isa指针

打开Runtime源码,全局搜索isa_t,锁定objc-private.h类,里面有这么一段代码,isa指针是isa_t的实例。代码如下:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
#endif
};

  • arm64之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在arm64架构之后,apple对isa进行了优化,变成了一个联合体union结构,同时使用位域来存储更多的信息。

  • 对象是通过isa的bits进行位运算,取出响应位置的值。runtime中的isa是被联合体位域优化过的,

  • 它不单单是指向类对象了,而是把64位中的每一位都运用了起来,其中的shiftcls为33位,代表了类对象的地址,其他的位都有各自的用处。

那么我来看看这个isa内部元素代表的是什么含义?

  • nonpointer:表示是否对isa指针开启指针优化

    • 0:不开启,表示纯isa指针。
    • 1开启,不单单是类对象的地址,isa中包含了类信息和对象的引用计数等。
  • has_assoc:表示是否有关联对象标识位

    • 0表示:没有,没有关联对象会释放的更快
    • 1表示:有
  • has_cxx_dtor:表示该对象是否有C++或者Objc的析构器

    • 如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls:表示存储类指针class的值。

    • 开启指针优化的情况下,在arm64 架构中有33位用来存储类指针。
  • magic:固定值为0xd2

    • 用于在调试时分辨对象是否完成初始化
  • weakly_referenced:表示对象是否被指向或者曾经指向一个 ARC 的弱引用变量。

    • 没有弱引⽤用的对象可以更更快释放。
  • deallocating:标志对象是否正在释放内存

    • 0 表示没有 1表示正在释放内存
  • has_sidetable_rc:表示当对象的引用计数大于10,以至于无法存储在isa指针中时,用散列表去计数。

  • extra_rc:表示该对象的引用计数,实际上是引用计数值减1

    • 例如如果对象的引用计数为10,那么extra_rc为9。如果引用计数⼤于10, 则需要使⽤到has_sidetable_rc

二、类对象(objc_class)

在OC中的类是用Class类型来表示的,它实际上是一个指向objc_class结构体的指针。

typedef struct objc_class *Class;

查看objc/runtime.h中objc_class结构体的定义如下:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

struct objc_class结构体定义了很多变量如下:

  • Class isa : 实现方法调用的关键

  • Class super_class : 父类

  • const char * name : 类名

  • long version : 类的版本信息,默认为0

  • long info : 类信息,供运行期使用的一些位标识

  • long instance_size : 该类的实例变量大小

  • struct objc_ivar_list * ivars : 该类的成员变量链表

  • struct objc_method_list ** methodLists : 方法定义的链表

  • struct objc_cache * cache : 方法缓存

  • struct objc_protocol_list * protocols : 协议链表

objc_class结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。

三、实例(objc_object)

实例对象的isa指针指向类对象,类对象的isa指针指向元类。类对象和元类的结构都是objc_class类型。

typedef struct objc_object *id;

struct objc_object {
    /*
     *实例对象的isa指针指向类对象
     *实质就是指向objc_class结构体的指针
     */
    Class isa; 
};

  • id表示泛型对象,指向objc_object结构体的指针。objc_object 就是我们平时常用的实例对象的定义,它只包含一个isa指针。
    • 一个对象唯一保存的信息就是它的 Class 的地址
    • 当我们调用一个对象的方法时,它会通过 isa 去找到对应的objc_class,然后再在objc_class的 methodLists 中找到我们调用的方法,然后执行。

四、元类(Meta Class)

在 Objective-C 中,类也被设计为一个对象。
结构体中也包含isa指针,这个指针指向元类。
调用一个对象的类方法的过程如下:

  • 通过对象的 isa 指针找到对应的类。
  • 通过类的 isa 指针找到对应元类。
  • 在元类的 methodLists 中,找到对应的方法,然后执行。

元类也是一个对象,也有一个isa指针,isa指针指向哪里呢?

为了不让这种结构无限延伸下去,Objective-C 的设计者让所有的元类的isa指向基类(比如 NSObject)的元类。而基类的元类的 isa 指向自己。这样就形成了一个完美的闭环。

因此消息传递整个结构图可以如下表示:

runtime_消息传递流程.png

五、objc_msgSend

当对象调用方法时候,[objc test] 会转换成

id objc_msgSend(id objc, SEL op, ...);
    
// 发送消息给无参数,无返回值的方法
((void (*)(id, SEL)) objc_msgSend)(self, NSSelectorFromString(@"functionName"));

// 发送消息给有参数,有返回值的方法
((NSString *(*)(id, SEL, NSString *)) objc_msgSend)(self, NSSelectorFromString(@"functionName"), @"parameter");

objc_msgSend 会默认传入 id 和 SEL,分别对应两个隐含参数, self 和 _cmd,其中 _cmd 指向方法本身。

六、objc_method_list(方法列表)、objc_cache(方法缓存)

* struct objc_method_list ** methodLists : 方法定义的链表
* struct objc_cache * cache : 方法缓存

methodLists:存着该对象的实例方法(类对象的话存放这类方法)

cache的作用就是:

  • 因为methodLists方法有很多,如果每次调用方法都去遍历一遍methodLists的话,效率很低。所以objc_cache方法缓存存在的意义。
  • 当methodLists遍历找到方法,找到该方法之后,cache会以map的方式把method_name作为key,method_imp作为value给存起来。
  • 下次当你在再调用该方法的时候,会先从cache中去找。

七、objc_method 方法

typedef struct objc_method *Method;
struct objc_method {
    
    SEL method_name                                          OBJC2_UNAVAILABLE; //方法名
    char *method_types                                       OBJC2_UNAVAILABLE; //方法类型
    IMP method_imp //方法实现
}    
    

在objc_method结构体中中,SEL和IMP都是Method的属性。

一、SEL(objc_selector)
/// 代表一个方法的不透明类型
typedef struct objc_selector *SEL;
//发送消息
id objc_msgSend(id self, SEL op, ...);

objc_msgSend函数第二个参数类型为SEL,selector是方法选择器,可以理解为区分方法的 ID(唯一标识符),而这个ID的数据结构是SEL:

@property SEL selector;

可以看到selector是SEL的一个实例。其实selector就是个映射到方法的C字符串,selector既然是一个string,我觉得应该是类似className+method的组合,命名规则有两条:

  • 同一个类,selector不能重复
  • 不同的类,selector可以重复

这就是我们OC没有重载的功能,一个方法不能有两个方法名一样,哪怕传的参数个数和类型都不一样。就是因为selector是通过方法名来找到对应方法的。

二、IMP

IMP表示: 指向一个方法实现的指针。

typedef id (*IMP)(id, SEL, ...); 

就是指向最终实现程序的内存地址的指针。

在iOS的Runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

八、Category(objc_category)

Category是表示一个指向分类的结构体的指针,结构体如下:

struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

  • name:是指 class_name 而不是 category_name。
  • cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
  • instanceMethods:category中所有给类添加的实例方法的列表。
  • classMethods:category中所有添加的类方法的列表。
  • protocols:category实现的所有协议的列表。
  • instanceProperties:表示Category里所有的properties
    这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。

Category的加载处理过程是怎样的呢?

  1. 通过Runtime加载某个类的所有Category数据
  2. 把所有Category的方法、属性、协议数据,合并到一个大数组中
  3. 后面参与编译的Category数据,会在数组的前面
  4. 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

最后对象发送消息,整个执行流程是什么样的?

八、Runtime消息传递

一个对象的方法像这样[obj work],编译器转成消息发送objc_msgSend(obj, work),Runtime时执行的流程是这样的:

  • 首先通过obj的isa指针找到它的类对象
  • class的methodlist找到work函数
  • 如果class中没找到work函数方法,会继续类对象的superclass(父类)中去找,一直找到父类是根类
  • 一旦知道work这个函数方法就去执行的work的实现IMP

如果只是这么简单的消息传递,效率就非常低了。往往一个class只有20%的函数会被经常调用,可能占总调用次数的80%。每个消息都需要遍历一次objc_method_list并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。objc_cache做的事情如下:

  • 当methodLists遍历找到方法,找到该方法之后,cache会以map的方式把method_name作为key,method_imp作为value给存起来。
  • 下次当你在再调用该方法的时候,会先从cache中去找。

还有一个问题就是发现类别的方法优先级更好,当一个对象和类别都有一个work方法的时候,发现类别方法的优先级更高,消息传递是怎么执行的呢?

  • 就是当你遍历类对象的methodlist以前,会把category里面的instanceMethods(实例方法列表)、classMethods(对象方法列表)会插入到methodlist的最前面。
  • 所以当遍历work函数,会优先找到类别的work函数,然后执行它的IMP。

runtime消息转发流程

当向一个对象发送一条消息,但它并没有实现的时候,通过_objc_msgForward尝试做消息转发。_objc_msgForward是 IMP类型。
为了展示消息转发的具体动作,可以尝试向一个对象发送一条错误的消息,并查看一下_objc_msgForward是如何进行转发的。

当上面消息传递过程中没有找到方法,消息就会进行转发,如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。消息转发的过程有三次机会。

消息转发的原理如下图:

runtime.png
  • 第一步,动态添加响应方法

    • Objective-C运行时会调用 实例方法调用+resolveInstanceMethod:
    • 类方法调用+resolveClassMethod:
- (void)viewDidLoad {
    [super viewDidLoad];
    //执行work函数
    [self performSelector:@selector(work:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(work:)) {//如果是执行work函数,就动态解析指定新的IMP
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//work函数
void workMethod(id obj, SEL _cmd) {
    NSLog(@"Working");
}

打印结果:

Working

表明虽然没有实现work:这个函数,但是我们通过class_addMethod动态添加workMethod函数,并执行workMethod这个函数的IMP。

如果上面方法返回是NO的话,那就做走下一步。

  • 第二步转发给响应该方法的对象

如果目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给能响应该方法的对象。

下面通过代码,首先写声明一个能响应该方法的对象Person:

@interface Person: NSObject

- (void)work;

@end

@implementation Person

- (void)work {
    NSLog(@"person working");//Person的foo函数
}

@end

其次通过forwardingTargetForSelector方法转发给响应该方法的对象

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
      //执行work函数
    [self performSelector:@selector(work)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;//返回NO,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(work)) {
        return [Person new];//转发给Person对象,让Person对象接收这个消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

打印结果:

person working

如果第二步中没有相应该消息的对象,就会走第三步消息转发,就把整个完整的消息转发流程走一遍了。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;//返回NO,进入下一步转发
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;//返回nil,进入下一步转发
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"work"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }
    else {
        [self doesNotRecognizeSelector:sel];
    }

}
@end

打印结果

working

从打印结果来看,我们实现了完整的转发。通过签名,Runtime生成了一个对象anInvocation,发送给了forwardInvocation,我们在forwardInvocation方法里面让Person对象去执行了work函数。签名参数v@:看苹果文档Type Encodings

如果这三步都没有成功的话,就会doesNotRecognizeSelector报异常了。

Runtime实际应用

Runtime简直就是做大型框架的利器。它的应用场景非常多,下面就介绍一些常见的应用场景。

  • 给分类添加属性(关联对象)
  • Method Swizzling交换方法
  • KVO原理实现
  • 消息转发(热更新)解决Bug(JSPatch)
  • 实现NSCoding的自动归档和自动解档
  • 实现字典和模型的自动转换(MJExtension)

一、给分类添加属性(关联对象)

关联对象Runtime提供了下面几个接口:

//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)

//移除关联的对象
void objc_removeAssociatedObjects(id object)

上面的参数解释:

  • id object:被关联的对象
  • const void *key:关联的key,要求唯一
  • id value:关联的对象
  • objc_AssociationPolicy policy:内存管理的策略

objc_AssociationPolicy是一个枚举如下:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    
    OBJC_ASSOCIATION_ASSIGN = 0,           //weak nonatomic 属性  
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //retain\strong nonatomic 属性  
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  //copy nonatomic属性                                            
    OBJC_ASSOCIATION_RETAIN = 01401, // retain\strong atomic属性 
    OBJC_ASSOCIATION_COPY = 01403    // copy atomic属性     
};

针对枚举如下面解释:

runtime_关联对象属性.png

下面就给Person关联一个name属性

@interface Person (Name)

@property (nonatomic, copy) NSString *name;

@end

#import "Person+Name.h"
#import <objc/runtime.h>

static NSString *nameKey = @"namekey";
@implementation Person (Name)

- (void)setName:(NSString *)name {
    
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    
    return objc_getAssociatedObject(self, &nameKey);
}

二、方法交换(Method Swizzling)

Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。

Method Swizzling是发生在运行时,主要用于运行时将两个Method进行交换,我们可以将Method Swizzle代码写到任何地方,但是只有在Method_Swizzling这段Method Swizzle代码执行完毕之后互换才起作用。

Method Swizzling交换时机:尽可能在+load方法中实现。原因:

+load方法会在加载类的时候就被调用,也就是ios应用启动的时候,就会加载所有的类,main函数之前,就会调用每个类的+load方法。
主要用途:当需求需要在调用系统方法的基础上需要添加额外的功能,然后要把项目中所有的方法都要实现都要改一遍的问题上,不需要动原有的系统方法。并且能使用原有的系统方法功能外,添加一下原有的功能。

例如:当系统需要适配ipad图片的时候,项目中调用imageNamed方法的地方非常多。所以每一处都需要进行更改肯定非常麻烦。现在通过Method_Swizzling就可以sw_imageNamed替换imageNamed并且做一些处理。当以调用imageNamed方法时候实际调用的是sw_imageNamed。

@implementation UIImage (Category)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(imageNamed);
        SEL swizzledSelector = @selector(sw_imageNamed);
     
        Method originalMethod = class_getInstanceMethod(class,originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        /*
         * NO:尝试添加需要交换的方法,若添加失败,则说明已经有该方法,直接交换
         * YES:如果添加成功,没有该方法,则把刚添加的方法替换为我们需要的方法
         */ 
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
 /*
  *这里特别注意,必须调用交换的方法,因为上面已经把方法交换了,所以不会死循环,
  *如果调用originalMethod,反而会死循环
  */ 
- (void)sw_imageNamed {
    UIImage * image;
    //这个时候的sw_imageNamed已经交换后变成了系统的imageNamed
    if( IS_IPHONE ){
        // iPhone处理
        UIImage * image =  [self sw_imageNamed:name];
        if (image != nil) {
            return image;
        } else {
            return nil;
        }
    } else {
        // ipad处理,_ipad是自己定义。
        UIImage *image = [self sw_imageNamed:[NSString stringWithFormat:@"%@_ipad",name]];
        if (image != nil) {
            return image;
        }else {
            image = [self swizze_imageNamed:name];
            return image;
        }
   }
}
@end

可能有些不明白为啥sw_imageNamed方法里面又调用[self swizze_imageNamed:name]竟然没有死循环。主要是因为这两个sw_imageNamed是不一样意义:

  • load方法表示在加载类的时候就执行了该方法,就已经交换完毕,交换的是IMP(就是函数实现的内存地址指针)。
  • 当对象调用[self imageNamed:name]调用的sw_imageNamed的IMP
  • (void)sw_imageNamed {}这个就是sw_imageNamed方法真正实现的IMP。
  • 然后在sw_imageNamed的IMP里调用[self swizze_imageNamed:name],[self swizze_imageNamed:name]这个就是selector方法名执行的是imageNamed的IMP。
  • (void)sw_imageNamed {}与[self swizze_imageNamed:name]这个两个是有区别的,一个IMP,一个Selector。

原理图如下:

Method_Swizzing.png

Method Swizzing作用就是让sel2、sel3与IMP2、IMP3交换。

三、热更新及解决线上bug(JSPatch)

JSPatch 是一个iOS动态更新框架,只需在项目中引入极小的引擎,就可以使用JavaScript调用任何Objective-C原生接口,获得脚本语言的优势:为项目动态添加模块,或替换项目原生代码动态修复bug。

JSPatch基础原理:

JSPatch 能做到通过JS调用和改写OC方法最根本的原因是 Objective-C是动态语言,OC上所有方法的调用/类的生成都通过 Objective-C Runtime在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法:

Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];

也可以替换某个类的方法为新的实现:

static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");

还可以新注册一个类,为类添加方法:

Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
class_addMethod(cls, selector, implement, typedesc);
objc_registerClassPair(cls);

理论上你可以在运行时通过类名/方法名调用到任何 OC 方法,替换任何类的实现以及新增任意类。

JSPatch的基本原理就是:JS传递字符串给OC,OC通过Runtime 接口调用和替换OC方法。这是最基础的原理,实际实现过程还有很多怪要打。

最后JSPatch被苹果禁止使用,还能用 JSPatch 吗?

  • 可以,但请不要自行接入,统一接入 JSPatch 平台。

为什么要统一接入 JSPatch 平台?

若从苹果的审核规则来看,JSPatch 和 React Native 是一样的,但苹果接受 React Native,拒绝 JSPatch,主要是因为JSPatch 可以调用任意原生 API,出于安全和滥用方面的顾虑而拒绝,之前大量 APP 自行接入 JSPatch,导致有几个风险点:

  • 无法保证每个 APP 接入方式都是安全的,容易传输过程被替换,被黑客中间人攻击,成为 iOS 上的一个漏洞。

  • 一些像地图/推送类 SDK 接入 JSPatch 后覆盖大量 APP,若这些 SDK 后台被攻破,可以对这些 APP 下发恶意脚本,造成大面积危害。

  • 开发者可以方便地不经过苹果审查使用 JSPatch 调用私有 API。

若大家还是通过一些混淆手段骗过苹果继续各自接入 JSPatch,出于上述三点考虑,苹果不可能再允许,但若接入 JSPatch 平台,上述三个问题可以帮苹果解决,不再有这类隐患,详见这里,苹果才会考虑允许继续使用。

用JSPatch平台最新的SDK接入可以通过审核吗?
可以通过,已有接入并通过审核的 APP。

为什么平台SDK能通过审核?

  • 因为做了简单的类名修改混淆。

用Github上的代码接入能通过审核吗?

  • 不能。前面也说了,不要自行接入JSPatch源码,不要自行混淆代码接入

四、NSCoding的自动归档、解档

原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encode和decode操作。
核心方法:在Model的基类中重写方法:

//解档
- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[I];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

//归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount);
    for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[I];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}

字典转模型

首先建立一个NSObject分类,做个简单的字典转模型

#import <Foundation/Foundation.h>

@interface NSObject (Json)

+ (instancetype)pf_objectWithJson:(NSDictionary *)json;

@end

#import "NSObject+Json.h"
#import <objc/runtime.h>

@implementation NSObject (Json)

+ (instancetype)cs_objectWithJson:(NSDictionary *)json {
    id obj = [[self alloc] init];
    
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        // 取出位置的成员变量
        Ivar ivar = ivars[I];
        NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
        [name deleteCharactersInRange:NSMakeRange(0, 1)];
        
        // 设值
        id value = json[name];
        if ([name isEqualToString:@"ID"]) {
            value = json[@"id"];
        }
        [obj setValue:value forKey:name];
    }
    free(ivars);
    return obj;
}

@end

字典转模型的模型及调用:

#import <Foundation/Foundation.h>

@interface Persion : NSObject

@property (assign, nonatomic) int ID;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) int age;
@property (copy, nonatomic) NSString *name;

@end

// 字典转模型调用
- (void)jsonToModel {
    // 字典转模型
    NSDictionary *json = @{
                           @"id" : @20,
                           @"age" : @20,
                           @"weight" : @60,
                           @"name" : @"Jack",
                           @"no" : @30
                           };
    Persion *persion = [Persion pf_objectWithJson:json];
    NSLog(@"id = %d, age = %d, weight = %d, name = %@",persion.ID,persion.age,persion.weight,persion.name);
}

Runtime使用场景很多,在这就不一一举例子了。大家可以看下面的应用图:

runtime应用.png

结尾

Objective-C语言最重要的特点之一就是运行时语言,可见掌握runtime至关重要。Objective-C是一个全动态语言,它的一切都是基于Runtime实现的平时编写的OC代码, 在程序运行过程中, 其实最终都是转成了runtime的C语言代码, runtime算是OC的幕后工作者。

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

推荐阅读更多精彩内容