Aspects 1.4.1

Aspects 1.4.1

测试已测试
Language语言 Obj-CObjective C
许可证 MIT
发布最后一次发布2014年12月

由 . 维护



Aspects 1.4.1

Aspects v1.4.1

@steipete 开发的一款面向对象编程的简单、令人愉悦的库。

将 Aspects 视为是强化版的 method swizzling。它允许您根据类或实例添加现有方法的代码,同时考虑插入点,例如在之前、替换或之后。Aspects 自动处理调用 super,比常规的 method swizzling 好用。

这是稳定的,并且已被数百个应用程序使用,它是 PSPDFKit,这是一个 iOS PDF 框架,Dropbox 或 Evernote 等应用程序附带的一部分,现在我终于将其开源了。

Aspects 通过以下方法扩展了 NSObject

/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Deregister an aspect.
/// @return YES if deregistration is successful, otherwise NO.
id<AspectToken> aspect = ...;
[aspect remove];

添加方面返回一个类型为 AspectToken 的不可见标记,可用来再次注销。所有调用都是线程安全的。

Aspects 使用 Objective-C 消息传递机制来挂钩消息。这会创建一些开销。不要向频繁调用的方法添加 aspects。Aspects 主要用于每秒不会调用 1000 次的 view/controller 代码。

Aspects 调用并匹配块参数。还包括无参数的块。第一个块参数的类型为 id<AspectInfo>

何时使用 Aspects

面向对象编程 (AOP) 用于封装“横切”关注点。这些是需要跨越系统中的许多模块,因此无法使用正常面向对象编程进行封装的要求。以下是一些这些类型要求的一些示例

  • 每次用户在服务客户端上调用方法时,都应该进行检查。
  • 每次用户与商店交互时,应根据他们的互动提供天才建议。
  • 所有调用都应该记录。

如果我们使用常规 OOP 实现上述要求,则会有一些不足

好的 OOP 声明一个类应该只有一个职责,但是添加额外的 横切 要求意味着承担其他职责的类。例如,您可能有一个 StoreClient,它应该专门用于从在线商店进行购买。添加横切要求后,它可能还需要承担记录、安全和推荐的角色。这不是很好。

  • 我们的 StoreClient 现在更难理解和维护。
  • 这些横切要求被重复并散布到我们的应用程序中。

AOP 允许我们将这些横切要求模块化,并清楚地确定它们应该应用的所有地点。如上例所示,横切要求可以是技术性的或业务性的。

以下是一些具体的示例

方面可用来仅对调试构建动态添加日志

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];

这可以将您的分析设置大大简化:https://github.com/orta/ARAnalytics


您可以在测试用例中检查方法是否真的被调用

- (void)testExample {
    TestClass *testClass = [TestClass new];
    TestClass *testClass2 = [TestClass new];

    __block BOOL testCallCalled = NO;
    [testClass aspect_hookSelector:@selector(testCall) withOptions:AspectPositionAfter usingBlock:^{
        testCallCalled = YES;
    } error:NULL];

    [testClass2 testCallAndExecuteBlock:^{
        [testClass testCall];
    } error:NULL];
    XCTAssertTrue(testCallCalled, @"Calling testCallAndExecuteBlock must call testCall");
}

这对于调试非常有用。这里我想知道何时触摸手势的状态确实改变

[_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
    NSLog(@"%@: %@", aspectInfo.instance, aspectInfo.arguments);
} error:NULL];

另一个方便的用法是为您不拥有的类添加处理程序。我在 PSPDFKit 中使用它,我们要求当视图控制器以模态形式被 dismissed 时接收通知。这包括 MFMailComposeViewControllerUIImagePickerController 等UIKit 视图控制器。我们本可以为这些控制器中的每一个都创建子类,但这将产生大量不必要的代码。Aspects 为您提供了一种更简单的解决方案来解决这个问题

@implementation UIViewController (DismissActionHook)

// Will add a dismiss action once the controller gets dismissed.
- (void)pspdf_addWillDismissAction:(void (^)(void))action {
    PSPDFAssert(action != NULL);

    [self aspect_hookSelector:@selector(viewWillDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        if ([aspectInfo.instance isBeingDismissed]) {
            action();
        }
    } error:NULL];
}

@end

调试

Aspects 在堆栈跟踪中标识良好,因此可以很容易地看出是否为方法安装了钩子

使用 Aspects 和非 void 返回类型一起使用

您可以使用调用对象来自定义返回值

    [PSPDFDrawView aspect_hookSelector:@selector(shouldProcessTouches:withEvent:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, NSSet *touches, UIEvent *event) {
        // Call original implementation.
        BOOL processTouches;
        NSInvocation *invocation = info.originalInvocation;
        [invocation invoke];
        [invocation getReturnValue:&processTouches];

        if (processTouches) {
            processTouches = pspdf_stylusShouldProcessTouches(touches, event);
            [invocation setReturnValue:&processTouches];
        }
    } error:NULL];

安装

最简单的方法是使用 pod "Aspects"

您还可以添加两个文件 Aspects.h/m。没有其他要求。

兼容性和限制

Aspects 使用大量运行时技巧来实现其功能。您可以将这些与常规的方法交换混合。

一个重要的限制是,对于基于类的钩子,方法只能在每个子类层次结构中安装一次。 见 #2 这不适用于被钩子安装的对象。Aspects 在这里创建了一个动态子类并完全控制系统。

KVO 在观察者在调用 aspect_hookSelector: 之后创建时有效。否则很可能崩溃。仍然在这里寻找解决方案 - 非常欢迎任何帮助。

由于 ObjC 运行时中丑陋的实现细节,如果代码运行在 arm64 运行时上,则可能无法正确处理返回联合体,其中还包含结构体的方法。

鸣谢

使用 _objc_msgForward 的想法以及 NSInvocation 论证选择的部分来自来自 GitHub 的高质量项目 ReactiveCocoa此文章 解释了它如何在工作底层运行。

支持的 iOS & SDK 版本

  • Aspects 需要 ARC。
  • Aspects 已在 iOS 6+ 和 OS X 10.7 或更高版本上进行测试。

许可证

MIT 许可,版权所有 (c) 2014 Peter Steinberger, [email protetced], @steipete

发行说明

版本 1.4.1

  • 重命名错误代码

版本 1.4.0

  • 添加了对匹配方法签名的块签名的支持。(感谢 @nickynick)

版本 1.3.1

  • 增加了对 OS X 10.7 或更高版本的支持。(感谢 @ashfurrow)

版本 1.3.0

  • 添加了自动注销功能。
  • 在尝试安装之前检查是否存在选择器。
  • 改进了 dealloc 钩子。 (不再需要 unsafe_unretained)
  • 更好的示例。
  • 始终记录错误。

版本 1.2.0

  • 添加错误参数。
  • 子类注册跟踪的改进。

版本 1.1.0

  • 将 NSObject+Aspects.m/h 文件重命名为 Aspects.m/h。
  • 现在可以通过调用 remove 对象来移除。
  • 允许挂钩 dealloc。
  • 修复了针对多个类使用相同方法时发生的无限循环问题。挂钩仅适用于层次结构中的一个类。
  • 额外的检查以防止挂钩 retain/release/autorelease 或 forwardInvocation 等操作。
  • 现在正确保存了 forwardInvocation 的原始实现。
  • 在最后一个挂钩取消注册后,类将正确清理并恢复到原始状态。
  • 有很多很多新的测试用例!

版本 1.0.1

  • 进行了微调并改进了文档。

版本 1.0.0

  • 首次发布