一,动态语言Objective-C是怎么样的?
1,当build后通过编译将代码转换成计算机能识别的语言,然后运行时把编译后的代码加载到内存运行起来。而oc总是尽可能的把编译和链接阶段做的事推迟到运行时,这就需要一个运行时系统来执行编译后的代码。这就是运行时系统存在的意义,它是oc运行框架的一块基石。runtime是一套底层的c语言api,提供了强大和实用的c语言数据结构和c语言函数,我们平时编写的oc代码,在运行时都会转为c语言代码。
2,oc提供了runtime库,在库中,对象可以用c语言中的结构体表示,方法可以用c语言函数表示,在加上额外的一些特性,这些结构体和函数被runtime函数封装后,我们就可以创建,检查,修改类、对象和方法了;找出方法的最终执行代码(消息机制原理)。
3,在运行时才会去确定对象的类型和方法的,OC语言的动态性主要体现在三个方面:
动态类型(Dynamic typing):将类型的确定延迟到了运行时
动态绑定(Dynamic binding):指的是方法确定的动态性,具体指的是利用OC的消息传递机制将要执行的方法的确定推迟到运行时,可以动态添加方法。
动态加载(Dynamic loading):主要包括两个方面,一个是动态资源加载,一个是一些可执行代码模块的加载,这些资源在运行时根据需要动态的选择性的加入到程序中,是一种代码和资源的‘懒加载’模式,可以降低内存需求,提高整个程序的性能,另外也大大提高了可扩展性。
4,oc中方法调用本质是,对象发送消息。这就是消息机制
二,内存分析
1,创建一个实例对象分两步,第一步先创建实例对象所对应的类(在编译过程就完成了系统做的,也可以在运行时的时候手动创建一个类objc_allocateClassPair),第二步在创建实例对象(运行时)。第一步一般是系统帮我们做的,第二步就是NSObject object = [NSObject alloc]init];的过程
第一步先创建实例对象所对应的类(项目中的类都是oc帮我们做的):
1、为 class pair分配存储空间 ,使用 objc_allocateClassPair函数
2、增加需要的方法使用class_addMethod函数,增加实 例变量用class_addIvar
3 、用objc_registerClassPair函数注册这个类,以便它能被别人使用。
这时类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量。
第二步在创建实例对象:调用alloc时最终会调用class_createInstance,在这里会分配内存,然后会调用objc_constructInstance,这里会初始化对象的 isa 指针,让实例对象的isa指针指向它所属的类
此篇文章分析的很好:https://www.jianshu.com/p/8e4887a43bd7
2,可以运用链表的结构来分析对象、类、元类、根类的内存,通过指针指向下一个元素。
oc中的对象都是继承NSObject,从NSObject源码可以看出有一个isa指针
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
在查看Class的源码,也有一个isa的指针,注意objc_class结构体就是构造一个类的,可以看作一个类,也就是所有的实例对象中isa指针都会指向objc_class
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
} OBJC2_UNAVAILABLE;
所以当我们创建 NSObject object = [NSObject alloc]init]时,NSObject的isa指针会指向对象所属的类即(objc_class),objc_class本身也是一个对象它中的isa指针指向元类,而元类中的isa指针指向根元类,而根元类中的isa指针指向它自己,完成闭环。
IMP指针是指向实现函数的指针.
如果用实例对象调用实例方法,会到实例的isa指针指向的对象(也就是类对象)访查cache和methodLists。
如果调用的是类方法,就会到类对象的isa指针指向的对象(也就是元类对象)中访查cache和methodLists。
三,消息机制
编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector (makeText));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中 通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针(IMP指针)跳转到对应的函数中去执行。
如果没找到程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX,但此之前会有三次补救措施即没有找到方法就会转向拦截调用。
那么什么是拦截调用呢。
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理。
- (BOOL)resolveClassMethod:(SEL)sel;(进入动态解析)
- (BOOL)resolveInstanceMethod:(SEL)sel;(进入消息重定向)
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;(开始消息转发)
- (void)forwardInvocation:(NSInvocation *)anInvocation;
第一个方法是当你调用一个不存在的类方法的时候,会调用这个方法,默认返回NO,你可以加上自己的处理然后返回YES。
第二个方法和第一个方法相似,只不过处理的是实例方法。
第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
第四个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。
四,运行时动态性的详解
动态类型:指的是对象指针类型的动态性,具体是指使用id任意类型将对象的类型确定推迟到运行时,由赋给它的对象类型决定对象指针的类型。也就是说id修饰的对象为动态类型对象,其他在编译器指明类型的为静态类型对象,通常如果不需要涉及到多态的话还是要尽量使用静态类型。
动态类型识别方法,1.首先是Class类型,2.Class动态类型和类名字符串的相互转换,3.判断对象是否属于某种动态类型,4.判断类中是否有对应的方法,5.方法名字符串和SEL类型的转换
动态绑定:具体指的是利用OC的消息传递机制将要执行的方法的确定推迟到运行时,可以动态添加方法。
动态加载:
五,runtime的几个结构体
Class
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class //父类,如果该类已经是最顶层的根类,那么它为NULL。
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 //方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在methodLists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
struct objc_protocol_list *protocols //协议链表
#endif
} OBJC2_UNAVAILABLE;
Method
Method 代表类中某个方法的类型
typedef struct objc_method *Method;
struct objc_method {
SEL method_name //方法名类型为 SEL
char *method_types //方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型
IMP method_imp //method_imp 指向了方法的实现,本质是一个函数指针
}
objc_method 存储了方法名,方法类型和方法实现
![我们绘制出完整的isa和superClass图](https://img.haomeiwen.com/i2099527/4972399d0104d2c8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
六,runtime 常见作用
1,动态交换两个方法的实现
2,动态添加属性
3,实现字典转模型的自动转换;实现 NSCoding 的自动归档和解档
4,发送消息;动态添加方法;拦截并替换方法(可以看上面的二消息机制)
5,runtime怎么添加属性、方法
1,/**
load方法: 把类加载进内存的时候调用,只会调用一次
方法应先交换,再去调用
*/
+ (void)load {
// 1.获取 imageNamed方法地址
// class_getClassMethod(获取某个类的方法)
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
// 2.获取 ln_imageNamed方法地址
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
// 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}
2,
@interface NSObject (Property)
// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
@property NSString *name;
@property NSString *height;
@end
@implementation NSObject (Property)
- (void)setName:(NSString *)name {
// objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
// object:给哪个对象添加属性
// key:属性名称
// value:属性值
// policy:保存策略
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @"name");
}
3,// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 1.创建对应的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的属性赋值
/**
class_copyIvarList: 获取类中的所有成员变量
Ivar:成员变量
第一个参数:表示获取哪个类中的成员变量
第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
count: 成员变量个数
*/
unsigned int count = 0;
// 获取类中的所有成员变量
Ivar *ivarList = class_copyIvarList(self, &count);
// 遍历所有成员变量
for (int i = 0; i < count; i++) {
// 根据角标,从数组取出对应的成员变量
Ivar ivar = ivarList[i];
// 获取成员变量名字
NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
NSString *key = [ivarName substringFromIndex:1];
// 根据成员属性名去字典中查找对应的value
id value = dict[key];
// 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】
// 而报错 (could not set nil as the value for the key age.)
if (value) {
// 给模型中属性赋值
[objc setValue:value forKey:key];
}
}
return objc;
}
5,
ivar表示成员变量
class_addIvar
class_addMethod
class_addProperty
class_addProtocol
class_replaceProperty
七,面试题
1,runtime 如何实现 weak 属性
首先要搞清楚weak属性的特点
weak策略表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似;然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)
那么runtime如何实现weak变量的自动置nil?
runtime对注册的类,会进行布局,会将 weak 对象放入一个 hash 表中。用 weak 指向的对象内存地址作为 key,value 是指向该对象的所有弱引用的指针。当此对象的引用计数为0的时候会调用对象的 dealloc 方法,假设 weak 指向的对象内存地址是a,那么就会以a为key,在这个 weak hash表中搜索,找到所有以a为key的 weak 对象,从而设置为 nil。
weak属性需要在dealloc中置nil么
在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
即便是编译器不帮我们做这些,weak也不需要在dealloc中置nil
在属性所指的对象遭到摧毁时,属性值也会清空
2,使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
无论在MRC下还是ARC下均不需要被关联的对象在生命周期内要比对象本身释放的晚很多,它们会在被 NSObject -dealloc 调用的object_dispose()方法中释放
补充:对象的内存销毁时间表,分四个步骤
1、调用 -release :引用计数变为零
* 对象正在被销毁,生命周期即将结束.
* 不能再有新的 __weak 弱引用,否则将指向 nil.
* 调用 [self dealloc]
2、 父类调用 -dealloc
* 继承关系中最直接继承的父类再调用 -dealloc
* 如果是 MRC 代码 则会手动释放实例变量们(iVars)
* 继承关系中每一层的父类 都再调用 -dealloc
3、NSObject 调 -dealloc
* 只做一件事:调用 Objective-C runtime 中object_dispose() 方法
4. 调用 object_dispose()
* 为 C++ 的实例变量们(iVars)调用 destructors
* 为 ARC 状态下的 实例变量们(iVars) 调用 -release
* 解除所有使用 runtime Associate方法关联的对象
* 解除所有 __weak 引用
* 调用 free()
3,能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量;
能向运行时创建的类中添加实例变量;
分析如下:
因为编译后的类已经注册在runtime中,类结构体中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同时runtime 会调用class_setIvarLayout 或 class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量
运行时创建的类是可以添加实例变量,调用 class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上。
4,我们所说的Objective-C是动态运行时语言是什么意思?
主要指的是OC语言的动态性,包括动态性和多态性两个方面。
动态性:即OC的动态类型、动态绑定和动态加载特性,将对象类型的确定、方法调用的确定、代码和资源的装载等推迟到运行时进行,更加灵活;
多态:多态是面向对象变成语言的特性,OC作为一门面向对象的语言,自然具备这种多态性,多态性指的是来自不同类的对象可以接受同一消息的能力,或者说不同对象以自己的方式响应相同的消息的能力。
5,动态绑定是在运行时确定要调用的方法?
动态绑定将调用方法的确定推迟到运行时。在编译时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,代码每次执行都可以得到不同的结果。运行时负责确定消息的接收者和被调用的方法。运行时的消息分发机制为动态绑定提供支持。当向一个动态类型确定了的对象发送消息时,运行环境会通过接收者的isa指针定位对象的类,并以此确定被调用的方法,方法是动态绑定的。
6,Object-C有私有方法吗? 私有变量呢?
首先要看私有的含义。私有主要指的是通过类的封装性,将不希望让外界看到的方法或属性隐藏在类内部,只有该类可以在内部访问,外部不可见不可访问。
表面上OC中是可以实现私有的变量和方法的,即将它们隐藏不暴露在头文件,不可以显式地直接访问,但是OC中这种私有并不是绝对的私有,例如即使将变量和方法隐藏在.m实现文件中,开发者仍然可以利用runtime运行时机制强行访问没有暴露在头文件的变量和方法。
7,为啥分类中不能直接添加属性
因为分类本身不是一个类,它只是运行时为类添加的一些方法和属性。
分类中声明的属性不会生成set、get方法和带下划线的成员变量。
首先理解属性、成员变量的关系,属性是用property声明的,会自动生成set、get方法及带下划线的成员变量,说白了属性就是一种特殊的方法,通过点语法去获取成员变量的值;而成员变量是类的东西,保存着成员变量的值,当使用点语法其实就是修改或者获取成员变量的值。而类的结构体中也是ivarlist和methodlist没有属性的。
即使通过objc_getAssociatedObject和objc_setAssociatedObject关联上也不会生成成员变量,只是关联上对这个属性的存取过程。
网友评论