前言:
由于android 原生的webview随着android版本的提高,出现了一些莫名奇妙的bug,例如:虚拟鼠标无法点击,h5兼容问题,无法上拉下拉,上拉下拉冲突,等等。所以就让我想到了使用第三方的替换方案,首先是网上推崇的x5内核,据说非常好用,可以共享使用微信和QQ内核,无需下载,也不占用apk大小,速度极快,而且支持的功能也丰富,而且目前还在维护更新中。还有一个是需要静态集成的crosswalk,最致命的缺点是会使apk包增大,而且好像官网也停止维护了。所以肯定是首先选择使用了x5。
提前预告使用感受:
x5内核:各种坑,而且并没有像文档和传闻说的那样,可以共享使用微信和QQ内核,实测只有当安装了QQ浏览器的时候,才能共享使用,纵然安装了微信和QQ,它还是需要重新下载内核才能加载成功。有时候首次下载进行加载的过程奇慢无比,还存在失败的概率。经过一系列的修改,勉强算是达到了可以正常使用。
crosswalk内核:就在使用x5遇到坑一筹莫展的时候,想起来试试这个货。结果这货也不是个省油的灯...适配性不好,我在android8.1的项目上可以正常使用,虽然有些小毛病,但是还能修修改改。但是整到android10和11以上项目之后,直接就崩了,一加载库就崩。。。
简单说下我的x5加载方案:第一次的时候进行初始化,加载,注册监听加载结果。打开h5页面功能写成统一的静态全局方法,每次调用方法的时候判断是否加载成功,未加载成功x5的时候使用原生的WebViewActivity打开,加载成功后使用X5WebViewActivity打开。如果加载x5失败了呢,就在失败的回调里初始化sdk并且手动启动下载。
一,集成sdk包
官方文档:腾讯浏览服务 (tencent.com)
//x5内核 tbs浏览服务
implementation files('libs/tbs_sdk_thirdapp_v4.3.0.151_44051.jar')
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" />
<!-- tencent H5 -->
<service
android:name="com.tencent.smtt.export.external.DexClassLoaderProviderService"
android:label="dexopt"
android:process=":dexopt">
</service>
混淆proguard.cfg
-dontwarn dalvik.**
-dontwarn com.tencent.smtt.**
-keep class com.tencent.smtt.** {
*;
}
-keep class com.tencent.tbs.** {
*;
}
二,加载和使用
在Application里直接加载,或者启动服务进行加载,实测效果没什么区别
2.1在Application里直接加载
class App : Application() {
private var isLoadingFinished = false //加载x5结束
private var isLoadingSuccess = false //加载x5是否成功
val isDebug = false
override fun onCreate() {
super.onCreate()
// 在调用TBS初始化、创建WebView之前进行如下配置
val map = HashMap<String, Any>()
map[TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER] = true
map[TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE] = true
// map[TbsCoreSettings.TBS_SETTINGS_USE_PRIVATE_CLASSLOADER] = true
QbSdk.initTbsSettings(map)
// X5CorePreLoadService.enqueueWork(this, Intent())//服务中进行加载
initX5()//直接加载
}
fun initX5() {
val cb: PreInitCallback = object : PreInitCallback {
override fun onViewInitFinished(arg0: Boolean) {
// TODO Auto-generated method stub
//x5內核初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会
//自动切换到系统内核。
Log.d("x5", " onViewInitFinished is $arg0")
if (arg0) {
//加载成功时
isLoadingFinished = true
isLoadingSuccess = true
sendBroadcast(Intent("closemyloading"))
showDebug("加载h5内核成功")
} else {
isLoadingFinished = true
isLoadingSuccess = false
sendBroadcast(Intent("closemyloading"))
showDebug("加载h5内核失败 重新下载")
reDownloadX5()//重新下载
}
}
override fun onCoreInitFinished() {
// TODO Auto-generated method stub
}
}
//x5内核初始化接口
QbSdk.initX5Environment(applicationContext, cb)
QbSdk.setDownloadWithoutWifi(true)//设置没有wifi也下载x5内核
QbSdk.setTbsListener(object : TbsListener {
override fun onDownloadFinish(i: Int) {
//tbs内核下载完成回调
//但是只有i等于100才算完成,否则失败
//此时大概率可能由于网络问题
//如果失败可增加网络监听器
showDebug("下载x5Core完成")
}
override fun onInstallFinish(i: Int) {
//内核安装完成回调,通常到这里也算安装完成,但是在
//极个别情况也会出现加载失败,比如笔者在公司内网下偶现,可以忽略
showDebug("安装x5Core完成")
}
override fun onDownloadProgress(i: Int) {
Log.d("x5", "progress: " + i)
//下载进度监听
sendBroadcast(Intent("updateProgress").putExtra("progress", i))
}
})
}
fun reDownloadX5(){
// 在调用TBS初始化、创建WebView之前进行如下配置
val map = HashMap<String, Any>()
map[TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER] = true
map[TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE] = true
// map[TbsCoreSettings.TBS_SETTINGS_USE_PRIVATE_CLASSLOADER] = true
QbSdk.initTbsSettings(map)
//判断是否要自行下载内核
//false表示下载完成 或者 下载了一部分 true表示完全没下载
val needDownload = TbsDownloader.needDownload(
applicationContext,
TbsDownloader.DOWNLOAD_OVERSEA_TBS
)
Log.d("X5", needDownload.toString() + "")
//重置
QbSdk.reset(applicationContext)
// 启动下载
TbsDownloader.startDownload(applicationContext)
}
fun showDebug(message:String){
if (isDebug){
shortShow(message)
}
}
fun isX5LoadingFinished(): Boolean {
return isLoadingFinished
}
fun setIsX5LoadingFinished(isFinished: Boolean) {
isLoadingFinished = isFinished
}
fun isX5LoadingSuccess(): Boolean {
return isLoadingSuccess
}
fun setIsX5LoadingSuccess(isFinished: Boolean) {
isLoadingSuccess = isFinished
}
}
2.2启动服务进行加载
放开Application里面的注释:
X5CorePreLoadService.enqueueWork(this, Intent())//服务中进行加载
class X5CorePreLoadService : JobIntentService() {
companion object {
val JOB_ID = 1
private var mContext: Context? = null
fun enqueueWork(context: Context, work: Intent) {
mContext = context
enqueueWork(context, X5CorePreLoadService::class.java, JOB_ID, work)
}
}
override fun onHandleWork(intent: Intent) {
//在这里添加我们要执行的代码,Intent中可以保存我们所需的数据,
//每一次通过Intent发送的命令将被顺序执行
initX5()
Log.d("x5", "initX5()")
}
/**
* 初始化X5内核
*/
private fun initX5() {
QbSdk.setTbsListener(object : TbsListener {
override fun onDownloadFinish(i: Int) {
Log.d("x5", "onDownloadFinish: " + i)
//tbs内核下载完成回调
//但是只有i等于100才算完成,否则失败
//此时大概率可能由于网络问题
//如果失败可增加网络监听器
}
override fun onInstallFinish(i: Int) {
Log.d("x5", "onInstallFinish: " + i)
//内核安装完成回调,通常到这里也算安装完成,但是在
//极个别情况也会出现加载失败,比如笔者在公司内网下偶现,可以忽略
}
override fun onDownloadProgress(i: Int) {
Log.d("x5", "progress: " + i)
//下载进度监听
sendBroadcast(Intent("updateProgress").putExtra("progress", i))
}
})
//x5内核初始化接口
QbSdk.initX5Environment(applicationContext, cb)
QbSdk.setDownloadWithoutWifi(true)//设置没有wifi也下载x5内核
}
val cb: PreInitCallback = object : PreInitCallback {
override fun onViewInitFinished(arg0: Boolean) {
// TODO Auto-generated method stub
//x5內核初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会自动切换到系统内核。
Log.d("x5", " onViewInitFinished is $arg0")
if (arg0) {
//加载成功时
App.instance?.setIsX5LoadingFinished(true)
App.instance?.setIsX5LoadingSuccess(true)
sendBroadcast(Intent("closemyloading"))
} else {
App.instance?.setIsX5LoadingFinished(true)
App.instance?.setIsX5LoadingSuccess(false)
sendBroadcast(Intent("closemyloading"))
// 在调用TBS初始化、创建WebView之前进行如下配置
val map = HashMap<String, Any>()
map[TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER] = true
map[TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE] = true
// map[TbsCoreSettings.TBS_SETTINGS_USE_PRIVATE_CLASSLOADER] = true
QbSdk.initTbsSettings(map)
//判断是否要自行下载内核
//false表示下载完成 或者 下载了一部分 true表示完全没下载
val needDownload =
TbsDownloader.needDownload(mContext, TbsDownloader.DOWNLOAD_OVERSEA_TBS)
Log.d("X5", needDownload.toString() + "")
//重置
QbSdk.reset(mContext)
// 启动下载
TbsDownloader.startDownload(mContext)
}
}
override fun onCoreInitFinished() {
}
}
}
注册:
<service
android:name=".app.X5CorePreLoadService"
android:enabled="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
全局打开方法:
fun showH5(context: Context, path:String){
if (App.instance?.isX5LoadingFinished()!!){
if (App.instance?.isX5LoadingSuccess()!!){
Log.d("x5", "x5 will be use to show H5!")
context.startActivity(Intent(context, X5WebViewActivity::class.java)
.putExtra("path", path))
}else{
Log.d("x5", "default core will be use to show H5!")
context.startActivity(Intent(context, WebViewActivity::class.java)
.putExtra("path", path))
}
}else{
// ToastUtil.shortShow("首次启动正在加载h5内核,请稍后点击")
Log.d("x5", "default core will be use to show H5!")
context.startActivity(Intent(context, WebViewActivity::class.java)
.putExtra("path", path))
}
}
class X5WebViewActivity : SimpleActivity(){
private lateinit var binding: ActivityX5WebviewBinding
private var mUploadMessage: ValueCallback<Uri>? = null
private var mUploadCallbackAboveL: ValueCallback<Array<Uri>>? = null
private var webView:WebView? = null
var isBottom = false
companion object {
private val FILE_CHOOSER_RESULT_CODE = 10000
}
override fun getLayout(): View {
binding = ActivityX5WebviewBinding.inflate(layoutInflater)
return binding.root
}
override fun initEventAndData() {
webView = WebView(applicationContext)
binding.webView.addView(webView, 0)
val url = intent.getStringExtra("path")
initWebViewSetting()
//覆盖WebView默认使用第三方或系统默认浏览器打开网页的行为,使网页用WebView打开
webView?.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
// TODO Auto-generated method stub
//返回值是true的时候控制去WebView打开,为false调用系统浏览器或第三方浏览器
view.loadUrl(url!!)
return true
}
}
webView?.webChromeClient = chromeClient
webView?.setBackgroundColor(Color.argb(1, 0, 0, 0))
//WebView加载web资源
webView?.loadUrl(url!!)
}
@SuppressLint("SetJavaScriptEnabled")
fun initWebViewSetting() {
val mWebSettings = webView?.settings!!
mWebSettings.javaScriptEnabled = true
mWebSettings.defaultTextEncodingName = "utf-8"
mWebSettings.useWideViewPort = true
mWebSettings.loadWithOverviewMode = true
mWebSettings.cacheMode = WebSettings.LOAD_NO_CACHE
mWebSettings.layoutAlgorithm = WebSettings.LayoutAlgorithm.NARROW_COLUMNS
mWebSettings.domStorageEnabled = true
mWebSettings.javaScriptCanOpenWindowsAutomatically = true
mWebSettings.setNeedInitialFocus(true)
mWebSettings.allowFileAccess = true
mWebSettings.allowContentAccess = true
mWebSettings.setAppCacheEnabled(true)
mWebSettings.setAllowUniversalAccessFromFileURLs(true)
mWebSettings.setAllowFileAccessFromFileURLs(true)
mWebSettings.setAppCacheMaxSize(Long.MAX_VALUE)
mWebSettings.setAppCachePath(this.getDir("appcache", Context.MODE_PRIVATE).path)
mWebSettings.databasePath = this.getDir("databases", Context.MODE_PRIVATE).path
mWebSettings.pluginState = WebSettings.PluginState.ON_DEMAND
mWebSettings.databaseEnabled = true
mWebSettings.blockNetworkImage = false
//不显示webview缩放按钮
mWebSettings.setSupportZoom(false)//支持缩放,默认为true。是下面这个API的前提。
mWebSettings.builtInZoomControls = false// 设置内置的缩放控件。若为false,则该WebView不可缩放
mWebSettings.displayZoomControls = false
val mAndroidJsUtils = AndroidJsUtils(this)
webView?.addJavascriptInterface(mAndroidJsUtils, "mobile")
}
@OnClick(R.id.back_btn)
fun back() {
if (webView?.canGoBack()!!) {
webView?.goBack()
} else {
finish()
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (webView?.canGoBack()!!) {
webView?.goBack() //返回上一页面
return true
} else {
finish() //退出
}
}
return super.onKeyDown(keyCode, event)
}
private var chromeClient = object: WebChromeClient(){
override fun openFileChooser(
uploadFile: ValueCallback<Uri>?,
acceptType: String?,
captureType: String?
) {
//保存对应的valuecallback供选择后使用
//通过startActivityForResult启动文件选择窗口或自定义文件选择
mUploadMessage = uploadFile
if (acceptType!!.contains("image")){
openImageChooserActivity()
}else{
openFileChooserActivity()
}
}
}
fun openImageChooserActivity() {
val i = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
i.type = "image/*"
startActivityForResult(Intent.createChooser(i, "选择图片"), FILE_CHOOSER_RESULT_CODE)
}
fun openFileChooserActivity() {
val i = Intent(Intent.ACTION_GET_CONTENT)
i.addCategory(Intent.CATEGORY_OPENABLE)
i.type = "*/*"
startActivityForResult(Intent.createChooser(i, "选择文件"), FILE_CHOOSER_RESULT_CODE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == FILE_CHOOSER_RESULT_CODE) {
if (resultCode == RESULT_OK) {
//给文件选择的ValueCallback设置onReceiveValue值
mUploadMessage?.onReceiveValue(data?.data!!)
mUploadMessage = null
} else if (resultCode == RESULT_CANCELED) {
//给文件选择的ValueCallback设置null值
mUploadMessage?.onReceiveValue(null)
mUploadMessage = null
}
}
}
override fun onDestroy() {
webView?.destroy()
super.onDestroy()
}
}
<FrameLayout
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
结语:
WebViewActivity是使用原生WebView加载url,这里就不进行赘述了。以上是主要核心代码了。还有关于网络断开,网络失败后重新下载之类的没有进行判断和优化,如果自己想更完善可以优化一下。目前测试的几个型号android10和android11的机器都能加载成功了。希望对大家有所帮助,代码篇幅有点长,有些多余的也没来得及裁剪,如有错误,请大家多多指教。