现在的HybridApp也就是混合开发的APP越来越普遍了,其原理就是在一个APP中嵌入一个webview,然后访问我们的html5页面,而html5页面这部分内容,在我们APP不升级的情况下能动态的更新,并且具有跨平台性,也就是说Android和IOS都可以用这一个html页面,从而减小了开发成本。现在很多APP都是混合开发的了,比如淘宝,京东,还有很多银行的APP,中国移动APP等。
优点:减小开发成本,缩短开发周期,具有跨平台性。
缺点:性能不如原生的好
现在也有混合开发的架构,比如React Native,阿里的Weex,但学习成本都太高,一个项目需要混合开发,一般都需要团队的人都要懂React Native。本篇主要讲的是基于x5内核的webview的混合开发基础。
因为用原生的webview有许多问题,比如对html5的兼容性较差,内存泄漏等问题,所有就用了腾讯的x5内核的webview。
X5内核的优势
1) 速度快:相比系统webview的网页打开速度有30+%的提升;
2) 省流量:使用云端优化技术使流量节省20+%;
3) 更安全:安全问题可以在24小时内修复;
4) 更稳定:经过亿级用户的使用考验,CRASH率低于0.15%;
5) 兼容好:无系统内核的碎片化问题,更少的兼容性问题;
6) 体验优:支持夜间模式、适屏排版、字体设置等浏览增强功能;
7) 功能全:在Html5、ES6上有更完整支持;
8) 更强大:集成强大的视频播放器,支持视频格式远多于系统webview;
9) 视频和文件格式的支持x5内核多于系统内核
10) 防劫持是x5内核的一大亮点
下面就讲解从接入SDK到webview的基本用法,最后再到安卓和JS的通信实现混合开发。
1.下载SDK jar包放入到工程中的libs目录下, 并右键jar包,点击Add As Library,下载地址:https://x5.tencent.com/tbs/sdk.html
2.x5暂时不提供64位so文件,为了保证64位手机能正常加载x5内核,请参照如下链接修改相关配置https://x5.tencent.com/tbs/technical.html#/detail/sdk/1/34cf1488-7dc2-41ca-a77f-0014112bcab7
3.AndroidManifest.xml里加入权限声明
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
1.初始化x5内核,一般可在application的onCreate方法中,或者activity的onCreate的方法中初始化。
执行QbSdk.initX5Environment()方法来初始化内核。第一个参数是context,第二个参数是callback,这个函数内部是异步的,当初始化完成后就回调里面的2个方法。启动APP的时候,会去检查手机中是否有可用的X5内核,如果没有就在后台下载,大概23MB,这个时候就会相对有点久。QbSdk.isTbsCoreInited()用来判断x5内核是否已经加载了,因为有时候你的app可能finish掉了,但是进程还在,此时x5内核也还在,再次启动app的时候就不需要初始化x5内核了
/**
* 初始化x5内核并加载
*/
private void initX5() {
//QbSdk.isTbsCoreInited()用来判断x5内核是否已经加载了
if (QbSdk.isTbsCoreInited()) {
//如果已经加载
Log.d(TAG, "QbSdk.isTbsCoreInited: true 已经加载x5内核");
progressBar.setVisibility(View.INVISIBLE);
textview.setText("x5内核初始化成功");
initListener();
} else {
//还没加载,就要初始化内核并加载
Log.d(TAG, "QbSdk.isTbsCoreInited: false 还没加载x5内核");
//初始化x5内核
QbSdk.initX5Environment(this, new QbSdk.PreInitCallback() {
@Override
public void onCoreInitFinished() {
}
@Override
public void onViewInitFinished(boolean b) {
progressBar.setVisibility(View.INVISIBLE);
Log.d(TAG, "onViewInitFinished: x5内核初始化:" + b);
if (b == true) {
textview.setText("x5内核初始化成功");
initListener();
} else {
textview.setText("x5内核初始化失败");
}
}
});
}
}
2.动态创建webview。一般我们需要动态创建webview,因为当我们x5内核还没初始化完的时候,我们就创建了webview,此时的webview就是系统的,我们想保证只用x5内核的,不用系统的,就要在x5内核初始化完成后再去创建。
//拿到父布局
LinearLayout linearlayout = (LinearLayout) findViewById(R.id.linearlayout);
//创建一个webview,并放到父布局中
mWebView = new WebView(this);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
linearlayout.addView(mWebView, layoutParams);
注意,这里实例化的Webview的导入的包是com.tencent.smtt.sdk
3.对WebView的生命周期处理
@Override
protected void onResume() {
//激活WebView为活跃状态,能正常执行网页的响应
mWebView.onResume();
super.onResume();
}
@Override
protected void onPause() {
//当页面被失去焦点被切换到后台不可见状态,需要执行onPause
//通过onPause动作通知内核暂停所有的动作,比如DOM的解析、plugin的执行、JavaScript执行。
mWebView.onPause();
super.onPause();
}
@Override
protected void onDestroy() {
if (mWebView != null) {
//先让webview加载null内容
mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebView.clearHistory();
//父布局移除webview
((ViewGroup) mWebView.getParent()).removeView(mWebView);
//最后webview在销毁
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}
//方式1. 加载一个网页:
webView.loadUrl("http://www.baidu.com/");
//方式2:加载apk包中的html页面
webView.loadUrl("file:///android_asset/index.html");
//方式3:加载手机本地的html页面
webView.loadUrl("content://com.example.fu.x5hybriddemo/sdcard/index.html");
//激活WebView为活跃状态,能正常执行网页的响应
webView.onResume() ;
//当页面被失去焦点被切换到后台不可见状态,需要执行onPause
//通过onPause动作通知内核暂停所有的动作,比如DOM的解析、plugin的执行、JavaScript执行。
webView.onPause();
//当应用程序(存在webview)被切换到后台时,这个方法不仅仅针对当前的webview而是全局的全应用程序的webview
//它会暂停所有webview的layout,parsing,javascripttimer。降低CPU功耗。
webView.pauseTimers()
//恢复pauseTimers状态
webView.resumeTimers();
//销毁Webview
//在关闭了Activity时,如果Webview的音乐或视频,还在播放。就必须销毁Webview
//但是注意:webview调用destory时,webview仍绑定在Activity上
//这是由于自定义webview构建时传入了该Activity的context对象
//因此需要先从父容器中移除webview,然后再销毁webview:
rootLayout.removeView(webView);
webView.destroy();
//判断是否可以后退网页,但是不是后退网页的动作,后退网页的动作是mWebview.goBack()
//这个方法的返回值是boolean,当不是第一个页面的时候,就是又点进其他页面,调用的话就返回true,如果就是在第一个页面就返回false
Webview.canGoBack()
//后退网页
Webview.goBack()
//是否可以前进
Webview.canGoForward()
//前进网页
Webview.goForward()
//以当前的index为起始点前进或者后退到历史记录中指定的steps
//如果steps为负数则为后退,正数则为前进
Webview.goBackOrForward(intsteps)
我们不对Back键做处理的话,按下Back键会把activity给finish掉,以至于WebView也destroy了,但通常情况下我们希望Back键是网页后退,当是首页的时候,我们再按Back键就finish掉了activity,只需这样实现。
@Override
public void onBackPressed() {
//判断是否可以后退网页,但是不是后退网页的动作,后退网页的动作是mWebview.goBack()
//这个方法的返回值是boolean,当不是第一个页面的时候,
//就是又点进其他页面,调用的话就返回true,如果就是在第一个页面就返回false
if (mWebView.canGoBack()) {
mWebView.goBack(); //后退网页
} else {
super.onBackPressed();
}
}
//清除网页访问留下的缓存
//由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序.
Webview.clearCache(true);
//清除当前webview访问的历史记录
//只会webview访问历史记录里的所有记录除了当前访问记录
Webview.clearHistory();
//这个api仅仅清除自动完成填充的表单数据,并不会清除WebView存储到本地的数据
Webview.clearFormData();
//声明WebSettings子类
WebSettings webSettings = webView.getSettings();
//如果访问的页面中要与Javascript交互,则webview必须设置支持Javascript
webSettings.setJavaScriptEnabled(true);
// 若加载的 html 里有JS 在执行动画等操作,会造成资源浪费(CPU、电量)
// 在 onStop 和 onResume 里分别把 setJavaScriptEnabled() 给设置成 false 和 true 即可
//支持插件
webSettings.setPluginsEnabled(true);
//设置自适应屏幕,两者合用
webSettings.setUseWideViewPort(true); //将图片调整到适合webview的大小
webSettings.setLoadWithOverviewMode(true); // 缩放至屏幕的大小
//缩放操作
webSettings.setSupportZoom(true); //支持缩放,默认为true。是下面那个的前提。
webSettings.setBuiltInZoomControls(true); //设置内置的缩放控件。若为false,则该WebView不可缩放
webSettings.setDisplayZoomControls(false); //隐藏原生的缩放控件
//其他细节操作
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); //关闭webview中缓存
webSettings.setAllowFileAccess(true); //设置可以访问文件
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口
webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片
webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式
//优先使用缓存:
WebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
//缓存模式如下:
//LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
//LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取数据。
//LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
//LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。
//不使用缓存:
WebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
结合使用(离线加载)
if (NetStatusUtil.isConnected(getApplicationContext())) {
//根据cache-control决定是否从网络上取数据。
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
//没网,则从本地获取,即离线加载
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}
// 开启 DOM storage API 功能
webSettings.setDomStorageEnabled(true);
//开启 database storage API 功能
webSettings.setDatabaseEnabled(true);
//开启 Application Caches 功能
webSettings.setAppCacheEnabled(true);
String cacheDirPath = getFilesDir().getAbsolutePath() + APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //设置 Application Caches 缓存目录
注意: 每个 Application 只调用一次 WebSettings.setAppCachePath(),WebSettings.setAppCacheMaxSize()。
/**
* 作用:处理各种通知 & 请求事件
*/
mWebView.setWebViewClient(new WebViewClient() {
//复写shouldOverrideUrlLoading会使打开网页不调用系统浏览器,就显示在本webview中
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
//这里回调的url就是你点击的页面的链接,会传到这里来
webView.loadUrl(url);
return true; //返回true,拦截url
}
//加载页面资源的时候会调用,比如图片,加载一个就调用一次
@Override
public void onLoadResource(WebView webView, String url) {
super.onLoadResource(webView, url);
// Log.d(TAG, "onLoadResource: ");
}
//加载的页面服务器出现错误的时候调用,如404
//一般这个方法用在比如出现404的错误,显示系统自带的提示错误的页面会很low,这时我们就可以加载自己的页面
@Override
public void onReceivedError(WebView webView, int errorCode, String description, String failingUrl) {
super.onReceivedError(webView, errorCode, description, failingUrl);
}
//webView默认是不处理https请求的,页面显示空白,需要进行如下设置
@Override
public void onReceivedSslError(WebView webView, SslErrorHandler handler, SslError error) {
handler.proceed(); //表示等待证书响应
Log.d(TAG, "onReceivedSslError: ");
// super.onReceivedSslError(webView, handler, error);
}
//页面开始加载的时候调用
@Override
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
super.onPageStarted(webView, url, favicon);
Log.d(TAG, "onPageStarted: 开始");
}
//页面加载完成的时候调用
@Override
public void onPageFinished(WebView webView, String url) {
super.onPageFinished(webView, url);
Log.d(TAG, "onPageFinished: 完成");
}
});
/**
* 作用:辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等。
* 要设置了这个WebChromeClient才能显示js的弹窗,如alert()
*/
mWebView.setWebChromeClient(new WebChromeClient() {
//获得网页的加载进度并显示
@Override
public void onProgressChanged(WebView webView, int newProgress) {
String progress = newProgress + "%";
Log.d(TAG, "progress:" + progress);
}
//获取Web页面中的标题
@Override
public void onReceivedTitle(WebView webView, String title) {
Log.d(TAG, "onReceivedTitle: 标题:" + title);
}
/**
* 当页面中触发了alert(),confirm(),prompt()这三个方法,就会对应回调到下面三个方法中
* 相当于js调用了安卓的方法,这也算是一种安卓和js的通信,
* 复写了下面这3方法并且返回true就不会显示页面的弹窗了,而且一定要有确认或者取消的操作,不然会卡死
* 但能拿到弹出里的所有信息,可以自己写原生弹窗等,如alert,toast等,很灵活
*
*/
@Override
public boolean onJsAlert(WebView webView, String url, String message, JsResult result) {
Log.d(TAG, "onJsAlert: message:" + message);
//这里我把js中的alert()变成安卓中的toast
Toast.makeText(WebViewTestActivity.this, "alert变成toast啦", Toast.LENGTH_SHORT).show();
result.confirm(); //确认
return true;
}
@Override
public boolean onJsConfirm(WebView webView, String url, String message, final JsResult result) {
Log.d(TAG, "onJsConfirm: message:" + message);
//这里我把js中的confirm()变成安卓中的dialog
new AlertDialog.Builder(WebViewTestActivity.this)
.setTitle("js的confirm变成安卓的dialog啦")
.setMessage(message)
.setPositiveButton("确认", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
result.confirm();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
result.cancel();
}
})
.setCancelable(false)
.show();
return true;
}
@Override
public boolean onJsPrompt(WebView webView, String url, String message, String defalutValue, JsPromptResult result) {
Log.d(TAG, "onJsPrompt: message:" + message);
Log.d(TAG, "onJsPrompt: defalutValue:" + message);
//这里prompt的确认是要传参数的,这个参数会传到js中去,所以这个方法能实现安卓和js的相互通信
result.confirm("啦啦啦啦,我是安卓原生来的内容");
return true;
}
});
WebView与JavaScript的交互其实就是我们需要的Android和JS的交互,交互其实就是
Android调用JS的方法有2种:
JS调用Android的方法有3种:
方式一:通过WebView的loadUrl()方法
- html页面
<html>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<title>simpletitle>
<script>
function callJS(){
alert("我是被安卓调用的");
document.getElementById("p").innerHTML = "我被安卓调用了"
}
script>
head>
<body>
<h4>Android调用JS方式一h4>
<p id="p">p>
body>
html>
//android调用js方式一
//此方式一定要在onPageFinished之后调用才有用
mWebView.loadUrl("javascript:callJS()"); //调用当前页面的javascript中的callJS()方法
方式二: 通过WebView的evaluateJavascript()方法
//android调用js方式二,这个方法能接受js方法的返回值
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
//此处为js返回的结果,value为js函数的返回值,没有返回值就为null
Log.i(TAG, "onReceiveValue: vlue:" + value);
Toast.makeText(AndroidCallJs2Activity.this, value, Toast.LENGTH_SHORT).show();
}
});
方式一: 通过WebView的addJavascriptInterface()进行对象映射
<html>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<title>simpletitle>
<script>
function callAndroid() {
androidObject.hello("啦啦啦啦,我是html页面通过js来的内容");
}
script>
head>
<body>
<h4>JS调用Android方式一h4>
<input type="button" onclick="callAndroid()" value="我是HTML按钮,要调用Android"/>
body>
html>
public class JSCallAndroid extends Object {
private Context context;
public JSCallAndroid(Context context){
this.context = context;
}
// 定义JS需要调用的方法
// 被JS调用的方法必须加入@JavascriptInterface注解
@JavascriptInterface
public void hello(String msg) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
}
//js调用android方式一:通过addJavascriptInterface()将Java对象映射到JS对象
//参数1:java对象,js要调用安卓的方法的那个对象要传过去
//参数2:javascript对象名,html页面中这个名字就相当于参数一的对象了
mWebView.addJavascriptInterface(new JSCallAndroid(JsCallAndroid1Activity.this), "androidObject");
<script>
function callAndroid() {
androidObject.hello("啦啦啦啦,我是html页面通过js来的内容");
}
script>
方式二: 通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
//这里回调的url就是你点击的页面的链接,会传到这里来
//js调用android方式二,在这里拦截url,进行解析然后调用安卓方法
//然和这个url的规则啊什么的都是自己定义,有点类似http协议
Log.d(TAG, "shouldOverrideUrlLoading: url:" + url);
Uri uri = Uri.parse(url);
String scheme = uri.getScheme();
String authority = uri.getAuthority();
Set<String> queryParameterNames = uri.getQueryParameterNames();
Log.d(TAG, "scheme: " + scheme);
Log.d(TAG, "authority: " + authority);
//拿到的参数
Log.d(TAG, "queryParameterNames: " + queryParameterNames.toString());
Iterator<String> iterator = queryParameterNames.iterator();
ArrayList<String> argList = new ArrayList<>();
while (iterator.hasNext()) {
String key = iterator.next();
String queryParameter = uri.getQueryParameter(key);
argList.add(queryParameter);
Log.d(TAG, key + "=" + queryParameter);
}
if (scheme.equals("js")) {
//说明是js要调用android啦,然后进行自己的安卓操作
Toast.makeText(JsCallAndroid2Activity.this, "安卓被js调用啦,还传来了参数,参数1:" + argList.get(0)
+ " 参数2:" + argList.get(1), Toast.LENGTH_LONG).show();
} else {
//这个url就是你在网页上点击后要跳转的那个url
//scheme不是js,说明不是要调用安卓的
webView.loadUrl(url);
}
return true; //返回true,拦截url
}
function callAndroid() {
// 相当于给请求这个url,会被webview中的shouldOverrideUrlLoading()捕捉到
document.location = "js://webview?arg1=111&arg2=222";
}
方式三: 通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息
这三个方法都可以用来JS调用安卓,但prompt要比另两个方便些,并且安卓还能向js会传参数,所以这里我们就采用prompt()来进行讲解。
//js调用android方式三要在这里面
mWebView.setWebChromeClient(new WebChromeClient() {
/**
* 复写了下面这3方法并且返回true就不会显示页面的弹窗了,而且一定要有确认或者取消的操作,不然会卡死
* 但能拿到弹出里的所有信息,可以自己写原生弹窗等,如alert,toast等,很灵活
* 这也算是一种java和js的通信
*
* js调用android方式三
*
* 这个3个方法都可以用来js和android通信,但最好用的是onJsPrompt
*/
@Override
public boolean onJsAlert(WebView webView, String s, String s1, JsResult jsResult) {
return super.onJsAlert(webView, s, s1, jsResult);
}
@Override
public boolean onJsConfirm(WebView webView, String s, String s1, JsResult jsResult) {
return super.onJsConfirm(webView, s, s1, jsResult);
}
@Override
public boolean onJsPrompt(WebView webView, String url, String message, String defalutValue, JsPromptResult result) {
Log.i(TAG, "onJsPrompt: message:" + message);
Log.i(TAG, "onJsPrompt: defalutValue:" + defalutValue);
Toast.makeText(JsCallAndroid3Activity.this, "我是被js调用的,还传了参数: " + message
+ ":" + defalutValue, Toast.LENGTH_LONG).show();
result.confirm("啦啦啦啦,我是安卓原生来的内容");
return true;
}
});
function callAndroid() {
var name = prompt('标题', '通过prompt与android通信');
if (name != null && name !== "") {
var e = document.getElementById("p");
e.innerHTML = "安卓返回给js的数据是:" + name;
}
}
以上就是基于X5内核的WebView安卓HybridApp开发模式的基础知识,也是本人对混合开发的初步理解,如有错误的地方,还望指正。
参考文献: