笔者由于工作的原因,需要接入多家广告厂商的SDK。但是厂商的SDK偶尔会出现奔溃,由于大厂流程原因没办法及时提供新版SDK,这就需要我们临时解决下;而且在部分场景下对方提供的接口不能满足我们的需求,因此需要灵活修改。这里结合自身经验分享下,希望有更多的朋友提供自己的想法。
临时解决奔溃
遵循合作方的使用规范
在大多数时候,使用第三方SDK出现奔溃都是因为没有按照合作方的方法来,比如有的SDK强制要求在Application初始化的时候初始化自己的SDK,有的时候我们为了启动性能的考虑会将其延后启动,这就会导致奔溃,本身SDK的接入就得考虑其带来的体积以及性能影响。
能用try-catch解决的问题都不叫问题
有经验的程序员都喜欢在关键代码出加上try-catch,同样的面对大多数SDK奔溃我们可以采用最简单的try防止异常抛出,防止整个app奔溃。举个例子,头条SDK1.9.8.8修改了初始化的方式,将其统一在Application初始化中,在使用时需要获取头条的native类,如果发现尚未初始化完成则会直接抛出奔溃(本质上说不算奔溃,只是api的修改对于老代码很不友好)。
但是呢,这种包裹只能在代码所在的同步线程上catch住奔溃,很多情况下是不够用的,比如新开的Activity中的奔溃
使用javassist 修改jar
有些时候奔溃发生在我们触碰不到的地方,这个时候我们发现奔溃想要停止他的话只能侵入代码了。这里介绍一个工具:javassist。
这个东西网上介绍它的比较多的是动态编程,通过修改字节码来达到动态修改的目的。所以这个东西也能被用来修改我们的目标class类型。
下面开码。
第一步,对我们的目标jar包进行修改,拿到奔溃的class,弄一个新的
//使用的javasist jar包为Javassist 3.23.1-GA.jar
//首先第一步,获取ClassPool对象
ClassPool pool = ClassPool.getDefault();
//随后加入我们的jar包路径,使得它能找到我们的目标class
pool.insertClassPath("你想要修改的Jar包.jar");
//insertClassPath可以多次调用
pool.insertClassPath("/Users/Android/sdk/platforms/android-28/android.jar");
//从类池里找到我们的目标类
CtClass adEventThreadClass = pool.get("com.cmcm.Gzoom.GzTest");
//打印目标类信息
System.out.println("class info : " + adEventThreadClass);
//找到目标类中的方法,如果被混淆过的话只能用混淆的方法名
CtMethod handleMessageMethod = adEventThreadClass.getDeclaredMethod("test");
//在方法里加上try-catch
CtClass etype = pool.get("java.lang.Exception");
//传进去的文本必须是大括号括起来的,还要加上返回值;$e代表异常值,这里我们打印出来,也就是我们传入android.jar的目的
handleMessageMethod.addCatch("{ android.util.Log.e(\"gzoomTTCatch\", \"tt adv sdk crash\", $e); return true; }", etype);
//可以将修噶后的方法打印出来
printMethod(handleMessageMethod);
// 写出来是个class
adEventThreadClass.writeFile("你想要打包的路径");
执行之后,在目标路径下就有了我们修改后的类
第二步,解压原来的jar包,将我们新生成的class替换进去
解压命令为:
unzip 目标Jar包.jar -d 解压文件夹名
然后顺着路径找到目标class,替换
第三步,打包我们的解压文件夹为jar包
打包命令为:
jar cvf 新Jar包名字.jar -C 解压出来的文件夹名/ .
请注意最后的.,不过你写错他也会提示你的
到这里就完成了,我们拿到了一个新的jar包,能catch住奔溃
如果是aar怎么办
在实际项目中,遇到很多的aar只是为了清单Manifest中注册而生成了aar,也就是说aar中只有Manifest、jar包以及部分资源(比如style)之外就没有了,所以大可以自己拷过来,只拿jar包
当然上面的不能包括全部,aar如何重新打包请参考:
Android修改第三方.aar后重新打包
根据自己业务需求灵活修改
很多时候合作方提供的api不能满足我们的需求,这个时候我们就要在合理范围内灵活运用了。
Activity的跳转指定
有的时候我们掉起合作方的功能,大多数是一个Activity,如果我们想要在Activity结束后回到某个Activity,这个时候一般有这么几个途径:
在结束方法中指定Intent
如果Activity有提供结束回调,比如关闭按钮的点击事件,我们可以在点击事件中主动startActivity进行跳转修改Activity的任务栈
在项目中,有些Activity会有自己的任务栈,这个时候如果不做修改,第三方的SDK的Activity一般会运行在App主任务栈上,跳转变得琐碎,这个时候就可以在清单中重写第三方这个Activity,将其规划到自己的其他任务栈中 ,也就能实现流畅跳转了。这个方法修改起来幅度下见效快,但是缺点也是很明显的,就是只能在你指定的那个任务栈上运行了,想在其他任务栈上使用会存在同样的问题;另外,这样的修改已经算侵入第三方SDK了,可能会带来奔溃等问题监听返回以及home事件
这个方法限定比较大,因为一是很多SDK的Activity本身已经监听了,第二是通过这个方法来监听容易造成误跳转,不够准确
Activity的取消
在某些时候,我们希望能够从外部杀掉Activity,比如第三方SDK没有做好数据恢复导致白屏等。这里我们需要反射拿到Application的栈,然后找到目标Activity实例finish它。
如何找到目标Activity呢?如果它能停留在页面上一会的话,你可以使用adb命令查看:
adb shell dumpsys activity
然后在合适的地方调用如下代码,finish掉它
private void clearTTRewardActivity(Application application) {
try {
Class applicationClass = Application.class;
Field mLoadedApkField = applicationClass.getDeclaredField("mLoadedApk");
mLoadedApkField.setAccessible(true);
Object mLoadedApk = mLoadedApkField.get(application);
Class> mLoadedApkClass = mLoadedApk.getClass();
Field mActivityThreadField = mLoadedApkClass.getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
Object mActivityThread = mActivityThreadField.get(mLoadedApk);
Class> mActivityThreadClass = mActivityThread.getClass();
Field mActivitiesField = mActivityThreadClass.getDeclaredField("mActivities");
mActivitiesField.setAccessible(true);
Object mActivities = mActivitiesField.get(mActivityThread);
if (mActivities instanceof Map) {
@SuppressWarnings("unchecked")
Map
当然了,上面的方法很强大,已经拿到了Activity实例了,你想调别的方法也可以,这里不拓展。
Activity的扩展
有时候第三方的Activity进行了混淆,内部代码没法看,但是提供的功能又不足,同时外部对它的使用是开放可见的(比如SDKActivity.start()),这样我们可以继承他,然后在Activity中增加一些需要的东西比如接口回调