來說說可惡的騰訊 X5 核心,記錄下今天問題的排查,定位,和修復過程。
背景
Android 專案中的 WebView 整合了騰訊的 X5 核心,由於 X5 在展示方面的相容性問題深受前端們的喜愛(我能說,他們的 H5 頁面的樣式相容問題,全部推鍋到移動開發嗎, 除了最新版本的 Chrome 核心,是否可以考慮下別的瀏覽器...)。
然而,就在今天,一個不幸的上午,捕獲到了如下異常:
UncaughtException detected: android.os.FileUriExposedException: file:///storage/emulated/0/DCIM/Camera/1536649175379.jpg exposed beyond app through ClipData.Item.getUri()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1958)
at android.net.Uri.checkFileUriExposed(Uri.java:2356)
at android.content.ClipData.prepareToLeaveProcess(ClipData.java:941)
at android.content.Intent.prepareToLeaveProcess(Intent.java:9747)
at android.content.Intent.prepareToLeaveProcess(Intent.java:9732)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1611)
at android.app.Activity.startActivityForResult(Activity.java:4536)
at android.support.v4.app.BaseFragmentActivityApi16.startActivityForResult(BaseFragmentActivityApi16.java:54)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:65)
at android.app.Activity.startActivityForResult(Activity.java:4494)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:711)
at android.app.Activity.startActivity(Activity.java:4855)
at android.app.Activity.startActivity(Activity.java:4823)
at android.content.ContextWrapper.startActivity(ContextWrapper.java:376)
at org.chromium.android_webview.ResourcesContextWrapperFactory$WebViewContextWrapper.startActivity(Unknown Source:11)
at com.tencent.tbs.core.partner.b.a$2.onClick(Unknown Source:406)
at android.view.View.performClick(View.java:6266)
at android.view.View$PerformClick.run(View.java:24730)
at android.os.Handler.handleCallback(Handler.java:789)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:171)
at android.app.ActivityThread.main(ActivityThread.java:6672)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:246)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
複製程式碼
一回頭
幸運的是找到了出現問題的呼叫: <input type="file" acctType="image/*"/>
。
上述標籤,最終會呼叫到 WebChromeClient 的 onShowFileChooser
方法(不同 Android 版本有所差異,>= 5.0 是此方法),然而,經過測試,這次崩潰並沒有呼叫到此處(一臉懵逼)。
乍一看是跨 APP 檔案共享導致的問題,恰好最近專案剛把 targetSdkVersion
從 22 升級到 26,也就是必須要相容 Android 6.0 的動態許可權以及 7.0 的跨應用檔案共享。
但是,仔細分析堆疊資訊,沒有發現專案主動呼叫的程式碼,可疑點鎖定到這一行 at com.tencent.tbs.core.partner.b.a$2.onClick(Unknown Source:406)
。但是下載下來的
騰訊核心 jar 檔案中並沒有包含 com.tencent.tbs.core.partner.**
,由於 X5 核心只提供了輕量級的 jar 檔案,實際核心的下載和更新是 APP 安裝後動態進行的,於是乎去了
/data/data/{packageUd}/app_tbs
, 並且把所有檔案都導到電腦上,裡面主要包含了包含資原始檔的 apk, .so 檔案以及一個 dex 檔案,經過 dex2jar 這些操作後,仍然沒有找到可疑的類檔案。
二回頭
上一條路被堵死。
很幸運,恰好今天幫測試解決 UI Automator
的問題,順道也用了一下, 獲得如下佈局:
emmm, 出問題的關鍵就是點選拍照,專案中沒有主動顯示這種樣式的彈窗,因此可能是系統做了攔截處理,或者就是 X5。
換了幾臺不同廠商的測試機實驗,樣式都是一樣,基本排除是系統攔截處理的鍋。最可疑的就是 X5 了。
中午還去了浜燒市場吃了頓飯,心好大....
三回頭
基本鎖定問題後,就開始各種預先申請許可權,StrictMode 上折騰,試圖解決許可權問題,無果。
但每次 APP 崩潰幾次後,再次呼叫,發現又會呼叫到 WebChromeClient 的 onShowFileChooser
方法,由於我們自己做過許可權處理,一切又恢復正常。
(後來發現是 X5 發現崩潰後,降級邏輯)。
四回頭
測試發現,出問題的點只是拍照這一個地方,可惡的騰訊 X5 核心並沒有做相容 7.0 的邏輯處理,並且惡意攔截 input file 標籤,美美的彈出自己的檔案選擇框。 相容都沒有做好,有碧蓮彈窗。。。。佩服!!!
五回頭
不知怎麼滴靈光一現,就想如果我們去掉拍照這個按鈕,問題不就解決了嗎?分析佈局,目測是 X5 在 WebView 後面動態 add 了一個 android.widget.LinearLayout
, 別問我怎麼知道的...
於是乎誕生了如下程式碼:
專案中 WebView.java
LinearLayout fuckTbsLayout;
List<TextView> fuckTextViews;
@Override
public void onViewAdded(View child) {
if (child.getClass().getName().startsWith("com.tencent.tbs.")
&&
child instanceof LinearLayout
&&
((LinearLayout) child).getChildCount() > 0
) {
fuckTbsLayout = (LinearLayout) child;
TextView fuckItem;
if (fuckTbsLayout.getChildAt(0) instanceof TextView) {
fuckItem = (TextView) fuckTbsLayout.getChildAt(0);
String fuckTitle = fuckItem.getText().toString();
if (fuckTitle.contains("請選擇上傳方式") || fuckTitle.contains("相簿") || fuckTitle.contains("拍照") || fuckTitle.contains("其它方式")) {
fuckTextViews = new ArrayList<>();
for (int i = 0; i < fuckTbsLayout.getChildCount(); i++) {
TextView fuckTextView = null;
if (fuckTbsLayout.getChildAt(i) instanceof TextView) {
fuckTextView = (TextView) fuckTbsLayout.getChildAt(i);
}
if (fuckTextView != null && fuckTextView.getText().toString().trim().contains("拍照")) {
fuckTextViews.add(fuckTextView);
}
}
if (fuckTextViews != null && fuckTextViews.size() > 0) {
for (TextView todoRemoveFuckTextView : fuckTextViews) {
fuckTbsLayout.removeView(todoRemoveFuckTextView);
}
}
fuckTextViews = null;
}
}
}
super.onViewAdded(child);
}
//sorry for the F words.
複製程式碼
主要思想就是在 WebView 的 onViewAdded 方法中做手腳,此方法是作甚的呢?
/**
* Called when a new child is added to this ViewGroup. Overrides should always
* call super.onViewAdded.
*
* @param child the added child view
*/
public void onViewAdded(View child) {
}
複製程式碼
非常通俗易懂,當 X5 偷偷的去動態 addView 的時候,所在的父元件此方法必定會被呼叫,只要過濾一下,記錄下來拍照的所屬的 View,然後從父元件上呼叫 removeView 移除掉就好了。
再回頭脖子就要斷了
上述問題,曲線救國得以解決。
方案的缺點
- 髒
- 髒
- 髒
- TextView 變化,文案變化,會再次失效
尾巴
-
在問題排查以及解決過程中,騰訊的 X5 文件,以及論壇,QQ 群形同虛設,幾乎沒有什麼有價值的線索,狂吐槽。
-
X5 估計自己都沒有做相容測試,就動態下發錯誤的邏輯程式碼,屬實是狂傲。
-
X5 檢測應用崩潰後,降級邏輯總算是有點良心,降低對使用者的影響,但是我們的 crash 率飆升了...唉