导图社区 移动APP性能监测技术方案
移动APP性能监测技术方案,本图知识全面详细,干货满满,非常实用,现在不收藏,还在等什么呢。
编辑于2022-05-17 22:04:08移动APP性能监测技术方案
要点
拦截到
调回去
名词约定:原始方法、代理方法
SEL数据类型
它是编译器运行Objective-C里的方法的环境参数。
IMP数据类型
他其实就是一个 编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。
监控 技术
NSURLProtocol
应用场景
1.自定义请求和响应
2.提供自定义的全局缓存支持
3.重定向网络请求
4.提供HTTP Mocking (方便前期测试)
5.其他一些全局的网络请求修改需求
6.内容过滤
基本步骤
1、定一个NSURLProtocol子类
2、注册这个类
[NSURLProtocol registerClass:[MyURLProtocol class]]; 当NSURLConnection准备发起请求时,它会遍历所有已注册的NSURLProtocol,询问它们能否处理当前请求。所以你需要尽早注册这个Protocol。 对于NSURLSession的请求,注册NSURLProtocol的方式稍有不同,是通过NSURLSessionConfiguration注册的 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSArray *protocolArray = @[ [MyURLProtocol class] ]; configuration.protocolClasses = protocolArray; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]]; NSURLSessionTask *task = [session dataTaskWithRequest:request]; [task resume];
3、NSURLProtocol子类实现
相关方法
canInitWithRequest:
整个流程的入口,只有这个方法返回YES我们才能够继续后续的处理
canonicalRequestForRequest
返回规范化后的request,一般就只是返回当前request即可。
requestIsCacheEquivalent
用于判断你的自定义reqeust是否相同,这里返回默认实现即可。它的主要应用场景是某些直接使用缓存而非再次请求网络的地方。
startLoading
实现请求和取消流程
stopLoading
实现请求和取消流程
4.实现代理
因为在第二步中我们接管了整个请求过程,所以需要实现相应的协议并使用NSURLProtocolClient将消息回传给URL Loading System。在我们的场景中推荐实现所有协议。 - (nullable NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response { if (response != nil) { [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response]; } return request; } - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { [self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge]; } - (void)connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { [self.client URLProtocol:self didCancelAuthenticationChallenge:challenge]; } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:[[self request] cachePolicy]]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.client URLProtocol:self didLoadData:data]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; }
NSURLConnectionDelegate
NSURLConnectionDataDelegate
5.注销
[NSURLProtocol unregisterClass:[MyURLProtocol class]];
坑
1.导致递归调用
企图在canonicalRequestForRequest:进行request的自定义操作,导致各种递归调用导致连接超时。这个API的表述其实很暧昧: It is up to each concrete protocol im
2.未实现足够的回调
没有实现足够的回调方法导致各种奇葩问题。如connection:willSendRequest:redirectResponse: 内如果没有通过[self client]回传消息,那么需要重定向的网页就会出现问题:host不对或者造成跨域调用导致资源无法加载。
3.崩溃报错
有一点苹果说明的不是很清楚,苹果自己实现CustomHTTPProtocol源码中很好的体现了这一点: NSURLProtocolClient回调动作必须跟请求的托管发送保持在一个线程、相同的Runloop,具体实现逻辑如下: (1)在start方法中记录当前线程和Runloop模式; (2)所有对于NSURLProtocolClient的回调,都在记录的线程、以相同的Runloop模式触发,使用如下方法: [objc] view plain copy [self performSelector:onThread:withObject:waitUntilDone:modes:];
4.httpBody
NSURLProtocol在拦截NSURLSession的POST请求时不能获取到Request中的HTTPBody。苹果官方的解释是Body是NSData类型,而且还没有大小限制。为了性能考虑,拦截时就没有拷贝。
5.拦截对证书认证影响
因为URLConnection新增了证书认证方法: [objc] view plain copy - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; 但是NSURLProtocolClient并没有增加对应的回调方法,会导致原始请求的证书校验代理方法不调用。
优点
拦截UIWebView中的数据
缺点
性能不好
Method swizzling
Method Swizzing是发生在运行时的,本质上就是对IMP和SEL进行交换。 在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。 在这里我推荐Github上星最多的一个第三方-jrswizzle
特点
针对一个类
基本步骤
1、用class_addMethod为某类添加“代理方法”
#import "UIViewController+swizzling.h" #import <objc/runtime.h> @implementation UIViewController (swizzling) + (void)load { // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。 Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad)); Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad)); /** * 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。 * 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。 * 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。 */ if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) { method_exchangeImplementations(fromMethod, toMethod); } } // 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。 - (void)swizzlingViewDidLoad { NSString *str = [NSString stringWithFormat:@"%@", self.class]; // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉 if(![str containsString:@"UI"]){ NSLog(@"统计打点 : %@", self.class); } [self swizzlingViewDidLoad]; }
2、调用method_exchangeImplementations交换IMP
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
3、确保你的“代理方法”调用了“原始方法”
缺点
死循环
具有继承关系的两个类,拦截同一个方法,对于self的调用可能是调子类的方法,如果子类调用super就会出现死循环
对类簇不起作用
这是因为Method Swizzling对NSArray这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。 所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。 下面我们实现了防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃: #import "NSArray+LXZArray.h" #import "objc/runtime.h" @implementation NSArray (LXZArray) + (void)load { Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:)); method_exchangeImplementations(fromMethod, toMethod); } - (id)lxz_objectAtIndex:(NSUInteger)index { if (self.count-1 < index) { // 这里做一下异常处理,不然都不知道出错了。 @try { return [self lxz_objectAtIndex:index]; } @catch (NSException *exception) { // 在崩溃后会打印崩溃信息,方便我们调试。 NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__); NSLog(@"%@", [exception callStackSymbols]); return nil; } @finally {} } else { return [self lxz_objectAtIndex:index]; } } @end 下面我们列举一些常用的类簇的“真身”: 类 “真身” NSArray __NSArrayI NSMutableArray __NSArrayM NSDictionary __NSDictionaryI NSMutableDictionary __NSDictionaryM
重复执行
如果进行Method Swizzling的类中,有两个类有继承关系的,并且Swizzling了同一个方法。例如同时对NSArray和NSMutableArray中的objectAtIndex:方法都进行了Swizzling,这样可能会导致父类Swizzling失效的问题。 可以通过GCD的dispatch_once函数来解决,利用dispatch_once函数内代码只会执行一次的特性。
改进版本
原理
利用block作为IMP
步骤
1.获取原始IMP,保存到变量
2.定义Block,其中引用原始IMP
3.Block专成IMP
4.直接替换目标方法
Isa swizzling
特点
针对一个对象
原理
修改Isa指针
基本步骤
1、(动态)创建目标类子类
2、(动态)为子类添加方法
3、修改目标对象的isa
object_setClass
场景
KVC
KVC运用了一个isa-swizzling技术。isa-swizzling就是类型混合指针机制。KVC主要通过isa-swizzling,来实现其内部查找定位的。isa指针,如其名称所指,(就是is a kind of的意思),指向维护分发表的对象的类。该分发表实际上包含了指向实现类中的方法的指针,和其它数据
KVO
在上面所介绍的KVC机制上加上KVO的自动观察消息通知机制就水到渠成了。 当观察者为一个对象的属性进行了注册,被观察对象的isa指针被修改的时候,isa指针就会指向一个中间类,而不是真实的类。所以isa指针其实不需要指向实例对象真实的类。所以我们的程序最好不要依赖于isa指针。在调用类的方法的时候,最好要明确对象实例的类名。
关于dealloc
调用objc_destructInstance()释放对象的所有实例变量和关联对象(该方法并未回收对象本身内存). isa-swizzling将该对象的类置为一个空的类对象. 调用free()回收该对象的内存.
Isa swizzling+NSProxy
NSProxy强调了转发机制
基本步骤
1、从NSProxy派生一个子类
2、实现那三个方法
methodSignatureForSelector
forwardInvocation
responseToSelector
3、把目标对象的isa指向这个类
Others
fishhook
原理
符号表的修改
Runlook观察者/CADisplayLink
sendEvent/addTarget:::
疑难
WKWebView
独立进程
TCP数据
iOS8可以,iOS9以后不可以,每一次启动,都会将自己的Framework整合到一个cache,地址就确定下来了.那些调用不经过符号表。
用一个入口拦截任意一个方法
64位比较难
不定参数用栈传的
固定参数用寄存器传的