Android接入第三方SDK可能帶來的問題以及解決方法

筆者由于工作的原因,需要接入多家廣告廠商的SDK。但是廠商的SDK偶爾會出現奔潰,由于大廠流程原因沒辦法及時提供新版SDK,這就需要我們臨時解決下;而且在部分場景下對方提供的接口不能滿足我們的需求,因此需要靈活修改。這里結合自身經驗分享下,希望有更多的朋友提供自己的想法。

臨時解決奔潰

遵循合作方的使用規范

在大多數時候,使用第三方SDK出現奔潰都是因為沒有按照合作方的方法來,比如有的SDK強制要求在Application初始化的時候初始化自己的SDK,有的時候我們為了啟動性能的考慮會將其延后啟動,這就會導致奔潰,本身SDK的接入就得考慮其帶來的體積以及性能影響。

能用try-catch解決的問題都不叫問題

有經驗的程序員都喜歡在關鍵代碼出加上try-catch,同樣的面對大多數SDK奔潰我們可以采用最簡單的try防止異常拋出,防止整個app奔潰。舉個例子,頭條SDK1.9.8.8修改了初始化的方式,將其統一在Application初始化中,在使用時需要獲取頭條的native類,如果發現尚未初始化完成則會直接拋出奔潰(本質上說不算奔潰,只是api的修改對于老代碼很不友好)。


1.9.8.8頭條獲取native

但是呢,這種包裹只能在代碼所在的同步線程上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中增加一些需要的東西比如接口回調


增加接口,寫成靜態的需要注意被替換等問題
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容