iOS13卡顿问题分析(二)避免竞争

在上文《iOS13卡顿问题分析(一)CPU on instruments》的分析中,我们可以看出,在App退到后台时,由于各个三方SDK都想在这个时机进行自己的操作,所以大家都挤在了一起,导致App在退到后台的一瞬间,CPU使用率陡然增高,从而bugly上出现了此类型的卡顿(这个结论不够全面,请看下文的发现),虽然此时机用户对于卡顿的感知不明显。

我们进一步拆分,退到后台又包含两个时机:

  1. 即将进入后台applicationWillResignActive

  2. 已经进入后台applicationDidEnterBackground

而这两个时机,必然会发送两个通知出来,分别为

  1. UIApplicationWillResignActiveNotification

  2. 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观察

Image

虽然退到后台一瞬间,CPU使用依然很高,倒是从两个高柱变成了一个高柱,另一个高柱Facebook被移到了后面。由此可以证明,hook各个SDK方法进行延迟处理是可行的。

但是,我们得注意两个地方:

  1. 同一实例的即将进入后台的方法,和已经进入后台的方法,如果都延迟处理,一定要延迟同样的时间,因为其内部有可能这两个方法存在依赖关系。

  2. 延迟后,或者放在子线程不能影响本身功能。并且需要明确测试条件和边界。

  3. 有源码的,例如facebook,sensor,需要看懂源码才能下手

  4. 没有源码的,需要彻底明白在后台进行了哪些操作才能下手

接下来,我们挨个分析了每一个我们得到的这些类和方法。并挨个对他们做了处理。我们先来看看处理之后的CPU使用情况:

Image

可以明显,看到,在退到后台后的操作,已经没有那么集中并激烈,反而转为分散并相对平稳。接下来,我们具体说说,我们针对他们每一个方法,都进行了怎样的操作。

e.g. JPush极光推送

    -[JPUSHService applicationDidEnterBackground]    -[JPUSHService applicationWillResignActive]    -[JCOREAddressAnalysisController applicationWillResignActive:]    -[JCommonServiceController applicationWillResignActive]

Jpush主要有以上几个方法。我们首先需要hook以上四个方法,此时有几个知识点:

  1. 如何hook,公开了类,但是没有公开方法名的方法?

  2. 如何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类中,都是些通用的公开方案,这里不赘述)

  1. 针对,JPUSHService,SDK已经公开了类的头文件,所以虽然它没有公开applicationDidEnterBackground。所以我们需要引用其头文件#import "JPUSHService.h",生成此类的类名进行操作。

  2. 针对,JCOREAddressAnalysisControllerJCommonServiceController,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的风险降到最小。

你可能感兴趣的:(iOS13卡顿问题分析(二)避免竞争)