筆者由于工作的原因,需要接入多家廣告廠商的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<Application> 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<Object, Object> arrayMap = (Map<Object, Object>) mActivities;
for (Map.Entry<Object, Object> entry : arrayMap.entrySet()) {
Object value = entry.getValue();
Class<?> activityClientRecordClass = value.getClass();
Field activityField = activityClientRecordClass.getDeclaredField("activity");
activityField.setAccessible(true);
Object o = activityField.get(value);
Activity activity = (Activity) o;
if ("你想要找的Activity全名".equals(activity.getLocalClassName())) {
activity.finish();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
當然了,上面的方法很強大,已經拿到了Activity實例了,你想調別的方法也可以,這里不拓展。
Activity的擴展
有時候第三方的Activity進行了混淆,內部代碼沒法看,但是提供的功能又不足,同時外部對它的使用是開放可見的(比如SDKActivity.start()),這樣我們可以繼承他,然后在Activity中增加一些需要的東西比如接口回調