RunTime的应用及相关的面试问题

本次主要记录几个问题
1、runtime是什么
2、method-swizzling使用及使用过程中的坑点
3、isKinkOfClass和isMemmberOfClass的使用
4、[self class]和[super class]的区别以及原理分析
5、runtime Associate方法的关联对象,及什么时间释放
6、__weak对象的存储及释放

1、runtime是什么

这个就说的比较宽泛一些,runtime是一套由C & C++编写的一套api,给我们OC提供运行时的功能,研究runtime对其他底层的学习会有一个比较有利的帮助

2、method-swizzling使用及使用过程中的坑点

Class class = [self class];

// 原方法名和替换方法名
SEL originalSelector = @selector(isEqualToString:);
SEL swizzledSelector = @selector(swizzle_IsEqualToString:);

// 原方法和替换方法
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

// 如果当前类没有原方法的实现IMP,先调用class_addMethod来给原方法添加实现
BOOL didAddMethod = class_addMethod(class,
                                    originalSelector,
                                  method_getImplementation(swizzledMethod),
                                    method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {// 添加方法实现IMP成功后,替换方法实现
    class_replaceMethod(class,
                        swizzledSelector,
                        method_getImplementation(originalMethod),
                        method_getTypeEncoding(originalMethod));
} else { // 有原方法,交换两个方法的实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

1、方法交换,一般我们是在+ (void)load方法中进行方法交换,为什么
1)执行比较早,在main函数之前就已经执行
2)自动执行,不需要手动调用,这里的调用 不是通过objc_msgSend调用而是直接通过指针找到相应的imp调用
3)唯一性,调用顺序为 先 父类 -> 子类 -> 分类
》这里有一个优化点,App启动的时候 main函数之前 若是当前类 实现了load方法 就会调用该方法,若是load中执行了耗时操作,就会延长App的启动时间,所以 我们减少App的启动时间的一个优化点就是 不要在load方法中进行耗时操作,也不要随便重写load方法
》还有一个面试题是 load方法是否可以被交换即被hook
答案是可以的,但是成本会比较高
我们看load方法的调用堆栈


load方法调用堆栈

动态链接器dyld,完成对二进制文件(动态库、可执行文件)的初始化后通过回调函数_dyld_objc_notify_register,调用load_images和call_load_method实现load方法的调用,

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

load_images通过prepare_load_methods将所有类的load方法加入到list中,包括父类和分类,这里记录的是所有有load方法的类 和 load方法的imp,之后通过call_load_methods调用所有的load方法
有上述问题 若是我们需要hook A的load方法 必须在A的load方法被加载进入load方法列表之前进行hook,即我们把hook A的load方法的代码放在B中,然后保证B动态库在A之前被链接,这样就可以进行hook了

isKinkOfClass和isMemmberOfClass的使用

对象、类对象、元类关系图

这里首先我们看一下下面代码的打印,先看实例方法,这个比较简单

- (void)test4 {
    
    NSObject *object = [[NSObject alloc] init];
    Person *person = [[Person alloc] init];
    
    bool o1 = [object isKindOfClass:[NSObject class]];
    bool o2 = [object isMemberOfClass:[NSObject class]];
    bool p1 = [person isKindOfClass:[Person class]];
    bool p2 = [person isMemberOfClass:[Person class]];
    // 1-1-1-1
    NSLog(@"%d-%d-%d-%d",o1,o2,p1,p2);
    
}

-isKindOfClass

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
  • 我们可以看到,对象方法的 for循环 初始值 变成了 [self class],也就是从当前类开始找superclass继承链。

  • 所以 [(id)[NSObject alloc] isKindOfClass:[NSObject class]] 和 [(id)[DZPerson alloc] isKindOfClass:[DZPerson class]] 都为 YES

-isMemberOfClass

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

  • -isMemberOfClass 对象方法更是简单了,直接就是判断当前类和传入类是否相等。
  • [(id)[NSObject alloc] isMemberOfClass:[NSObject class]] 和 [(id)[DZPerson alloc] isMemberOfClass:[DZPerson class]] 自然都是 YES。

再看类方法

- (void)test5 {
    
    Class object = [NSObject class];
    Class person = [Person class];
    
    bool o1 = [object isKindOfClass:[NSObject class]];
    bool o2 = [object isMemberOfClass:[NSObject class]];
    bool p1 = [person isKindOfClass:[Person class]];
    bool p2 = [person isMemberOfClass:[Person class]];
    
    // 1-0-0-0
    NSLog(@"===class=%d-%d-%d-%d",o1,o2,p1,p2);
    
}

为什么呢?我们看一下isKindOfClass的源码

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

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

我们可以看出

  • Class tcls = object_getClass((id)self);

    从源码可以看到,self 是类本身,object_getClass((id)self) 则是获取 isa,而 isa 是指向元类的,所以 tcls 实际上是当前类的元类。
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass)

  • for循环实际上就是从当前类的元类开始,沿着继承链中的 superclass 一直向上循环,在如下 isa指向图 中标注部分,NSObject元类 的父类是 NSObject。所以在第二次循环的时候,NSObject元类 的 superclass 是本身NSObject。
  • 但是 DZPerson元类 的继承链是DZPerson元类 -> NSObject元类 -> NSObject,所以在 DZPerson元类 的继承链上永远不会有自身DZPerson。
  • 因此 [(id)[NSObject class] isKindOfClass:[NSObject class]] = YES ,而 [(id)[DZPerson class] isKindOfClass:[DZPerson class]] == NO。

再看 isMemberOfClass的源码

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

  • 从源码中可以看到,代码是直接判断当前类的元类是否等于传入类。
  • 所以 [(id)[NSObject class] isMemberOfClass:[NSObject class]] 和 [(id)[DZPerson class] isMemberOfClass:[DZPerson class]]中,NSObject元类 不等于 NSObject,DZPerson元类 也不等于 DZPerson,结果自然都是 NO。

[self class]和[super class]的区别以及原理分析

[self class]

- (Class)class {
    return object_getClass(self);
}
///
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

也就是获取当前类的isa指向 实例的isa指向类
[super class]
本质上是调用objc_msgSendSuper

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
第一个参数为一个objc_super的结构体
struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};
那么[super class]就等价于
struct objc_super lg_super = {
//我们这里研究对象为当前对象所以消息接收这位self
   self,
   class_getSuperclass([self class]),
};
objc_msgSendSuper(&lg_super,@selector(class))

消息接受者 还是self 即 还是会打印实例的类

  • [self class] 就是发送消息objc_msgSend,消息接受者是 self 方法编号:class

  • [super class] 本质就是objc_msgSendSuper, 消息的接受者还是 self 方法编号:class 只是objc_msgSendSuper 会更快 直接跳过 self 的查找

不能向编译后的类中添加实例变量,但是可以向运行时创建的类中添加实例变量

  • 类编译后只读结构体class_ro_t就被确定了,运行时不可修改
  • ro结构体中的iver_list也是不可修改的,并且instanceSize决定了创建对象时需要的空间大小
  • 运行时添加实例变量 必须在objc_allocateClassPair和objc_registerClassPair之间调用class_addIver

runtime Associate方法的关联对象,及什么时间释放

  • 需要调用objc_setAssociatedObject,询问是否存在全局的AssociationsManager有就获取,没有就创建,然后跟拒当前类获取 ObjectAssociationMap,没有就创建一个,存入我们的关联对象
  • _object_get_associative_reference,获取也是同样的逻辑 全局AssociationsManager,根据当前类获取ObjectAssociationMap,再根据关联对象的key获取值
  • 删除关联对象 ,在主类dealloc时调用析构函数 objc_destructInstance,然后看当前类是否存在析构,存在的话调用_object_remove_assocations,删除析构

__weak对象的存储及释放

image.png
  • 从全局的SideTables中,利用对象本身的地址取得该对象的弱引用表weak_table
  • 如果有分配新值,则检查新值对应的类是否进行过初始化,如果没有则就地初始化weak_register_no_lock
  • 若是有旧值,则需要把旧值对应的弱引用表进行注销weak_unregister_no_lock
  • 将新值注册到对应的弱引用表中,将isa.weakly_referenced设置为true,表示该类有弱引用变量,释放时需要清空弱引用表

链接弱引用表

id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;// 被引用的对象
    objc_object **referrer = (objc_object **)referrer_id;// 弱引用变量

    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // 确保弱引用对象是否可行
    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }
    // 如果正在释放中,则根据 crashIfDeallocating 判断是否触发 crash
    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored
    // 每个对象对应的一个弱引用记录
    weak_entry_t *entry;
    
    // 如果当前表中有该对象的记录则直接加入该 weak 表中对应记录
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    } 
    else {
        
        // 没有在 weak 表中找到对应记录,则新建一个记录
        weak_entry_t new_entry(referent, referrer);
        
        // 查看是否需要扩容
        weak_grow_maybe(weak_table);
        
        // 将记录插入 weak 表中
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}

上述代码主要功能如下:

  • 判断被指向对象是否可行,也就是判断其是否正在释放,并且会根据crashIfDeallocating判断是否触发crash。
  • 在weak_table中检测是否有被指向对象的entry,如果有的话,直接将该弱引用变量指针加入到该entry中
  • 如果没有找到对应的entry,新建一个entry,并将弱引用变量指针地址加入entry,同时检查weaktable是否扩容。

移除

void
weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, 
                        id *referrer_id)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    weak_entry_t *entry;
    // 指向对象为空直接返回
    if (!referent) return;

    // 在weak表中查找
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        
        // 找到相应记录后,将该引用从记录中移除。
        remove_referrer(entry, referrer);
        // 移除后检查该记录是否为空
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            // 不为空 将标记记录为false
            empty = false;
        }
        else {
            // 对比到记录的每一行
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }
        // 如果当前记录为空则移除记录

        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }

    // Do not set *referrer = nil. objc_storeWeak() requires that the 
    // value not change.
}


这段代码的主要流程

  • 从weak_table中根据找到被引用对象对应的entry,然后将弱引用变量指针referrer从entry中移除。
  • 移除弱引用变量指针referrer之后,检查entry是否为空,如果为空将其从weak_table中移除
static void weak_entry_remove(weak_table_t *weak_table, weak_entry_t *entry)
{
    // 释放 entry 中的所有弱引用
    if (entry->out_of_line()) free(entry->referrers);
    // 置空指针
    bzero(entry, sizeof(*entry));
    // 更新 weak_table 对象数量,并检查是否可以缩减表容量
    weak_table->num_entries--;
    weak_compact_maybe(weak_table);
}

  • 释放entry和其中的弱引用变量。
  • 更新 weak_table 对象数量,并检查是否可以缩减表容量

entry 和 referrer
entry以及比较熟悉了,一个对象的弱引用记录,referrer则是代表弱引用变量,每次被弱引用时,都会将弱引用变量指针referrer加入entry中,而当原对象被释放时,会将entry清空并移除

从entry移除referrer的步骤:

out_of_line为false时,从有序数组inline_referrers中查找并移除。
out_of_line为true时,从哈希表中查找并移除

dealloc
当被引用的对象被释放后,会去检查isa.weakly_referenced标志位,每个被弱引用的对象weakly_referenced标志位都为true。

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    // 根据指针获取对应 Sidetable
    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        // 存在弱引用
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}


从上面的代码可以看出,在对象释执行dealloc函数时,会检查isa.weakly_referenced标志位,然后判断是否要清理weak_table中的entry。

这最后还是走到了前面的remove。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容