前言:本文簡述簡單使用Aspects實(shí)現(xiàn)自動日志打點(diǎn),僅是簡單使用,深層次需要大神來深究
一、知名AOP庫 Aspects
https://github.com/steipete/Aspects
一款比較成熟的面向切面庫
/// 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];
[轉(zhuǎn)載]大致原理:替換原方法的IMP
為 消息轉(zhuǎn)發(fā)函數(shù)指針 _objc_msgForward
或_objc_msgForward_stret
,把原方法IMP
添加并對應(yīng)到SEL
aspects_originalSelector
,將forwardInvocation:
的IMP
替換為參數(shù)對齊的C函數(shù)__ASPECTS_ARE_BEING_CALLED__(NSObject *self, SEL selector, NSInvocation *invocation)
的指針。在__ASPECTS_ARE_BEING_CALLED__
函數(shù)中,替換invocation
的selector
為aspects_originalSelector
,相當(dāng)于要發(fā)送調(diào)用原始方法實(shí)現(xiàn)的消息。對于插入位置在前面,替換,后面的多個(gè)block,構(gòu)建新的blockInvocation,從invocation中提取參數(shù),最后通過invokeWithTarget:block
來完成依次調(diào)用。有關(guān)消息轉(zhuǎn)發(fā)的介紹,不多贅述。持hook類的單個(gè)實(shí)例對象的方法(類似于KVO的isa-swizzlling)。不適合對每秒鐘超過1000次的方法增加切面代碼。此外,使用其他方式對Aspect hook過的方法進(jìn)行hook時(shí),如直接替換為新的IMP,新hook得到的原始實(shí)現(xiàn)是_objc_msgForward,之前的aspect_hook會失效,新的hook也將執(zhí)行異常。
二、Aspects簡單使用
1.Pods導(dǎo)入
target 'xiaobaitu' do
pod 'Aspects'
end
2.新建NSObject用于Aspects配置【demo】
對UIViewController開始hook viewDidAppear
方法
Logging.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Logging : NSObject
+ (void)setupWithConfiguration;
@end
NS_ASSUME_NONNULL_END
Logging.m
#import "Logging.h"
#import <UIKit/UIKit.h>
#import <Aspects/Aspects.h>
@implementation Logging
+ (void)setupWithConfiguration{
// Hook Page Impression
static NSString *className = @"123";
[UIViewController aspect_hookSelector:@selector(viewDidAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo){
className = NSStringFromClass([[aspectInfo instance] class]);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
className = NSStringFromClass([[aspectInfo instance] class]);
NSLog(@"===========%@",className);
});
} error:NULL];
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Config" ofType:@"plist"];
NSMutableArray *configs = [[NSMutableArray alloc] initWithContentsOfFile:filePath];
for (NSDictionary *classItem in configs){
Class class = NSClassFromString(classItem[@"Controller"]);
if (classItem[@"Events"]){
for (NSDictionary *event in classItem[@"Events"]) {
SEL selector = NSSelectorFromString(event[@"SelectorName"]);
[class aspect_hookSelector:selector withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"------[%@]:%@",event[@"SelectorName"],event[@"EventName"]);
});
}error:NULL];
}
}
}
}
@end
3. 新建配置文件Config.plist
在配置文件中注冊相關(guān)控制器及相關(guān)方法,plist格式和相關(guān)方法名稱自定義,Logging文件中對應(yīng)解析即可
Config.plist 簡單示例
<dict>
<key>Controller</key>
<string>UserViewController</string>
<key>Events</key>
<array>
<dict>
<key>EventName</key>
<string>退出登錄</string>
<key>SelectorName</key>
<string>logOut</string>
</dict>
<dict>
<key>EventName</key>
<string>版本更新</string>
<key>SelectorName</key>
<string>updateVersion</string>
</dict>
<dict>
<key>EventName</key>
<string>意見反饋</string>
<key>SelectorName</key>
<string>recommond</string>
</dict>
</array>
</dict>
4.注冊Logging
在AppDelegate.swift中注冊Logging
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
Logging.setupWithConfiguration()
return true
}
三、實(shí)現(xiàn)效果
- 1.可以輸出所有加載過的Controller
- 2.可以根據(jù)配置文件注冊的方法,點(diǎn)擊可打印出方法 or 操作
控制臺輸出如下:
===========TabViewController
===========OneViewController
===========TwoViewController
===========UserViewController
------[updateVersion]:版本更新
------[recommond]:意見反饋
------[logOut]:退出登錄
Aspects注意
1.Aspects無法hook類中不存在的方法,或者未實(shí)現(xiàn)的方法。
2.Aspects不支持hook類的類方法
3.Aspects對多個(gè)類繼承自同一基類需要單獨(dú)處理hook
【備注】
本文僅簡單使用Aspects實(shí)現(xiàn)簡單日志打點(diǎn)功能,如有錯(cuò)誤請指出,相關(guān)原理和更多操作請參考大神blog