Android WebView & H5 Hybrid 混和开辟

打印 上一主题 下一主题

主题 884|帖子 884|积分 2652

对于故乡,我忽然有了新的理解:人的故乡,并不止于一块特定的土地,而是一种广阔无比的心情,不受空间和时间的限制;这心情一经唤起,就是你已经回到了故乡。——《记忆与印象》
  前言

移动互联网发展至今,Android开辟模式在不断更迭, 目前主要有三种开辟模式 :原生开辟、Hybrid开辟以及跨平台开辟。


  • 原生开辟: 移动终端的开辟主要分为两大阵营, Android(Java、Kotlin) 研发与 IOS(Swift)研发。
  • Hybrid开辟: 多种技术栈混淆开辟App, 在Android中主要指Native与前端(JavaScript)技术的混淆开辟方式。
  • 跨平台研发: 同一个技术栈, 同一套代码可以在差别的终端上运行,极大的缩减了研发成本, 好比当下比较火的Flutter。
起首,我们需要做一些准备工作:为应用添加一个启用了 JavaScript 的 WebView,声明 INTERNET 权限(WebView 需此权限才华加载页面,纵然页面内容为当地资源),在 Assets 资源文件夹中放置页面并加载。
Layout
  1.     ...
  2.     <WebView
  3.         android:id="@+id/webview"
  4.         android:layout_width="match_parent"
  5.         android:layout_height="match_parent"
  6.     />
  7.     ...
复制代码
XML
Manifest
  1.     <manifest ... >
  2.         <uses-permission android:name="android.permission.INTERNET" />
  3.         ...
  4.     </manifest>
复制代码
XML
MainActivity
  1. import android.annotation.SuppressLint;
  2. import android.os.Bundle;
  3. import android.webkit.WebView;
  4.     ...
  5.     @SuppressLint("SetJavaScriptEnabled")
  6.     @Override
  7.     protected void onCreate(Bundle savedInstanceState) {
  8.         WebView mWebView = findViewById(R.id.webview);
  9.         mWebView.getSettings().setDefaultTextEncodingName("utf-8");
  10.         mWebView.getSettings().setJavaScriptEnabled(true);
  11.         mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to load the files in the assets folder
  12.     }
  13.     ...
复制代码
WebView & H5 Hybrid混淆开辟基础知识

H5 Runtime支撑 - 欣赏器内核

对于Java来说, 最大的一个长处是build once run anywhere(一处编译处处运行), 这一长处主要是通过JVM在差别的平台表明执行(在Android端利用的是基于JVM针对低性能小内存的设备优化的dalvik和art假造机)。
对于前端技术栈来说, Runtime依赖欣赏器的支持, 欣赏器主要依赖内核驱动,内核的两个主要功能一个是界面渲染, 一个是JavaScript 引擎(JS语法剖析),当前的主流欣赏器以及内核:
欣赏器渲染内核JS引擎IE/Edge(微软)Trident; EdgeHtmlJScript; ChakraSafari(苹果)Webkit/Webkit2JavaScripCore/Nitro(4+)Chrome(Google)Chromium(Webkit);BlinkV8FireFoxGeckoSpiderMonkey(❤️.0);TackMonkey(<4.0);JaegerMonkey(4.0+)OperaPresto;BlinkFuthark(9.5-10.2);CaraKan(10.5) Chromium 是 Google 公司一个开源欣赏器项目,利用 Blink 渲染引擎,V8 是 Blink 内置的JavaScript 引擎, Android端的WebView是基于Chromium的移动端欣赏器组件。当前Android和IOS移动端的欣赏器内核说到底都是基于Webkit。

WebKit主要分为四个部门:


  • 最上层 WebKit Embedding API 是 Browser UI 举行交互的 API 接口
  • 最下层 Platform API 提供与底层驱动的交互,如网络,字体渲染,影音文件解码,渲染引擎等
  • WebCore 它实现了对文档的模型化,包括了 CSS, DOM, Render 等的实现
  • JSCore 是专门处理 JS 脚本的引擎, 以及Hybrid通信支持
WebKit 所包罗的绘制引擎 和 JS引擎,均是从KDE的KHTML及KJS引擎衍生而来。它们都是自由软件,在GPL条约下授权,同时支持BSD体系的开辟。所以Webkit也是自由软件,同时开放源代码。
   KDE: K桌面环境(K Desktop Environment)的缩写。一种著名的运行于 Linux、Unix 以及FreeBSD 等操纵体系上的自由图形桌面环境
  GNU: 通用公共许可协议(英语:GNU General Public License,缩写GNU GPL 或 GPL),是被广泛利用的自由软件许可证,给予了终端用户运行、学习、共享和修改软件的自由。
  BSD: Berkeley Software Distribution,伯克利软件套件,是Unix的衍生体系,在1977至1995年间由加州大学伯克利分校开辟和发布的。
  JSBridge

JSBridge 是一座 Native 与 JavaScript 举行通讯的桥梁,它的焦点是 构建 Native 和 JavaScript 双向通信的通道。

所谓 双向通信的通道:


  • JS 向 Native 发送消息 : 调用相关功能、关照 Native 当前 JS 的相关状态等。
  • Native 向 JS 发送消息 : 回溯调用结果、消息推送、关照 JS 当前 Native 的状态等。
JavascriptInterface

在 Android 和 Web 混淆开辟中,免不了 Java 与 JavaScript 代码相互调用,而 WebView 就给我们提供了这样一个接口:JavascriptInterface
   public abstract @interface JavascriptInterface implements Annotation
  Annotation that allows exposing methods to JavaScript. Starting from API level Build.VERSION_CODES.JELLY_BEAN_MR1 and above, only methods explicitly marked with this annotation are available to the Javascript code.
  简单来说,在 Android 4.2 Jelly Bean(API 17)后,应用需要在方法中声明 @JavascriptInterface 注解,并将其地点类添加到 WebView 中,答应应用内启用了 JavaScript 的 WebView 直接调用其类成员方法。
MainActivity
  1. import android.annotation.SuppressLint;
  2. import android.content.Context;
  3. import android.os.Bundle;
  4. import android.webkit.JavascriptInterface;
  5. import android.webkit.WebView;
  6. import android.widget.Toast;
  7.     ...
  8.     @SuppressLint("StaticFieldLeak")
  9.     private static Context mContext;
  10.     @SuppressLint("SetJavaScriptEnabled")
  11.     @Override
  12.     protected void onCreate(Bundle savedInstanceState) {
  13.         super.onCreate(savedInstanceState);
  14.         setContentView(R.layout.activity_main);
  15.         mContext = getApplicationContext();
  16.         WebView mWebView = findViewById(R.id.webview);
  17.         mWebView.getSettings().setDefaultTextEncodingName("utf-8");
  18.         mWebView.getSettings().setJavaScriptEnabled(true);
  19.         mWebView.addJavascriptInterface(new JavaScriptBridge(), "Android"); // Export class JavaScriptBridge to WebView and map it to window.Android object in JavaScript
  20.         mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to access the assets folder, or use file://android_res/ to access the res folder
  21.     }
  22.     @SuppressWarnings("unused")
  23.     public static class JavaScriptBridge {
  24.         @JavascriptInterface
  25.         public void makeToast(final String message) {
  26.             Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
  27.         }
  28.     }
  29.     ...
复制代码
WebPage
  1. ...
  2. <script type="text/javascript">
  3.     "use strict";
  4.     window.Android.makeToast("Hello world");
  5. </script>
  6. ...
复制代码
HTML
上述示例代码将答应 JavaScript 通过 window.Android 对象,调用 JavaScriptBridge 类中声明白 @JavascriptInterface 注解的 makeToast 方法。运行后表现一个内容为 Hello world 的 Toast。

链接访问拦截

WebViewClient 提供了 shouldOverrideUrlLoading 事件,可以让我们在 URL 加载时做一些变乱,好比拦截某个链接。
   public boolean shouldOverrideUrlLoading (WebView view, WebResourceRequest request)
  Give the host application a chance to take control when a URL is about to be loaded in the current WebView. If a WebViewClient is not provided, by default WebView will ask Activity Manager to choose the proper handler for the URL. If a WebViewClient is provided, returning true causes the current WebView to abort loading the URL, while returning false causes the WebView to continue loading the URL as usual.
  MainActivity
  1. import android.annotation.SuppressLint;
  2. import android.content.Context;
  3. import android.content.Intent;
  4. import android.os.Bundle;
  5. import android.webkit.WebResourceRequest;
  6. import android.webkit.WebView;
  7. import android.webkit.WebViewClient;
  8.     ...
  9.     @SuppressLint("StaticFieldLeak")
  10.     private static Context mContext;
  11.     @SuppressLint("SetJavaScriptEnabled")
  12.     @Override
  13.     protected void onCreate(Bundle savedInstanceState) {
  14.         super.onCreate(savedInstanceState);
  15.         setContentView(R.layout.activity_main);
  16.         mContext = getApplicationContext();
  17.         WebView mWebView = findViewById(R.id.webview);
  18.         mWebView.getSettings().setDefaultTextEncodingName("utf-8");
  19.         mWebView.getSettings().setJavaScriptEnabled(true);
  20.         mWebView.setWebViewClient(new WebViewClient() {
  21.             @Override
  22.             public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
  23.                 if (request.getUrl().toString().equalsIgnoreCase("https://www.google.cn/")) {
  24.                     view.loadUrl("https://www.google.com/ncr");
  25.                     return true;
  26.                 } else if (request.getUrl().toString().startsWith("meowcat://open_settings")) {
  27.                     final Intent intent = mContext.getPackageManager().getLaunchIntentForPackage("com.android.settings");
  28.                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
  29.                     mContext.startActivity(intent);
  30.                     return true;
  31.                 }
  32.                 return false;
  33.             }
  34.         });
  35.         mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to access the assets folder, or use file://android_res/ to access the res folder
  36.     }
  37.     ...
复制代码
上述示例代码将在加载 https://www.google.cn/ 时跳转到 https://www.google.com/ncr*1,或在链接为 meowcat://open_settings 时打开体系设置。
除示例代码外,也可以直接 return true; 来中断页面加载。
注:该方法不适用于 POST 请求,页面在举行表单提交等 POST 请求时不会调用。

在页面内执行外部 JavaScript 代码

出于调试需求,我们大概需要通过 Java 代码在页面内执行一些 JavaScript 代码,利用 loadUrl(String) 或 evaluateJavascript(String, ValueCallback<String>) 方法即可轻松实现该需求。若代码需要在页面加载完毕后执行,WebViewClient 也为我们提供了 onPageFinished 事件。
   public void loadUrl (String url)
  Loads the given URL.
Also see compatibility note on evaluateJavascript(String, ValueCallback).
    public void evaluateJavascript (String script, ValueCallback resultCallback)
  Asynchronously evaluates JavaScript in the context of the currently displayed page. If non-null, resultCallback will be invoked with any result returned from that execution. This method must be called on the UI thread and the callback will be made on the UI thread.
Compatibility note. Applications targeting Build.VERSION_CODES.N or later, JavaScript state from an empty WebView is no longer persisted across navigations like loadUrl(java.lang.String). For example, global variables and functions defined before calling loadUrl(java.lang.String) will not exist in the loaded page. Applications should use addJavascriptInterface(Object, String) instead to persist JavaScript objects across navigations.
    public void onPageFinished (WebView view, String url)
  Notify the host application that a page has finished loading. This method is called only for main frame. Receiving an onPageFinished() callback does not guarantee that the next frame drawn by WebView will reflect the state of the DOM at this point. In order to be notified that the current DOM state is ready to be rendered, request a visual state callback with WebView#postVisualStateCallback and wait for the supplied callback to be triggered.
  MainActivity
  1. import android.annotation.SuppressLint;
  2. import android.content.Context;
  3. import android.os.Bundle;
  4. import android.webkit.WebView;
  5. import android.webkit.WebViewClient;
  6.     ...
  7.     @SuppressLint("StaticFieldLeak")
  8.     private static Context mContext;
  9.     @SuppressLint("SetJavaScriptEnabled")
  10.     @Override
  11.     protected void onCreate(Bundle savedInstanceState) {
  12.         super.onCreate(savedInstanceState);
  13.         setContentView(R.layout.activity_main);
  14.         mContext = getApplicationContext();
  15.         WebView mWebView = findViewById(R.id.webview);
  16.         mWebView.getSettings().setDefaultTextEncodingName("utf-8");
  17.         mWebView.getSettings().setJavaScriptEnabled(true);
  18.         mWebView.setWebViewClient(new WebViewClient() {
  19.             @Override
  20.             public void onPageFinished(WebView view, String url) {
  21.                 if (url.startsWith("https://www.google.")) {
  22.                     view.loadUrl("javascript:(() => {window.location = 'https://www.google.com/ncr';})();");
  23.                     // Equals with
  24.                     // view.evaluateJavascript("window.location = 'https://www.google.com/ncr';", null);
  25.                 }
  26.                 super.onPageFinished(view, url);
  27.             }
  28.         });
  29.         mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to access the assets folder, or use file://android_res/ to access the res folder
  30.     }
  31.     ...
复制代码
上述示例代码将在页面加载完毕后,打开 https://www.google.cn/,而后被 shouldOverrideUrlLoading 方法跳转到 https://www.google.com/ncr。
代码中 loadUrl 与 evaluateJavascript 的示例等价,选用其一即可。
注:若利用 evaluateJavascript 方法的回调功能,则此方法与回调方法都必须在主线程(UI 线程)中执行或声明。

当地资源加载

在上面的示例代码中,我们利用了 file:///android_asset/ 来直接加载 assets 资源文件夹中的资源。但由于一些逼迫执行的安全战略(Content Security Policy)限制,使得该非同源 URL 无法正常被加载,这时间就可以利用 WebViewClient 提供的 shouldInterceptRequest 事件来辅助加载。
   public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)
  Notify the host application of a resource request and allow the application to return the data. If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return response and data will be used.
This callback is invoked for a variety of URL schemes (e.g., http(s):, data:, file:, etc.), not only those schemes which send requests over the network. This is not called for javascript: URLs, blob: URLs, or for assets accessed via file:///android_asset/ or file:///android_res/ URLs.
In the case of redirects, this is only called for the initial resource URL, not any subsequent redirect URLs.
  MainActivity
  1. import android.annotation.SuppressLint;
  2. import android.content.Context;
  3. import android.net.Uri;
  4. import android.os.Bundle;
  5. import android.webkit.JavascriptInterface;
  6. import android.webkit.WebResourceRequest;
  7. import android.webkit.WebResourceResponse;
  8. import android.webkit.WebView;
  9. import android.webkit.WebViewClient;
  10. import android.widget.Toast;
  11. import java.io.IOException;
  12.     ...
  13.     @SuppressLint("StaticFieldLeak")
  14.     private static Context mContext;
  15.     @SuppressLint("SetJavaScriptEnabled")
  16.     @Override
  17.     protected void onCreate(Bundle savedInstanceState) {
  18.         super.onCreate(savedInstanceState);
  19.         setContentView(R.layout.activity_main);
  20.         mContext = getApplicationContext();
  21.         WebView mWebView = findViewById(R.id.webview);
  22.         mWebView.getSettings().setDefaultTextEncodingName("utf-8");
  23.         mWebView.getSettings().setJavaScriptEnabled(true);
  24.         mWebView.addJavascriptInterface(new JavaScriptBridge(), "Android"); // Export class JavaScriptBridge to WebView and map it to window.Android object in JavaScript
  25.         mWebView.setWebViewClient(new WebViewClient() {
  26.             @Override
  27.             public void onPageFinished(WebView view, String url) {
  28.                 view.loadUrl("javascript:(() => {const script = document.createElement('script'); script.src = '/MeowCat-Android-Asset/www/js/main.js'; document.body.append(script);})();");
  29.                 super.onPageFinished(view, url);
  30.             }
  31.             @Override
  32.             public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest webResourceRequest) {
  33.                 String url = webResourceRequest.getUrl().toString();
  34.                 Uri uri = Uri.parse(url);
  35.                 String key = uri.getScheme() + "://" + uri.getHost() + "/MeowCat-Android-Asset/";
  36.                 if (url.contains(key)) {
  37.                     String assetsPath = url.replace(key, "");
  38.                     try {
  39.                         return new WebResourceResponse("text/plain", "UTF-8", getAssets().open(assetsPath));
  40.                     } catch (IOException e) {
  41.                         e.printStackTrace();
  42.                     }
  43.                 }
  44.                 return super.shouldInterceptRequest(view, webResourceRequest);
  45.             }
  46.         });
  47.         mWebView.loadUrl("https://www.google.com/ncr");
  48.     }
  49.     @SuppressWarnings("unused")
  50.     private static class JavaScriptBridge {
  51.         @JavascriptInterface
  52.         public void makeToast(final String message) {
  53.             Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
  54.         }
  55.     }
  56.     ...
复制代码
Main
  1. "use strict";
  2. window.Android.makeToast("Hello world");
复制代码
JavaScript
上述示例代码将打开 https://www.google.com/ncr(因 shouldInterceptRequest 方法不会在加载特殊 Schemes 时被调用,故选用 Google 作为示例),页面加载完毕后插入 script 标签,加载并执行位于 file://android_asset/www/js/main.js 中的代码。运行后表现一个内容为 Hello world 的 Toast。
注:在 Android 官方开辟文档 中,还有另一种利用 WebViewAssetLoader 的当地资源加载方式,感兴趣的可以自行研究一下,本文不再赘述。

JavaScript 弹窗提示

上面的示例代码已经可以帮助我们完成大多数需求,但在实际应用中发现了别的一个问题,JavaScript 的 alert() comfirm() prompt() 函数全部失效,这不是我们期望的行为。WebChromeClient 为我们提供了 onJsAlert onJsConfirm onJsPrompt 事件,分别对应上述函数,我们需要自行实现上述方法。
MainActivity
  1. import androidx.appcompat.app.AlertDialog;
  2. import android.annotation.SuppressLint;
  3. import android.content.Context;
  4. import android.os.Bundle;
  5. import android.webkit.JsPromptResult;
  6. import android.webkit.JsResult;
  7. import android.webkit.WebChromeClient;
  8. import android.webkit.WebView;
  9.     ...
  10.     @SuppressLint("StaticFieldLeak")
  11.     private static Context mContext;
  12.     @SuppressLint("SetJavaScriptEnabled")
  13.     @Override
  14.     protected void onCreate(Bundle savedInstanceState) {
  15.         super.onCreate(savedInstanceState);
  16.         setContentView(R.layout.activity_main);
  17.         mContext = getApplicationContext();
  18.         WebView mWebView = findViewById(R.id.webview);
  19.         mWebView.getSettings().setDefaultTextEncodingName("utf-8");
  20.         mWebView.getSettings().setJavaScriptEnabled(true);
  21.         mWebView.setWebChromeClient(new WebChromeClient() {
  22.             @Override
  23.             public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
  24.                 onJsDialog(DialogType.ALERT, view, url, message, result, null, null);
  25.                 return true;
  26.             }
  27.             @Override
  28.             public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
  29.                 onJsDialog(DialogType.CONFIRM, view, url, message, result, null, null);
  30.                 return true;
  31.             }
  32.             @Override
  33.             public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
  34.                 onJsDialog(DialogType.PROMPT, view, url, message, null, defaultValue, result);
  35.                 return true;
  36.             }
  37.         });
  38.         mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to access the assets folder, or use file://android_res/ to access the res folder
  39.     }
  40.     private enum DialogType {
  41.         ALERT,
  42.         CONFIRM,
  43.         PROMPT
  44.     }
  45.     private static void onJsDialog(DialogType type, WebView view, String url, String message, final JsResult result, String defaultValue, final JsPromptResult promptResult) {
  46.         AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
  47.         String[] content = message.split(":", 2);
  48.         builder.setTitle(content[0]);
  49.         builder.setMessage(content[1] + "\n" + url);
  50.         builder.setCancelable(false);
  51.         switch (type) {
  52.             case PROMPT:
  53.                 builder.setPositiveButton(android.R.string.ok, (dialog, which) -> promptResult.confirm(defaultValue)); // TODO: Input
  54.                 break;
  55.             case CONFIRM:
  56.                 builder.setCancelable(true);
  57.                 builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> result.cancel());
  58.             case ALERT:
  59.             default:
  60.                 builder.setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm());
  61.         }
  62.         builder.create().show();
  63.     }
  64.     ...
复制代码
HTML
  1. ...
  2. <script type="text/javascript">
  3.     "use strict";
  4.     alert("Alert Title:This is an alert");
  5.     confirm("Confirm Title:This is a confirm") ? alert("Alert Title (Confirm):You confirmed the dialog") : alert("Alert Title (Confirm):You canceled the dialog");
  6.     alert("Alert Title (Prompt):Prompt content is " + prompt("Prompt Title:This is a prompt", "Hello world"));
  7. </script>
  8. ...
复制代码
HTML
上述示例代码中,onJsDialog 方法统一处理了来自 WebChromeClient 的 onJsAlert onJsConfirm onJsPrompt 事件,添加了标题(JavaScript 函数仅支持信息传参,这里以第一个 : 作为标题和信息的分隔符),弹出对话框并返回;DialogType 用于判断事件类型。
运行后依次弹出对话框,内容分别为:
  1. Alert Title
  2. This is an alert
  3. file:///android_asset/www/index.html
  4.                                     [OK]
  5. Confirm Title
  6. This is a confirm
  7. file:///android_asset/www/index.html
  8.                     [CANCEL] [OK]
复制代码
若点击了 OK
  1. Alert Title (Confirm)
  2. You confirmed the dialog
  3. file:///android_asset/www/index.html
  4.                                     [OK]
复制代码
若点击了 CANCEL
  1. Alert Title (Confirm)
  2. You canceled the dialog
  3. file:///android_asset/www/index.html
  4.                                     [OK]
  5. Prompt Title
  6. This is a prompt
  7. file:///android_asset/www/index.html
  8.                                     [OK]
  9. Alert Title (Prompt)
  10. Prompt content is Hello world
  11. file:///android_asset/www/index.html
  12.                                     [OK]
复制代码
亦可根据其他需求定制对话框的样式和(或)功能。
注:onJsDialog 方法仅作为示例,并未实现 prompt() 函数的输入功能,以默认值返回。

实战:在页面中插入 vConsole 并在乐成插入后弹出提示对话框

vConsole 是腾讯出品的一个轻量、可拓展、针对手机网页的前端开辟者调试面板,可以在 Vue、React 或其他任何框架中利用。用于移动设备调试非常好用,下面的实例将利用本文所介绍的所有技巧,在页面底部插入 vConsole。
下载 vconsole.min.js 并保存至 assets 资源文件夹中:https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js
Java
  1. import androidx.appcompat.app.AlertDialog;
  2. import android.annotation.SuppressLint;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.net.Uri;
  6. import android.os.Bundle;
  7. import android.webkit.JavascriptInterface;
  8. import android.webkit.JsPromptResult;
  9. import android.webkit.JsResult;
  10. import android.webkit.WebChromeClient;
  11. import android.webkit.WebResourceRequest;
  12. import android.webkit.WebResourceResponse;
  13. import android.webkit.WebView;
  14. import android.webkit.WebViewClient;
  15. import android.widget.Toast;
  16. import java.io.IOException;
  17.     ...
  18.     @SuppressLint("StaticFieldLeak")
  19.     private static Context mContext;
  20.     @SuppressLint("SetJavaScriptEnabled")
  21.     @Override
  22.     protected void onCreate(Bundle savedInstanceState) {
  23.         super.onCreate(savedInstanceState);
  24.         setContentView(R.layout.activity_main);
  25.         mContext = getApplicationContext();
  26.         WebView mWebView = findViewById(R.id.webview);
  27.         mWebView.getSettings().setJavaScriptEnabled(true);
  28.         mWebView.addJavascriptInterface(new JavaScriptBridge(), "Android"); // Export class JavaScriptBridge to WebView and map it to window.Android object in JavaScript
  29.         mWebView.setWebViewClient(new WebViewClient() {
  30.             @Override
  31.             public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
  32.                 if (request.getUrl().toString().equalsIgnoreCase("https://www.google.cn/")) {
  33.                     view.loadUrl("https://www.google.com/ncr");
  34.                     return true;
  35.                 } else if (request.getUrl().toString().startsWith("meowcat://open_settings")) {
  36.                     final Intent intent = mContext.getPackageManager().getLaunchIntentForPackage("com.android.settings");
  37.                     intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
  38.                     mContext.startActivity(intent);
  39.                     return true;
  40.                 }
  41.                 return false;
  42.             }
  43.             @Override
  44.             public void onPageFinished(WebView view, String url) {
  45.                 view.loadUrl("javascript:(() => {const script = document.createElement('script'); script.src='/MeowCat-Android-Asset/www/js/vconsole.min.js'; document.body.append(script); script.onload = () => {alert('vConsole:Loaded!'); if (typeof VConsole !== 'undefined') {new VConsole({onReady: () => {const vc = document.getElementById('__vconsole'); const vc_switch = vc.querySelector('.vc-switch'); vc.style.position = 'relative'; vc.style.zIndex = 9999999999; vc_switch.style.opacity = 'opacity' in this ? this.opacity : .5;},});}};})();");
  46.                 super.onPageFinished(view, url);
  47.             }
  48.             @Override
  49.             public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest webResourceRequest) {
  50.                 String url = webResourceRequest.getUrl().toString();
  51.                 Uri uri = Uri.parse(url);
  52.                 String key = uri.getScheme() + "://" + uri.getHost() + "/MeowCat-Android-Asset/";
  53.                 if (url.contains(key)) {
  54.                     String assetsPath = url.replace(key, "");
  55.                     try {
  56.                         return new WebResourceResponse("text/plain", "UTF-8", getAssets().open(assetsPath));
  57.                     } catch (IOException e) {
  58.                         e.printStackTrace();
  59.                     }
  60.                 }
  61.                 return super.shouldInterceptRequest(view, webResourceRequest);
  62.             }
  63.         });
  64.         mWebView.setWebChromeClient(new WebChromeClient() {
  65.             @Override
  66.             public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
  67.                 onJsDialog(DialogType.ALERT, view, url, message, result, null, null);
  68.                 return true;
  69.             }
  70.             @Override
  71.             public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
  72.                 onJsDialog(DialogType.CONFIRM, view, url, message, result, null, null);
  73.                 return true;
  74.             }
  75.             @Override
  76.             public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
  77.                 onJsDialog(DialogType.PROMPT, view, url, message, null, defaultValue, result);
  78.                 return true;
  79.             }
  80.         });
  81.         mWebView.loadUrl("file:///android_asset/www/index.html"); // You can directly use file://android_asset/ to load the files in the assets folder
  82.     }
  83.     private enum DialogType {
  84.         ALERT,
  85.         CONFIRM,
  86.         PROMPT
  87.     }
  88.     private static void onJsDialog(DialogType type, WebView view, String url, String message, final JsResult result, String defaultValue, final JsPromptResult promptResult) {
  89.         AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
  90.         String[] content = message.split(":", 2);
  91.         builder.setTitle(content[0]);
  92.         builder.setMessage(content[1] + "\n" + url);
  93.         builder.setCancelable(false);
  94.         switch (type) {
  95.             case PROMPT:
  96.                 builder.setPositiveButton(android.R.string.ok, (dialog, which) -> promptResult.confirm(defaultValue)); // TODO: Input
  97.                 break;
  98.             case CONFIRM:
  99.                 builder.setCancelable(true);
  100.                 builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> result.cancel());
  101.             case ALERT:
  102.             default:
  103.                 builder.setPositiveButton(android.R.string.ok, (dialog, which) -> result.confirm());
  104.         }
  105.         builder.create().show();
  106.     }
  107.     @SuppressWarnings("unused")
  108.     private static class JavaScriptBridge {
  109.         @JavascriptInterface
  110.         public void makeToast(final String message) {
  111.             Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
  112.         }
  113.     }
  114.     ...
复制代码
WebPage
  1. ...
  2. <script type="text/javascript">
  3.     "use strict";
  4.     alert("Alert Title:This is an alert");
  5.     confirm("Confirm Title:This is a confirm") ? alert("Alert Title (Confirm):You confirmed the dialog") : alert("Alert Title (Confirm):You canceled the dialog");
  6.     alert("Alert Title (Prompt):Prompt content is " + prompt("Prompt Title:This is a prompt", "Hello world"));
  7.     window.Android.makeToast("Hello world");
  8.     window.location = "https://www.google.cn/";
  9. </script>
  10. ...
复制代码
HTML
运行代码,最终您将可以或许看到如下提示:
  1. vConsole
  2. Loaded!
  3. https://www.google.com/
  4.                                     [OK]
复制代码
然后在页面的右下角,会出现一个绿色按钮,上面写着 vConsole。我们做到了,那正是我们想要的。
常见问题

1. 前端怎样调试WebView



  • 起首,要在WebView页面打开可以debug的设置。(不过只支持KITKAT以上版本)
  1. scss 代码解读复制代码if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
  2.    mWeb.setWebContentsDebuggingEnabled(true);
  3. }
复制代码


  • Android端需要开启开辟者模式, 然后打开usb调试, 最后插上电脑。
  • 在Chrome地点栏输入:Chrome://inspect。你会看到如下界面。

正常的话在App中打开WebView时,chrome中会监听到并表现页面。


  • 点击页面下的inspect,就可以及时看到手机上WebView页面的表现状态了。

2.JS 怎样通报 Uint8Array到 Android端:



  • 方法1: 注入参数为String data 的方法。通过Base64作为传输载体, 前端将Uint8Array数据转Base64, Native侧将Base64剖析为byte[]。
  • 方法2: 注入参数为byte[] bytes 的方法。
   直接通报字符串, 无论字符串多长,通报时间都在 10ms内, 推断字符串通报大概采用内存映射, 直接通报内存地点.
  通报uint8array, 数据越长时间越长, 推断大概底层涉及某些转换操纵, 从 js uint8 到 java byte。
  3.Android端 怎样加载当地前端资源



  • 资源文件放置Assert文件夹中
标签加载
  1. ini 代码解读复制代码<script type="module"
  2.     crossorigin src="/android_asset/parkingtest/dist/assets/index.34d4f8c4.js"/>
  3. <link rel="stylesheet"
  4.     href="/android_asset/parkingtest/dist/assets/index.cf521aaf.css">
复制代码
代码加载URL
  1. arduino
  2. 代码解读
  3. 复制代码"file:///android_asset/xxx/xxx/src.js"
复制代码


  • 资源文件放在当地SD存储
通过请求拦截方式, 拦截前端资源请求, 获取需要加载的文件名称,通过JAVA IO 加载 File 返回给前端。
加载代码(伪代码)
  1. scala 代码解读复制代码 public class MyWebViewClient extends WebViewClient {
  2.         @Nullable
  3.         @Override
  4.         public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
  5.             String url = request.getUrl().toString();
  6.             String fileName = Fileurl.getFileName();
  7.             ByteArrayInputStream fileStream = JavaIO.loadFile(filePath + fileName);
  8.             return new WebResourceResponse(
  9.             mimeType,
  10.             encoding,
  11.             statusCode,
  12.             reasonPhrase,
  13.             responseHeaders,
  14.             byteArrayInputStream);
  15.         }
  16.     }
复制代码
引申阅读:在 Android 开辟者文档 中,还有更多关于 Android WebView 混淆开辟的内容。

*1: NCR: No Country Redirect,Google 支持禁用地域跳转功能。
参考:Android WebView & H5 Hybrid开辟知识点整理
DSBridge for Android
Java & V8 通讯
深入理解JSCore

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

羊蹓狼

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表