iOS Runtime 小结

2,174 阅读10分钟

0 Runtime是什么

程序运行之后的内存管理。

OC具有动态特性,即运行时才检查对象类型和方法实现。

0-1 RunTime做了哪些事

绑定:对象动态加载、swizzling、关联对象

查找:方法查找、反射

释放:引用计数、weak、autoreleasepool

1 源码下载

opensource.apple.com

2 SEL -> IMP,方法查找

hit / miss / add。

缓存方法列表 -> 对象方法列表 -> 父类的方法列表 -> 消息转发机制 -> 抛异常

2.1 SEL只认方法名,OC不支持函数重载

3 Class对象

3.1 Class对象和metaClass对象

可以认为,Class是用来维护对象方法列表的,而MetaClass是用来维护类方法列表的。

在进行方法查找时,

  1. [obj msg]通过obj的isa找到Class对象,从而找到obj的方法列表。
  2. 如果没找到,通过superclass找到父类的方法列表。
  3. 如果还没有找到,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)

注意:

  1. object_getClass的返回对象,都是参数对象类对象,包含了参数对象的方法列表。
  2. objc_getClass(char *),string -> Class,而object_getClass则是object -> Class

3.2 类方法中的self

  1. +方法中的self,表示Class对象。
  2. Class对象可调用的methodlist,保存在metaClass对象中。 类方法中,用self调用函数,只在Class对象可调用的methodlist中查找。

3.3 super

内部实现,见Runtime源码

3.3.1 self调用[super method],父类method方法中的[self xxx]

  1. [self method]和[super method]的调用对象都是self,super的意思是从父类的方法列表中找method
  2. 案例:被调用的[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。 下面的代码中,

  1. UIView.animate类方法中的self,是drawFollowing:实例方法中的隐藏参数。
  2. 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

  1. +load方法中交换
  2. dispatch_once
  3. 类方法swizz,会用到object_getClass

4.1 可能出现的死循环

  1. 如果继承关系中的两个类 都 进行了同名方法的swizzling,会出现死循环。
  2. 解决方法:只在某个结点进行swizzling(如UIViewController),或只在叶子结点交换(不好)。
  3. 死循环原因:就是3.3节super的问题导致

4.2 有就交换,没有就添加

4.3 类簇和Swizzling

对类簇的理解,只停留在诸如NSArray这种集合类上。

  1. 类簇可以看出是抽象工厂模式,根据输入、构造方法,返回不同的子类。
  2. 类簇主要是要注意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 加载时机

  1. category添加的属性和方法,编译阶段已经连接好了,运行时,加载Class对象的时候,全部加入属性列表方法列表中,不影响方法查找流程。

  2. category添加的同名方法会覆盖原类中的方法,无论是否import category。

  3. 多个category,+load方法不会分类覆盖,而是按照project.pbxproj文件中PBXSourcesBuildPhase的顺序依次执行。

  4. 多个category,+initialize方法,只会有一个生效,project.pbxproj文件中PBXSourcesBuildPhase中最后的category。

  5. 多个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) 其区别在于,

  1. NSMutableDictionary的key是否相等,用isEquals:
  2. 关联对象的key是否相等,直接用==
  3. 也就是说,特殊情况下,如果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有更好的方式。

  1. 尝试动态添加一个方法处理,自己内部消化。
  2. 尝试找备援对象处理,找其他对象。
  3. 将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件事,

  1. 记录 obj的引用数,以在清零时,清理obj
  2. 记录 obj -> 弱引用列表,以在清理obj时,把弱引用置为nil

具体实现

  1. 引用计数 isa管理对象的引用计数

    1. 增加时,如果引用数超过2^8-1,就会减半,把一半的值保存在SideTable中。
    2. 减少时,如果引用数<0,去SideTable中取一半出来,如果取完后,引用数仍为0,就会做dealloc处理,释放内存、weak表
  2. SideTables,全局变量,容量为64的数组,数组元素是SideTable

//    n   1
//obj <---> SideTable

这里有一个二次hash,原因:SideTables最大是64。可以理解为大小表,降低索引量,另外联系到hash冲突是用index+1的方式,而不是拉链法,也可能是为了降低hash冲突的可能性。 第一次,SideTables分配obj,即obj->SideTable。 第二次,SideTable分配obj,即obj->计数表、弱引用表。

  1. SideTable,聚合了计数表和弱引用表 实际上,如果不看SideTables,然后把计数表、弱引用表移到SideTables中。结构就非常清晰了。

数据结构, 见Runtime源码

上图中下面的部分,表示,对象被清除后,weak引用置nil的原理。

10 内存分布

  1. 内存中的五大区域.

    1. 栈: 存储局部变量.
    2. BSS段: 存储未初始化的全局变量、静态变量.
    3. 数据段(常量区): 存储已经初始化的全局变量、静态变量、常量数据.
    4. 堆: 其他对象.
    5. 代码段:存储程序的代码.
  2. 类加载.

    1. 当创建对象的时候,访问这个类
    2. 如果只是声明类指针的时候,也会访问这个类,以确定这个类型是否存在。
    3. 当类第一次被import的时候,会将类存储到代码段之中。
      1. 将类的代码以字符串的形式存储在代码段中。
      2. 只有类第1次被访问的时候,才会有类加载。
      3. 一旦类被加载到代码区,直到程序结束的时候才会被回收。
  3. 内存对齐

    • 64位地址空间中,oc对象的指针为8字节。
    • 对于指针对内存空间的可能浪费,苹果采用了Tagged Pointer技术。 即,如果指针所指对象的内容可以用8字节(其实是小于8字节,因为还有标志位)表示,该指针本身就会存储内容。该方案应用在了NSData,NSString,NSNumber中。
    • 也因此,isa并不一定是一个真正的对象,但是object_getClass可以获取到对象的类型。

11 字节对齐

  1. iOS中,OC对象,最小8个字节

字节对齐,目的:为了兼容、效率。迎合硬件,CPU -> 系统

  1. iOS,64位系统中,

分配空间的时候:8字节对齐。

(x+7)>>3<<3 好读

(x+7)& ~7 通用(后三位清零)

11.1 基础类型字节占用

指针,占8个字节

short,占2个字节

int/float,占4个字节

long/double,占8个字节

注意:关联对象,不属于成员变量,所以占用字节为0