iOS AOP实战
程序员文章站
2023-11-10 23:49:10
AOP: 面向切面编程,偏向于处理业务的某个阶段 适用场景: 1. 参数校验:网络请求前的参数校验,返回数据的格式校验等等 2. 无痕埋点:统一处理埋点,降低代码耦合度 3. 页面统计:帮助统计页面访问量 4. 事务处理:拦截指定事件,添加触发事件 5. 异常处理:发生异常时使用面向切面的方式进行处 ......
aop: 面向切面编程,偏向于处理业务的某个阶段
适用场景:
1. 参数校验:网络请求前的参数校验,返回数据的格式校验等等
2. 无痕埋点:统一处理埋点,降低代码耦合度
3. 页面统计:帮助统计页面访问量
4. 事务处理:拦截指定事件,添加触发事件
5. 异常处理:发生异常时使用面向切面的方式进行处理
6. 热修复:aop可以让我们在某方法执行前后或者直接替换为另一段代码,我们可以根据这个思路,实现bug修复
我们希望将以上需求分离到非业务逻辑的方法中,尽可能的不影响业务逻辑的代码。
demo 从配置aop到实际应用,记得给咱点个star~
源码分析
0. 类说明
mdaspectinfo:作为对象,包含调用信息(nsinvocation)的对象
作为协议,提供访问对象的属性 mdaspectidentifier:包含一个hook的信息,调用者,时机,回调处理等
mdaspecttracker:防止重复hook mdaspectscontainer:通过runtime给被hook的对象添加属性,提供存储和移除hook的方法 mdaspecttoken:提供移除hook的协议
1. hook时机
typedef ns_options(nsuinteger, mdaspectoptions) { mdaspectpositionafter = 0, /// 默认,当原方法执行完调用 mdaspectpositioninstead = 1, /// 替换原方法 mdaspectpositionbefore = 2, /// 原方法执行前调用 mdaspectoptionautomaticremoval = 1 << 3 /// will remove the hook after the first execution. };
2. 配置文件
配置hook的类,hook时机,实例方法和类方法,以及回调处理
为了区分实例方法和类方法,需要在类方法前加一个“+”
+(nsdictionary *)aop_mdviewcontrollerconfigdic{ nsdictionary *configdic = @{ @"mdviewcontroller":@{//hook那个类名 @"trackevents":@[ @{//实例方法 @"moment":@"before",//hook之前调用 @"eventselectorname":@"instancemethod",//实例方法名 @"block":^(id<mdaspectinfo>aspectinfo){//回调处理 // 获取方法的参数 nslog(@"跳转"); }, }, @{//类方法 @"moment":@"instead",//替换原方法 @"eventselectorname":@"+hookclassmethod",//类方法名 @"block":^(id<mdaspectinfo>aspectinfo){//回调处理 // 获取方法的参数 nslog(@"到处可以hook到我"); }, }, ] }, }; return configdic; }
3. 解析管理类
// hook到方法回调,完全控制 typedef void (^aspecteventblock)(id<mdaspectinfo> aspectinfo); @implementation mdaopmanager +(void)load{ // 加载配置文件 nsmutabledictionary *mutabledic = [nsmutabledictionary dictionary]; [mutabledic addentriesfromdictionary:[mdaopmanager aop_mdviewcontrollerconfigdic]]; [mutabledic addentriesfromdictionary:[mdaopmanager aop_mdsecviewcontrollerconfigdic]]; [self configaopwithdic:mutabledic]; } +(void)configaopwithdic:(nsdictionary *)configdic{ // 解析配置文件 for (nsstring *classname in configdic) { class clazz = nsclassfromstring(classname);//拿到类名 nsdictionary *config = configdic[classname];//配置信息 nsarray *trackarr = config[@"trackevents"];//方法数组 if (trackarr) { for (nsdictionary *event in trackarr) { aspecteventblock buttonblock = event[@"block"];//回调 nsstring *method = event[@"eventselectorname"];//方法名 nsstring *moment = event[@"moment"];//hook时机 mdaspectoptions option = mdaspectpositionafter; if ([moment isequaltostring:@"before"]) { option = mdaspectpositionbefore; }else if ([moment isequaltostring:@"instead"]){ option = mdaspectpositioninstead; } sel selector = nsselectorfromstring(method); if ([method hasprefix:@"+"]) {//hook类方法 method = [method substringfromindex:1]; selector = nsselectorfromstring(method); [clazz aspect_hookclassselector:selector withoptions:option usingblock:^(id<mdaspectinfo> aspectinfo) { dispatch_async(dispatch_get_global_queue(dispatch_queue_priority_default, 0), ^{ buttonblock(aspectinfo); }); } error:null]; }else{//hook实例方法 [clazz aspect_hookselector:selector withoptions:option usingblock:^(id<mdaspectinfo> aspectinfo) { dispatch_async(dispatch_get_global_queue(dispatch_queue_priority_default, 0), ^{ buttonblock(aspectinfo); }); } error:null]; } } } } }
4. 对外接口
// 类直接调用,hook实例方法
+ (id<mdaspecttoken>)aspect_hookselector:(sel)selector withoptions:(mdaspectoptions)options usingblock:(id)block error:(nserror **)error;
// 对象调用,hook实例方法
- (id<mdaspecttoken>)aspect_hookselector:(sel)selector withoptions:(mdaspectoptions)options usingblock:(id)block error:(nserror **)error;
// 类直接调用,hook类方法
+ (id<mdaspecttoken>)aspect_hookclassselector:(sel)selector withoptions:(mdaspectoptions)options usingblock:(id)block error:(nserror *__autoreleasing *)error;
// 对象调用,hook类方法
- (id<mdaspecttoken>)aspect_hookclassselector:(sel)selector withoptions:(mdaspectoptions)options usingblock:(id)block error:(nserror *__autoreleasing *)error;
总结
核心步骤:把目标selector的imp更换为runtime中的imp,从而直接进入消息转发,检查是否能添加hook,如果能,进行存储,接着方法交换处理,在消息转发里运行before instead after方法