騰訊tbs 記憶體泄露

一、背景

TBS(騰訊瀏覽服務)是騰訊提供的移動端webview體驗的整套解決方案(//x5.tencent.com/docs/index.html),可以用於移動端載入doc、xls、pdf等文檔,實現文檔瀏覽載入服務。

Android端詳細使用方法可參考://blog.csdn.net/czj1998/article/details/122494381

使用TbsReaderView 的openFile方法載入文件;

在關閉頁面前執行TbsReaderView 的onStop方法執行記憶體清理,防止下次載入出現異常。

二、現象

結束文件瀏覽,執行TbsReaderView 的onStop方法後,發現瀏覽文件的Activity發生泄露,記憶體無法釋放。

三、原因

(1)記憶體泄露發生在tbs sdk內部,從網路載入到本地jar包
         具體包為:data\data\包名\app_tbs_64\core_share\tbs_jars_fusion_dex.jar
(2)TBS使用dex載入器載入了本地jar包的方法,發生泄露的地方具體為com.tencent.tbs.log.TBSLog類的靜態成員變數 List<Abstracte> c
         而且,每次使用 DexLoader都會再次載入,持有一個新的對象,在退出時雖然提供onStop方法將此列表置空,但是列表原持有的對象持有了Activity,導致無指針指向,記憶體泄露。
(3)為什麼不在最開始傳入ApplicationContext呢?因為這個context最終是由TbsReaderView類的openFile方法傳入的,TbsReaderView類涉及UI繪製,要求必須傳入Activity。進一步的,通過CLassLoader載入jar包方法,最終通過DexClassLoader傳給了TBSLog類的記憶體泄露靜態變數。

四、解決方法

(1)獲取TBSLog的靜態成員
(2)在執行它本身的清理方法之前,通過靜態成員反射到記憶體泄露的類
(3)將持有的Activity置空,避免變成無指向的對象

五、程式碼

/**
     * tbs 存在記憶體泄露
     * 發生在tbs sdk內部,而且是tbs從網路載入到本地jar包
     * 存放在:data\data\包名\app_tbs_64\core_share\tbs_jars_fusion_dex.jar
     * 然後使用dex載入器載入的方法
     * 具體為com.tencent.tbs.log.TBSLog類
     * 靜態成員 List<Abstracte> c
     * 而且,每次使用 DexLoader都會再次載入,持有一個新的對象
     * 在退出時雖然提供onStop方法將此列表置空
     * 但是列表原持有的對象持有了Activity,導致無指針指向,記憶體泄露。
     * ?為什麼不在最開始傳入ApplicationContext呢
     * 因為這個context最終是由TbsReaderView類的openFile方法傳入的
     * TbsReaderView類涉及UI繪製,要求必須傳入Activity
     * 再進一步的最終通過DexClassLoader傳給了TBSLog類
     * <p>
     * 解決思路:獲取TBSLog的靜態成員
     * 在執行它本身的清理方法之前
     * 通過靜態成員反射到記憶體泄露的類
     * 將持有的Activity置空
     * 避免變成無指向的對象
     */
    private void clearTbsLogContext() {
        try {
            DexLoader dexLoader = getTbsDexLoader();

            Class<?> tbsLogClass = dexLoader.loadClass("com.tencent.tbs.log.TBSLog");
            Field[] fields = tbsLogClass.getDeclaredFields();
            for (int i = 0; i < fields.length; i++) {
                Field field = fields[i];
                //找靜態對象
                if (Modifier.isStatic(field.getModifiers())) {
                    //是私有變數
                    if (Modifier.isPrivate(field.getModifiers())) {
                        field.setAccessible(true);
                        Object fieldObject = field.get(null);
                        //不直接通過成員名反射獲取對象,是因為
                        //tbs的包使用了混淆
                        //不能保證下載的包的對象名是一致的
                        //使用對象類型來嘗試匹配吧
                        if (fieldObject instanceof List) {
                            List<Object> objectList = (List) fieldObject;
                            for (int j = 0; j < objectList.size(); j++) {
                                Object leakObject = objectList.get(j);
                                if (leakObject != null) {
                                    Field[] leakFields = leakObject.getClass().getDeclaredFields();
                                    for (int m = 0; m < leakFields.length; m++) {
                                        Field leakField = leakFields[m];
                                        //找到持有的context
                                        if (leakField.getType().equals(Context.class)) {
                                            leakField.setAccessible(true);
                                            //釋放持有的context
                                            leakField.set(leakObject, null);
                                            leakField.setAccessible(false);
                                        }
                                    }
                                }
                            }
                        }
                        field.setAccessible(false);
                    }
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * tbsSDK提供了DexLoader對象來載入它下載的包
     * 通過反射直接獲取它生成的對象吧
     * 避免還要根據路徑、包名來載入dex或jar
     *
     * @return DexLoader
     */
    private DexLoader getTbsDexLoader() {
        DexLoader dexLoader = null;
        try {
            Field[] fields = mTbsReaderView.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.getType().equals(ReaderWizard.class)) {
                    field.setAccessible(true);
                    ReaderWizard wizard = (ReaderWizard) field.get(getTbsReaderView());
                    Field[] wizardFields = wizard.getClass().getDeclaredFields();
                    for (Field wizardField : wizardFields) {
                        //匹配到TbsReaderView對象已生成的DexLoader
                        if (wizardField.getType().equals(DexLoader.class)) {
                            wizardField.setAccessible(true);
                            dexLoader = (DexLoader) wizardField.get(wizard);
                            wizardField.setAccessible(false);
                            break;
                        }
                    }
                    field.setAccessible(false);
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return dexLoader;
    }
View Code