騰訊tbs 記憶體泄露
- 2022 年 5 月 7 日
- 筆記
- Android基礎知識
一、背景
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; }

