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

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

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

@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一样现场翻车了。

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

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

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

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


iOS动态特性

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

先来了解isa指针

以向UIView发送resignFirstResponder研究。

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

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

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


  1. 但看到真相后哦?口

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

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

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

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

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

objc_object可以看出isa 声明是个 Class 。和上面说到的每个对象都有个isa相呼应。 然后Class是结构体objc_class的指针。 于是Command进这个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发送消息 和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方法。


参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值