iOS 笔记 Aspects 源码解读

一. Aspects 是什么?

Aspect-oriented programming(AOP)通常被我们称为面向切面编程。有时候一些需求更需要面向切面编程,而不是传统的面向对象编程。比如以下几种情况:

  1. 无论何时,用户调用一个 API 必须进行安全检查。
  2. 所有 API 的调用都必须打印日志
  3. 总得来说就是面向对象要求我们类有单一职责原则,而有时候业务需要它们额外干一些和他们本职无关的事情。

二. 源码解读,主要涉及四个方法。

1. aspect_add: 是添加 hook 的主入口方法。
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    __block AspectIdentifier *identifier = nil;
    // 1.block内代码加锁(OSSpinLock 自旋锁)
    aspect_performLocked(^{
        // 2.判断selector是否可以进行hook(比如retain是不允许的)
        if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
            // 3.创建一个存储identifier的容器,并且把容器通过关联对象技术(objc_setAssociatedObject),关联到self上,这样self销毁的时候,也销毁Hook相关资源,防止内存泄露.
            AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
            // 4.用传入参数构建一个AspectIdentifier对象,销毁释放hook的时候使用.
            identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
            if (identifier) {
                // 5.将AspectIdentifier对象加入到容器中
                [aspectContainer addAspect:identifier withOptions:options];
                // 6.核心代码:修改self的Class使得它能够方法劫持
                aspect_prepareClassAndHookSelector(self, selector, error);
            }
        }
    });
    return identifier;
}
2. aspect_prepareClassAndHookSelector:第二个核心方法
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    // 6.1 hook改变self的Class类型.
    Class klass = aspect_hookClass(self, error);
    // 6.2 获取 selector 的 Method 对象
    Method targetMethod = class_getInstanceMethod(klass, selector);
    // 6.3 获取 selector 的 IMP
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    // 6.4 判断如果不是消息转发的IMP
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // Make a method alias for the existing method implementation, it not already copied.
        // 6.5 为 selector 创建一个别名,并且 copy 它的实现
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        SEL aliasSelector = aspect_aliasForSelector(selector);
        if (![klass instancesRespondToSelector:aliasSelector]) {
            __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
            NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
        }
        // We use forwardInvocation to hook in.
        // 6.6 把 selector 的实现换成 _objc_msgForward,_objc_msgForward是一个函数地址,该函数执行会让selector的调用进入消息转发阶段.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
        AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
    }
}
3. aspect_hookClass:动态创建子类,并替换掉 forwardInvocation:方法让消息转发进入 __ASPECTS_ARE_BEING_CALLED__()方法
static Class aspect_hookClass(NSObject *self, NSError **error) {
    NSCParameterAssert(self);
    // 6.1.1 拿到类对象
    Class statedClass = self.class;
    // 6.1.2 拿到类对象
    Class baseClass = object_getClass(self);
    // 6.1.3 类对象的字符串
    NSString *className = NSStringFromClass(baseClass);
    // Default case. Create dynamic subclass.
    // 6.1.4 默认情况,创建动态子类
    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;
        }
        // 6.1.5 替换原有的forwardInvocation:方法,换成__ASPECTS_ARE_BEING_CALLED__(NSObject *self, SEL selector, NSInvocation *invocation)方法
        aspect_swizzleForwardInvocation(subclass);
        // 6.1.6 替换subclass的 class 方法,都返回 statedClass
        aspect_hookedGetClass(subclass, statedClass);
        aspect_hookedGetClass(object_getClass(subclass), statedClass);
        objc_registerClassPair(subclass);
    }
    // 6.1.7 把 self 的Class类型改成 subclass
    object_setClass(self, subclass);
    return subclass;
}
4.__ASPECTS_ARE_BEING_CALLED__: 当我们 hook 的方法被调用时,就会来到这个方法。会根据我们配置的参数来调用我们 hook 添加的 block
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    SEL originalSelector = invocation.selector;
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    invocation.selector = aliasSelector;
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
    NSArray *aspectsToRemove = nil;

    // Before hooks. 调用
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks. 调用
    aspect_invoke(classContainer.insteadAspects, info);
    aspect_invoke(objectContainer.insteadAspects, info);
    
    // After hooks. 调用
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);
}

三. Aspects 无法解决和 KVO 共存的问题,作者已经不推荐大家在生产环境中使用 Aspects了。

  1. 使用 字节跳动团队 开源的 SDMagicHook,它们的目的就是取代 Aspects
  • 他们通过深入研究 KVO 的本质,解决了 兼容KVO 的问题。
  • 不过我觉得他们的 hook 逻辑为了处理 KVO 问题,变得相当复杂,本身 hook 就是一个比较不让人省心的做法,所以一旦出现问题会比较难调试。
  • 仁者见仁智者见智的使用吧。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351