一、android WebView 替换方案
1.腾讯X5(推荐)X5内核下载地址
2.Crosswalk(包会打10--20mb
可能导致第三方APP无法开启X5内核的情况)
二、TBS(腾讯浏览服务)的优势
速度快:相比系统webview的网页打开速度有30+%的提升;
省流量:使用云端优化技术使流量节省20+%;
更安全:安全问题可以在24小时内修复;
更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;
兼容好:无系统内核的碎片化问题,更少的兼容性问题;
体验优:支持夜间模式、适屏排版、字体设置等浏览增强功能;
功能全:在Html5、ES6上有更完整支持;
更强大:集成强大的视频播放器,支持视频格式远多于系统webview;
视频和文件格式的支持x5内核多于系统内核
防劫持是x5内核的一大亮点
三、腾讯X5内核的使用
1 首先我们按照官方文档集成 SDK,下载jar包,以及配置权限! 在application里面进行初始化。
具体例子看官方demo,已经很详细了
腾讯 X5官网 http://x5.tencent.com/tbs/
2 MainActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set layout
getWindow().setFormat(PixelFormat.TRANSLUCENT);
setContentView(R.layout.activity_main);
mWebView = (com.tencent.smtt.sdk.WebView) findViewById(R.id.forum_context);
mWebView.getSettings().setJavaScriptEnabled(true);// 支持js
mWebView.getSettings().setUseWideViewPort(true); //自适应屏幕
mWebView.loadUrl("http://res.ky-express.com/h5/video/72.html");
}
3 activity_main
<com.tencent.smtt.sdk.WebView
android:id="@+id/forum_context"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingLeft="5dp"
android:paddingRight="5dp"/>
四、一些注意的问题
1 多个.So库的问题
要把对应的so包导入。如果你的项目当中也用到了其他的.so库,这时你不仅要分包导入。so库,同时还要在gradle里面进行配置!
ndk {
abiFilters"armeabi","armeabi-v7a","x86","mips"
}
2 关于首期启动加载X5内核会出现过慢的问题
可以考虑用线程加载或者服务加载
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
initX5();
preinitX5WebCore();
}
private void initX5() {
QbSdk.initX5Environment(getApplicationContext(), QbSdk.WebviewInitType.FIRSTUSE_AND_PRELOAD, cb);
Log.d("gggbbb","预加载中...");
}
QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() {
@Override
public void onViewInitFinished(boolean arg0) {
// TODO Auto-generated method stub
Log.e("0912", " onViewInitFinished is " + arg0);
}
@Override
public void onCoreInitFinished() {
// TODO Auto-generated method stub
}
};
private void preinitX5WebCore() {
if(!QbSdk.isTbsCoreInited()) {
// preinit只需要调用一次,如果已经完成了初始化,那么就直接构造view
QbSdk.preInit(MainActivity.this, null);// 设置X5初始化完成的回调接口
}
}
Application代码
@Override
public void onCreate() {
super.onCreate();
initX5();
}
private void initX5() {
Intent intent = new Intent(this, PreLoadX5Service.class);
startService(intent);
}
3 我们会发现集成X5后,项目编译变慢了,可以在build.config里面加上下面这段代码试试
dexOptions {
javaMaxHeapSize "4g"
preDexLibraries = false
}
4 视频全屏的时候有个qq浏览器的推广的去掉
设置布局改变的监听:
getWindow().getDecorView().addOnLayoutChangeListener()
监听里面通过 getWindow().getDecorView() 的 findViewsWithText() 方法可以拿到显示推广文案的 TextView,把拿到的 view 设置为 GONE 状态就可以去掉了。
五、webview与x5的测试对比
1、一个链接的第一次的加载webview会比x5速度快一点。
2、如果重复点击一个链接,来回切换,只要次数足够多,X5速度会比webview快。
3、如果点击不同链接,并且次数不是很多,webview会比x5速度快。
另外补充一下webview的基本常识
关于webview的加载
//打开本包内asset目录下的index.html文件
wView.loadUrl(" file:///android_asset/index.html ");
//打开本地sd卡内的index.html文件
wView.loadUrl("content://com.android.htmlfileprovider/sdcard/index.html");
//打开指定URL的html文件
wView.loadUrl(" http://m.oschina.net");
关于js 调用 native
有三种方式
- 第一种方式:通过 addJavascriptInterface 方法进行添加对象映射
mWebView.getSettings().setJavaScriptEnabled(true);
这个函数会有一个警告,因为在特定的版本之下会有非常危险的漏洞,设置完这个属性之后,Native 需要定义一个类:
public class JSObject {
private Context mContext;
public JSObject(Context context) {
mContext = context;
}
@JavascriptInterface
public String showToast(String text) {
Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
return "success";
}
}
...
需要注意的是在 API17 版本之后,需要在被调用的地方加上 @addJavascriptInterface 约束注解,因为不加上注解的方法是没有办法被调用的,JS 代码也很简单:
function showToast(){
var result = myObj.showToast("我是来自web的Toast");
}
- 第二种方式:利用 WebViewClient 接口回调方法拦截 url
需要使用WebViewClient 中 shouldOverrideUrlLoading (WebView view, WebResourceRequest request) 利用这个拦截 url,然后解析这个 url 的协议,如果发现是我们预先约定好的协议就开始解析参数,执行相应的逻辑。
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//假定传入进来的 url = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(url);
String scheme = uri.getScheme();
//如果 scheme 为 js,代表为预先约定的 js 协议
if (scheme.equals("js")) {
//如果 authority 为 openActivity,代表 web 需要打开一个本地的页面
if (uri.getAuthority().equals("openActivity")) {
//解析 web 页面带过来的相关参数
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
}
//代表应用内部处理完成
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
我们看一下 JS 的代码
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}
这个代码执行之后,就会触发本地的 shouldOverrideUrlLoading 方法,然后进行参数解析,调用指定方法。这个方式不会存在第一种提到的漏洞问题,但是它也有一个很繁琐的地方是,如果 web 端想要得到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 JS 方法把返回值传递回去,相关的代码如下:
//java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
//javascript
function returnResult(result){
alert("result is" + result);
}
所以说第二种方式在返回值方面还是很繁琐的,但是在不需要返回值的情况下,比如打开 Native 页面,还是很合适的,制定好相应的协议,就能够让 web 端具有打开所有本地页面的能力了。
- 第三种方式:利用 WebChromeClient 回调接口的三个方法拦截消息
这个方法的原理和第二种方式原理一样,都是拦截相关接口,只是拦截的接口不一样:
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
//假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(message);
String scheme = uri.getScheme();
if (scheme.equals("js")) {
if (uri.getAuthority().equals("openActivity")) {
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
//代表应用内部处理完成
result.confirm("success");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
onJsAlert 方法是弹出警告框,一般情况下在 Android 中为 Toast,在文本里面加入\n就可以换行。onJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击时确认还是取消,true表示点击了确认,false表示点击了取消。onJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
关于native 调用 js
//java
mWebView.loadUrl("javascript:show(" + result + ")");
//javascript
<script type="text/javascript">
function show(result){
alert("result"=result);
return "success";
}
</script>
已知的 WebView 任意代码执行漏洞有 4 个:
- 针对某些特定机型会存在 addJavascriptInterface API 引起的远程代码执行漏洞
- WebView 中内置导出的 “searchBoxJavaBridge_” Java Object 可能被利用,实现远程任意代码。
- WebView 内置导出 “accessibility” 和 “accessibilityTraversal” 两个 Java Object 接口,可被利用实现远程任意代码执行。
判断WebView是否已经滚动到页面底端 或者 顶端:
getScrollY() //方法返回的是当前可见区域的顶端距整个页面顶端的距离,也就是当前内容滚动的距离.
getHeight()或者getBottom() //方法都返回当前WebView这个容器的高度
getContentHeight()返回的是整个html的高度,但并不等同于当前整个页面的高度,因为WebView有缩放功能,所以当前整个页面的高度实际上应该是原始html的高度再乘上缩放比例.因此,更正后的结果,准确的判断方法应该是:
if (webView.getContentHeight() * webView.getScale() == (webView.getHeight() + webView.getScrollY())) {
//已经处于底端
}
if(webView.getScrollY() == 0){
//处于顶端
}
前进、后退
goBack()//后退
goForward()//前进
goBackOrForward(intsteps) //以当前的index为起始点前进或者后退到历史记录中指定的steps,
如果steps为负数则为后退,正数则为前进
canGoForward()//是否可以前进
canGoBack() //是否可以后退
返回键:返回上一次浏览的页面
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
mWebView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
在 WebView 中长按保存图片
//1. 给 WebView添加监听
mWebview.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
}
});
//2、获取点击的图片地址:先获取类型,根据相应的类型来处理对应的数据。
WebView.HitTestResult result = ((WebView) v).getHitTestResult();
int type = result.getType();
//3、获取具体信息,图片这里就是图片地址
String imgurl = result.getExtra();
//4、操作图片(完整代码)
mWebView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
WebView.HitTestResult result = ((WebView)v).getHitTestResult();
if (null == result)
return false;
int type = result.getType();
if (type == WebView.HitTestResult.UNKNOWN_TYPE)
return false;
// 这里可以拦截很多类型,我们只处理图片类型就可以了
switch (type) {
case WebView.HitTestResult.PHONE_TYPE: // 处理拨号
break;
case WebView.HitTestResult.EMAIL_TYPE: // 处理Email
break;
case WebView.HitTestResult.GEO_TYPE: // 地图类型
break;
case WebView.HitTestResult.SRC_ANCHOR_TYPE: // 超链接
break;
case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
break;
case WebView.HitTestResult.IMAGE_TYPE: // 处理长按图片的菜单项
// 获取图片的路径
String saveImgUrl = result.getExtra();
// 跳转到图片详情页,显示图片
Intent i = new Intent(MainActivity.this, ImageActivity.class);
i.putExtra("imgUrl", saveImgUrl);
startActivity(i);
break;
default:
break;
}
}
});
type有这几种类型:
- WebView.HitTestResult.UNKNOWN_TYPE 未知类型
- WebView.HitTestResult.PHONE_TYPE 电话类型
- WebView.HitTestResult.EMAIL_TYPE 电子邮件类型
- WebView.HitTestResult.GEO_TYPE 地图类型
- WebView.HitTestResult.SRC_ANCHOR_TYPE 超链接类型
- WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE 带有链接的图片类型
- WebView.HitTestResult.IMAGE_TYPE 单纯的图片类型
- WebView.HitTestResult.EDIT_TEXT_TYPE 选中的文字类型
webview的封装
public class WebViewActivity extends AppCompatActivity {
private FrameLayout mFrameLayout;
private WebView mWebView;
private MyWebChromeClient mMyWebChromeClient;
private String URL = "http://m.tv.sohu.com/20130704/n380744170.shtml";
@Override
protected void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
mFrameLayout = (FrameLayout) findViewById(R.id.mFrameLayout);
mWebView = (WebView) findViewById(R.id.mWebView);
initWebView();
mWebView.loadUrl(URL);
}
private void initWebView() {
WebSettings settings = mWebView.getSettings();
//设置了这个属性后我们才能在 WebView 里与我们的 Js 代码进行交互
settings.setJavaScriptEnabled(true);
//WebView 是否支持多窗口,如果设置为 true,需要重写
//WebChromeClient#onCreateWindow(WebView, boolean, boolean, Message) 函数,默认为 false
//settings.setSupportMultipleWindows(true);
//显示WebView提供的缩放控件
settings.setDisplayZoomControls(false);
settings.setBuiltInZoomControls(true);
//设置页面是否支持缩放
webSettings.setSupportZoom(true);
//设置文本的缩放倍数,默认为 100
//webSettings.setTextZoom(2);
//打开 WebView 的 storage 功能,这样 JS 的 localStorage,sessionStorage 对象才可以使用
//settings .setDomStorageEnabled(true);
//打开 WebView 的 LBS 功能,这样 JS 的 geolocation 对象才可以使用
//settings.setGeolocationEnabled(true);
// settings.setGeolocationDatabasePath("");
//设置是否打开 WebView 表单数据的保存功能
//settings.setSaveFormData(true);
//设置 WebView 的默认 userAgent 字符串
//settings.setUserAgentString("");
//设置 WebView 的字体,可以通过这个函数,改变 WebView 的字体,默认字体为 "sans-serif"
//settings.setStandardFontFamily("");
//设置 WebView 字体的大小,默认大小为 16
//settings.setDefaultFontSize(20);
//设置 WebView 支持的最小字体大小,默认为 8
//settings.setMinimumFontSize(12);
//设置 JS 是否可以打开 WebView 新窗口
settings.setJavaScriptCanOpenWindowsAutomatically(true);
//被这个 tag 声明的宽度将会被使用,如果页面没有这个 tag 或者没有提供一个宽度,那么一个宽型 viewport 将会被使用。
settings.setUseWideViewPort(true);
settings.setPluginState(WebSettings.PluginState.ON);
settings.setAllowFileAccess(true);
settings.setLoadWithOverviewMode(true);
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
mMyWebChromeClient = new MyWebChromeClient();
mWebView.setWebChromeClient(mMyWebChromeClient);
//WebViewClient主要辅助WebView执行处理各种响应请求事件的
mWebView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
}
});
}
//WebChromeClient 主要辅助 WebView 处理J avaScript 的对话框、网站 Logo、网站 title、load 进度等处理
private class MyWebChromeClient extends WebChromeClient {
private View mCustomView;
private CustomViewCallback mCustomViewCallback;
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
super.onShowCustomView(view, callback);
if (mCustomView != null) {
callback.onCustomViewHidden();
return;
}
mCustomView = view;
mFrameLayout.addView(mCustomView);
mCustomViewCallback = callback;
mWebView.setVisibility(View.GONE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
public void onHideCustomView() {
mWebView.setVisibility(View.VISIBLE);
if (mCustomView == null) {
return;
}
mCustomView.setVisibility(View.GONE);
mFrameLayout.removeView(mCustomView);
mCustomViewCallback.onCustomViewHidden();
mCustomView = null;
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
super.onHideCustomView();
}
}
@Override
public void onConfigurationChanged(Configuration config) {
super.onConfigurationChanged(config);
switch (config.orientation) {
case Configuration.ORIENTATION_LANDSCAPE:
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
break;
case Configuration.ORIENTATION_PORTRAIT:
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
break;
}
}
@Override
public void onPause() {
super.onPause();
mWebView.onPause();
}
@Override
public void onResume() {
super.onResume();
mWebView.onResume();
}
@Override
public void onBackPressed() {
if (mWebView.canGoBack()) {
mWebView.goBack();
return;
}
super.onBackPressed();
}
@Override
public void onDestroy() {
super.onDestroy();
mWebView.destroy();
}
}
一个完整的Html5Activity
https://github.com/Wing-Li/Html5WebView/tree/master
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.KeyEvent;
import android.webkit.GeolocationPermissions;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.lyl.test.R;
public class Html5Activity extends AppCompatActivity {
private String mUrl;
private LinearLayout mLayout;
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web);
Bundle bundle = getIntent().getBundleExtra("bundle");
mUrl = bundle.getString("url");
Log.d("Url:", mUrl);
mLayout = (LinearLayout) findViewById(R.id.web_layout);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);
mLayout.addView(mWebView);
WebSettings mWebSettings = mWebView.getSettings();
mWebSettings.setSupportZoom(true);
mWebSettings.setLoadWithOverviewMode(true);
mWebSettings.setUseWideViewPort(true);
mWebSettings.setDefaultTextEncodingName("utf-8");
mWebSettings.setLoadsImagesAutomatically(true);
//调用JS方法.安卓版本大于17,加上注解 @JavascriptInterface
mWebSettings.setJavaScriptEnabled(true);
saveData(mWebSettings);
newWin(mWebSettings);
mWebView.setWebChromeClient(webChromeClient);
mWebView.setWebViewClient(webViewClient);
mWebView.loadUrl(mUrl);
}
@Override
public void onPause() {
super.onPause();
webView.onPause();
webView.pauseTimers(); //小心这个!!!暂停整个 WebView 所有布局、解析、JS。
}
@Override
public void onResume() {
super.onResume();
webView.onResume();
webView.resumeTimers();
}
/**
* 多窗口的问题
*/
private void newWin(WebSettings mWebSettings) {
//html中的_bank标签就是新建窗口打开,有时会打不开,需要加以下
//然后 复写 WebChromeClient的onCreateWindow方法
mWebSettings.setSupportMultipleWindows(false);
mWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);
}
/**
* HTML5数据存储
*/
private void saveData(WebSettings mWebSettings) {
//有时候网页需要自己保存一些关键数据,Android WebView 需要自己设置
mWebSettings.setDomStorageEnabled(true);
mWebSettings.setDatabaseEnabled(true);
mWebSettings.setAppCacheEnabled(true);
String appCachePath = getApplicationContext().getCacheDir().getAbsolutePath();
mWebSettings.setAppCachePath(appCachePath);
}
WebViewClient webViewClient = new WebViewClient(){
/**
* 多页面在同一个WebView中打开,就是不新建activity或者调用系统浏览器打开
*/
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
};
WebChromeClient webChromeClient = new WebChromeClient() {
//=========HTML5定位==========================================================
//需要先加入权限
//<uses-permission android:name="android.permission.INTERNET"/>
//<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
//<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
super.onReceivedIcon(view, icon);
}
@Override
public void onGeolocationPermissionsHidePrompt() {
super.onGeolocationPermissionsHidePrompt();
}
@Override
public void onGeolocationPermissionsShowPrompt(final String origin, final GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);//注意个函数,第二个参数就是是否同意定位权限,第三个是是否希望内核记住
super.onGeolocationPermissionsShowPrompt(origin, callback);
}
//=========HTML5定位==========================================================
//=========多窗口的问题==========================================================
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(view);
resultMsg.sendToTarget();
return true;
}
//=========多窗口的问题==========================================================
};
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
mWebView.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mWebView != null) {
mWebView.clearHistory();
((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.loadUrl("about:blank");
mWebView.stopLoading();
mWebView.setWebChromeClient(null);
mWebView.setWebViewClient(null);
mWebView.destroy();
mWebView = null;
}
}
}
App通过调用外部浏览器打开网页
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);//"android.intent.action.VIEW"
Uri content_url = Uri.parse("www.ycxc.com");
intent.setData(content_url);
//方案一
//startActivity(Intent.createChooser(intent, "请选择浏览器"));
//方案二
/*if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}*/
//方案三
intent.setClassName("com.android.browser","com.android.browser.BrowserActivity");
startActivity(intent);