最近研究了一下 iOS 平台上几个 hook 框架的 hook 方案,写文记录一下分析的过程
Hook 代码
- //替换方法
- BOOL qhd_replaceMethod(Class cls, SEL originSelector, char * returnType) {
- Method originMethod = class_getInstanceMethod(cls, originSelector);
- if (originMethod == nil) {
- return NO;
- }
- const char * originTypes = method_getTypeEncoding(originMethod);
- IMP msgForwardIMP = _objc_msgForward;#
- if ! defined(__arm64__) if (qhd_isStructType(returnType)) {
- //Reference JSPatch:
- //In some cases that returns struct, we should use the '_stret' API:
- //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html
- //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription.
- NSMethodSignature * methodSignature = [NSMethodSignature signatureWithObjCTypes: originTypes];
- if ([methodSignature.debugDescription rangeOfString: @"is special struct return? YES"].location != NSNotFound) {
- msgForwardIMP = (IMP) _objc_msgForward_stret;
- }
- }#endif
- IMP originIMP = method_getImplementation(originMethod);
- if (originIMP == nil || originIMP == msgForwardIMP) {
- return NO;
- }
- //把原方法的IMP换成_objc_msgForward,使之触发forwardInvocation方法
- class_replaceMethod(cls, originSelector, msgForwardIMP, originTypes);
- //把方法forwardInvocation的IMP换成qhd_forwardInvocation
- class_replaceMethod(cls, @selector(forwardInvocation: ), (IMP) qhd_forwardInvocation, "v@:@");
- //创建一个新方法,IMP就是原方法的原来的IMP,那么只要在qhd_forwardInvocation调用新方法即可
- SEL newSelecotr = qhd_createNewSelector(originSelector);
- BOOL isAdd = class_addMethod(cls, newSelecotr, originIMP, originTypes);
- if (!isAdd) {
- DEV_LOG(@"class_addMethod fail");
- }
- return YES;
- }
阐述一下具体的过程:
从代码很明显的可以看出,这是利用 OC 的消息转发机制,选择了合适的时机,进行打桩。 相较于传统的 Swizzle 方法,这种方法打主桩,是有可行性的。 并且在 ForwardInvocation: 处理,虽然相较其余两个转发机制调用的方法的消耗大,但是更灵活一些,最切合问题。
Aspects 的代码我看的比较仔细,相对于 AnyMethodLog, Aspects 对 Hook 的处理更成熟,各种情况都做了考虑,这里来重点分析下。
相较于 AnyMethodLog, Aspects 不仅可以 hook 类,也可以对实例进行 hook, 粒度更小,适用的场景更加多样化。
这是 Aspects Hook 的代码,可以看到对实例和类的处理是不同的。
- static Class aspect_hookClass(NSObject * self, NSError * *error) {
- NSCParameterAssert(self);
- Class statedClass = self.class;
- Class baseClass = object_getClass(self);
- NSString * className = NSStringFromClass(baseClass);
- // Already subclassed
- if ([className hasSuffix: AspectsSubclassSuffix]) {
- return baseClass;
- // We swizzle a class object, not a single object.
- } else if (class_isMetaClass(baseClass)) {
- return aspect_swizzleClassInPlace((Class) self);
- // Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
- } else if (statedClass != baseClass) {
- return aspect_swizzleClassInPlace(baseClass);
- }
- // Default case. Create dynamic subclass.
- const char * subclassName = [className stringByAppendingString: AspectsSubclassSuffix].UTF8String;
- Class subclass = objc_getClass(subclassName);
- if (subclass == nil) {
- subclass = objc_allocateClassPair(baseClass, subclassName, 0);
- if (subclass == nil) {
- NSString * errrorDesc = [NSString stringWithFormat: @"objc_allocateClassPair failed to allocate class %s.", subclassName];
- AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
- return nil;
- }
- aspect_swizzleForwardInvocation(subclass);
- aspect_hookedGetClass(subclass, statedClass);
- aspect_hookedGetClass(object_getClass(subclass), statedClass);
- objc_registerClassPair(subclass);
- }
- object_setClass(self, subclass);
- return subclass;
- }
先看看对实例的处理
- subclass = objc_allocateClassPair(baseClass, subclassName, 0);
- aspect_swizzleForwardInvocation(subclass);
- aspect_hookedGetClass(subclass, statedClass);
- aspect_hookedGetClass(object_getClass(subclass), statedClass);
- objc_registerClassPair(subclass);
- object_setClass(self, subclass);
熟悉 kvo 原理的同学,一眼就应该看明白了,这是做了什么事情。 这里可谓是相当巧妙的避免了父类和子类实例 hook 相同的 IMP 可能导致的循环调用问题。(下一部分会说明如何避免的)
对类的 hook 和 AnyMethodLog 十分类似。就不再多阐述了。网上相关介绍 使用 forwardInvocation+hook 类 的资料很多。
Aspects 和 AnyMethodLog 都是利用了 forwardInvocation 进行处理,这是一致的。
自己经常 hook 的同学可能会发现,在 hook 时,会出现调用循环的问题。
无论是 AnyMethodLog 和 Aspects 都无法同时 hook 父类和子类的同一个方法到一个相同的 IMP 上。为什么呢?
思考一下为什么会出现循环调用? 那必定是,调用方又被调用者调用了一次,在 iOS Hook 中,如果我们 hook 了 父类和子类的同一个方法,让他们拥有相同的实现,就会出现这种问题。
基于桥的全量方法 Hook 方案 - 探究苹果主线程检查实现 假设我们现在对 UIView、UIButton 都 Hook 了 initWithFrame: 这个方法,在调用 [[UIView alloc] initWithFrame:] 和[[UIButton alloc] initWithFrame:]都会定向到 C 函数 qhd_forwardInvocation 中,在 UIView 调用的时候没问题。但是在 UIButton 调用的时候,由于其内部实现获取了 super initWithFrame:,就产生了循环定向的问题。
Aspects 中,Hook 之前,是要对能否 hook 进行检查了,对于类,有严格的限制,对于实例则没有限制。
类为什么要限制,上面已经阐释了,那么实例为什么可以呢?
这就是 实例 Hook 实现方式所产生的结果。
来理一下实例 hook 怎么实现的:
如果我们有 ClassA 的 实例 a, SubClassA 的 实例 suba. 对他们进行 hook
方法, 那么会生成两个子类,我们记为 prefix_ClassA, prefix_SubClassA, 我们对 forwardInvocation IMP 的替换,实际上是在这两个类上进行的。
- viewdidload
当方法调用时: suba -> forwardInvocation(我们替换的 IMP) ->self viewdidload(SubClassA 的 IMP) -> super viewdidload(ClassA 的实现) 这显然不会导致循环的问题。
如果是真正的消息转发响应的处理,有兴趣的同学可以看一下。
https://github.com/steipete/Aspects/blob/master/Aspects.m#L508
JSPatch 的方法替换也是利用了 forwardInvocation 进行处理。
如果有错误,希望指出,共同学习
来源: https://juejin.im/post/5a313c11f265da433562c345