0 Runtime是什么
程序运行之后的内存管理。
OC具有动态特性,即运行时才检查对象类型和方法实现。
0-1 RunTime做了哪些事
绑定:对象动态加载、swizzling、关联对象
查找:方法查找、反射
释放:引用计数、weak、autoreleasepool
1 源码下载
2 SEL -> IMP,方法查找
hit / miss / add。
缓存方法列表 -> 对象方法列表 -> 父类的方法列表 -> 消息转发机制 -> 抛异常
2.1 SEL只认方法名,OC不支持函数重载
3 Class对象
3.1 Class对象和metaClass对象
可以认为,Class是用来维护对象方法列表的,而MetaClass是用来维护类方法列表的。
在进行方法查找时,
- [obj msg]通过obj的isa找到Class对象,从而找到obj的方法列表。
- 如果没找到,通过superclass找到父类的方法列表。
- 如果还没有找到,goto 2,直到NSObject。
/*
传入 instance 对象,返回 class 对象
传入 class 对象,返回 meta-class 对象
传入 meta-class 对象,返回 NSObject 基类的 meta-class 对象
*/
Class object_getClass(id obj)
//只能返回类对象,无论调用几次
- (Class)class
+ (Class)class
//返回类对象
Class objc_getClass(const char *aClassName)
//返回元类对象
Class objc_getMetaClass(const char *aClassName)
//获取父类
class_getSuperclass(Class cls)
注意:
- object_getClass的
返回对象
,都是参数对象
的类对象
,包含了参数对象
的方法列表。 - objc_getClass(char *),string -> Class,而
object
_getClass则是object
-> Class
3.2 类方法中的self
- +方法中的self,表示Class对象。
- Class对象可调用的methodlist,保存在metaClass对象中。 类方法中,用self调用函数,只在Class对象可调用的methodlist中查找。
3.3 super
内部实现,见Runtime源码
3.3.1 self调用[super method],父类method方法中的[self xxx]
- [self method]和[super method]的
调用对象
都是self,super的意思是从父类的方法列表中找
method - 案例:被调用的
[super method]
方法中,调用了[self amethod]
这里self的调用先从obj自己的方法列表中找
amethod。
这是因为[self amethod]时,self所代表的对象,是子类对象。
3.3.2 [super class]为什么返回值和[self class]一样
因为都是调用的NSObect的-(Class)class
方法,实现如下:
return objc_getClass(self);
3.4 UIView的动画中使用self会不会有循环引用?
不会,因为,无论self是谁,都不会去引用UIView。 下面的代码中,
- UIView.animate类方法中的self,是
drawFollowing:
实例方法中的隐藏参数。 - UIView类方法的block,正常捕获self。
- (void)drawFollowing:(CGPoint)previousPoint
{
CGRect frame = CGRectZero;
UIImageView *aview = [self createImageView:frame];
[UIView animateWithDuration:2
delay:0
usingSpringWithDamping:1
initialSpringVelocity:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
//这里是2秒后的状态,所以rect.size.width=0没问题
aimageView.alpha = 0.7;
CGRect rect = imageView.frame;
rect.size.width = 0;
rect.size.height = 0;
//self
imageView.transform=CGAffineTransformMakeRotation(M_PI/[self getRandomNumber:1 to:6]);
imageView.frame = rect;
}
}
];
}
4. Swizzling, class_addMethod
- +load方法中交换
- dispatch_once
- 类方法swizz,会用到object_getClass
4.1 可能出现的死循环
- 如果继承关系中的两个类 都 进行了同名方法的swizzling,会出现死循环。
- 解决方法:只在某个结点进行swizzling(如UIViewController),或只在叶子结点交换(不好)。
- 死循环原因:就是3.3节super的问题导致
4.2 有就交换,没有就添加
4.3 类簇和Swizzling
对类簇的理解,只停留在诸如NSArray这种集合类上。
- 类簇可以看出是抽象工厂模式,根据输入、构造方法,返回不同的子类。
- 类簇主要是要注意Swizzling 对类簇进行swizz,需要知道内部结构
5. property
5.1 property & dynamic & synthesize
@dynamic
,不生成实例变量、getter/setter。
@synthesize
,自动合成getter/setter,实例变量。iOS6之后,编译器有了,属性自动合成,无需此句了。
5.2 property & 实例变量
\ | 父类属性 | dynamic | category声明的属性 | @implemetaion A{NSString * aInstance} |
---|---|---|---|---|
Ivar* | 不含 | 不含 | 不含 | 包含 |
objc_property_t * | 不含 | 包含 | 包含 | 不含 |
5.3 property & category & 关联对象 & KVO
5.3.1 加载时机
-
category添加的属性和方法,编译阶段已经连接好了,运行时,加载Class对象的时候,全部加入
属性列表
和方法列表
中,不影响方法查找流程。 -
category添加的同名方法会覆盖原类中的方法,无论是否import category。
-
多个category,+load方法不会分类覆盖,而是按照project.pbxproj文件中PBXSourcesBuildPhase的顺序依次执行。
-
多个category,+initialize方法,只会有一个生效,project.pbxproj文件中PBXSourcesBuildPhase中最后的category。
-
多个category,实例方法,会覆盖,只会有一个生效,是谁由编译器决定。
5.3.2 dynamic,只添加了getter/setter,没有添加成员变量
@dynamic pages;
- (void)setPages:(NSArray *)pages {
objc_setAssociatedObject(self, _cmd, pages, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSArray *)pages {
return objc_getAssociatedObject(self, @selector(pages));
}
5.3.3 添加基础类型的属性, 装箱拆箱
- (void)setBb:(NSInteger)bb {
objc_setAssociatedObject(self, @selector(bb), @(bb), OBJC_ASSOCIATION_RETAIN);
}
- (long)bb {
NSLog(@"Current method: %@ %@",[self class],NSStringFromSelector(_cmd));
return [objc_getAssociatedObject(self, _cmd) integerValue];
}
5.3.4 使关联对象添加的属性可以被KVO
- (void)setPageView:(MyPageView *) pageView{
[self willChangeValueForKey:@"pageView"];
objc_setAssociatedObject(self, @selector(pageView),
pageView,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self didChangeValueForKey:@"pageView"];
}
5.3.5 关联对象的本质及实现
objc_setAssociatedObject
方法的描述如下:“Sets an associated value for a given object using a given key and association policy.”
也就是说,可以理解为,
对于given object(大多数时候都是self),应该有一个MutableDictionary来记录所有的关联对象,其中key是SEL,value就是关联的对象本身。
也因此,关联对象不属于实例变量
需要注意的是,他跟NSMutableDictionary还有本质区别的 (eoc-10) 其区别在于,
- NSMutableDictionary的key是否相等,用isEquals:
- 关联对象的key是否相等,直接用==
- 也就是说,特殊情况下,如果key是mutableString的,同样内容的key,在关联对象中,可以对应不同的对象。 因此,关联对象的key通常都是静态全局变量(NSString符合要求)
6. 反射
objc_xx、class_xx、iver_xx、property_xx、method_xx、sel_xx等
#import "objc/runtime.h"
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (int i=0; i < count; i++) {
Ivar const ivar = ivars[i];
//获取属性名
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
//获取属性值,当心crash
id value = [self valueForKey:key];
}
if(ivars){
free(ivars);
}
对方法的反射,是针对.m的。
注意,方法反射,只能拿到-
方法(静态方法拿不到),拿不到父类的方法,可以拿到分类方法。
7 autoreleasepool
每个线程都会自动创建自己最外层的autorealsepool。 autoreleasepool可以嵌套。
没有手动创建autoreleasepool的情况下,等到线程执行
下一次事件循环
(如for循环整体结束后、线程执行完等)时,才去清空不再使用的对象。
对于频繁的文件操作、for循环中的图片
等操作,使用单独的autoreleasepool可能会降低memory peak
。(eoc-34)
注意:这里提到的事件循环,与RunLoop无关。
8 消息转发
缓存方法列表 -> 对象的方法列表 -> 父类的方法列表 -> 动态方法解析 -> 消息转发。 应用场景,除了捕获Crash,暂时没找到,而且捕获Crash有更好的方式。
- 尝试
动态添加
一个方法处理,自己内部消化。 - 尝试找
备援对象
处理,找其他对象。 - 将SEL和参数封装成
Invocation
,找其他对象处理,这里可以对参数进行一些增删改
//动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {} (实例方法)
+ (BOOL)resolveClassMethod:(SEL)sel {} (类方法)
//备援接受者
- (id)forwardingTargetForSelector:(SEL)aSelector {}
//完整的消息转发
//第一个要求返回一个方法签名,第二个方法转发具体的实现。二者相互依赖,只有返回了正确的方法签名,才会执行第二个方法。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {}
- (void)forwardInvocation:(NSInvocation *)anInvocation {}
// ViewController.m 中
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
//下面几行是告诉编译器,不要对特定类型(未声明方法)报警
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if (aSelector == @selector(myTestPrint:)) {
#pragma clang diagnostic pop
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
Person *person = [Person new];
Animal *animal = [Animal new];
if ([person respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:person];
}
if ([animal respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:animal];
}
}
8.1 ObjcType
//在NSMethodSignature.h中
+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types; //1
//在NSObject.h中
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""); //2
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""); //3
NSMethodSignature是方法签名,是用来记录返回值类型和参数类型的一个对象。
2和3两个方法是根据SEL来构造NSMethodSignature
1是根据ObjCTypes来构造NSMethodSignature
ObjCTypes是一个是字符串数组,该数组包含了方法的类型编码
- (void)saySth:(Something *)sth;
其ObjcTypes就是 "v@:@",有两种方式获取该字符串
- 直接查表。在Type Encodings里面列出了对应关系。
- 使用
@encode()
计算。(NSLog(@"%s",@encode(BOOL))
的结果为B )
举例,消息转发中
[sbody saySth:sth];
//-> void objc_msgSend(sbody, @selector(saySth:), sth);
v@:@
各符号含义如下,
v
指代void,(连起来,是 返回值void)@
指代对象,(连起来,是 消息的接受者,即sbody):
指代SEL@
指代对象,(连起来,是 消息的参数,即sth)
9 引用计数、weak、isa、SideTable
引用计数做了2件事,
- 记录 obj的引用数,以在清零时,清理obj
- 记录 obj -> 弱引用列表,以在清理obj时,把弱引用置为nil
具体实现
-
引用计数 isa管理对象的引用计数
- 增加时,如果引用数超过2^8-1,就会减半,把一半的值保存在SideTable中。
- 减少时,如果引用数<0,去SideTable中取一半出来,如果取完后,引用数仍为0,就会做dealloc处理,释放内存、weak表
-
SideTables,全局变量,容量为64的数组,数组元素是SideTable
// n 1
//obj <---> SideTable
这里有一个二次hash,原因:SideTables最大是64。可以理解为大小表,降低索引量,另外联系到hash冲突是用index+1的方式,而不是拉链法,也可能是为了降低
hash冲突
的可能性。 第一次,SideTables分配obj,即obj->SideTable。 第二次,SideTable分配obj,即obj->计数表、弱引用表。
- SideTable,聚合了计数表和弱引用表 实际上,如果不看SideTables,然后把计数表、弱引用表移到SideTables中。结构就非常清晰了。
数据结构, 见Runtime源码
上图中下面的部分,表示,对象被清除后,weak引用置nil的原理。10 内存分布
-
内存中的五大区域.
- 栈: 存储局部变量.
- BSS段: 存储未初始化的全局变量、静态变量.
- 数据段(常量区): 存储已经初始化的全局变量、静态变量、常量数据.
- 堆: 其他对象.
- 代码段:存储程序的代码.
-
类加载.
- 当创建对象的时候,访问这个类
- 如果只是声明类指针的时候,也会访问这个类,以确定这个类型是否存在。
- 当类第一次被import的时候,会将类存储到代码段之中。
- 将类的代码以字符串的形式存储在代码段中。
- 只有类第1次被访问的时候,才会有类加载。
- 一旦类被加载到代码区,直到程序结束的时候才会被回收。
-
内存对齐
- 64位地址空间中,oc对象的指针为8字节。
- 对于指针对内存空间的可能浪费,苹果采用了Tagged Pointer技术。 即,如果指针所指对象的内容可以用8字节(其实是小于8字节,因为还有标志位)表示,该指针本身就会存储内容。该方案应用在了NSData,NSString,NSNumber中。
- 也因此,isa并不一定是一个真正的对象,但是object_getClass可以获取到对象的类型。
11 字节对齐
- iOS中,OC对象,最小8个字节
字节对齐,目的:为了兼容、效率。迎合硬件,CPU -> 系统
- iOS,64位系统中,
分配空间的时候:8字节对齐。
(x+7)>>3<<3
好读
(x+7)& ~7
通用(后三位清零)
11.1 基础类型字节占用
指针,占8个字节
short,占2个字节
int/float,占4个字节
long/double,占8个字节
注意:关联对象,不属于成员变量,所以占用字节为0