来说说可恶的腾讯 X5 内核,记录下今天问题的排查,定位,和修复过程。

背景

Android 项目中的 WebView 集成了腾讯的 X5 内核,由于 X5 在展示方面的兼容性问题深受前端们的喜爱(我能说,他们的 H5 页面的样式兼容问题,全部推锅到移动开发吗, 除了最新版本的 Chrome 内核,是否可以考虑下别的浏览器…)。

然而,就在今天,一个不幸的上午,捕获到了如下异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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, 别问我怎么知道的…

于是乎诞生了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
项目中 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 方法中做手脚,此方法是作甚的呢?

1
2
3
4
5
6
7
8
/**
* 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 率飙升了…唉