Stinger--實踐實現特定實例對象的AOP

     在 iOS完整實踐: 使用Libffi實現AOP 一文中,咱們介紹了實現AOP的一種方式,經過解析目標方法的簽名,使用ffi_prep_cifffi_prep_closure_loc構造殼函數替換原函數實現,以感知原方法調用時機及捕獲參數,最後經過ffi_call利用預生成的模板動態調用原實現和block的函數指針。在Stinger中編寫了具體實現。git

     但上述方式由於替換了類的方法實現,意味着類的全部實例對象的方法調用都發生了改變。在實際開發中,咱們每每有這樣的需求,在某個類的特定實例對象的方法裏執行aop代碼,其餘實例對象仍舊執行原方法。實現這類aop的方式有KVO, RACObserve, rac_signalForSelector等。本文介紹了Stinger的新特性實現細節,可構造與原方法參數相近的block,做爲切面代碼插入到特定實例對象的方法實現中。使用以下:github

@implementation ASViewController
- (void)print3:(NSString *)s {
  NSLog(@"---original print4: %@", s);
}
- (void)viewDidLoad {
  [super viewDidLoad];
  // hook for specific instance
  [self st_hookInstanceMethod:@selector(print3:) option:STOptionAfter usingIdentifier:@"hook_print3_after1" withBlock:^(id<StingerParams> params, NSString *s) {
    NSLog(@"---specific instance-self after print3: %@", s);
  }];
}
@end
複製代碼

實現

     一個類能夠有不少實例,要對特定實例對象hook方法,免不了有更換方法實現這一步,既然要達到不影響其餘實例的方法,這裏能想到的是像KVO的實現細節同樣,新建一個所屬類的子類,而後更改特定實例的isa指針爲新建子類,而後對該子類進行單獨hook。另外須要設計一種結構,保存hook信息關聯到實例對象,執行方法的時候,既執行該對象所屬類原有的aop代碼,又執行本身的aop代碼。bash

Hook class

     下邊是hook實例對象的代碼,首先爲實例對象的類新建了子類ST_##Class, 並關聯到了對象上,避免重複生成,同一個類的多個實例對象可使用同一個子類做爲isa指針指向。 此外,與KVO相似,hook了子類的class方法,以返回原有的類。jsp

add hook info

     在iOS完整實踐: 使用Libffi實現AOP中,hookInfoPool關聯到了類上,hook info也添加到了該hookInfoPool中。hook實例對象的方法時,新建的子類有一個hookInfoPool,僅僅是爲了存放殼函數,實例對象也有一個hookInfoPool,針對實例對象的hook info就存於此。這也就意味着,同一個類下的多個實例對象能夠共用新建子類的hookInfoPool的殼函數,而真正的hook info存在對象自己關聯的的hookInfoPool裏。ide

#pragma mark - For specific instance

- (STHookResult)st_hookInstanceMethod:(SEL)sel option:(STOption)option usingIdentifier:(STIdentifier)identifier withBlock:(id)block {
  @synchronized(self) {
    Class stSubClass = getSTSubClass(self);
    if (!stSubClass) return STHookResultOther;
    
    STHookResult hookMethodResult = hookMethod(stSubClass, sel, option, identifier, block);
    if (hookMethodResult != STHookResultSuccuss) return hookMethodResult;
    if (!objc_getAssociatedObject(self, STSubClassKey)) {
      object_setClass(self, stSubClass);
      objc_setAssociatedObject(self, STSubClassKey, stSubClass, OBJC_ASSOCIATION_ASSIGN);
    }
    
    id<STHookInfoPool> instanceHookInfoPool = st_getHookInfoPool(self, sel);
    if (!instanceHookInfoPool) {
      instanceHookInfoPool = [STHookInfoPool poolWithTypeEncoding:nil originalIMP:NULL selector:sel];
      st_setHookInfoPool(self, sel, instanceHookInfoPool);
    }
    
    STHookInfo *instanceHookInfo = [STHookInfo infoWithOption:option withIdentifier:identifier withBlock:block];
    return [instanceHookInfoPool addInfo:instanceHookInfo] ? STHookResultErrorIDExisted : STHookResultSuccuss;
  }
}

NS_INLINE Class getSTSubClass(id object) {
  NSCParameterAssert(object);
  Class stSubClass = objc_getAssociatedObject(object, STSubClassKey);
  if (stSubClass) return stSubClass;
    
  Class isaClass = object_getClass(object);
  NSString *isaClassName = NSStringFromClass(isaClass);
  const char *subclassName = [STClassPrefix stringByAppendingString:isaClassName].UTF8String;
  stSubClass = objc_getClass(subclassName);
  if (!stSubClass) {
    stSubClass = objc_allocateClassPair(isaClass, subclassName, 0);
    NSCAssert(stSubClass, @"Class %s allocate failed!", subclassName);
    if (!stSubClass) return nil;
    
  objc_registerClassPair(stSubClass);
  Class realClass = [object class];
  hookGetClassMessage(stSubClass, realClass);
  hookGetClassMessage(object_getClass(stSubClass), realClass);
}
  return stSubClass;
}

複製代碼

invoke hook info

     在當對象isa指向的類的方法被調用時,殼函數被執行,hookInfoPool會做爲userData傳進來,這個hookInfoPool時關聯在類上的。若是關聯的類的類名帶有st_class_前綴, 代表這是Hook實例對象新建的子類,那麼hookInfoPool裏的hook Info是空的,須要嘗試取到預存的真實的類statedCls裏的hook信息,即針對類的全部實例對象的hook。函數

     在args裏,咱們能夠在第一位拿到self, 即此時調用方法的實例對象,若是有關聯的hookInfoPool,代表有針對此對象的Hook。執行切面block代碼時,先執行鍼對的類的hook info,再執行鍼對實例對象的hook Info,若是是替換,則優先以實例的爲準。post

#define ffi_call_infos(infos) \
for (id<STHookInfo> info in infos) { \
  id block = info.block; \
  innerArgs[0] = &block; \
  ffi_call(&(hookedClassInfoPool->_blockCif), impForBlock(block), NULL, innerArgs); \
}  \

static void ffi_function(ffi_cif *cif, void *ret, void **args, void *userdata) {
  STHookInfoPool *hookedClassInfoPool = (__bridge STHookInfoPool *)userdata;
  STHookInfoPool *statedClassInfoPool = nil;
  STHookInfoPool *instanceInfoPool = nil;
  SEL sel = hookedClassInfoPool->_sel;
  if ([NSStringFromClass(hookedClassInfoPool->_hookedCls) hasPrefix:STClassPrefix]) {
    statedClassInfoPool = st_getHookInfoPool(hookedClassInfoPool->_statedCls, sel);
  } else {
    statedClassInfoPool = hookedClassInfoPool;
  }
  NSUInteger count = hookedClassInfoPool->_signature.argumentTypes.count;
  void **innerArgs = malloc(count * sizeof(*innerArgs));
  StingerParams *params = [[StingerParams alloc] init];
  void **slf = args[0];
  instanceInfoPool = st_getHookInfoPool((__bridge id)(*slf), sel);
  params.slf = (__bridge id)(*slf);
  params.sel = sel;
  [params addOriginalIMP:hookedClassInfoPool->_originalIMP];
  NSInvocation *originalInvocation = [NSInvocation invocationWithMethodSignature:hookedClassInfoPool->_ns_signature];
  
  for (int i = 0; i < count; i ++) {
    [originalInvocation setArgument:args[i] atIndex:i];
  }
  [params addOriginalInvocation:originalInvocation];
  
  innerArgs[1] = &params;
  memcpy(innerArgs + 2, args + 2, (count - 2) * sizeof(*args));
  
  // before hooks
  ffi_call_infos(statedClassInfoPool->_beforeInfos);
  if (instanceInfoPool) ffi_call_infos(instanceInfoPool->_beforeInfos);
  
  // instead hooks
  if (instanceInfoPool && instanceInfoPool->_insteadInfos.count) {
    id <STHookInfo> info = instanceInfoPool->_insteadInfos[0];
    id block = info.block;
    innerArgs[0] = &block;
    ffi_call(&(hookedClassInfoPool->_blockCif), impForBlock(block), ret, innerArgs);
  } else if (statedClassInfoPool->_insteadInfos.count) {
    id <STHookInfo> info = statedClassInfoPool->_insteadInfos[0];
    id block = info.block;
    innerArgs[0] = &block;
    ffi_call(&(hookedClassInfoPool->_blockCif), impForBlock(block), ret, innerArgs);
  } else {
    // original IMP
    /// if hooked by aspects or jspatch.. which use message-forwarding.
    BOOL isForward = hookedClassInfoPool->_originalIMP == _objc_msgForward
#if !defined(__arm64__)
    || hookedClassInfoPool->_originalIMP == (IMP)_objc_msgForward_stret
#endif
    ;
    if (isForward) {
      [params invokeAndGetOriginalRetValue:ret];
    } else {
      ffi_call(cif, (void (*)(void))hookedClassInfoPool->_originalIMP, ret, args);
    }
  }
  // after hooks
  ffi_call_infos(statedClassInfoPool->_afterInfos);
  if (instanceInfoPool) ffi_call_infos(instanceInfoPool->_afterInfos);
  
  free(innerArgs);
}

複製代碼

兼容性

兼容性:考慮到類或單個對象已經被相似於rac aspects jspatch使用msg_Forward替換過方法實現。執行原方法實現時,若是函數指針是msg_Forward,則不利用ffi_call使用模板導入函數指針和參數動態調用以增長效率,須要invoke Original invocation走系統發送消息觸發消息轉發走下邊的邏輯。ui

謝謝觀看,若有錯誤,請多指正。

文中代碼:github.com/Assuner-Lee…spa

相關文章
相關標籤/搜索