说明
因为手机自带的webview内核不统一,而且大都版本过低。
为了更好的体验,选择了x5。
虽然x5内置的chrome版本不是最新的,但是也是相当新了(截止目前为109)!
x5内核的集成方式分为两种,在线版本(也叫公网版)和离线版本!
不过后来又增加了自运营版本(方便部署到内网服务器等不方便访问外网的环境) 并把离线版改名为 自运营静态!
3者区别是
公网版:App在启动后,从腾讯服务器动态下载并共享X5内核,接入简单,APK体积小;内核自动更新,无需随App升级。
自运营静态内核版:启动快,无网络依赖;与App绑定将X5内核的.so库文件直接打包到APK中,体积大。
自运营动态内核版:将X5内核服务部署在自有或内网服务器上,完全可控,不依赖外网,而且安装包也小,启动后从私有服务器动态下载并共享X5内核。
这里我们讲的是公网版!
前置工作
注册、实名认真和创建APP,下载内核(因为是在线版,所以非常小)和 配置文件
上传apk的时候,记得使用v1签名,不支持v2和v3!
开始编码
创建项目名字随意 我这里叫 x5demo。
kotlin(java)代码
java/com/example/x5demo 目录有3个文件
MainActivity.kt
package com.example.x5demo
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.tencent.smtt.export.external.TbsCoreSettings
import com.tencent.smtt.sdk.QbSdk
import com.tencent.smtt.sdk.TbsFramework
import com.tencent.smtt.sdk.core.dynamicinstall.DynamicInstallManager
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import com.tencent.smtt.sdk.ProgressListener;
import android.widget.ProgressBar;
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var progressBar: ProgressBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
progressBar = findViewById(R.id.progress)
findViewById
initPublicTBS()
}
}
private fun saveInputStreamToFile(inputStream: InputStream, filePath: String): File? {
return try {
// 1. 创建目标文件对象 - 在应用的内部存储目录中
val file = File(applicationContext.filesDir, filePath)
// 最终路径类似:/data/data/你的包名/files/config/config_47405.tbs
// 2. 创建输出流准备写入
val out = FileOutputStream(file)
// 3. 创建缓冲区,提高复制效率
val buffer = ByteArray(1024) // 1KB 缓冲区
var bytesRead: Int
// 4. 循环读取输入流并写入输出流
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
out.write(buffer, 0, bytesRead) // 将读取的数据写入文件
}
// 5. 关闭流,释放资源
out.close()
inputStream.close()
// 6. 记录成功信息
Log.e(TAG, "saveInputStreamToFile: ${file.path}")
file // 返回复制后的文件对象
} catch (e: IOException) {
Log.e(TAG, "出现异常: ${e.localizedMessage}")
null // 出错时返回 null
}
}
private fun getConfigFile(): File? {
return try {
val inputStream = assets.open(TBSEnv.CONFIG_PATH)
val inputFileName = TBSEnv.CONFIG_PATH.substringAfter("/")
saveInputStreamToFile(inputStream, inputFileName)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
// 预初始化回调
private val preInitCallback = object : QbSdk.PreInitCallback {
override fun onCoreInitFinished() {
Log.e(TAG, "onCoreInitFinished: 初始化成功")
val intent = Intent(this@MainActivity, WebActivity::class.java)
startActivity(intent)
finish()
}
override fun onViewInitFinished(isX5Code: Boolean) {
Log.e(TAG, "是否使用X5内核: $isX5Code")
}
}
private fun downloadConfigTBS(configFile: File) {
// 3. 设置TBS框架
TbsFramework.setUp(this, configFile)
// 4. 动态安装管理
val manager = DynamicInstallManager(applicationContext)
manager.registerListener(object : ProgressListener {
override fun onProgress(progress: Int) {
Log.i(TAG, "downloadConfigTBS: $progress")
runOnUiThread {
progressBar.progress = progress
}
}
override fun onFinished() {
Log.i(TAG, "下载完成,开始预初始化")
QbSdk.preInit(this@MainActivity, preInitCallback)
}
override fun onFailed(code: Int, msg: String) {
Log.i(TAG, "onError: $code; msg: $msg")
}
})
manager.startInstall()
}
private fun initPublicTBS() {
// 1. 初始化TBS设置
val map = HashMap
map[TbsCoreSettings.MULTI_PROCESS_ENABLE] = 1
QbSdk.initTbsSettings(map)
// 2. 获取配置文件
val configFile = getConfigFile()
if (configFile != null && configFile.exists()) {
Log.e(TAG, "拿到文件")
Log.e(TAG, "文件路径: ${configFile.absolutePath}")
downloadConfigTBS(configFile)
} else {
Log.e(TAG, "未拿到文件")
}
}
}
TBSEnv.kt
package com.example.x5demo
object TBSEnv {
const val CONFIG_PATH = "config.tbs"
const val LOAD_URL = "https://www.baidu.com"
}
WebActivity.kt
package com.example.x5demo
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.tencent.smtt.export.external.interfaces.JsResult
import com.tencent.smtt.sdk.QbSdk
import com.tencent.smtt.sdk.WebChromeClient
import com.tencent.smtt.sdk.WebView
import com.tencent.smtt.sdk.WebViewClient
class WebActivity : AppCompatActivity() {
private var webView: WebView? = null
private val TAG = "WebActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_web)
webView = findViewById
webView?.let { web ->
val settings = web.settings
settings.javaScriptEnabled = true
settings.allowFileAccess = true
settings.setSupportZoom(true)
settings.databaseEnabled = true
settings.allowFileAccess = true
settings.domStorageEnabled = true
WebView.setWebContentsDebuggingEnabled(true)
web.loadUrl(TBSEnv.LOAD_URL)
val tbsVersion = QbSdk.getTbsVersion(this)
Log.e("webActivity", "QbSdk.getTbsVersion: $tbsVersion")
Toast.makeText(
this@WebActivity,
"内核版本:" + tbsVersion + web.isX5Core,
Toast.LENGTH_LONG
).show()
web.webChromeClient = object : WebChromeClient() {
override fun onJsAlert(
webView: WebView,
url: String,
message: String,
result: JsResult
): Boolean {
AlertDialog.Builder(this@WebActivity).setTitle("JS弹窗Override")
.setMessage(message)
.setPositiveButton(
"OK"
) { _: DialogInterface?, _: Int -> result.confirm() }
.setCancelable(false)
.show()
return true
}
}
web.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean {
Log.e(TAG, "overrideUrlLoading: $url")
return !url.startsWith("http")
}
}
}
}
override fun onDestroy() {
webView?.destroy()
super.onDestroy()
}
}
配置文件
AndroidManifest.xml
xmlns:tools="http://schemas.android.com/tools"> android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.X5demo" tools:targetApi="31"> android:name=".WebActivity" android:exported="false" /> android:name=".MainActivity" android:exported="true"> android:name="com.tencent.smtt.services.ChildProcessService$Privileged0" android:exported="false" android:isolatedProcess="false" android:process=":privileged_process0" />
/x5demo/app/build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.example.x5demo"
compileSdk = 36
defaultConfig {
applicationId = "com.example.x5demo"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
// 增加这一段(填写自己的证书信息)
signingConfigs {
create("release") {
storeFile = file("wutong.jks")
storePassword = "123456789"
keyAlias = "cert"
keyPassword = "123456789"
enableV1Signing = true
}
getByName("debug") {
storeFile = file("wutong.jks")
storePassword = "123456789"
keyAlias = "cert"
keyPassword = "123456789"
enableV1Signing = true
}
}
// 增加这一段
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
}
getByName("debug") {
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar")))) // 增加这一行
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
x5demo/app/src/main/assets/config.tbs
将前置工作中下载的配置文件复制到此处!
xxx.jks证书文件
比如我这里是wutong.jsk,复制到 app 目录下!
布局文件
在x5demo/app/src/main/res/layout新增两个文件
activity_main.xml
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical" android:gravity="center">
activity_web.xml
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".WebActivity"> android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent"/>
预览
其它
以上我的代码都是kotlin,如果你是java,那正好官网提供了java版本的demo!
这里是公网版本的官方文档!
Android Kotlin 中页面跳转的标准方式
Android 页面跳转的基本方式
// 1. 创建 Intent(意图)对象
val intent = Intent(mainActivity, WebActivity::class.java)
// 2. 启动目标 Activity
mainActivity.startActivity(intent)
// 3. 关闭当前 Activity(可选)
mainActivity.finish()
Intent(意图)
val intent = Intent(mainActivity, WebActivity::class.java)
Intent 是 Android 中用于组件间通信的对象
第一个参数:mainActivity - 当前的上下文(Context)
第二个参数:WebActivity::class.java - 目标 Activity 的 Class 对象
::class.java 是 Kotlin 获取 Java Class 对象的语法
startActivity()
mainActivity.startActivity(intent)
启动目标 Activity
会打开 WebActivity 页面
finish()
mainActivity.finish()
关闭当前 Activity(MainActivity)
调用后,用户按返回键不会回到 MainActivity
如果不调用 finish(),返回键会回到 MainActivity
其他常见的跳转方式
带参数跳转
val intent = Intent(this, WebActivity::class.java)
intent.putExtra("url", "https://www.baidu.com")
intent.putExtra("title", "百度")
startActivity(intent)
简化写法
startActivity(Intent(this, WebActivity::class.java))
带动画跳转
val intent = Intent(this, WebActivity::class.java)
startActivity(intent)
overridePendingTransition(R.anim.slide_in, R.anim.slide_out)
获取返回结果(新方式)
val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
// 处理返回结果
}
}
优化版本
弹窗展示加载内核,加载完成自动打开页面。
MainActivity.kt
package com.example.x5demo
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.tencent.smtt.export.external.interfaces.JsResult
import com.tencent.smtt.sdk.QbSdk
import com.tencent.smtt.sdk.WebChromeClient
import com.tencent.smtt.sdk.WebView
import com.tencent.smtt.sdk.WebViewClient
class MainActivity : AppCompatActivity() {
private var webView: WebView? = null
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初始化 X5 内核
TbsHelper(this, this@MainActivity) {
// 初始化成功后的回调,创建并加载 WebView
Log.e(TAG, "初始化成功后的回调,创建并加载 WebView")
initWebView()
}.initPublicTBS()
}
private fun initWebView() {
// X5 已经初始化完成后,动态创建 WebView
// (不能直接写到activity_main里,然后 webView = findViewById
// 因为这样会被提前自动提前加载(则会启用系统自带的),必须在这里显式手动加载)
webView = WebView(this)
val container = findViewById
container.addView(webView, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
))
// 设置webView
webView?.let { web ->
val settings = web.settings
settings.javaScriptEnabled = true
settings.allowFileAccess = true
settings.setSupportZoom(true)
settings.databaseEnabled = true
settings.domStorageEnabled = true
WebView.setWebContentsDebuggingEnabled(true)
val tbsVersion = QbSdk.getTbsVersion(this)
val isX5Core = web.isX5Core
Log.e(TAG, "=== X5内核状态 ===")
Log.e(TAG, "TBS版本: $tbsVersion")
Log.e(TAG, "是否X5内核: $isX5Core")
web.loadUrl("file:///android_asset/index.html")
web.webChromeClient = object : WebChromeClient() {
override fun onJsAlert(
webView: WebView,
url: String,
message: String,
result: JsResult
): Boolean {
AlertDialog.Builder(this@MainActivity)
.setTitle("JS弹窗")
.setMessage(message)
.setPositiveButton("OK") { _: DialogInterface?, _: Int ->
result.confirm()
}
.setCancelable(false)
.show()
return true
}
}
web.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean {
Log.e(TAG, "overrideUrlLoading: $url")
return !url.startsWith("http")
}
}
}
}
override fun onDestroy() {
webView?.destroy()
super.onDestroy()
}
}
TbsHelper.kt
package com.example.x5demo
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.example.x5demo.TBSEnv.CONFIG_PATH
import com.tencent.smtt.export.external.TbsCoreSettings
import com.tencent.smtt.sdk.QbSdk
import com.tencent.smtt.sdk.TbsFramework
import com.tencent.smtt.sdk.core.dynamicinstall.DynamicInstallManager
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import com.tencent.smtt.sdk.ProgressListener
class TbsHelper (
private val applicationContext: Context,
private val mainActivity: MainActivity,
private val onInitSuccess: () -> Unit // 初始化成功的回调
) {
private val TAG = "MainActivity"
private var progressDialog: AlertDialog? = null
private var progressBar: ProgressBar? = null
private var progressText: TextView? = null
companion object {
private const val PREF_NAME = "tbs_config"
private const val KEY_FIRST_INSTALL = "first_install_done"
}
private fun saveInputStreamToFile(inputStream: InputStream, filePath: String): File? {
return try {
// 1. 创建目标文件对象 - 在应用的内部存储目录中
val file = File(applicationContext.filesDir, filePath)
// 最终路径类似:/data/data/你的包名/files/config/config_47405.tbs
// 2. 创建输出流准备写入
val out = FileOutputStream(file)
// 3. 创建缓冲区,提高复制效率
val buffer = ByteArray(1024) // 1KB 缓冲区
var bytesRead: Int
// 4. 循环读取输入流并写入输出流
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
out.write(buffer, 0, bytesRead) // 将读取的数据写入文件
}
// 5. 关闭流,释放资源
out.close()
inputStream.close()
// 6. 记录成功信息
Log.e(TAG, "saveInputStreamToFile: ${file.path}")
file // 返回复制后的文件对象
} catch (e: IOException) {
Log.e(TAG, "出现异常: ${e.localizedMessage}")
null // 出错时返回 null
}
}
private fun getConfigFile(): File? {
return try {
val inputStream = applicationContext.assets.open(CONFIG_PATH)
val inputFileName = CONFIG_PATH.substringAfter("/")
saveInputStreamToFile(inputStream, inputFileName)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
// 预初始化回调
private val preInitCallback = object : QbSdk.PreInitCallback {
override fun onCoreInitFinished() {
Log.e(TAG, "onCoreInitFinished: 初始化成功")
mainActivity.runOnUiThread {
progressDialog?.dismiss()
}
}
override fun onViewInitFinished(isX5Code: Boolean) {
Log.e(TAG, "是否使用X5内核: $isX5Code")
mainActivity.runOnUiThread {
val kernelType = if (isX5Code) "X5内核" else "系统WebView内核"
val message = "初始化完成\n使用: $kernelType"
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
// 调用初始化成功的回调
onInitSuccess()
}
}
}
fun initPublicTBS() {
// 1. 初始化TBS设置
val map = HashMap
map[TbsCoreSettings.MULTI_PROCESS_ENABLE] = 1
QbSdk.initTbsSettings(map)
// 2. 获取配置文件
val configFile = getConfigFile()
// 3. 设置TBS框架
TbsFramework.setUp(applicationContext, configFile)
// 显示进度对话框
mainActivity.runOnUiThread {
val dialogView = LayoutInflater.from(mainActivity).inflate(R.layout.dialog_progress, null)
progressBar = dialogView.findViewById(R.id.progress_bar)
progressText = dialogView.findViewById(R.id.progress_percent)
progressDialog = AlertDialog.Builder(mainActivity)
.setView(dialogView)
.setCancelable(false)
.create()
progressDialog?.show()
}
// 4. 动态安装管理
val manager = DynamicInstallManager(applicationContext)
manager.registerListener(object : ProgressListener {
override fun onProgress(progress: Int) {
Log.i(TAG, "downloadConfigTBS: $progress")
mainActivity.runOnUiThread {
progressBar?.progress = progress
progressText?.text = "$progress%"
}
}
override fun onFinished() {
Log.i(TAG, "下载完成,开始预初始化")
mainActivity.runOnUiThread {
val messageText = progressDialog?.findViewById
messageText?.text = "初始化中..."
progressBar?.isIndeterminate = true
}
QbSdk.preInit(mainActivity, preInitCallback)
}
override fun onFailed(code: Int, msg: String) {
Log.e(TAG, "onError: $code; msg: $msg")
}
})
manager.startInstall()
}
}
activity_main.xml
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/webview_container" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
dialog_progress.xml
android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="24dp"> android:id="@+id/progress_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="正在加载X5内核" android:textSize="18sp" android:textStyle="bold" android:textColor="@android:color/black" android:layout_marginBottom="16dp"/> android:id="@+id/progress_message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="请稍候..." android:textSize="14sp" android:textColor="@android:color/darker_gray" android:layout_marginBottom="16dp"/> android:id="@+id/progress_bar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="100" android:progress="0"/> android:id="@+id/progress_percent" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0%" android:textSize="12sp" android:textColor="@android:color/darker_gray" android:layout_gravity="end" android:layout_marginTop="8dp"/>