N_Android

Web View

如果您希望在客户端应用中提供 Web 应用 (或只是网页) , 则可以使用 WebView 执行该操作.WebView 类是 Android 的 View 类的扩展, 可让您将网页显示为 Activity 布局的一部分.它不会包含功能全面的网络浏览器的任何功能, 例如导航控件或地址栏.WebView 默认只显示网页.

使用 WebView 非常有用的一种常见情形是, 您希望在应用中提供可能需要更新的信息, 例如最终用户协议或用户指南.在 Android 应用中, 您可以创建一个包含 WebView 的 Activity, 然后使用它来显示在线托管的文档.

另一种 WebView 可能会有所帮助的情形是, 如果您的应用向用户提供始终需要互联网连接才能检索数据的数据 (例如电子邮件) 在这种情况下, 您可能会发现相比于执行网络请求, 然后解析数据并在 Android 布局中呈现数据, 在 Android 应用中编译 WebView 以显示包含所有用户数据的网页更加轻松.您可以改为设计一个专为 Android 设备定制的网页, 然后在加载该网页的 Android 应用中实现 WebView.

管理 WebView 对象 - https://developer.android.google.cn/guide/webapps/managing-webview?hl=zh_cn 基于网络的内容 - https://developer.android.google.cn/guide/webapps?hl=zh_cn

实例

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
 
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
 
    <application
        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:networkSecurityConfig="@xml/network_security_config"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Ffmpegmake"
        tools:targetApi="31">
 
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/title_activity_main"
            android:theme="@style/Theme.Ffmpegmake">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

webview 访问网络的白名单

android:networkSecurityConfig="@xml/network_security_config"

network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">tianditu.gov.cn</domain>
        <domain includeSubdomains="true">192.168.20.130</domain>
    </domain-config>
</network-security-config>

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:fitsSystemWindows="true"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
 
    tools:context=".MainActivity">
 
    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
 
</androidx.coordinatorlayout.widget.CoordinatorLayout>

MainActivity

package org.yang.webrtc.ffmpeg.make
 
import android.os.Bundle
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
 
class MainActivity : AppCompatActivity() {
 
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main);
        val webView = findViewById<WebView>(R.id.web_view)
        WebView.setWebContentsDebuggingEnabled(true)
        webView.settings.apply {
            setSupportZoom(false)
            displayZoomControls = false
            builtInZoomControls = false
            useWideViewPort = false
            javaScriptEnabled = true
            loadWithOverviewMode = true
            defaultTextEncodingName = "UTF-8"
            domStorageEnabled = true
            allowContentAccess = true
        }
        webView.loadUrl("http://127.0.0.1/android/index_webrtc_test.html")
    }
 
}

加载本地 html

android_asset 方式

将 html 资源文件 放入\src\main\assets\sample

//web 视图 初始化
WebView web_view = (WebView)findViewById(R.id.web_view);
WebSettings webSettings = web_view.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); //设置可以访问文件, 即本地HTML assets
webSettings.setJavaScriptCanOpenWindowsAutomatically(true); //支持通过JS打开新窗口 
webSettings.setLoadsImagesAutomatically(true); //支持自动加载图片
webSettings.setDefaultTextEncodingName("utf-8");//设置编码格式
web_view.loadUrl("file:///android_asset/sample/sample.html");

WebViewAssetLoader 安全域名方式

private fun createWebView(onPageFinished: Consumer<WebView>) {
	val webView = findViewById<WebView>(R.id.webView)
	WebView.setWebContentsDebuggingEnabled(true)
	webView.settings.apply {
		setSupportZoom(false)
		displayZoomControls = false
		builtInZoomControls = false
		useWideViewPort = false
		javaScriptEnabled = true
		loadWithOverviewMode = true
		defaultTextEncodingName = "UTF-8"
		domStorageEnabled = true
		allowContentAccess = true
	}
	// 使用 WebViewAssetLoader 安全加载
	val assetLoader = WebViewAssetLoader.Builder()
		.setDomain("xxxx.cn")
		.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context))
		.build()
	webView.webViewClient = object : WebViewClient() {
		override fun shouldInterceptRequest(
			view: WebView,
			request: WebResourceRequest
		): WebResourceResponse? {
			return assetLoader.shouldInterceptRequest(request.url)
		}
		override fun onPageFinished(view: WebView, url: String) { //这里不行 会和 onResume 重复通知
			if (firstLoad) {
				onPageFinished.accept(webView)
				firstLoad = false; // 标记第一次加载已完成
			}
		}
		override fun onReceivedSslError(
			view: WebView?,
			handler: SslErrorHandler?,
			error: SslError?) {
			XLog.e("web onReceivedSslError ", error)
			super.onReceivedSslError(view, handler, error)
		}
		override fun onReceivedError(
			view: WebView?,
			errorCode: Int,
			description: String,
			failingUrl: String?) {
			XLog.e("web onReceivedError %s, %s", failingUrl, description)
		}
	}
	interactiveMediation = Optional.of(
		WebInteractiveFactory.create(webView, this)
	)
	//正式
	webView.loadUrl("https://xxxx.cn/assets/html/dist-product/index.html")
}

WebView 的状态

//激活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();
 

Java & Javascript交互

/**
 * Created by yangfh on 2019/6/20.
 */
public class ContainerJsOption {
 
    private final WebView web;
 
    public ContainerJsOption(WebView web) {
        this.web = web;
    }
 
    public void removeTakeListView(SampleBean bean){
        web.evaluateJavascript("javascript:removeTakeListView("+bean.getId()+")", (String value) ->{
        });
    }
 
    public void removeReturnListView(SampleBean bean){
        web.evaluateJavascript("javascript:removeReturnListView("+bean.getId()+")", (String value) ->{
        });
    }
 
    public void removeViewByID(SampleBean bean){
        web.evaluateJavascript("javascript:removeViewByID("+bean.getId()+")", (String value) ->{
        });
    }
}
 
/**
 @android.webkit.JavascriptInterface 注解表明该方法, 可以被js调用
*/
public class ContainerMediator {
    @JavascriptInterface
    public String getLocalReturnList(){
        Collection<SampleBean> beans = this.container.getCopyOfLocalReturnList();
        return JacksonUtil.beanToJson(beans);
    }
    @JavascriptInterface
    public String getLocalTakeList() {
        Collection<SampleBean> beans = this.container.getCopyOfLocalTakeList();
        return JacksonUtil.beanToJson(beans);
    }
}

注入 Java 对象

加载时, 注入一个全局 Container js 对象, 与java交互

WebView web_view = (WebView)findViewById(R.id.web_view);
ContainerJsOption jsopt = new ContainerJsOption(web_view);
containerMediator = new ContainerMediator(Container.getInstance(), jsopt);
web_view.addJavascriptInterface(containerMediator,"Container");
web_view.loadUrl("file:///android_asset/sample/list.html");

调用 Javascript 函数

WebView web_view = (WebView)findViewById(R.id.web_view);
web_view.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
	    //此处为 js 返回的结果, 且只能传递字符串
    }
});

二进制数据分段

在传输文件时, 比如: 视频, 前端 js 占用内存太多, 会导致页面卡顿, 可以通过分段的方式读取

	/**
     * 由js分段读取, 防止内存太大
     **/
     WIM._storageBinaryReader = function(topic){
      function append(allBinary, off, arr){
        for (let index = off; index < allBinary.length; index++) {
            allBinary[index] = arr.charCodeAt(index-off);
        }
      }
      /**
       * @param off 文件总偏移量
       * @param capacity 缓冲大小
       */
      var frist = WIM.storageBinaryReaderFragment(topic, 0, 0);
      // console.log("WIM_WEB_518, storageBinaryReaderFragment frist = ", frist);
      if (!frist) {  return  }
      var fj = JSON.parse(frist)
      /**  meta 结构
      {
        total: 文件总大小
        size: 本次读取的大小
        data: 数据 base64编码
      }*/
      const total = fj.total
      const capacity = 1024 * 1024 * 2
      let off = 0
      const allBinary = new Uint8Array(total); 
      while(off < total){
        var meta =WIM.storageBinaryReaderFragment(topic, off, capacity);
        var metaJson = JSON.parse(meta)
        // console.log("WIM_WEB_518, storageBinaryReaderFragment metaJson = ", metaJson);
        var binaryData = atob(metaJson.data);
        append(allBinary, off, binaryData)
        off += metaJson.size
      }
      var blob = new Blob([allBinary], { type: 'application/octet-stream' });
      return blob;
    }
 
    WIM._storageBinaryWriter = function(topic, content){
      // console.log("WIM_WEB_518, _storageBinaryWriter topic = ", topic, content.length);
      WIM.storageBinaryWriter(topic, content)
    }
 
@JavascriptInterface
public synchronized String storageBinaryReaderFragment(String topic, long off, int capacity) {
	File storageFile = getStorageFile(topic);
	if (!storageFile.exists()) {
		return null;
	}
	try {
		RandomAccessFile ras = new RandomAccessFile(storageFile, "r");
		ras.seek(off);
		byte[] binaryArray = new byte[capacity];
		int size = ras.read(binaryArray);
		String encodeToString = Base64.encodeToString(binaryArray, 0, size, Base64.DEFAULT);
		JSONObject result = new JSONObject();
		result.put("total", storageFile.length());
		result.put("size", size);
		result.put("data", encodeToString);
		return result.toString();
	} catch (Exception e) {
		XLog.e(TAG+ "Exception storageBinaryReaderFragment: ", e);
	}
	return null;
}
 
@JavascriptInterface
public synchronized boolean storageBinaryWriter(String topic, String content) {
	File storageFile = getStorageFile(topic);
	try {
		FileUtil.writeBytes(content.getBytes(Charsets.UTF_8), storageFile);
		return true;
	} catch (Exception e) {
		XLog.e(TAG+ "Exception storageBinaryWriter: ", e);
	}
	return false;
}

重定向 Console

提供实现 onConsoleMessage() 方法的 WebChromeClient,以便控制台消息显示在 Logcat 中。然后,使用 setWebChromeClient() 将 WebChromeClient 应用于 WebView。如需了解详情,请参阅 Webview 文档。

val myWebView: WebView = findViewById(R.id.webview)
myWebView.webChromeClient = object : WebChromeClient() {
 
    override fun onConsoleMessage(message: ConsoleMessage): Boolean {
        Log.d("MyApplication", "${message.message()} -- From line " +
              "${message.lineNumber()} of ${message.sourceId()}")
        return true
    }
}

远程调试

https://developer.chrome.com/docs/devtools/remote-debugging/?hl=zh-cn https://developer.chrome.com/docs/devtools/remote-debugging/webviews?hl=zh-cn

1.配置 WebView 以进行调试

必须在您的应用中启用 WebView 调试。如需启用 WebView 调试,请对 WebView 类调用静态方法 setWebContentsDebuggingEnabled

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
  WebView.setWebContentsDebuggingEnabled(true);
}

此设置适用于应用的所有 WebView。

若一些特殊情况, 使用的是XWalkView, 可使用如下命令 XWalkPreferences.setValue(XWalkPreferences.REMOTE_DEBUGGING, true);

2.桌面Chrome 浏览器

Chrome浏览器地址栏中输入 chrome://inspect/#devices 命令并回车列出所有可调试界面

chrome://inspect/#devices

点击inspect, 打开调试工具, 开始调式

踩坑指南

自动旋转 Activity 被销毁!?

<activity
		 android:name=".aar.web.activity.WebActivity"
		 android:label="@string/title_activity_main"
		 android:theme="@style/Base.Theme.MyApplication"
		 android:screenOrientation="portrait"
             android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|density|layoutDirection|colorMode"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>

当设备从纵向旋转到横向(或反之)时,系统会销毁并重新创建Activity如果调用外部的Activity 旋转了, 也会导致调用方 (WebActivity) 被销毁的;

  • android:configChanges 属性用于告诉系统,当指定的配置变化发生时,当前的 Activity 不需要被销毁并重新创建,而是由 Activity 本身来处理这些变化。

404 或者空白页

原因是chrome 需要下载依赖插件; 然而地址被墙了; SO…(挂代理)

加载资源时 ERR_FILE_NOT_FOUND

net::ERR_FILE_NOT_FOUND; 注意路径, rebuild 一下, 有缓存

加载非https

对于以 Android 6.0(API 级别 23)及更高版本为目标平台的应用,仅 HTTPS 等安全的起点支持 Geolocation API。 如果不调用相应的 onGeolocationPermissionsShowPrompt() 方法,非安全起点向 Geolocation API 发出的任何请求都会被自动拒绝。

配置 android:networkSecurityConfig 属性即可 AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
 
 
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher_round"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        <!-- 注意这里只是引用配置 network_security_config-->
        android:networkSecurityConfig="@xml/network_security_config"
        android:theme="@style/Theme.MyApplication"
        tools:targetApi="31">
 

实际配置 network_security_config 白名单 src\main\res\xml\network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">192.168.20.130</domain>
        <domain includeSubdomains="true">192.168.20.81</domain>
        <domain includeSubdomains="true">192.168.20.82</domain>
        <!-- Add other domains if needed -->
    </domain-config>
</network-security-config>
 

忽略https证书验证

 private void initWebView() {
        XLog.i(TAG+ " initWebView , getVersionName = "+getVersionName());
        webView = findViewById(R.id.webView);
 
        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);
        webSettings.setDefaultTextEncodingName("UTF-8");
        webSettings.setDomStorageEnabled(true);
        webSettings.setAllowFileAccess(true);
        WIM = initJavaScriptEngineEnvironment(webView);
        /**
         *  设置 WebViewClient 来监测页面加载状态
         */
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(WebView view, String url) {
                if (isFirstLoading) {
                    isFirstLoading = false;
                }
            }
//            允许SSL 错误
            @Override
            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                handler.proceed();
            }
        });
 
        mHandler.post(() -> {
            webView.loadUrl("file:///android_asset/html/dist/index.html");
            // 测试
            WebView.setWebContentsDebuggingEnabled(true);
//            webView.loadUrl("http://192.168.20.130/android/index.html");
//            webView.loadUrl("http://192.168.20.36:8080");
//            webView.loadUrl("file:///android_asset/html/test/index.html");
        });
    }
 

混合开发 (MixApp)

Vue3 + Element Plus + axios 安卓源码

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <script src="./js/axios/axios.min.js"></script>
  <script src="./js/vue/vue@3.2.26.js"></script>
</head>
<style>
  html,body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    overflow: hidden;
  }
  #app {
    padding: 15px;
    width: 100%;
    height: 100%;
    flex-direction: column;
  }
</style>
<body>
  <div id="app">
    <div>
      <button @click="clickEvent">clickEvent</button>
      <button @click="clickRequset">clickRequset</button>
    </div>
 
    <div>
      <div>data.count: {{data.count}} </div>
      <div> {{data.res}}</div>
    </div>
  </div>
</body>
 
<script type="module" >
  import request from './js/utils/request.js'
  const app = Vue.createApp(...);
</script>
 
</html>