mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 20:00:40 +00:00
118 lines
6.4 KiB
Markdown
118 lines
6.4 KiB
Markdown
# Runtime
|
||
|
||
> 做很多需求或者是技术细节验证的时候会用到 Runtime 技术,用了挺久的了,本文就写一些场景和源码分析相关的文章。
|
||
|
||
## 场景
|
||
|
||
最简单的一个场景就是防止按钮多次点击吧,比如短时间内点击了多次按钮,可以用「节流」来实现。用到的技术是 runtime。再举一个例子,比如无痕埋点的实现里面对各种控件的点击、页面的跳转等也需要用到 runtime,想看无痕埋点的设计与实现,可以看我这篇[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.55.md)。
|
||
|
||
这里不得不提的一个知识点就是为什么给类或者对象进行 hook 的操作,要放到 load 方法中进行了。
|
||
|
||
|
||
|
||
## 一、 load 和 initialize 方法
|
||
|
||
**load 方法**
|
||
|
||
对于加入运行期系统中的每个类及分类来说,必定会调用 load 方法,而且仅调用1次。当包含类或分类的程序库载入系统时(通常指应用程序启动的时候),就会执行 load 方法。
|
||
|
||
- 类的 load 方法会在它所有父类 load 方法后调用
|
||
- 分类的 load 方法会在类本身的 load 方法后调用
|
||
- load 方法不遵循继承
|
||
- load 方法内部的实现必须简单,如果逻辑太复杂有可能会导致阻塞
|
||
|
||
load 方法有个需要注意的地方:执行该方法时,运行期系统处于脆弱状态。在执行子类的 load 方法之前,必须先执行完所有的超类的 load 方法,假如代码中还依赖了其他的程序库,那么程序库里的相关 load 方法也会先执行。在开发中,load 方法中使用其他类是不安全的。
|
||
|
||
```objective-c
|
||
@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 同其他方法一样,如果某个类并未实现它,而其实现了,那么就会运行超类实现的代码。
|
||
|
||
```objective-c
|
||
@implentation AClass
|
||
+ (void)initialize
|
||
{
|
||
NSLog(@"%@ initialize", self);
|
||
}
|
||
@end
|
||
|
||
@interface BClass:AClass
|
||
|
||
@end
|
||
@implentation BClass
|
||
|
||
@end
|
||
|
||
// log
|
||
AClass initialize
|
||
BClass initialize
|
||
```
|
||
|
||
基于上述特点,我们一般需要 initialize 方法中做些判断,如下
|
||
|
||
```objective-c
|
||
+ (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 遍历查找,一直到根部 NSObject;3. 动态特性:
|
||
- 动态方法解析,动态的添加一个方法,在方法列表中新建一个 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: 消息,程序也就崩溃了。
|
||
|