在上文《iOS13卡顿问题分析(一)CPU on instruments》的分析中,我们可以看出,在App退到后台时,由于各个三方SDK都想在这个时机进行自己的操作,所以大家都挤在了一起,导致App在退到后台的一瞬间,CPU使用率陡然增高,从而bugly上出现了此类型的卡顿(这个结论不够全面,请看下文的发现),虽然此时机用户对于卡顿的感知不明显。
我们进一步拆分,退到后台又包含两个时机:
-
即将进入后台
applicationWillResignActive
-
已经进入后台
applicationDidEnterBackground
而这两个时机,必然会发送两个通知出来,分别为
-
UIApplicationWillResignActiveNotification
-
UIApplicationDidEnterBackgroundNotification
所以,我们可以通过拦截NSNotificationCenter的addObserver方法,来获取监听这两个通知的实例和方法。得到了以下列表:
Bugly BLYEnvironmentInfo applicationDidEnterBackgroundHandler: BLYEnvironmentInfo applicationWillResignActiveHandler:
Facebook FBSDKApplicationDelegate applicationDidEnterBackground: FBSDKTimeSpentData resetSourceApplication FBSDKAppEvents applicationMovingFromActiveStateOrTerminating FBSDKLikeActionController _applicationWillResignActiveNotification:
ADJust ADJActivityHandler applicationWillResignActive
Sensor SensorsAnalyticsSDK applicationDidEnterBackground: SensorsAnalyticsSDK applicationWillResignActive:
YYCache YYMemoryCache _appDidEnterBackgroundNotification
MMKV MMKV didEnterBackground
JPush JPUSHService applicationDidEnterBackground JPUSHService applicationWillResignActive JCOREAddressAnalysisController applicationWillResignActive: JCommonServiceController applicationWillResignActive
SDWebImage SDImageCache applicationDidEnterBackground:
SDAutolayout 各种layoutSubViews
其中,从instrument可以看到:facebook,JPush,ADJust的操作集中在即将进入后台时。
Image神策,SDAutoLayout,SDWebimage,集中在已经进入后台之后。
Image但是在即将进入后台到完全进入后台,是有1.5s左右的空窗期的。我们尝试拉平CPU使用,则选择hook这些三方方法,然后将他们重新规划组织调用顺序。
我们以FBSDKAppEvents applicationMovingFromActiveStateOrTerminating
为例,hook它,然后将它的内部操作全部放在子线程,并延迟0.3s处理。再用instruments观察
虽然退到后台一瞬间,CPU使用依然很高,倒是从两个高柱变成了一个高柱,另一个高柱Facebook被移到了后面。由此可以证明,hook各个SDK方法进行延迟处理是可行的。
但是,我们得注意两个地方:
-
同一实例的即将进入后台的方法,和已经进入后台的方法,如果都延迟处理,一定要延迟同样的时间,因为其内部有可能这两个方法存在依赖关系。
-
延迟后,或者放在子线程不能影响本身功能。并且需要明确测试条件和边界。
-
有源码的,例如facebook,sensor,需要看懂源码才能下手
-
没有源码的,需要彻底明白在后台进行了哪些操作才能下手
接下来,我们挨个分析了每一个我们得到的这些类和方法。并挨个对他们做了处理。我们先来看看处理之后的CPU使用情况:
Image可以明显,看到,在退到后台后的操作,已经没有那么集中并激烈,反而转为分散并相对平稳。接下来,我们具体说说,我们针对他们每一个方法,都进行了怎样的操作。
e.g. JPush极光推送
-[JPUSHService applicationDidEnterBackground] -[JPUSHService applicationWillResignActive] -[JCOREAddressAnalysisController applicationWillResignActive:] -[JCommonServiceController applicationWillResignActive]
Jpush主要有以上几个方法。我们首先需要hook以上四个方法,此时有几个知识点:
-
如何hook,公开了类,但是没有公开方法名的方法?
-
如何hook,既没有公开类,也没有公开方法名的方法?
我们先看一下,具体的hook类:
#import "JPUSHService+YG.h"#import "NSObjectSafe.h"@implementation JPUSHService(YG)+ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:@selector(applicationDidEnterBackground) withMethod:@selector(hookApplicationDidEnterBackground)]; [self swizzleInstanceMethod:@selector(applicationWillResignActive) withMethod:@selector(hookApplicationWillResignActive)]; });}- (void)hookApplicationDidEnterBackground {}- (void)hookApplicationWillResignActive { }@end@interface JCOREAddressAnalysisController : NSObject@end@implementation JCOREAddressAnalysisController(YG)+ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:@selector(applicationWillResignActive:) withMethod:@selector(hookApplicationWillResignActive:)]; });}- (void)hookApplicationWillResignActive:(NSNotification *)noti { }@end@interface JCommonServiceController : NSObject@end@implementation JCommonServiceController(YG)+ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:@selector(applicationWillResignActive) withMethod:@selector(hookApplicationWillResignActive)]; });}- (void)hookApplicationWillResignActive { }@end
(具体的method swizzling方法在NSObjectSafe类中,都是些通用的公开方案,这里不赘述)
-
针对,
JPUSHService
,SDK已经公开了类的头文件,所以虽然它没有公开applicationDidEnterBackground
。所以我们需要引用其头文件#import "JPUSHService.h"
,生成此类的类名进行操作。 -
针对,
JCOREAddressAnalysisController
和JCommonServiceController
,SDK根本连其具体的类都没公开。但是我们可以使用@interface
关键字,将它解放出来。
所以,在OC的世界中,没有我们想的那么安全,只要我知道了你的真名,那我就可以呼唤你。现在,我们已经成功hook到了方法,那么接下来,需要考量的是,我们需要如何对待这些方法呢?是延迟它?还是直接干掉它?
如果,对功能,对业务没有影响,那我当然选择干掉它了。所以,我们需要搞清楚,它退到后台后具体执行了什么功能呢?对于,静态库来说,因为没有源码,所以,我选择想办法反编译它来对其内部方法一探究竟。(虽然也是有限的探索,但是对于我们搞清楚是否可以干掉它,已经足够)
这个时候,轮到Hopper Disassembler上场了。我们可以利用这个工具,看到.a文件的内部结构,以及部分函数调用。
Image从得到的函数表以及其内部部分汇编,以及其注释可以得到这几个类,在退到后台后触发的大致功能。基本都是,写日志,重新分析各个IP的DNS质量,做本地的DNS策略等等,和业务几乎无关。
另外,我们再来看看JPush推送的数据流向,以及原理:
Image红线代表APNS推送,iOS的JPushSDK在这条数据通路上,只扮演入口,即注册设备,注册用户等操作。蓝线代表JPush的应用内消息,我们并没有用到。
再经过实际的推送验证,发现,及时屏蔽了这些方法,也是可以收到正常的APNS推送消息的,无论是App在后台,还是App被完全杀死。
所以,可以得到结论,这些方法都是可以放心屏蔽掉的,对我们的App功能和业务没有影响。
其他类似的处理还有ADJust,Facebook,SDWebimage等SDK,这里就不再一一赘述了,大同小异。
三、总结
总而言之,我们针对卡顿,开机启动等指标,最终分解问题都应该落地到计算机的硬件数值,CPU,RAM等,软件要做的,就是如何合理的分配硬件资源,如何更有效的利用硬件资源。
我相信,在有这个认识基础的情况下,我们后面做很多工作的时候,及时不能水到渠成,也会有很多思路。
而针对各种三方的SDK,在我们没有办法干掉他们的基础上,我希望尽可能的控制它,而控制它的基础便是了解它。
有源码的看源码,没有源码的想办法看,弄清楚其功能,我们业务需要哪些,不需要哪些,我是否可以只用到这些,是否可以摒弃一些(包括不是我们显式调用的),将它对我们App的风险降到最小。
网友评论