洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

    • Runloop系列

    • Runtime系列

      • RunTime系列
        • 一. RunTime简介
        • RunTime消息机制
          • 方法的本质是什么
          • SEL和IMP的关系
          • 能否向运⾏时创建的类中添加实例变量
        • 利用runtime-API创建对象
          • 动态创建类
          • 添加成员变量
          • 注册到内存
          • 添加属性变量
          • 添加方法
          • 整体使用
        • 关联对象分析
          • 1.分类直接添加属性的后果
          • 2.为什么不能直接添加属性
          • 3.解决方案
          • 关联对象原理
        • weak置空原理
          • weak创建过程
    • 内存管理系列

    • Block系列

    • 线程系列

    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • Runtime系列
洋仔
2023-08-11
目录

RunTime系列

# 一. RunTime简介

提示

RunTime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。 对于C语言,函数的调用在编译的时候会决定调用哪个函数,如果调用未实现的函数就会报错。对于OC语言,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。

# RunTime消息机制

消息机制是运行时里面最重要的机制,OC中任何方法的调用,本质都是发送消息。使用运行时,发送消息需要导入框架并且xcode5之后,苹果不建议使用底层方法,如果想要使用运行时,需要关闭严格检查objc_msgSend的调用,BuildSetting->搜索msg 改为NO。

# 方法的本质是什么

方法的本质是发送消息objc_msgSend,即寻找IMP的过程

发送消息会有以下⼏个流程:

  • 快速查找流程——通过汇编objc_msgSend查找缓存cache_t是否有imp实现
  • 慢速查找流程——通过C++中lookUpImpOrForward递归查找当前类和父类的rw中methodlist的方法
  • 动态方法解析——通过调用resolveInstanceMethod和resolveClassMethod来动态方法决议——实现消息动态处理
  • 快速转发流程——通过CoreFoundation来触发消息转发流程,forwardingTargetForSelector实现快速转发,由其他对象来实现处理方法
  • 慢速转发流程——先调用methodSignatureForSelector获取到方法的签名,生成对应的invocation;再通过forwardInvocation来进行处理
  • 以上流程均无法挽救就崩溃并报错

# SEL和IMP的关系

SEL是方法编号,也是方法名,在dyld加载镜像到内存时,通过_read_image方法加载到内存的表中了 IMP是函数实现指针,找IMP就是找函数实现的过程

SEL和IMP的关系就可以解释为:

  • SEL就相当于书本的⽬录标题
  • IMP就是书本的⻚码
  • 函数就是具体页码对应的内容

# 能否向运⾏时创建的类中添加实例变量

具体情况具体分析:

编译好的类不能添加实例变量 运行时创建的类可以添加实例变量,但若已注册到内存中就不行了

原因:

编译好的实例变量存储的位置在ro,而ro是在编译时就已经确定了的 ⼀旦编译完成,内存结构就完全确定就⽆法修改 只能修改rw中的方法或者可以通过关联对象的方式来添加属性

# 利用runtime-API创建对象

# 动态创建类

点击查看
/**
 *创建类
 *
 *superClass: 父类,传Nil会创建一个新的根类
 *name: 类名
 *extraBytes: 额外的内存空间,一般传0
 *return:返回新类,创建失败返回Nil,如果类名已经存在,则创建失败
 */
Class FXPerson = objc_allocateClassPair([NSObject class], "LGPerson", 0);
1
2
3
4
5
6
7
8
9

# 添加成员变量

点击查看
/**
*添加成员变量
*这个函数只能在objc_allocateClassPair和objc_registerClassPair之间调用。不支持向现有类添加一个实例变量
*这个类不能是元类,不支持在元类中添加一个实例变量
*实例变量的最小对齐为1 << align,实例变量的最小对齐依赖于ivar的类型和机器架构。对于任何指针类型的变量,请通过log2(sizeof(pointer_type))
*
*cls 往哪个类添加
*name 添加的名字
*size 大小
*alignment 对齐处理方式
*types 签名
*/
class_addIvar(FXPerson, "fxName", sizeof(NSString *), log2(sizeof(NSString *)), "@");
1
2
3
4
5
6
7
8
9
10
11
12
13

# 注册到内存

点击查看
/**
 *往内存注册类
 *
 * cls 要注册的类
 */
 objc_registerClassPair(FXPerson);
1
2
3
4
5
6

# 添加属性变量

点击查看
/**
*往类里面添加属性
*
*cls 要添加属性的类
*name 属性名字
*attributes 属性的属性数组。
*attriCount 属性中属性的数量。
*/
class_addProperty(targetClass, propertyName, attrs, 4);
1
2
3
4
5
6
7
8
9

# 添加方法

点击查看
/**
 *往类里面添加方法
 *
 *cls 要添加方法的类
 *sel 方法编号
 *imp 函数实现指针
 *types 签名
 */
class_addMethod(FXPerson, @selector(setHobby), (IMP)fxSetter, "v@:@");
1
2
3
4
5
6
7
8
9

# 整体使用

点击查看
// hobby的setter-IMP
void fxSetter(NSString *value) {
    printf("%s/n",__func__);
}

// hobby的getter-IMP
NSString *fxHobby() {
    return @"iOS";
}

// 添加属性变量的封装方法
void fx_class_addProperty(Class targetClass, const char *propertyName) {
    objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
    objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
    objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
    objc_property_attribute_t backingivar  = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String };  //variable name
    objc_property_attribute_t attrs[] = {type, ownership0, ownership, backingivar};
    class_addProperty(targetClass, propertyName, attrs, 4);
}

// 打印属性变量的封装方法
void fx_printerProperty(Class targetClass){
    unsigned int outCount, i;
    objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 动态创建类
        Class FXPerson = objc_allocateClassPair([NSObject class], "FXPerson", 0);
        // 添加成员变量
        class_addIvar(FXPerson, "name", sizeof(NSString *), log2(sizeof(NSString *)), "@");
        // 注册到内存
        objc_registerClassPair(FXPerson);
        // 添加属性变量
        fx_class_addProperty(FXPerson, "hobby");
        fx_printerProperty(FXPerson);
        // 添加方法(为属性方法添加setter、getter方法)
        class_addMethod(FXPerson, @selector(setHobby:), (IMP)fxSetter, "v@:@");
        class_addMethod(FXPerson, @selector(hobby), (IMP)fxHobby, "@@:");

        // 开始使用
        id person = [FXPerson alloc];
        [person setValue:@"Felix" forKey:@"name"];
        NSLog(@"FXPerson的名字是:%@ 爱好是:%@", [person valueForKey:@"name"], [person valueForKey:@"hobby"]);
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

提示

注意事项

  • 记得导入<objc/runtime.h>
  • 添加成员变量class_addIvar必须在objc_registerClassPair前,因为注册到内存时ro已经确定了,不能再往ivars添加(同第四个面试题)
  • 添加属性变量class_addProperty可以在注册内存前后,因为是往rw中添加的
  • class_addProperty中“属性的属性”——nonatomic/copy是根据属性的类型变化而变化的
  • class_addProperty不会自动生成setter和getter方法,因此直接调用KVC会崩溃

不只可以通过KVC打印来检验,也可以下断点查看ro、rw的结构来检验

# 关联对象分析

# 1.分类直接添加属性的后果

编译会出现警告:没有setter方法和getter方法

运行会报错:-[FXPerson setName:]: unrecognized selector sent to instance 0x100f61180'

# 2.为什么不能直接添加属性

点击查看
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;
    ...
};
1
2
3
4
5
6
7
8
9
10
11

里面虽然可以添加属性变量,但是这些properties并不会自动生成Ivar,也就是不会有 @synthesize的作用,dyld加载期间,这些分类会被加载并patch到相应的类中。这是一个动态过程,Ivar不能动态添加

# 3.解决方案

手动实现setter、getter方法,关联对象

点击查看
- (void)setName:(NSString *)name {
    /**
    参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
    参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
    参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
    参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
    */
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    /**
    参数一:id object : 获取哪个对象里面的关联的属性。
    参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
    */
    return objc_getAssociatedObject(self, @"name");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 关联对象原理

setter方法——objc_setAssociatedObject分析

苹果设计接口时往往会加个中间层——即使底层实现逻辑发生变化也不会影响到对外接口

提示

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
1
2
3

跟进去看看_object_set_associative_reference实现

点击查看
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    assert(object);

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    // retain the new value (if any) outside the lock.
    // 在锁之外保留新值(如果有)。
    ObjcAssociation old_association(0, nil);
    // acquireValue会对retain和copy进行操作,
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        // 关联对象的管理类
        AssociationsManager manager;
        // 获取关联的 HashMap -> 存储当前关联对象
        AssociationsHashMap &associations(manager.associations());
        // 对当前的对象的地址做按位去反操作 - 就是 HashMap 的key (哈希函数)
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            // 获取 AssociationsHashMap 的迭代器 - (对象的) 进行遍历
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                // 根据key去获取关联属性的迭代器
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    // 替换设置新值
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    // 到最后了 - 直接设置新值
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                // 如果AssociationsHashMap从没有对象的关联信息表,
                // 那么就创建一个map并通过传入的key把value存进去
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            // 如果传入的value是nil,并且之前使用相同的key存储过关联对象,
            // 那么就把这个关联的value移除(这也是为什么传入nil对象能够把对象的关联value移除)
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    // 最后把之前使用传入的这个key存储的关联的value释放(OBJC_ASSOCIATION_SETTER_RETAIN策略存储的)
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
  • ObjcAssociation old_association(0, nil)处理传进来的值得到new_value

  • 获取到管理所有关联对象的hashmap总表的管理者AssociationsManager,然后拿到hashmap总表AssociationsHashMap

  • DISGUISE(object)对关联对象的地址进行取反操作得到哈希表对应的下标index

  • 如果new_value为空(即对属性赋值为nil)就直接找到相应的表进行删除

  • 如果new_value不为空,就拿到总表的迭代器通过拿到的下标index进行遍历查找;如果找到管理对象的关联属性哈希map表,然后再通过key去遍历取值

  • 如果取到了,就先把新值设置到key上,再将旧值释放掉

  • 如果没取到,就直接将新值设置在key上 还是不明白就LLDB断点调试呗 Screenshot-2023-08-20-at-10

getter方法——objc_getAssociatedObject分析

提示

id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        // 关联对象的管理类
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 生成伪装地址。处理参数 object 地址
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 所有对象的额迭代器
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            // 内部对象的迭代器
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // 找到 - 把值和策略读取出来
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                // OBJC_ASSOCIATION_GETTER_RETAIN - 就会持有一下
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

objc_getAssociatedObject是objc_setAssociatedObject的逆过程 Screenshot-2023-08-20-at-11

# weak置空原理

在weak一行打下断点运行项目

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *person = [[FXPerson alloc] init];
        id __weak person = object;
    }
    return 0;
}
1
2
3
4
5
6
7

Xcode菜单栏Debug->Debug Workflow->Always show Disassembly打上勾查看汇编——汇编代码会来到libobjc库的objc_initWeak

# weak创建过程

①objc_initWeak

  • location:表示__weak指针的地址(我们研究的就是__weak指针指向的内容怎么置为nil)

  • newObj:所引用的对象,即例子中的person

    id
    objc_initWeak(id *location, id newObj)
    {
      if (!newObj) {
          *location = nil;
          return nil;
      }
    
      return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
          (location, (objc_object*)newObj);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
②storeWeak
- HaveOld:weak指针之前是否已经指向了一个弱引用
- HaveNew:weak指针是否需要指向一个新引用
- CrashIfDeallocating:如果被弱引用的对象正在析构,此时再弱引用该对象,是否应该crash

storeWeak最主要的两个逻辑点(源码太长,这里不贴了)

![Screenshot-2023-08-20-at-11](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-11.08.45.5nw0kjvmpxk0.webp)

由于是第一次调用,所以走haveNew分支——获取到的是新的散列表SideTable,主要执行了weak_register_no_lock方法来进行插入

③weak_register_no_lock

- 主要进行了isTaggedPointer和deallocating条件判断
- 将被弱引用对象所在的weak_table中的weak_entry_t哈希数组中取出对应的weak_entry_t
- 如果weak_entry_t不存在,则会新建一个并插入
- 如果存在就将指向被弱引用对象地址的指针referrer通过函数append_referrer插入到对应的weak_entry_t引用数组

![Screenshot-2023-08-20-at-11](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-11.09.56.39pwnzxcpig0.webp)

④append_referrer

找到弱引用对象的对应的weak_entry哈希数组中插入

![Screenshot-2023-08-20-at-11](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-11.10.48.6z8fee2t8ho0.webp)


### weak创建流程
![Screenshot-2023-08-20-at-11](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-11.12.11.30k5gabiypu0.webp)


### weak销毁过程

由于弱引用在析构dealloc时自动置空,所以查看dealloc的底层实现并LLVM调试

- _objc_rootDealloc->rootDealloc
- rootDealloc->object_dispose
- object_dispose->objc_destructInstance
- objc_destructInstance->clearDeallocating
- clearDeallocating->sidetable_clearDeallocating
- sidetable_clearDeallocating3->table.refcnts.erase(it)


### weak销毁流程
![Screenshot-2023-08-20-at-11](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-11.17.31.5vp4p8tbvjk0.webp)

### 总结

#### 创建流程小结:
![Screenshot-2023-08-20-at-11](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-11.12.11.30k5gabiypu0.webp)
Runtime维护了一个弱引用表,将所有弱引用obj的指针地址都保存在obj对应的weak_entry_t中。

- 创建时,先从找到全局散列表SideTables中对应的弱引用表weak_table
- 在weak_table中被弱引用对象的referent,并创建或者插入对应的weak_entry_t
- 然后append_referrer(entry, referrer)将我的新弱引⽤的对象加进去entry
- 最后weak_entry_insert 把entry加⼊到我们的weak_table

#### 销毁流程小结:
![Screenshot-2023-08-20-at-11](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-11.17.31.5vp4p8tbvjk0.webp)
- 首先根据对象地址获取所有weak指针地址的数组
- 然后遍历这个数组把对应的数据清空置为nil
- 同时,将weak_entry_t移除出弱引用表weak_table。


## Method Swizzing坑点

### 1.黑魔法应用

在日常开发中,再好的程序员都会犯错,比如数组越界

因此为了避免数组越界这种问题,大神们开始玩起了黑魔法——method swizzing

- 新建NSArray分类
- 导入runtime头文件——<objc/runtime.h>
- 写下新的方法
- 在+load利用黑魔法交换方法

::: details
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

#import "NSArray+FX.h" #import <objc/runtime.h>

@implementation NSArray (FX)

  • (void)load { // 交换objectAtIndex方法 Method oriMethod1 = class_getInstanceMethod(self, @selector(objectAtIndex:)); Method swiMethod1 = class_getInstanceMethod(self, @selector(fx_objectAtIndex:)); method_exchangeImplementations(oriMethod1, swiMethod1);

    // 交换取下标方法 Method oriMethod2 = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:)); Method swiMethod2 = class_getInstanceMethod(self, @selector(fx_objectAtIndexedSubscript:)); method_exchangeImplementations(oriMethod2, swiMethod2); }

  • (void)fx_objectAtIndex:(NSInteger)index { if (index > self.count - 1) {

      NSLog(@"objectAtIndex————————数组越界");
      return;
    

    } return [self fx_objectAtIndex:index]; }

  • (void)fx_objectAtIndexedSubscript:(NSInteger)index { if (index > self.count - 1) {

      NSLog(@"取下标————————数组越界");
      return;
    

    } return [self fx_objectAtIndexedSubscript:index]; }

@end

:::

然而程序还是无情的崩了...

其实在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇(Class Clusters),一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行方法交换,必须获取到其真身进行方法交换,直接对NSArray进行操作是无效的


以下是NSArray和NSDictionary本类的类名

![Screenshot-2023-08-20-at-19](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-19.55.49.34bcbx9bkpg0.webp)

这样就好办了,可以使用runtime取出本类

![Screenshot-2023-08-20-at-19](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-19.56.31.2tv7k7iuddq0.webp)

#### 2.坑点一

黑魔法最好写成单例,避免多次交换

比如说添加了[NSArray load]代码,方法实现又交换回去了导致了崩溃

![Screenshot-2023-08-20-at-19](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-19.57.39.1aqyolkamgsg.webp)

将+load方法改写成单例(虽然不常见,但也要避免)


::: details
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{

      // 交换objectAtIndex方法
      Method oriMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
      Method swiMethod1 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndex:));
      method_exchangeImplementations(oriMethod1, swiMethod1);
    
      // 交换取下标方法
      Method oriMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
      Method swiMethod2 = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fx_objectAtIndexedSubscript:));
      method_exchangeImplementations(oriMethod2, swiMethod2);
    

    }); }

:::


#### 坑点二

①子类交换父类实现的方法

- 父类FXPerson类中有-doInstance方法,子类FXSon类没有重写
- FXSon类新建分类做了方法交换,新方法中调用旧方法
- FXPerson类、FXSon类调用-doInstance

![Screenshot-2023-08-20-at-20](https://cdn.staticaly.com/gh/214070779/picx-images-hosting@master/20230813/Screenshot-2023-08-20-at-20.57.33.6oo27h3392g0.webp)

子类打印出结果,而父类调用却崩溃了,为什么会这样呢?

因为FXSon类交换方法时取得doInstance先在本类搜索方法,再往父类里查找,在FXFather中找到了方法实现就把它跟新方法进行交换了。可是新方法是在FXSon分类中的,FXFather找不到imp就unrecognized selector sent to instance 0x600002334250

所以这种情况下应该只交换子类的方法,不动父类的方法

::: details
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{

      Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
      Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));
    
      BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
      if (didAddMethod) {
          class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
      } else {
          method_exchangeImplementations(oriMethod, swiMethod);
      }
    

    }); }

    :::
    
    1
  • 通过class_addMethod给FXSon类添加方法(class_addMethod不会取代本类中已存在的实现,只会覆盖本类中继承父类的方法实现)

    • 取得新方法swiMethod的实现和方法类型
    • 往方法名@selector(fx_doInstance)添加方法
    • class_addMethod 把新方法实现放到旧方法名中,此刻调用doInstance就是调用fx_doInstance,但是调用fx_doInstance会崩溃

根据didAddMethod判断是否添加成功

  • 添加成功说明之前本类没有实现——class_replaceMethod替换方法
  • 添加失败说明之前本类已有实现——method_exchangeImplementations交换方法
  • class_replaceMethod用doInstance方法实现替换掉fx_doInstance中的方法实现

Screenshot-2023-08-20-at-21

Screenshot-2023-08-20-at-21

②FXPerson类只写了方法声明,没有方法实现,却做了方法交换——会造成死循环

Screenshot-2023-08-20-at-21

doInstance方法中添加了新的方法实现

fx_doInstance方法中想用旧的方法实现替代之前的方法实现,可是找不到doInstance实现,导致class_replaceMethod无效->在fx_doInstance中调用fx_doInstance就会死循环

Screenshot-2023-08-20-at-21

因此改变代码逻辑如下

点击查看
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oriMethod = class_getInstanceMethod(self, @selector(doInstance));
        Method swiMethod = class_getInstanceMethod(self, @selector(fx_doInstance));

        if (!oriMethod) {
            class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
                NSLog(@"方法未实现");
            }));
        }

        BOOL didAddMethod = class_addMethod(self, @selector(doInstance), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            class_replaceMethod(self, @selector(fx_doInstance), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        } else {
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 未实现方法时用新的方法实现添加方法,此时调用doInstance就是调用fx_doInstance
  • 由于此时fx_doInstance方法内部还是调用自己,用block修改fx_doInstance的实现,就可以断开死循环了
  • 由于oriMethod(0x0),method_exchangeImplementations交换失败

Screenshot-2023-08-20-at-21

4.注意事项 使用Method Swizzling有以下注意事项:

  • 尽可能在+load方法中交换方法
  • 最好使用单例保证只交换一次
  • 自定义方法名不能产生冲突
  • 对于系统方法要调用原始实现,避免对系统产生影响
  • 做好注释(因为方法交换比较绕)
  • 迫不得已情况下才去使用方法交换

这是一份做好封装的Method Swizzling交换方法

点击查看
+ (void)FXMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {

    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);

    if (!oriMethod) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd) {
            NSLog(@"方法未实现");
        }));
    }

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    } else {
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

补充面试题一

问:某个方法进行多次交换,最终的调用顺序是怎样的?

Screenshot-2023-08-20-at-22

补充面试题二

问:如果发现方法交换之后不生效,应该怎么排查?

答:解决的方案有多种,主要从两个方向进行思考

方法交换的class有没有问题

方法交换的method有没有问题

面试题二:

如果是一个同名函数,在load方法内进行方法交换,跟分类中的方法是同名函数,哪么两个同名函数,先调用哪一个。

先调用load方法的,因为load方法在main启动之前就开始调用。

分类方法,是运行时才会被调用

如果是一个同名函数,不在load的方法内进行方法交换,跟分类中的方法是同名函数,哪么两个同名函数,先调用哪一个

先调用方法交换,因为方法交换前先调用此方法。

编辑 (opens new window)
上次更新: 2024/10/23, 23:26:17
监控runloop卡顿
weak的实现原理

← 监控runloop卡顿 weak的实现原理→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式