├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── github │ │ └── leavesczy │ │ └── robustwebview │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── ic_launcher.webp │ │ └── javascript.html │ ├── java │ │ └── github │ │ │ └── leavesczy │ │ │ └── robustwebview │ │ │ ├── JsInterface.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainApplication.kt │ │ │ ├── base │ │ │ ├── RobustWebView.kt │ │ │ ├── WebViewCacheHolder.kt │ │ │ ├── WebViewInitTask.kt │ │ │ └── WebViewInterceptRequestProxy.kt │ │ │ └── utils │ │ │ ├── ContextHolder.kt │ │ │ └── Utils.kt │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── github │ └── leavesczy │ └── robustwebview │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /captures 7 | .externalNativeBuild 8 | .cxx 9 | /.idea/ 10 | /app/release/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RobustWebView 2 | 3 | 文章讲解: 4 | 5 | - [Android WebView H5 秒开方案总结](https://juejin.cn/post/7016883220025180191) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | } 5 | 6 | android { 7 | namespace = "github.leavesczy.robustwebview" 8 | compileSdk = 35 9 | defaultConfig { 10 | applicationId = "github.leavesczy.robustwebview" 11 | minSdk = 21 12 | targetSdk = 35 13 | versionCode = 1 14 | versionName = "1.0.0" 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | isMinifyEnabled = false 20 | proguardFiles( 21 | getDefaultProguardFile("proguard-android-optimize.txt"), 22 | "proguard-rules.pro" 23 | ) 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility = JavaVersion.VERSION_11 28 | targetCompatibility = JavaVersion.VERSION_11 29 | } 30 | kotlinOptions { 31 | jvmTarget = "11" 32 | } 33 | } 34 | 35 | dependencies { 36 | testImplementation("junit:junit:4.13.2") 37 | androidTestImplementation("androidx.test.ext:junit:1.2.1") 38 | androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") 39 | implementation("androidx.appcompat:appcompat:1.7.0") 40 | implementation("com.google.android.material:material:1.12.0") 41 | implementation("com.squareup.okhttp3:okhttp:4.12.0") 42 | val chuckerVersion = "4.0.0" 43 | debugImplementation("com.github.chuckerteam.chucker:library:${chuckerVersion}") 44 | releaseImplementation("com.github.chuckerteam.chucker:library-no-op:${chuckerVersion}") 45 | implementation("com.tencent.tbs:tbssdk:44286") 46 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontwarn dalvik.** 24 | -dontwarn com.tencent.smtt.** 25 | -keep class com.tencent.smtt.** { 26 | *; 27 | } 28 | -keep class com.tencent.tbs.** { 29 | *; 30 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/github/leavesczy/robustwebview/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("github.leavesczy.robustwebview", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/assets/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leavesCZY/RobustWebView/a8cee699ad99f59f0b0ee14ee082798b4f96358d/app/src/main/assets/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/assets/javascript.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Android WebView H5 秒开方案总结 7 | 12 | 25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/JsInterface.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview 2 | 3 | import android.webkit.JavascriptInterface 4 | import github.leavesczy.robustwebview.utils.log 5 | import github.leavesczy.robustwebview.utils.showToast 6 | 7 | /** 8 | * @Author: leavesCZY 9 | * @Date: 2021/9/21 15:08 10 | * @Desc: 11 | * @Github:https://github.com/leavesCZY 12 | */ 13 | class JsInterface { 14 | 15 | @JavascriptInterface 16 | fun showToastByAndroid(log: String) { 17 | log("showToastByAndroid:$log") 18 | showToast(log) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview 2 | 3 | import android.Manifest 4 | import android.os.Build 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.LinearLayout 9 | import android.widget.TextView 10 | import androidx.activity.OnBackPressedCallback 11 | import androidx.activity.result.contract.ActivityResultContracts 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.core.app.NotificationManagerCompat 14 | import github.leavesczy.robustwebview.base.RobustWebView 15 | import github.leavesczy.robustwebview.base.WebViewCacheHolder 16 | import github.leavesczy.robustwebview.base.WebViewListener 17 | import github.leavesczy.robustwebview.utils.showToast 18 | 19 | /** 20 | * @Author: leavesCZY 21 | * @Date: 2021/10/1 23:08 22 | * @Desc: 23 | * @Github:https://github.com/leavesCZY 24 | */ 25 | class MainActivity : AppCompatActivity() { 26 | 27 | private val webViewContainer by lazy { 28 | findViewById(R.id.webViewContainer) 29 | } 30 | 31 | private val tvTitle by lazy { 32 | findViewById(R.id.tvTitle) 33 | } 34 | 35 | private val tvProgress by lazy { 36 | findViewById(R.id.tvProgress) 37 | } 38 | 39 | private val requestNotificationPermissionLauncher = registerForActivityResult( 40 | ActivityResultContracts.RequestPermission() 41 | ) { 42 | checkNotificationPermission() 43 | } 44 | 45 | private val url1 = "https://juejin.cn/post/7016883220025180191" 46 | 47 | private val url2 = "https://www.bilibili.com/" 48 | 49 | private val url3 = 50 | "https://p26-passport.byteacctimg.com/img/user-avatar/6019f80db5be42d33c31c98adaf3fa8c~300x300.image" 51 | 52 | private lateinit var webView: RobustWebView 53 | 54 | private val webViewListener = object : WebViewListener { 55 | override fun onProgressChanged(webView: RobustWebView, progress: Int) { 56 | tvProgress.text = progress.toString() 57 | } 58 | 59 | override fun onReceivedTitle(webView: RobustWebView, title: String) { 60 | tvTitle.text = title 61 | } 62 | 63 | override fun onPageFinished(webView: RobustWebView, url: String) { 64 | 65 | } 66 | } 67 | 68 | override fun onCreate(savedInstanceState: Bundle?) { 69 | super.onCreate(savedInstanceState) 70 | setContentView(R.layout.activity_main) 71 | webView = WebViewCacheHolder.acquireWebViewInternal(this) 72 | webView.webViewListener = webViewListener 73 | val layoutParams = LinearLayout.LayoutParams( 74 | ViewGroup.LayoutParams.MATCH_PARENT, 75 | ViewGroup.LayoutParams.MATCH_PARENT 76 | ) 77 | webViewContainer.addView(webView, layoutParams) 78 | findViewById(R.id.tvBack).setOnClickListener { 79 | onBackPressedDispatcher.onBackPressed() 80 | } 81 | findViewById(R.id.btnOpenUrl1).setOnClickListener { 82 | webView.loadUrl(url1) 83 | } 84 | findViewById(R.id.btnOpenUrl2).setOnClickListener { 85 | webView.loadUrl(url2) 86 | } 87 | findViewById(R.id.btnOpenUrl3).setOnClickListener { 88 | webView.toLoadUrl(url3, "") 89 | } 90 | findViewById(R.id.btnReload).setOnClickListener { 91 | webView.reload() 92 | } 93 | findViewById(R.id.btnOpenHtml).setOnClickListener { 94 | webView.loadUrl("""file:/android_asset/javascript.html""") 95 | } 96 | findViewById(R.id.btnCallJsByAndroid).setOnClickListener { 97 | val parameter = "\"业志陈\"" 98 | webView.evaluateJavascript( 99 | "javascript:callJsByAndroid(${parameter})" 100 | ) { 101 | showToast("evaluateJavascript: $it") 102 | } 103 | // webView.loadUrl("javascript:callJsByAndroid(${parameter})") 104 | } 105 | findViewById(R.id.btnShowToastByAndroid).setOnClickListener { 106 | webView.loadUrl("javascript:showToastByAndroid()") 107 | } 108 | findViewById(R.id.btnCallJsPrompt).setOnClickListener { 109 | webView.loadUrl("javascript:callJsPrompt()") 110 | } 111 | onBackPressedObserver() 112 | requestNotificationPermission() 113 | } 114 | 115 | private fun requestNotificationPermission() { 116 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 117 | requestNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) 118 | } else { 119 | checkNotificationPermission() 120 | } 121 | } 122 | 123 | private fun checkNotificationPermission() { 124 | if (!NotificationManagerCompat.from(this).areNotificationsEnabled()) { 125 | showToast("请开启消息通知权限,以便查看网络请求") 126 | } 127 | } 128 | 129 | private fun onBackPressedObserver() { 130 | onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { 131 | override fun handleOnBackPressed() { 132 | if (webView.canGoBack()) { 133 | webView.toGoBack() 134 | } else { 135 | finish() 136 | } 137 | } 138 | }) 139 | } 140 | 141 | override fun onDestroy() { 142 | super.onDestroy() 143 | WebViewCacheHolder.prepareWebView() 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview 2 | 3 | import android.app.Application 4 | import github.leavesczy.robustwebview.base.WebViewInitTask 5 | import github.leavesczy.robustwebview.utils.ContextHolder 6 | 7 | /** 8 | * @Author: leavesCZY 9 | * @Date: 2021/9/12 22:22 10 | * @Desc: 11 | * @Github:https://github.com/leavesCZY 12 | */ 13 | class MainApplication : Application() { 14 | 15 | override fun onCreate() { 16 | super.onCreate() 17 | ContextHolder.init(application = this) 18 | WebViewInitTask.init(application = this) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/base/RobustWebView.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview.base 2 | 3 | import android.content.Context 4 | import android.content.MutableContextWrapper 5 | import android.graphics.Bitmap 6 | import android.util.AttributeSet 7 | import android.view.ViewGroup 8 | import androidx.lifecycle.DefaultLifecycleObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import com.tencent.smtt.export.external.interfaces.JsPromptResult 11 | import com.tencent.smtt.export.external.interfaces.JsResult 12 | import com.tencent.smtt.export.external.interfaces.SslError 13 | import com.tencent.smtt.export.external.interfaces.SslErrorHandler 14 | import com.tencent.smtt.export.external.interfaces.WebResourceRequest 15 | import com.tencent.smtt.export.external.interfaces.WebResourceResponse 16 | import com.tencent.smtt.sdk.CookieManager 17 | import com.tencent.smtt.sdk.WebChromeClient 18 | import com.tencent.smtt.sdk.WebSettings 19 | import com.tencent.smtt.sdk.WebView 20 | import com.tencent.smtt.sdk.WebViewClient 21 | import github.leavesczy.robustwebview.JsInterface 22 | import github.leavesczy.robustwebview.utils.log 23 | import java.io.File 24 | 25 | /** 26 | * @Author: leavesCZY 27 | * @Date: 2021/9/20 22:45 28 | * @Desc: 29 | * @Github:https://github.com/leavesCZY 30 | */ 31 | interface WebViewListener { 32 | 33 | fun onProgressChanged(webView: RobustWebView, progress: Int) { 34 | 35 | } 36 | 37 | fun onReceivedTitle(webView: RobustWebView, title: String) { 38 | 39 | } 40 | 41 | fun onPageFinished(webView: RobustWebView, url: String) { 42 | 43 | } 44 | 45 | } 46 | 47 | class RobustWebView(context: Context, attributeSet: AttributeSet? = null) : 48 | WebView(context, attributeSet) { 49 | 50 | private val baseCacheDir by lazy { 51 | File(context.cacheDir, "webView") 52 | } 53 | 54 | private val databaseCachePath by lazy { 55 | File(baseCacheDir, "databaseCache").absolutePath 56 | } 57 | 58 | private val appCachePath by lazy { 59 | File(baseCacheDir, "appCache").absolutePath 60 | } 61 | 62 | var hostLifecycleOwner: LifecycleOwner? = null 63 | 64 | var webViewListener: WebViewListener? = null 65 | 66 | private val mWebChromeClient = object : WebChromeClient() { 67 | 68 | override fun onProgressChanged(webView: WebView, newProgress: Int) { 69 | super.onProgressChanged(webView, newProgress) 70 | log("onProgressChanged-$newProgress") 71 | webViewListener?.onProgressChanged(this@RobustWebView, newProgress) 72 | } 73 | 74 | override fun onReceivedTitle(webView: WebView, title: String?) { 75 | super.onReceivedTitle(webView, title) 76 | log("onReceivedTitle-$title") 77 | webViewListener?.onReceivedTitle(this@RobustWebView, title ?: "") 78 | } 79 | 80 | override fun onJsAlert( 81 | webView: WebView, 82 | url: String?, 83 | message: String?, 84 | result: JsResult 85 | ): Boolean { 86 | log("onJsAlert: $webView $message") 87 | return super.onJsAlert(webView, url, message, result) 88 | } 89 | 90 | override fun onJsConfirm( 91 | webView: WebView, 92 | url: String?, 93 | message: String?, 94 | result: JsResult 95 | ): Boolean { 96 | log("onJsConfirm: $url $message") 97 | return super.onJsConfirm(webView, url, message, result) 98 | } 99 | 100 | override fun onJsPrompt( 101 | webView: WebView, 102 | url: String?, 103 | message: String?, 104 | defaultValue: String?, 105 | result: JsPromptResult? 106 | ): Boolean { 107 | log("onJsPrompt: $url $message $defaultValue") 108 | return super.onJsPrompt(webView, url, message, defaultValue, result) 109 | } 110 | } 111 | 112 | private val mWebViewClient = object : WebViewClient() { 113 | 114 | private var startTime = 0L 115 | 116 | override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean { 117 | webView.loadUrl(url) 118 | return true 119 | } 120 | 121 | override fun onPageStarted(webView: WebView, url: String?, favicon: Bitmap?) { 122 | super.onPageStarted(webView, url, favicon) 123 | startTime = System.currentTimeMillis() 124 | } 125 | 126 | override fun onPageFinished(webView: WebView, url: String?) { 127 | super.onPageFinished(webView, url) 128 | log("onPageFinished-$url") 129 | webViewListener?.onPageFinished(this@RobustWebView, url ?: "") 130 | log("onPageFinished duration: " + (System.currentTimeMillis() - startTime)) 131 | } 132 | 133 | override fun onReceivedSslError( 134 | webView: WebView, 135 | handler: SslErrorHandler?, 136 | error: SslError? 137 | ) { 138 | log("onReceivedSslError-$error") 139 | super.onReceivedSslError(webView, handler, error) 140 | } 141 | 142 | override fun shouldInterceptRequest(webView: WebView, url: String): WebResourceResponse? { 143 | return super.shouldInterceptRequest(webView, url) 144 | } 145 | 146 | override fun shouldInterceptRequest( 147 | webView: WebView, 148 | request: WebResourceRequest 149 | ): WebResourceResponse? { 150 | return WebViewInterceptRequestProxy.shouldInterceptRequest(request) 151 | ?: super.shouldInterceptRequest(webView, request) 152 | } 153 | } 154 | 155 | init { 156 | webViewClient = mWebViewClient 157 | webChromeClient = mWebChromeClient 158 | initWebViewSettings(this) 159 | initWebViewSettingsExtension(this) 160 | addJavascriptInterface(JsInterface(), "android") 161 | setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> 162 | log( 163 | "setDownloadListener: $url \n" + 164 | "$userAgent \n " + 165 | " $contentDisposition \n" + 166 | " $mimetype \n" + 167 | " $contentLength" 168 | ) 169 | } 170 | } 171 | 172 | fun toLoadUrl(url: String, cookie: String) { 173 | val mCookieManager = CookieManager.getInstance() 174 | mCookieManager?.setCookie(url, cookie) 175 | mCookieManager?.flush() 176 | loadUrl(url) 177 | } 178 | 179 | fun toGoBack(): Boolean { 180 | if (canGoBack()) { 181 | goBack() 182 | return false 183 | } 184 | return true 185 | } 186 | 187 | private fun initWebViewSettings(webView: WebView) { 188 | val settings = webView.settings 189 | // settings.userAgentString = "android-leavesCZY" 190 | settings.javaScriptEnabled = true 191 | settings.pluginsEnabled = true 192 | settings.useWideViewPort = true 193 | settings.loadWithOverviewMode = true 194 | settings.setSupportZoom(false) 195 | settings.builtInZoomControls = false 196 | settings.displayZoomControls = false 197 | settings.allowFileAccess = true 198 | settings.allowContentAccess = true 199 | settings.loadsImagesAutomatically = true 200 | settings.safeBrowsingEnabled = false 201 | settings.domStorageEnabled = true 202 | settings.databaseEnabled = true 203 | settings.databasePath = databaseCachePath 204 | settings.setAppCacheEnabled(true) 205 | settings.setAppCachePath(appCachePath) 206 | settings.cacheMode = WebSettings.LOAD_DEFAULT 207 | settings.javaScriptCanOpenWindowsAutomatically = true 208 | settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW 209 | } 210 | 211 | private fun initWebViewSettingsExtension(webView: WebView) { 212 | val settingsExtension = webView.settingsExtension ?: return 213 | //开启后前进后退将不再重新加载页面 214 | settingsExtension.setContentCacheEnable(true) 215 | //对于刘海屏机器如果WebView被遮挡会自动padding 216 | settingsExtension.setDisplayCutoutEnable(true) 217 | settingsExtension.setDayOrNight(true) 218 | } 219 | 220 | override fun onAttachedToWindow() { 221 | super.onAttachedToWindow() 222 | log("onAttachedToWindow : $context") 223 | (hostLifecycleOwner ?: findLifecycleOwner(context))?.let { 224 | addHostLifecycleObserver(it) 225 | } 226 | } 227 | 228 | private fun findLifecycleOwner(context: Context): LifecycleOwner? { 229 | if (context is LifecycleOwner) { 230 | return context 231 | } 232 | if (context is MutableContextWrapper) { 233 | val baseContext = context.baseContext 234 | if (baseContext is LifecycleOwner) { 235 | return baseContext 236 | } 237 | } 238 | return null 239 | } 240 | 241 | private fun addHostLifecycleObserver(lifecycleOwner: LifecycleOwner) { 242 | log("addLifecycleObserver") 243 | lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { 244 | override fun onResume(owner: LifecycleOwner) { 245 | onHostResume() 246 | } 247 | 248 | override fun onPause(owner: LifecycleOwner) { 249 | onHostPause() 250 | } 251 | 252 | override fun onDestroy(owner: LifecycleOwner) { 253 | onHostDestroy() 254 | } 255 | }) 256 | } 257 | 258 | private fun onHostResume() { 259 | log("onHostResume") 260 | onResume() 261 | } 262 | 263 | private fun onHostPause() { 264 | log("onHostPause") 265 | onPause() 266 | } 267 | 268 | private fun onHostDestroy() { 269 | log("onHostDestroy") 270 | release() 271 | } 272 | 273 | private fun release() { 274 | hostLifecycleOwner = null 275 | webViewListener = null 276 | webChromeClient = null 277 | webViewClient = null 278 | (parent as? ViewGroup)?.removeView(this) 279 | destroy() 280 | } 281 | 282 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/base/WebViewCacheHolder.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview.base 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.MutableContextWrapper 6 | import android.os.Looper 7 | import github.leavesczy.robustwebview.utils.log 8 | import java.util.Stack 9 | 10 | /** 11 | * @Author: leavesCZY 12 | * @Date: 2021/10/4 18:57 13 | * @Desc: 14 | * @Github:https://github.com/leavesCZY 15 | */ 16 | object WebViewCacheHolder { 17 | 18 | private val webViewCacheStack = Stack() 19 | 20 | private const val CACHED_WEB_VIEW_MAX_NUM = 4 21 | 22 | private lateinit var application: Application 23 | 24 | fun init(application: Application) { 25 | this.application = application 26 | prepareWebView() 27 | } 28 | 29 | fun prepareWebView() { 30 | if (webViewCacheStack.size < CACHED_WEB_VIEW_MAX_NUM) { 31 | Looper.myQueue().addIdleHandler { 32 | log("WebViewCacheStack Size: " + webViewCacheStack.size) 33 | if (webViewCacheStack.size < CACHED_WEB_VIEW_MAX_NUM) { 34 | webViewCacheStack.push(createWebView(MutableContextWrapper(application))) 35 | } 36 | false 37 | } 38 | } 39 | } 40 | 41 | fun acquireWebViewInternal(context: Context): RobustWebView { 42 | if (webViewCacheStack.isEmpty()) { 43 | return createWebView(context) 44 | } 45 | val webView = webViewCacheStack.pop() 46 | val contextWrapper = webView.context as MutableContextWrapper 47 | contextWrapper.baseContext = context 48 | return webView 49 | } 50 | 51 | private fun createWebView(context: Context): RobustWebView { 52 | return RobustWebView(context) 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/base/WebViewInitTask.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview.base 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.tencent.smtt.export.external.TbsCoreSettings 6 | import com.tencent.smtt.sdk.QbSdk 7 | import github.leavesczy.robustwebview.utils.log 8 | 9 | /** 10 | * @Author: leavesCZY 11 | * @Date: 2021/9/20 23:47 12 | * @Desc: 13 | * @Github:https://github.com/leavesCZY 14 | */ 15 | object WebViewInitTask { 16 | 17 | fun init(application: Application) { 18 | initWebView(application) 19 | WebViewCacheHolder.init(application) 20 | WebViewInterceptRequestProxy.init(application) 21 | } 22 | 23 | private fun initWebView(context: Context) { 24 | QbSdk.setDownloadWithoutWifi(true) 25 | val map = mutableMapOf() 26 | map[TbsCoreSettings.TBS_SETTINGS_USE_PRIVATE_CLASSLOADER] = true 27 | map[TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER] = true 28 | map[TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE] = true 29 | QbSdk.initTbsSettings(map) 30 | val callback = object : QbSdk.PreInitCallback { 31 | override fun onViewInitFinished(isX5Core: Boolean) { 32 | log("onViewInitFinished: $isX5Core") 33 | } 34 | 35 | override fun onCoreInitFinished() { 36 | log("onCoreInitFinished") 37 | } 38 | } 39 | QbSdk.initX5Environment(context, callback) 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/base/WebViewInterceptRequestProxy.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview.base 2 | 3 | import android.app.Application 4 | import com.chuckerteam.chucker.api.ChuckerCollector 5 | import com.chuckerteam.chucker.api.ChuckerInterceptor 6 | import com.tencent.smtt.export.external.interfaces.WebResourceRequest 7 | import com.tencent.smtt.export.external.interfaces.WebResourceResponse 8 | import github.leavesczy.robustwebview.utils.log 9 | import okhttp3.Cache 10 | import okhttp3.Interceptor 11 | import okhttp3.OkHttpClient 12 | import okhttp3.Request 13 | import java.io.File 14 | 15 | /** 16 | * @Author: leavesCZY 17 | * @Date: 2021/10/4 18:56 18 | * @Desc: 19 | * @Github:https://github.com/leavesCZY 20 | */ 21 | object WebViewInterceptRequestProxy { 22 | 23 | private lateinit var application: Application 24 | 25 | private val webViewResourceCacheDir by lazy { 26 | File(application.cacheDir, "RobustWebView") 27 | } 28 | 29 | private val okHttpClient by lazy { 30 | OkHttpClient.Builder().cache(Cache(webViewResourceCacheDir, 600L * 1024 * 1024)) 31 | .followRedirects(false) 32 | .followSslRedirects(false) 33 | .addInterceptor(getChuckerInterceptor(application = application)) 34 | .addNetworkInterceptor(getWebViewCacheInterceptor()) 35 | .build() 36 | } 37 | 38 | private fun getChuckerInterceptor(application: Application): Interceptor { 39 | return ChuckerInterceptor.Builder(application) 40 | .collector(ChuckerCollector(application)) 41 | .maxContentLength(250000L) 42 | .alwaysReadResponseBody(true) 43 | .build() 44 | } 45 | 46 | private fun getWebViewCacheInterceptor(): Interceptor { 47 | return Interceptor { chain -> 48 | val request = chain.request() 49 | val response = chain.proceed(request) 50 | response.newBuilder() 51 | .removeHeader("pragma") 52 | .removeHeader("Cache-Control") 53 | .header("Cache-Control", "max-age=" + (360L * 24 * 60 * 60)) 54 | .build() 55 | } 56 | } 57 | 58 | fun init(application: Application) { 59 | this.application = application 60 | } 61 | 62 | fun shouldInterceptRequest(webResourceRequest: WebResourceRequest): WebResourceResponse? { 63 | if (toProxy(webResourceRequest)) { 64 | return getHttpResource(webResourceRequest) 65 | } 66 | return null 67 | } 68 | 69 | private fun toProxy(webResourceRequest: WebResourceRequest): Boolean { 70 | if (webResourceRequest.isForMainFrame) { 71 | return false 72 | } 73 | val url = webResourceRequest.url ?: return false 74 | if (!webResourceRequest.method.equals("GET", true)) { 75 | return false 76 | } 77 | if (url.scheme == "https" || url.scheme == "http") { 78 | val urlString = url.toString() 79 | if (urlString.endsWith(".js", true) || 80 | urlString.endsWith(".css", true) || 81 | urlString.endsWith(".jpg", true) || 82 | urlString.endsWith(".png", true) || 83 | urlString.endsWith(".webp", true) || 84 | urlString.endsWith(".awebp", true) 85 | ) { 86 | return true 87 | } 88 | } 89 | return false 90 | } 91 | 92 | private fun getHttpResource(webResourceRequest: WebResourceRequest): WebResourceResponse? { 93 | try { 94 | val url = webResourceRequest.url.toString() 95 | val requestBuilder = Request 96 | .Builder() 97 | .url(url) 98 | .method(webResourceRequest.method, null) 99 | webResourceRequest.requestHeaders?.forEach { 100 | requestBuilder.addHeader(it.key, it.value) 101 | } 102 | val response = okHttpClient 103 | .newCall(requestBuilder.build()) 104 | .execute() 105 | val body = response.body 106 | val code = response.code 107 | if (body == null || code != 200) { 108 | return null 109 | } 110 | val mimeType = response.header("content-type", body.contentType()?.type) 111 | val encoding = response.header("content-encoding", "utf-8") 112 | val responseHeaders = buildMap { 113 | response.headers.map { 114 | put(it.first, it.second) 115 | } 116 | } 117 | var message = response.message 118 | if (message.isBlank()) { 119 | message = "OK" 120 | } 121 | val resourceResponse = WebResourceResponse(mimeType, encoding, body.byteStream()) 122 | resourceResponse.responseHeaders = responseHeaders 123 | resourceResponse.setStatusCodeAndReasonPhrase(code, message) 124 | return resourceResponse 125 | } catch (e: Throwable) { 126 | e.printStackTrace() 127 | } 128 | return null 129 | } 130 | 131 | private fun getAssetsImage(url: String): WebResourceResponse? { 132 | if (url.contains(".jpg")) { 133 | try { 134 | val inputStream = application.assets.open("ic_launcher.webp") 135 | return WebResourceResponse( 136 | "image/webp", 137 | "utf-8", inputStream 138 | ) 139 | } catch (e: Throwable) { 140 | log("Throwable: $e") 141 | } 142 | } 143 | return null 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/utils/ContextHolder.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview.utils 2 | 3 | import android.app.Application 4 | 5 | /** 6 | * @Author: leavesCZY 7 | * @Date: 2021/10/1 23:16 8 | * @Desc: 9 | * @Github:https://github.com/leavesCZY 10 | */ 11 | object ContextHolder { 12 | 13 | lateinit var application: Application 14 | private set 15 | 16 | fun init(application: Application) { 17 | this.application = application 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/github/leavesczy/robustwebview/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package github.leavesczy.robustwebview.utils 2 | 3 | import android.os.Build 4 | import android.util.Log 5 | import android.view.View 6 | import android.view.ViewParent 7 | import android.widget.Toast 8 | import java.lang.reflect.Method 9 | 10 | /** 11 | * @Author: leavesCZY 12 | * @Date: 2021/9/20 0:11 13 | * @Desc: 14 | * @Github:https://github.com/leavesCZY 15 | */ 16 | fun log(log: Any?) { 17 | Log.e("RobustWebView-" + Thread.currentThread().name, log.toString()) 18 | } 19 | 20 | fun showToast(msg: String) { 21 | Toast.makeText(ContextHolder.application, msg, Toast.LENGTH_SHORT).show() 22 | } 23 | 24 | /** 25 | * 让 activity transition 动画过程中可以正常渲染页面 26 | */ 27 | fun setDrawDuringWindowsAnimating(view: View) { 28 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M 29 | || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 30 | ) { 31 | //小于 4.3 和大于 6.0 时不存在此问题,无须处理 32 | return 33 | } 34 | try { 35 | val rootParent: ViewParent = view.rootView.parent 36 | val method: Method = rootParent.javaClass 37 | .getDeclaredMethod("setDrawDuringWindowsAnimating", Boolean::class.javaPrimitiveType) 38 | method.isAccessible = true 39 | method.invoke(rootParent, true) 40 | } catch (e: Throwable) { 41 | e.printStackTrace() 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 |