Files
knowledge-kit/Chapter1 - iOS/1.82.md
杭城小刘 e89fe0ca1c docs: 内容
2020-11-08 15:51:47 +08:00

6.4 KiB
Raw Blame History

Runtime

做很多需求或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写一些场景和源码分析相关的文章。

场景

最简单的一个场景就是防止按钮多次点击吧,比如短时间内点击了多次按钮,可以用「节流」来实现。用到的技术是 runtime。再举一个例子比如无痕埋点的实现里面对各种控件的点击、页面的跳转等也需要用到 runtime想看无痕埋点的设计与实现可以看我这篇文章

这里不得不提的一个知识点就是为什么给类或者对象进行 hook 的操作,要放到 load 方法中进行了。

一、 load 和 initialize 方法

load 方法

对于加入运行期系统中的每个类及分类来说,必定会调用 load 方法而且仅调用1次。当包含类或分类的程序库载入系统时通常指应用程序启动的时候就会执行 load 方法。

  • 类的 load 方法会在它所有父类 load 方法后调用
  • 分类的 load 方法会在类本身的 load 方法后调用
  • load 方法不遵循继承
  • load 方法内部的实现必须简单,如果逻辑太复杂有可能会导致阻塞

load 方法有个需要注意的地方:执行该方法时,运行期系统处于脆弱状态。在执行子类的 load 方法之前,必须先执行完所有的超类的 load 方法,假如代码中还依赖了其他的程序库,那么程序库里的相关 load 方法也会先执行。在开发中load 方法中使用其他类是不安全的。

@implentation ClassB
+ (void)load
{
	ClassA *classA = [[ClassA alloc] init];
	[classA setUp];
}
@end

上面的代码不太推荐且不太安全,因为你没办法确定在执行 ClassB 的 load 方法的时候, ClassA 是否已经被加载到系统中。

load 方法并不像普通方法那样具备继承规则。普通的方法在面向对象程序设计中,父类的方法、属性等都会在子类中存在,假如 Person 类有 eat、sleep 方法Student 类继承子 Person 类,虽然 Person 类没有重写 eat 方法,但是你给 Student 类发送 eat 消息是可以响应的,因为方法被继承了。 Load 方法就不会,假如子类没有 load 方法,不管超类是否有 load 方法,子类都不会调用 load 方法。

load 方法内代码逻辑必须精简。因为整个应用程序在执行 load 方法的时候会被阻塞。如果 load 方法中包含繁杂的代码,那么应用程序可能会变得无法响应。更加不要使用锁。想通过 load 方法在类加载之前做些操作的,都属于错误的打开方式。它真正的用法应该是 Debug 吧,比如在 Category 中判断当前分类是否被成功 load 进去。

inintialize 方法

对于每个类来说,该方法会在程序首次调用该类之前调用,且只调用一次。它是由运行时系统来调用的,不应该通过代码的方式直接调用。

与 load 的区别:

  • 惰性调用的。也就是说某个类的 initialize 方法也许永远不会被调用,当且仅当程序用到了该类的时候才会调用。 load 是加载进 runtime 肯定会调用initizlize 第一次使用前会被调用。对 load 来说,应用程序必须阻塞并且等着所有类 load 方法执行完毕才可以继续。
  • 运行期系统在执行 initialize 方法时,是处于正常状态的,因此从运行期完整度方面来讲,此时可以安全使用并调用任何类的任意方法,而且 runtime 保证了在 initizlize 方法时期一定会在一个线程安全的环境中执行,也就是说执行 initialize 方法的这个线程可以操作类或者实例,其他线程先阻塞,等待执行完毕
  • initialize 同其他方法一样,如果某个类并未实现它,而其实现了,那么就会运行超类实现的代码。
@implentation AClass
+ (void)initialize
{
	NSLog(@"%@ initialize", self);
}
@end
 
@interface BClass:AClass

@end
@implentation BClass

@end
  
// log
AClass initialize
BClass initialize

基于上述特点,我们一般需要 initialize 方法中做些判断,如下

+ (void)initialize
{
	if (self == [ACLass class]) {
    // 确定了我是 AClass 的 initialize 方法执行时期
  }
}

经常说不要在 App 中写太多的 load 方法,会影响 App 的启动时间。原因就是 load 方法的执行特点决定的。某个类的 load 方法执行是在它所有父类 load 方法执行后执行的,该类的分类的 load 方法执行是在当前类 load 方法之后执行的。如果某个类的 load 方法中引用了其他的类或者其他库的代码,则该类的 load 方法必须是其他类或者其他库中类的 load 方法执行后执行,所以类的 load 方法中最好做本类相关的逻辑,比如 runtime method swizzling。

TagPointerString 不走消息转发 CFString 走消息转发

Objective-C 方法调用则先通过对象的 isa 找到类对象,然后根据类对象的 cache_t 查找方法缓存列表,根据 sel mask 去计算 index这个 index 代表当前方法缓存在哈希表中的下标索引。 sel 比较,如果没命中,则继续走 objc_msgSend_uncached 流程。

1ookUpImpOrForward : 1. 当前类对象中的方法列表中遍历方法列表2. 继承链中 superClass 遍历查找,一直到根部 NSObject3. 动态特性:

  • 动态方法解析,动态的添加一个方法,在方法列表中新建一个 SEL 和对应的 IMP resolveInstanceMethod、resolveClassMethod

  • 重定向 - (id)forwardingTargetForSelector:(SEL)aSelector

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果此方法返回 nil 或者 self则会进入下一步

  • 消息重定向:methodSignatureForSelector 获取函数的参数和返回值类型 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象函数签名Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation: 消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。

如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出 doesNotRecognizeSelector: 消息,程序也就崩溃了。