前言 差点在师弟面前装逼翻车
昨天晚上,帅气的师弟突然微信找我。。
@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
研究。
- 记得之前在《Objective-C基础教程》上看到过,没深入了解,然后这么长时间里天真地以为是这样的。
- 每个Objective-C对象第一个成员变量是isa指针。
- 分开来更能理解其含义is a,指向对象的父类,说明对象is a 什么类。而类里也有个
is
a指针,再指向其父类。如此递归直到根类中isa
指向nil
为止。 第一点是对的。 第二点被平时思维误导了,是错的。
- 再画个图阐述一下年轻时候错误的想法 跟真正的比起来容易理解太多了,真是幼稚想得太简单了。
如向UIView的对象发送resignFirstResponder
,首先会在UIView的代码空间里找,找不到就根据isa指针去父类UIResponder
找到调用。 若调用方法最终找不到,程序崩溃。 根据这个原理,验证了覆写方法后,会调用本类方法而不是父类方法。
- 但看到真相后哦?口
划重点,类里放着-方法,元类里放着+方法!!
还有一张大神图,对着学习效果更好,后面提到的知识会对照着这图。
之前幼稚理解的那条线只是父子线,isa线指向的是元类。 惯性思维让我们平常新建一个UIView
的子类ChildView后,会说ChildView
这个view
。实际上严谨来说,我们并不能说ChildView is a class of UIView,只能说ChildView is a subclass of UIView对吧。
有些偏门知识但对后面原理很有用 类对象在程序运行时一直存在。 类对象所保存的信息在程序编译时确定,在程序启动时加载到内存中。
- 最后研究
isa
这个?东西*id
是结构体objc_object
的指针。在后面动态绑定会用到。
从objc_object
可以看出isa
声明是个 Class
。和上面说到的每个对象都有个isa相呼应。 然后Class是结构体objc_class
的指针。 于是Command进这个objc_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;
复制代码
看到了methodLists
和cache
就知道那个例子是怎么实现的了。 向UIView的实例发送resignFirstResponder
后,首先通过isa指针找到UIView类,由于是-方法,就在该类中cahce
中找,再到methodLists
找,(然后还会在动态方法找,这里先不用管)。找不到,由于是-方法,就通过super_class
指针,往父类找。
总结
- 其实质上像是二叉树,有两个指针分别指向父类和元类。
- 调用-方法走红线。调用+方法走蓝线。
动态类型和动态绑定
- 简单了解动态类型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
进行动态绑定
提供了可能。
- 动态绑定与动态类型的关联。
基于动态类型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)
复制代码
给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。 就是写了set
和get
方法。