Runtime

启动流程

objc-os.mm的init方法是初始化的入口。

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

几个初始化

  • environ_init,读取环境配置方法,在这个方法里会读取在Xcode 中配置的环境变量参数
  • tls_init,用于初始化不是使用pthread_key_create()创建的线程的析构函数
  • static_init,执行 C++ 静态构造函数功能
  • lock_init,初始化后台线程和主线程优先级
  • exception_init,异常初始化

map_images

在map_images函数中,内部也是做了一个调用中转。然后调用到map_images_nolock函数,内部核心就是_read_images函数。先整体梳理一遍_read_images函数内部的逻辑:

  1. 加载所有类到类的gdb_objc_realized_classes表中。
  2. 对所有类做重映射。
  3. 将所有SEL都注册到namedSelectors表中。
  4. 修复函数指针遗留。
  5. 将所有Protocol都添加到protocol_map表中。
  6. 对所有Protocol做重映射。
  7. 初始化所有非懒加载的类,进行rw、ro等操作。
  8. 遍历已标记的懒加载的类,并做初始化操作。
  9. 处理所有Category,包括Class和Meta Class。
  10. 初始化所有未初始化的类。

load_images

在load_images函数中主要做了两件事,

  1. 首先通过prepare_load_methods函数准备Class load list和Category load list,
  2. 然后通过call_load_methods函数调用已经准备好的两个方法列表

isa

在ARM 64之前,isa是一个指针,存储着ClassMeta-Class对象的内存地址。
ARM 64之后,isa是一个共用体,除了存储ClassMeta-Class对象的内存地址,还保存了更多的信息。ARM 64情况下,isa共用体的定义。

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
}

isa的位域的意义

  • nonpointer
    - 0 ,表示指针,存储Class对象地址
    - 1,使用共用体,存储更多信息
  • has_assoc
    - 是否设置过关对象。如果没有,该对象会释放地更快
  • has_cxx_dtor
    - 是否有C++析构函数。如果没有,该对象会释放地更快
  • shiftcls
    - 类对象,元类对象的内存地址
  • magic
    - 对象是否完成初始化
  • weakly_referenced
    - 是否被若引用指向过。如果没有,该对象会释放地更快
  • deallocating
    - 是否正在被释放
  • has_sidetable_rc
    - 是否引用计数器过大,不能保存在isa中。如果过大,会保存在SideTable
  • extra_rc
    - 保存该对象的引用计数减1

method_t

struct method_t {
    SEL name;       // 底层结构类似于char *
    const char *types;
    IMP imp;     // 该表函数的具体实现
};

SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似

  • 可以通过@selector()sel_registerName()获得
  • 可以通过sel_getName()NSStringFromSelector()
  • 不同类中相同名字的方法,所对应的方法选择器是相同的

types包含了函数返回值,参数编码的字符串

// i 24  @ 0  : 8   i 16  f 20
/*  i 24,表示所有参数占据的字节数
    @ 0,表示第一个参数,self,从第0个参数开始
    : 8,表示第二个参数,SEL,从第8个开始
    i 16,表示(int)age
    f 20,表示(float)height
 */
- (void)test:(int)age height:(float)height {
 
}

IMP表示函数的具体实现

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

cache_t

struct cache_t {
    struct bucket_t *_buckets;      // 数组
    mask_t _mask;          // 数组长度
    mask_t _occupied;    // 已经使用的长度
}

struct bucket_t {
private:
    cache_key_t _key;      // SEL
    IMP _imp;
}

散列表使用的hash算法,就是&运算

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

如果发生Hash碰撞,则从前一个开始找,直到索引为0。如果还找不到,就再从数组最后一个,倒序开始找。

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

子类调用父类方法之后,先在子类找不到方法,然后去父类找,

  • 也是先找父类的缓存,再找父类的方法列表
  • 找到之后,会把方法缓存到子类的cache中

缓存的时候,如果缓存满了,则清除所有缓存,并且2倍扩容

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}

obj_msgSend()

OC中的方法调用,都是转换成调用obj_msgSend()函数。整个过程可以分成3个阶段:

  • 消息发送:根据OC对象模型图,从子类到父类,去找方法
  • 动态方法解析:可能会向类对象添加方法
  • 消息转发:可能将该方法调用,转到其他对象去调用。
_class_lookupMethodAndLoadCache3
lookUpImpOrForward
getMethodNoSuper_nolock、search_method_list、log_and_fill_cache
cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache
_class_resolveInstanceMethod
_objc_msgForward_impcache

汇编调用

由于obj_msgSend()调用十分频繁,所以obj_msgSend()有一部分是使用汇编实现的。
一般如果是C函数是objc_msgSend,则对应的汇编函数就是加上下划线_objc_msgSend
汇编做了如下工作:

  • 判断receiver是否为nil,如果是nil就直接返回
  • 查找缓存,如果缓存没有命中,就在方法列表查找方法
objc-msg-arm64.s
ENTRY _objc_msgSend
b.le    LNilOrTagged
CacheLookup NORMAL
.macro CacheLookup
.macro CheckMiss
STATIC_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
__class_lookupMethodAndLoadCache3

查找方法

汇编函数是__class_lookupMethodAndLoadCache3,则对应的runtime方法是_class_lookupMethodAndLoadCache3
在C函数还是会查找一遍缓存,原因是:再执行到这次的查找缓存之前,可能动态添加一些方法,缓存方法变化。

  • 在本类进行查找
    1. 再次查找缓存,
    2. 查找方法,在本类对象的方法列表,还可以分成二分查找(如果已经排好序)和线性查找。如果找到了,会进行缓存
  • 不断地向上,在父类进行查找。不管是
    1. 先找父类的缓存。如果找到了,在自己的类缓存
    2. 在找父类的方法列表。如果找到了,在自己的类缓存

动态方法解析

当通过上面的方法查找,找不到方法,就会进入动态方法解析。

  • 通过triedResolver,只解析一次
  • goto retry会重新进入方法查找:先查找本类缓存,在找本类方法列表;再父类
  if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

使用如下示例代码,进行动态方法解析

void c_anotherMethod(id self, SEL _cmd)
{
    NSLog(@"c_another class method");
}

- (void)anotherTest {
    NSLog(@"instance %s",__func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(test)) {
        // 第一个参数是object_getClass(self)
        class_addMethod(object_getClass(self), sel, (IMP)c_anotherMethod, "v16@0:8");
        return YES;
    }
    return [super resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        Method anotherMethod = class_getInstanceMethod(self, @selector(anotherTest));
        class_addMethod(self,
                        sel,
                        method_getImplementation(anotherMethod),
                        method_getTypeEncoding(anotherMethod));
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

消息转发

消息转发.png

首先是调用forwardingTargetForSelector:,如果返回不为nil,就进入target的方法调用流程。forwarding:没有开源,可以通过逆向,了解其实现的。
如果最终能够走到forwardInvocation:,即使forwardInvocation:是空实现,该方法调用也能成功完成。

  1. 调用forwardInvocation之前,系统会先调用resolveInstanceMethod:,传入的selector_forwardStackInvocation:
  2. 如果实现methodSignatureForSelector :,但是没有实现forwardInvocation :,也会unrecognized selector
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return [[ForwardTarget alloc] init];
}
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v24@0:8@16"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
//    anInvocation.target 方法调用者
//    anInvocation.selector 方法名
//    anInvocation.methodSignature methodSignatureForSelector:方法返回的
//    [anInvocation getArgument:NULL atIndex:0]
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation invokeWithTarget:[[ForwardTarget alloc] init]];
}

Super

[super message]的底层实现,最初是objc_msgSendSuper(arg, sel),其中第一个参数是结构体,该结构体包含两个成员变量:

  1. 消息接收者,就是子类实例对象
  2. 父类类对象,找方法会从父类类对象开始找
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接收者
    __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
};

后来实现变成objc_msgSendSuper2(arg, sel),第一个参数是结构体,其成员变量是:

  1. 消息接收者,仍然是子类对象
  2. 消息接收者的类对象。但是其内部实现,仍然是获取父类类对象,从父类开始找
struct objc_super2 {
    __unsafe_unretained _Nonnull id receiver; // 消息接收者
    __unsafe_unretained _Nonnull Class current_class; // 消息接收者的类对象
};
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]); // MJStudent
        NSLog(@"[self superclass] = %@", [self superclass]); // MJPerson

        NSLog(@"--------------------------------");

        // objc_msgSendSuper({self, [MJPerson class]}, @selector(class));
        NSLog(@"[super class] = %@", [super class]); // MJStudent
        NSLog(@"[super superclass] = %@", [super superclass]); // MJPerson
    }
    return self;
}

isKindOf

objc的源码如下,要特别注意的是

  1. + (BOOL)isKindOfClass:,要求传入的是metaClass,但是最后一步会走到NSObject的class对象。
// 这句代码的方法调用者不管是哪个类(只要是NSObject体系下的),都返回YES
NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1
NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); // 0
NSLog(@"%d", [MJPerson isKindOfClass:[Person class]]); // 0
NSLog(@"%d", [MJPerson isMemberOfClass:[Person class]]); // 0

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

面试题

以下代码中,是否能够正常打印

@interface Person : NSObject
@property (copy, nonatomic) NSString *name;

- (void)print;
@end

@implementation MJPerson

- (void)print
{
    NSLog(@"my name is %@", self->_name);
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    id cls = [MJPerson class];

    void *obj = &cls;

    [(__bridge id)obj print];
}

首先分析,变量之间的关系,如下图所示

  1. 与正常调用实例方法的结构相同,都包含指针变量,对象的ISA指针,类对象
  2. 所以可以调用到print方法
    变量之间的关系.png

执行方法时,栈中变量地址的位置,如下图所示

  1. 栈中的变量地址是从高到低,即先出现的变量,占据高地址
  2. [super viewDidLoad];,上文说过,会有一个结构体变量;接着是cls,接着是obj
  3. 根据实例对象的结构图,访问name成员变量,就是访问self占据的指针
    栈中的变量地址.png

API使用

  • 成员变量是基本数据类型,需要先转成指针,再bridge
    // 创建类
    Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0);
    class_addIvar(newClass, "_age", 4, 1, @encode(int));
    class_addIvar(newClass, "_weight", 4, 1, @encode(int));
    class_addMethod(newClass, @selector(run), (IMP)run, "v@:");
    // 注册类
    objc_registerClassPair(newClass);
    // 如果是基本数据类型,需要先转成指针,再bridge
    object_setIvar(person, ageIvar, (__bridge id)(void *)10);

具体应用

  • 利用关联对象(AssociatedObject)给分类添加属性
  • 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  • 交换方法实现(交换系统的方法,method_exchangeImplementions会清空方法缓存
  • 利用消息转发机制解决方法找不到的异常问题
+ (void)load { 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        Class aClass = [self class]; 
 
        SEL originalSelector = @selector(viewWillAppear:); 
        SEL swizzledSelector = @selector(xxx_viewWillAppear:); 
 
        Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); 
        
        // When swizzling a class method, use the following:
        // Class aClass = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(aClass, originalSelector);
        // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
 
        BOOL didAddMethod = 
            class_addMethod(aClass, 
                originalSelector, 
                method_getImplementation(swizzledMethod), 
                method_getTypeEncoding(swizzledMethod)); 
 
        if (didAddMethod) { 
            class_replaceMethod(aClass, 
                swizzledSelector, 
                method_getImplementation(originalMethod), 
                method_getTypeEncoding(originalMethod)); 
        } else { 
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 

数组和字典的方法交换

集合类型,由于使用了类簇.

  • 可变数组真正的类型是__NSArrayM
  • 可变字典真正的类型是__NSDictionaryM
  • 不可变字典真正的类型是__NSDictionaryI
   static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
       // 类簇:NSString、NSArray、NSDictionary,真实类型是其他类型
        Class cls = NSClassFromString(@"__NSArrayM");
        Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(cls, @selector(my_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = NSClassFromString(@"__NSDictionaryM");
        Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:));
        Method method2 = class_getInstanceMethod(cls, @selector(my_setObject:forKeyedSubscript:));
        method_exchangeImplementations(method1, method2);
        
        Class cls2 = NSClassFromString(@"__NSDictionaryI");
        Method method3 = class_getInstanceMethod(cls2, @selector(objectForKeyedSubscript:));
        Method method4 = class_getInstanceMethod(cls2, @selector(my_objectForKeyedSubscript:));
        method_exchangeImplementations(method3, method4);
    });
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,540评论 33 466
  • 参考链接: http://www.cnblogs.com/ioshe/p/5489086.html 简介 Runt...
    乐乐的简书阅读 2,129评论 0 9
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,170评论 0 7
  • 有时候心甘情愿的去做一些事情,是不会感到累的,越是辛苦,倒越是觉得幸福。遇到他之前,我从来没有想过什么是喜欢,在遇...
    七汐子阅读 257评论 0 2