最近在维护一个项目同时要兼容Android4.4和Android5.0两种机型,在调试Android5.0的时候多次因为WebView而造成程序崩溃。在项目完成之后,就来总结一下WebView的兼容性问题。
1. All WebView methods must be called on the same thread.
在Android5.0,WebView添加了线程检测,要求WebView的所有方法必须在相同的线程中被调用。当然如果使用WebView比较简单的情况下,WebView的参数设置和网页的加载在同一个方法里,就不会遇到这些问题。但是使用WebView承载太多的功能就会将问题暴露出来。
使用场景介绍:一份试卷,一道大题中有n个小题【以html的形式展示,通过Js调用小题点击事件】,点击小题的编号创建一个Fragment,在Fragment中向服务器请求数据,然后将数据组装成Html文件,然后通过WebView显示出来。下面我们贴一下相关代码:
protected void initViews() {
if (examinationId == null || examCategoryId == null || questionId == null) {
showNoDataView();
return;
}
onlineTestRest.setRootUrl(application.serverInfo.getIpAddressAndPort());
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mQuestionWebView = new WebView(getActivity());
mQuestionWebView.setLayoutParams(layoutParams);
mQuestionWebView.getSettings().setJavaScriptEnabled(true);
mQuestionWebView.getSettings().setPluginState(PluginState.ON);
mQuestionWebView.enableSlowWholeDocumentDraw();
// View级别 硬件加速 setlayertype
// 您可以在运行时用以下的代码关闭单个view的硬件加速:
// 注:您不能在view级别开启硬件加速
mQuestionWebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Log.d(TAG, "oncreate"+Thread.currentThread().getName()+"|"+Thread.currentThread().getId());
mQuestionWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.d(TAG,"onPageFinishe"+Thread.currentThread().getName()+"|"+Thread.currentThread().getId());
showContentView();
Log.d(TAG, "onPageFinished");
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// TODO Auto-generated method stub
super.onPageStarted(view, url, favicon);
Log.d(TAG, "onPageStarted"+url+" | "+view);
}
});
mQuestionWebView.addJavascriptInterface(new ExamWebViewJsInterface(getActivity()), "contact");
question_content_Linear.addView(mQuestionWebView);
initHtmlContent(examinationId, examCategoryId, questionId);
mCorrectRateTextView.setText(String.format(getString(R.string.exam_question_correct_rate),
questionCorrectStatistics.getNumber(),
(int) (questionCorrectStatistics.getCorrectCount() * 100 / questionCorrectStatistics.getStudentCount())));
}
@Background
protected void initHtmlContent(String examinationId, String examCategoryId, String questionId) {
String result = "";
// 访问网络
try {
result = onlineTestRest.queryClassRoomExplainJson(examinationId, examCategoryId, questionId);
// LogUtil.logCatDebug(getClass(), "result=" + result);
} catch (Exception e) {
LogUtil.logCatDebug(getClass(), e.toString());
}
parseExamHtml(result);
}
private void parseExamHtml(String result) {
// 网络异常
if (StringUtil.isNullOrEmpty(result)) {
showNoDataView();
return;
}
// 解析数据
try {
exmUtil = new ReadExmUtil(getActivity(), result, application.serverInfo.getIpAddressAndPort(),false);
Integer index=questionCorrectStatistics.getNumber();
if(index!=null) index-=1;
QuestionUtils.setQuestionIndex(exmUtil, index);
exmUtil.handleExamList();
//TODO
} catch (Exception e) {
showNoDataView();
return;
}
allContent = EducExamFactory.createGroupApi(StaticUtil.EDUC_ONEQUESTION_EXPLAIN,
application.serverInfo.getIpAddressAndPort()).execute(getActivity(), exmUtil);
savedPath = FileUtil.saveHtmlFile(allContent,
String.format("education.teacher_exam_preview_%s", examinationId), StaticUtil.OBJTESTHTML_DIR);
if (savedPath == null) {
showNoDataView();
return;
}
initWebViewContent();
Log.d(TAG,"parseExamHtml"+ Thread.currentThread().getName()+"|"+Thread.currentThread().getId());
}
protected void initWebViewContent() {
myHandle.sendEmptyMessage(100);
}
Handler myHandle = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 100:
Log.d(TAG,"handleMessage"+ Thread.currentThread().getName()+"|"+Thread.currentThread().getId());
mQuestionWebView.loadUrl("file://" + savedPath);
break;
default:
break;
}
};
};
我们要分析几个关键方法, initViews
和handleMessage
中涉及到WebView方法的调用,initViews
、handleMessage
和initHtmlContent
中涉及到线程的变换,initViews
运行在onCreate()
中,即在main线程中;initHtmlContent
要进行数据的获取和html组装等耗时操作,所以要放在新建一个后台线程;myhandle
是在Fragment中新建的,线程应该和Fragemnt一致。所以我们要保证
我们看一下打印的Log信息:
02-08 13:33:06.558: D/QuestionDetailListFragment(31497): oncreate main|1
02-08 13:33:06.694: D/QuestionDetailListFragment(31497): handleMessage main|1
02-08 13:33:06.694: D/QuestionDetailListFragment(31497): parseExamHtml pool-2-thread-1|1872
02-08 13:33:07.189: D/QuestionDetailListFragment(31497): onPageFinishe main|1
02-08 13:33:23.583: D/QuestionDetailListFragment(31497): oncreate main|1
02-08 13:33:23.664: D/QuestionDetailListFragment(31497): parseExamHtml pool-2-thread-1|1872
02-08 13:33:23.664: D/QuestionDetailListFragment(31497): handleMessage JavaBridge|1915
首次加载的时候oncreate
和handleMessage
在同一个线程(main)中,在第二次加载的时候 oncreate
和handleMessage
在不同线程,就会出现下面的异常信息。JavaBridge
线程是WebView通过Js调用Android方法新开的线程,而不是在UI线程中,所以会报异常。
E/QuestionDetailListFragment_(23794): A runtime exception was thrown while executing code in a runnable
E/QuestionDetailListFragment_(23794): java.lang.RuntimeException: java.lang.Throwable: A WebView method was called on thread 'JavaBridge'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {2912b0ee} called on Looper (JavaBridge, tid 1915) {2c3f1fdc}, FYI main Looper is Looper (main, tid 1) {2912b0ee})
解决方案就是让mQuestionWebView.loadUrl("file://" + savedPath);
也运行在UI线程就可以了,因为onCreate()
一直运行在UI线程中,这样就保持了线程的统一。下面直接贴代码:
protected void initWebViewContent() {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mQuestionWebView.loadUrl("file://" + savedPath);
}
});
}
总结:
1.WebView调用JS会重新开启一个线程,而不是在UI线程中;
2.Handle不一定都在UI线程中,而是根据Looper来确定;
3.WebView在Android5.0之后会要求所有的方法都必须在同一线程中调用;
4.如果遇到线程不一致的情况下,先跟踪WebView每个方法的调用线程,然后将线程统一。