iOS点点滴滴程序员iOS开发笔记

CJMethodLog(一)Runtime原理:从监控还原APP

2018-03-02  本文已影响252人  lele8446

导语:

CJMethodLog 对于Objective-C中的任意类、任意方法,均可实时根据用户的操作行为,监控还原对应的函数调用日志,而且能够自定义记录当前函数的参数类型、返回类型、执行时间……

背景说明

是否遇到过如此场景:对于项目中一些不是Crash的问题,由于缺乏log日志,排查起来很是麻烦;又或者对于一些特定设备、特定场景的问题,由于缺乏条件没法重现,最后只能不了了之。比如下面的例子:

你负责开发了一个考勤打卡页面。某天你正当心情愉悦哼着小曲开始一天工作时,产品经理突然闯入:又有用户反馈明明打了卡却提示旷工!并且那用户未能提供当时的操作记录,而且也无法再重现问题。无语的你只好再次默默过了一遍打卡代码,没有发现问题,然后求助后端同事查看接口调用日志,却发现该用户反馈问题的那一天根本就没有相关的接口调用记录……
——最后结论是用户当时忘打卡了(但其实你却不能十足确定打卡代码没有问题)

  那是否有这样一种系统:它能够实时记录用户对APP的操作行为,并还原当前操作对应的运行代码,然后将操作记录写入日志,生成一份详细的函数调用堆栈日志。
  事实上,如果是只针对Crash错误,那么使用NSSetUncaughtExceptionHandler就完全可以收集到APP崩溃时刻的函数调用堆栈信息了(这点有时间我再起一篇文章说明)。
   CJMethodLog就是这样一个类库:对于Objective-C中的任意类、任意方法,均可实时根据用户的操作行为,监控还原对应的函数调用日志,而且能够自定义记录当前函数的参数类型、返回类型、执行时间等等。


实现原理

flag立的有点大😅,但是如果你了解Objective-C运行时(Runtime)的语言特性,就知道理论上这是完全可以实现的。

一些基本概念

在xcode中用快捷键shift+cmd+O 分别搜索objc.h runtime.h NSObject.h

isa.jpg

可以看出:

再看一下 objc_class 的定义

runtime.png

看到这里你可能有点晕了。id代表一个objc_object结构体指针,该结构体包含一个isa指针指向所属的类;但是类里面又还有一个isa,那这个isa指向的又是谁呢?这里就引入了Meta Class(元类),Meta Class本身也是一个类,它跟其他类一样也有自己的 isa 和 super_class 指针。

比如对于下面例子,类Son继承自FatherFather继承自NSObject

@interface Father : NSObject
+ (void)sendMessage;
@end
@implementation Father
+ (void)sendMessage {
    NSLog(@"sendMessage");
}
@end

@interface Son : Father
- (void)doSomething:(id)parameter;
- (void)somethingWrong;
@end
@implementation Son
- (void)doSomething:(id)parameter {
    NSLog(@"doSomething:");
}
@end

那么其结构如下:

MetaClass.png
Meta Class 结论:
  • 每个Class都有一个isa指针指向一个唯一的Meta Class
  • 每一个Meta Class的isa指针都指向最上层的Meta Class(图中为NSObject的Meta Class)
  • 最上层的Meta Class的isa指针指向自己,从而形成一个回路
  • 每一个Meta Class的super class指针指向它原本Class的 Super Class对应的Meta Class。但是最上层的Meta Class的 Super Class指向NSObject Class本身
  • 最上层的NSObject Class的super class指向 nil

执行代码,验证一下:

SonClass.png
  1. SonClassIsaSonClassMetaClass是Son类的MetaClass(这里用了两种不同的方式获取MetaClass)
  2. FatherClassIsa是Father类的MetaClass
  3. NSObjectClassIsa是NSObject类的MetaClass
  4. MetaClass0SonClassIsa的MetaClass,MetaClass1FatherClassIsa的MetaClass,MetaClass2NSObjectClassIsa的MetaClass;MetaClass0 MetaClass1 MetaClass2都指向NSObjectClassIsa本身(四者代表同一个对象)
  1. 类:Son -> Father -> NSObject -> nil
  2. 元类: SonClassIsa -> FatherClassIsa -> NSObjectClassIsa -> NSObject -> nil

你以为是Son和Father,但其实输出的都是Son !!!
要知道,在Objective-C中,self是一个隐藏参数,它指向当前调用方法的对象(receiver);而super却不是,它只是一个预编译指令。调用super方法,底层执行的是objc_msgSendSuper(下一节有详细说明)

objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

其中objc_super结构体的构成如下:

objc_super.png
receiver表示当前调用方法的对象,super_class表示其父类,objc_msgSendSuper的作用是先构建objc_super结构体,然后从结构体指向的父类开始寻找方法实现,找不到再往上一级父类寻找。找到后最终会转变为以下方法:
objc_msgSend(objc_super->receiver, @selector(class))

这时候 self 和 objc_super->receiver 表示的是同一个接收者(receiver),同一个接收者执行相同的方法@selector(class),那结果肯定是相同的。

消息传递

Objective-C中对于方法的调用,其实都是以消息的方式在传递。

Son *mySon = [Son new];
//调用方法
[mySon doSomething:@"parameter"];
[mySon performSelector:@selector(somethingWrong)];   
[Son sendMessage];

以上三个方法的调用,编译器最终会将其转换为C函数:

objc_msgSend(mySon,@selector(doSomething:),@"parameter")
objc_msgSend(mySon,@selector(somethingWrong))
objc_msgSend(Son,@selector(sendMessage))

其中:

下面是以上三个方法调用时的流程图:


消息传递.png
消息转发

消息转发是Objective-C运行时的一个重要特性,具体表现是当调用一个不存在的方法时,并不会立马Crash,Runtime会有三次挽救的机会(准确的说是1次动态方法解析 + 1次快速消息转发 + 1次完整消息转发;)

调用阶段 调用方法 备注
动态方法解析 +resolveInstanceMethod: (实例方法)

+resolveClassMethod:(类方法)
这里可以动态添加方法
快速消息转发
(也叫备援接收者)
-forwardingTargetForSelector: 可以在此将消息转发到指定对象,触发新的消息传递
完整消息转发 -methodSignatureForSelector:

-forwardInvocation:
获取签名,并根据方法签名包装成的Invocation,对方法进行处理
消息处理失败 -doesNotRecognizeSelector: 抛出异常

接上面,对于调用方法:

[mySon performSelector:@selector(somethingWrong)];

这是一个没有实现的方法,最终会进入消息转发阶段,那么其完整的执行流程如下:


objc_msgSend.png

到此为止已经梳理完了Objective-C中完整的方法调用过程。细心的你可能会发现

- (id)forwardingTargetForSelector:(SEL)aSelector;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (void)doesNotRecognizeSelector:(SEL)aSelector;

这几个方法都是实例方法,那是否意味着消息转发只针对实例方法有效呢?答案是否定的!前面基本概念的介绍中提到,Classs的isa指针指向的是它的Meta Class,那么意味着一个Class的类方法会加入到它的Meta Class对应的methodLists中,所以你只需要在类中重写下面的类方法,同样可以实现对未知类方法的消息转发。

+ (id)forwardingTargetForSelector:(SEL)aSelector;
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
+ (void)forwardInvocation:(NSInvocation *)anInvocation;
+ (void)doesNotRecognizeSelector:(SEL)aSelector

未完待续

上面已经完整介绍了Objective-C中的消息传递以及消息转发原理,回顾开篇提到的需求,聪明的你是否已经get到了 CJMethodLog 的实现要点,这里先卖个关子。

本来是想只用一篇文章说完所有的,但是写着写着发现篇幅已经不小了,所以另起一篇文章
CJMethodLog 二:从监控还原APP运行的每一行代码说起
来讲解 CJMethodLog 的具体实现。

上一篇 下一篇

猜你喜欢

热点阅读