美文网首页iOS开发攻城狮的集散地
iOS 菜鸟钻研动态特性——动态类型、绑定、加载

iOS 菜鸟钻研动态特性——动态类型、绑定、加载

作者: Hsusue | 来源:发表于2018-07-28 21:54 被阅读139次
    居居镇楼

    前言 差点在师弟面前装逼翻车

    昨天晚上,帅气的师弟突然微信找我。。


    WX20180727-170057@2x.png
    @property (nonatomic ,strong) NSMutableArray *datas;
    
    - (NSMutableArray *)datas {
        if(!_datas) {
          _datas = [NSMutableArray arrayWithCapacity:0];
        }
        return _datas;
    }
    
    - (void)viewDidLoad {
            [super viewDidLoad];
            self.datas = @[@"1",@"2"];
    }
    

    然后他在另一个地方删除元素,报错:
    [_NSArrayl removeObjectAtIndex:]: unrecognized selecotr sent to instance
    他刚学习C语言转iOS开发没多久,对此很迷惑,声明类型不是可变数组吗。
    C语言中
    int a = 1.5; a会强制转为整型1。
    int a = {1,2}; 编译不过。
    反正能运行的话a一定是int类型。

    还好当时反应过来了,这不就是动态绑定吗。脸是没丢光。
    @[@"1", @"2"]这种写法是不可变数组,self.datas自然就变成不可变数组了,而不可变数组没有删除元素这个方法崩溃了。
    我就告诉他,iOS有种机制叫动态绑定,很骚。并敲了更骚的代码给他看。编译也能过,只是有警告。运行后str会是一个NSArray实例。

    一坨*

    最后忽悠他
    "你这个暂时知道有这么回事就行了。现在换个方法生成可变数组,解决你的问题。"

    回去后偷偷摸摸研究一波。
    我印象中运行时str能用NSArray的方法。
    结果打的时候提示的全是NSString的方法,调用NSArray的方法编译就不过了。


    一?脸?懵?比

    这下我懵逼了。还好刚刚没装逼打上这句。不然要像FaFa一样现场翻车了。

    改成这样编译就过了,运行也没问题。


    image.png

    听人说,不要怕写的知识低级,写出来一起分享才能进步得更快。
    用自己的话写出来能加深理解和记忆,为了写的知识尽量不出错也更有责任、动力深入学。

    印象最深的还是那句“菜鸡口中说出的可能更有共鸣呢”
    就冲这句话,我就要写。

    如果有理解错误的地方,欢迎指出来!


    iOS动态特性

    动态特性分为三种:动态类型id动态绑定动态加载

    先来了解isa指针

    以向UIView发送resignFirstResponder研究。

    1. 记得之前在《Objective-C基础教程》上看到过,没深入了解,然后这么长时间里天真地以为是这样的。
    • 每个Objective-C对象第一个成员变量是isa指针。
    • 分开来更能理解其含义is a,指向对象的父类,说明对象is a 什么类。而类里也有个isa指针,再指向其父类。如此递归直到根类中isa指向nil为止。
      第一点是对的。
      第二点被平时思维误导了,是错的。

    1. 再画个图阐述一下年轻时候错误的想法
      跟真正的比起来容易理解太多了,真是幼稚想得太简单了。


      错误的印象中的isa

    如向UIView的对象发送resignFirstResponder,首先会在UIView的代码空间里找,找不到就根据isa指针去父类UIResponder找到调用。
    若调用方法最终找不到,程序崩溃。
    根据这个原理,验证了覆写方法后,会调用本类方法而不是父类方法。


    1. 但看到真相后哦😯口
    image.png

    划重点,类里放着-方法,元类里放着+方法!!

    还有一张大神图,对着学习效果更好,后面提到的知识会对照着这图。

    isa 的 正解

    之前幼稚理解的那条线只是父子线,isa线指向的是元类。
    惯性思维让我们平常新建一个UIView的子类ChildView后,会说ChildView这个view。实际上严谨来说,我们并不能说ChildView is a class of UIView,只能说ChildView is a subclass of UIView对吧。

    有些偏门知识但对后面原理很有用
    类对象在程序运行时一直存在。
    类对象所保存的信息在程序编译时确定,在程序启动时加载到内存中。

    1. 最后研究isa这个🐶东西
      isa声明
      先记住这里 *id是结构体objc_object的指针。在后面动态绑定会用到。

    objc_object可以看出isa 声明是个 Class 。和上面说到的每个对象都有个isa相呼应。
    然后Class是结构体objc_class的指针。
    于是Command进这个objc_class一览风景。

    objc_class
    看到里面又有个声明为Class 的 isa成员和 super_class成员。
    这不就相当于个二叉树吗。和上面那张图虚线相呼应。

    然后这个结构体还有一些其他成员。看看都是些什么妖魔鬼怪。

      // 类名
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
      // 版本号,默认0
        long version                                             OBJC2_UNAVAILABLE;
      // 供运行期使用的一些位标识。
        long info                                                OBJC2_UNAVAILABLE;
      // 该类的实例变量大小 
      // 疑惑alloc 和 init有参照这里吗?
        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;
    

    看到了methodListscache就知道那个例子是怎么实现的了。
    向UIView的实例发送resignFirstResponder后,首先通过isa指针找到UIView类,由于是-方法,就在该类中cahce中找,再到methodLists找,(然后还会在动态方法找,这里先不用管)。找不到,由于是-方法,就通过super_class指针,往父类找。

    总结

    • 其实质上像是二叉树,有两个指针分别指向父类和元类。
    • 调用-方法走红线。调用+方法走蓝线。


      调用方法路线

    动态类型和动态绑定

    1. 简单了解动态类型id,与isa之间的联系

    先分析动态类型和静态类型的区别

    动态类型id:通用指针类型,弱类型,编译时不进行类型检查。

    静态类型,像上面,NSString *str在编译时会进行类型检查。
    这就为Xcode自动联想提供了条件。
    还能提前避免调用不存在的方法(NSString里没有count方法),防止运行时找不到方法崩溃。

    再来分析id基本原理

    前面看到过

    struct objc_object {
      Class _Nonnull isa  OBJC_ISA_AVAITABILITY;
    };
    
    typedef struct objc_object *id;
    

    id 就是个通用对象指针类型,能指向任何object对象

    还是用引发这次血案的例子来展开吧。
    在师弟面前装的b

      NSString *str = @[@"1", @"2"]; // 编译仅警告
      NSLog(@"%d",[str isKindOfClass:[NSArray class]]);//输出为1
      [str count];// 编译报错
    

    改成这样就能编译过了,但str并不是一个好的命名。

      id str = @[@"1", @"2"];
      [str count];
    

    id编译时不进行检查,所以以上情形就解释通了。

    通常id类型和-isMemberOfClass:或者-isKindOfClass:配套使用,就能达到编译时找不到方法报错和自动联想。当然还有(respondsToSelector:
    conformsToProtocol:)。

    id的常用套路

    那向id发送消息 和isa是怎么关联起来的呢。
    id是结构体objc_object的指针,而objc_object的成员有isa。向id发送消息,会根据-还是+方法沿着isa指针跟踪到类空间中找对应的方法。所以isa为动态类型id进行动态绑定提供了可能。

    1. 动态绑定与动态类型的关联。

    基于动态类型id,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性响应的消息也被完全确定,这就是动态绑定。

    我的理解就是id赋值后,类型其实已经确定下来了。其isa指向的空间里存放着属性和方法列表,于是能访问的也确定下来了。


    以上只是介绍了id类型和isa之间的联系,以及动态绑定的基本含义。
    但认真想想这些场景完全可以用静态类型顶替。
    那动态特性有什么特殊的用途或者优点呢?


    动态绑定的使用——动态加载

    分为三种应用场景:添加方法、交换方法、添加属性。
    觉得交换方法最有用

    动态加载的好处

    个人理解
    感觉就像是在做懒加载一样,归根到底就是内存和硬盘读取的问题。

    我们把App装进手机后,代码、图片、设置什么的会放到硬盘中。
    当然以上提到的isa中存放的方法列表、属性列表、缓存列表也放在硬盘中。

    程序运行时,系统会从硬盘中把该类的代码空间内容复制到内存中,以加快读取速度。
    而如果有些方法或者属性不一定调用,这时候就可以用到懒加载原理,省下这些代码从硬盘中转移到内存的时间。既加快速度,又省内存。要用的时候再从去实现。

    因为对计算机原理不怎么感冒,也就理解成这样。

    动态加载的三种应用场景 代码在这

    本文按照 实现流程->原理->优点 来进行剖析。

    添加方法

    背景:动态添加方法处理 调用一个未实现的对象或类方法 和 去除报错。

    先认识两个方法,是NSObject中声明的方法。
    当调用类中没有实现的对象方法时,会把调用的方法名字作为参数 跑进这。因为要在.m文件中实现以下方法,故动态添加方法只针对自己写的类或分类有用。

    // 判断对象方法有没有实现
    +(BOOL)resolveInstanceMethod:(SEL)sel
    // 判断类方法有没有实现
    + (BOOL)resolveClassMethod:(SEL)sel
    
    • 实现流程
    #import "Cat.h"
    #import <objc/runtime.h>
    @implementation Cat
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == NSSelectorFromString(@"showMOE")) {
            class_addMethod(self, sel, (IMP)showMOE, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    void showMOE(id self, SEL _cmd) {
        NSLog(@"动态添加了一个卖萌的方法");
    }
    @end
    

    在VC中调用会警告,因为.h文件中未声明这方法。



    但还是打印出来了


    注意:该方法不是对象方法,是C函数!

    void showMOE(id self, SEL _cmd) {
        NSLog(@"动态添加了一个卖萌的方法");
    }
    

    总结流程
    首先,在VC中cat实例调用了showMOE的方法,因为cat.m中未实现该对象方法,所以跳进了+ (BOOL)resolveInstanceMethod:。在该方法中我们加入了针对方法名为showMOE的处理方法,让程序不会崩溃。

    • 原理
      先说这个C函数中的两个参数。
      id self:自身类
      SEL _cmd:方法名称
    

    再说添加方法这个函数,其声明在rumtime.h中。

    class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) 
    
    Class _Nullable cls:给哪个类添加方法
    SEL _Nonnull name:添加方法的方法名
    IMP _Nonnull imp:添加方法的函数实现(函数地址)
    const char * _Nullable types:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
    

    再一边对着例子来谈这四个参数。
    class_addMethod(self, sel, (IMP)showMOE, "v@:");
    1.给自身类添加方法。
    2.VC中调用了showMOE,方法名就是sel(showMOE)。
    3.(IMP)showMOE,IMP强制转换成函数地址。
    4.函数的类型void showMOE(id self, SEL _cmd),对应上面的转换规则 -> v@:

    最后要补充的原理知识。

    • IMP
    //Method方法结构体
    typedef struct objc_method *Method;
    
    struct objc_method {
        SEL method_name ;    //方法名,也就是selector.
        char *method_types ;    //方法的参数类型.
        IMP method_imp ;    //函数指针,指向方法具体实现的指针..也即是selector的address.
    } ;
    
    // SEL 和 IMP 配对是在运行时决定的.并且是一对一的.也就是通过selector去查询IMP,找到执行方法的地址,才能确定具体执行的代码.
    // 消息选标SEL:selector / 实现地址IMP:address 在方法链表(字典)中是以key / value 形式存在的
    
    • 第四个参数的规则
      返回值+参数类型
      转化规则官方文档

    • 最后来说说优点


      添加方法的优点
    交换方法

    应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。

    • 实现流程
      以修改imageNamed:方法,有图片 输出“加载成功”,无图片 输出“加载失败” 为例。

    新建UIImage的分类UIImage+LoadSuccess,并实现交换代码

    #import "UIImage+LoadSuccess.h"
    #import <objc/runtime.h>
    
    @implementation UIImage (LoadSuccess)
    + (void)load {
        Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
        Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
        method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
    }
    + (UIImage *)ln_imageNamed:(NSString *)name {
        UIImage *image = [UIImage ln_imageNamed:name];
        if (image) {
            NSLog(@"加载成功");
        } else {
            NSLog(@"加载失败");
        }
        return image;
    }
    @end
    

    然后直接在其他地方调用imageNamed就会发现被替换了。

    *原理

    // 获取方法的函数
    // cls : 从哪个类获取
    // name: 函数名
    class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
    // 交换方法的函数
    method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    

    一看这些函数就知道怎么用了。
    为什么要写在load里呢?
    因为类被加载运行的时候就会调用load。
    而类对象所保存的信息在程序编译时确定,在程序启动时加载到内存中。
    所以程序启动就会调用load。

    然后还有个奇怪的地方,在替换的方法里。

    + (UIImage *)ln_imageNamed:(NSString *)name {
        UIImage *image = [UIImage ln_imageNamed:name];
    }
    

    这跟继承类后用super 调用方法不一样,仔细品尝才能懂。
    在交换代码后

    + (UIImage *)ln_imageNamed:(NSString *)name {
        原生iOS imageNamed代码
    }
    
    + (UIImage *)imageNamed:(NSString *)name {
        UIImage *image = [UIImage ln_imageNamed:name];
        if (image) {
            NSLog(@"加载成功");
        } else {
            NSLog(@"加载失败");
        }
        return image;
    }
    

    现在调用imageNamed,会先跑进ln_imageNamed。而ln_imageNamed里存放着原生代码。

    • 优点
      虽然能够用继承系统的类,然后重写方法达到相同的效果。但是每次都要导入。

    添加属性

    • 实现流程
      以给猫加个名字为例吧

      新建Cat的分类Cat+name,以增加属性

    #import "Cat+name.h"
    #import <objc/runtime.h>
    @implementation Cat (name)
    //定义常量 必须是C语言字符串
    static char *PersonNameKey = "PersonNameKey";
    -(void)setName:(NSString *)name{
        objc_setAssociatedObject(self, PersonNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    -(NSString *)name{
        return objc_getAssociatedObject(self, PersonNameKey);
    }
    @end
    

    然后不用导入,直接调用存取方法。

        [juju performSelector:@selector(setName:) withObject:@"juju"];
        NSLog(@"%@",[juju performSelector:@selector(name)]);
    
    • 原理
    // 需要加属性的对象
    // 设置一个静态常量,也就是Key 值,通过这个我们可以找到我们关联对象的那个数据值
    // id value 这个是我们打点调用属性的时候会自动调用set方法进行传值
    // objc_AssociationPolicy policy : 关联策略
    objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                             id _Nullable value, objc_AssociationPolicy policy)
    
    // get很好理解
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    

    给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
    就是写了setget方法。


    参考

    相关文章

      网友评论

      • libtinker:如果不是为了装逼,学习将毫无意义

      本文标题:iOS 菜鸟钻研动态特性——动态类型、绑定、加载

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