iOS Hook 框架 AnyMethodLog與Aspects分析

最近研究了一下iOS平臺上幾個hook框架的hook方案,寫文記錄一下分析的過程html

現有hook框架

  1. AnyMethodLog
  2. Aspects

AnyMethodLog hook 方案分析

Hook 代碼git

//替換方法
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;
}
複製代碼

闡述一下具體的過程:github

  1. 如何讓方法每次都走_objc_msgForward呢?把原來的 sel的IMP改爲_objc_msgForward.微信

  2. 這時咱們須要保存原來的 IMP 而後hook forwardInvocation ... 換成本身的實現,調用原來的IMP和新增的代碼框架

從代碼很明顯的能夠看出,這是利用OC的消息轉發機制,選擇了合適的時機,進行打樁。 相較於傳統的Swizzle方法,這種方法打主樁,是有可行性的。 而且在ForwardInvocation: 處理,雖然相較其他兩個轉發機制調用的方法的消耗大,可是更靈活一些,最切合問題。函數

Aspects

Aspects 的代碼我看的比較仔細,相對於AnyMethodLog, Aspects 對Hook的處理更成熟,各類狀況都作了考慮,這裏來重點分析下。學習

相較於AnyMethodLog, Aspects 不只能夠hook類,也能夠對實例進行hook, 粒度更小,適用的場景更加多樣化。ui

這是Aspects Hook 的代碼,能夠看到對實例和類的處理是不一樣的。spa

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的同窗可能會發現,在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怎麼實現的:

  1. 生成子類
  2. hook 子類的forwardInvocation(這是一系列操做,不過這個尤其重要)
  3. 對實例的類信息進行假裝

若是咱們有 ClassA 的 實例 a, SubClassA 的 實例 suba. 對他們進行hook viewdidload 方法, 那麼會生成兩個子類,咱們記爲prefix_ClassA, prefix_SubClassA,咱們對forwardInvocation IMP的替換,其實是在這兩個類上進行的。

當方法調用時: suba -> forwardInvocation(咱們替換的IMP) ->self viewdidload(SubClassA 的IMP) -> super viewdidload(ClassA的實現) 這顯然不會致使循環的問題。

若是是真正的消息轉發響應的處理,有興趣的同窗能夠看一下。

https://github.com/steipete/Aspects/blob/master/Aspects.m#L508

JSPatch 的方法替換也是利用了 forwardInvocation進行處理。

若是有錯誤,但願指出,共同窗習

個人博客

若是各位同窗對文章有什麼疑問或者工做之中遇到一些小問題均可以在羣裏找到我或者其餘羣友交流討論,期待你的加入喲~

Emmmmm..因爲微信羣人數過百致使不能夠掃碼入羣,因此請掃描上面的小姐姐二維碼,加完微信好友後回覆「iOS」驗證身份即會被邀請入羣。

相關文章
相關標籤/搜索