软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

Hybrid App体验(Android)

很早以前就听说过Hybrid App(混合模式移动应用),也大概了解它使用哪些技术,今天自己动手体验一把。大厂一般都是自己编译Chromium / Blink,还添加自己的定制。没有那个实力的话就选择别人提供好的SDK,比如腾讯浏览服务X5。我本次主要为了体验技术应用,不需要考虑不同Android版本的差异,就使用Android SDK中自带的WebView。

1. 新建Android工程,添加WebView

在Android Studio中新建Empty Activity的项目,再添加WebView。

activity_main.xml的内容如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/blog_webview"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:background="#FFFFFF"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

AndroidManifest.xml中需要添加网络权限与修改布局:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.qinzhiqiang.webview1">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.NoActionBar">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <uses-permission android:name="android.permission.INTERNET"/>
</manifest>

MainActivity.java中添加WebView,并开始浏览我们的网站:

package cn.qinzhiqiang.webview1;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    WebView webView;
    private long exitTime = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initWebView();
        webView.loadUrl("https://micro.qinzhiqiang.cn");
    }

    private void initWebView() {
        webView = (WebView)findViewById(R.id.blog_webview);

        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setSupportZoom(false);
        settings.setBuiltInZoomControls(false);
        settings.setDefaultFontSize(16);

        //设置缓存模式
        settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
        // 开启DOM storage API 功能
        settings.setDomStorageEnabled(true);

        webView.setWebViewClient(new WebViewClient() {
            //在webview里打开新链接,否则会使用系统中浏览器打开新链接
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }
        });

        webView.addJavascriptInterface(new JavaScriptInjectedNative(), "injectedNative");
    }


    private void sendMessageToWeb(String msgId, String payload) {
        String jsContent = "if (this.nativeMessageHandler) { this.nativeMessageHandler("
                + Util.toJsString(msgId) + "," + Util.toJsString(payload) + ")}";
        webView.loadUrl("javascript:" + jsContent);
    }

    @Override
    public void onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack();
        } else {
            if ((System.currentTimeMillis() - exitTime) > 2000) {
                Toast.makeText(getApplicationContext(), "再按一次退出程序",
                        Toast.LENGTH_SHORT).show();
                exitTime = System.currentTimeMillis();
            } else {
                finish();
            }
        }
    }
}

2. JS Bridge连接Native与Web之间的通信

因Web中JS在一个独立的沙盒中运行,跟Android Native是不能直接通信的,我们需要给Web与Native提供通信桥接,否则都没法使用相机等设备。目前流行的技术是JS Bridge,其中也有几种实现机制。本文中,我们只看其中的一种。

2.1. Web向Native发送消息

只需要给WebView注入一个对象,在JS中调用该对象的方法injectedNative.sendMessage(msgId, payload)即可。注入对象的代码如下:

package cn.qinzhiqiang.webview1;

import android.webkit.JavascriptInterface;

public class JavaScriptInjectedNative {

    @JavascriptInterface
    public String sendMessage(String msgId, String payload) {
        return "Native processed: " + msgId + (payload != null ? ", " + payload : "");
    }
}

注入方法,见:

webView.addJavascriptInterface(new JavaScriptInjectedNative(), "injectedNative");

在真实项目中,我们可以让Java中的代码来订阅与并处理这些消息。

2.2. Native向Web发送消息

Native向Web发送消息,其解决方案是直接让WebView执行一段JS代码。

private void sendMessageToWeb(String msgId, String payload) {
    String jsContent = "if (this.nativeMessageHandler) { this.nativeMessageHandler("
            + Util.toJsString(msgId) + "," + Util.toJsString(payload) + ")}";
    webView.loadUrl("javascript:" + jsContent);
}

我们可以在Web中直接定义function nativeMessageHandler(msgId, payload)函数,或者在启动WebView时给它注入一段JS代码来完成这件事情。

3. 总结

Hybrid App中比较新颖的地方,就是JS Bridge。只要有通信的桥梁,不同的软件体系就可以融合到一块。

Hybrid App,一般还提供离线运行、在线升级的功能,Web文件会缓存到手机中。应用要加载本地资源时,务必进行数字签名校验,确保客户端的安全。

WebView in Android