One, the introduction

This article mainly discusses the interaction between H5 and native hybrid development. Of course, this is just my opinion, seeking common ground while reserving differences.

This paper mainly summarizes the following issues:

  1. How to implementJSwithAndriodThe interaction?
  2. forWebViewHow to optimize the problem of slow startup?
  3. If there are multipleH5Module package, how to achieve complete and partial update module package?
  4. How to build a common toolset (framework?) for the above problems? ?
  5. Problems encountered and solutions.

OK, here we go!

Second, the interaction

For details on how to implement JS and Android interaction, read the official Building Web Apps in WebView article. If you don’t understand English, that’s ok, because the following content will cover these technical points.

  • Interaction model:

In fact, we can further abstract the Webview, so we get the following relationship:

The obvious problem here is how to implement JsExecutor and JsInterfaces.

For JsExecutor (Android calls JS), this is a fixed way of writing. For example, if we wanted to dynamically fetch the HTML of a tag in a web page, we would say:

// Assume that the id parameter is content Stirng elementId ="content";
String jsCode = "javascript:document.getElementById(\" + elementId +\").innerHtml"; webView.evaluateJavascript(jsCode, new ValueCallback<String>() { @Override public void onReceiveValue(String html) { // ... }});Copy the code

This is fixed, but it can be a pain when the method has too many parameters. Piecing together method names and multiple parameters is annoying and error-prone, so we can abstract the following utility classes:

/** * @author horseLai * CreatedAt 2018/10/22 17:42 * Desc: JS code executor, which contains general methods for executing JS code via WebView. * Update: */ public final class JsExecutor { private static final String TAG ="JsExecutor";
    private JsExecutor() {} /** * JS method with no arguments, * * @param webView * @param jsCode */ public static void executeJsRaw(@nonnull webView webView, @NonNull String jsCode) { executeJsRaw(webView, jsCode, null); } /** * JS method with parameters, * * @param webView * @param jsCode * @param callback */ public static void executeJsRaw(@nonnull webView webView, @NonNull String jsCode, @Nullable ValueCallback<String> callback) {if (Build.VERSION.SDK_INT >= 19) {
            webView.evaluateJavascript(jsCode, callback);
        } else{// note that there is no direct result callback in this way, but it can be done in a roundabout way. For example, we can execute a fixed method in JS, pass in a type parameter, and then match the method in the JS method with the type parameter // and execute it. After execution, call the corresponding callback method we injected to pass the result back to //. This will solve the result callback problem, for Android versions below 4.4. webView.loadUrl(jsCode); }} /** * JS method with parameters, Method * * @param webView * @param methodName * @param callback * @param params */ public static void executeJs(@NonNull WebView webView, @NonNull CharSequence methodName, @Nullable ValueCallback<String> callback, @NonNull CharSequence... params) { StringBuilder sb = new StringBuilder(); sb.append("javascript:")
                .append(methodName)
                .append("(");
        if(params ! = null && params.length > 0) {for (int i = 0; i < params.length; i++) {
                sb.append("\" ")
                        .append(params[i])
                        .append("\" ");
                if (i < params.length - 1)
                    sb.append(",");
            }
        }
        sb.append(");");
        Log.i(TAG, "executeJs: "+ sb); executeJsRaw(webView, sb.toString(), callback); } /** * JS method with parameters, @param params */ public static void executeJs(@nonnull webView) webView, @NonNull CharSequence methodName, @NonNull CharSequence... params) { executeJs(webView, methodName, null, params); }}Copy the code

Here, WebView is directly regarded as a tool for us to execute JS code. The following example is to pass the current network type to H5. Since the stitching process of JS code is integrated, only the specific method name and method string parameters need to be passed in.

JsExecutor.executeJs(webView, "onNetStatusChanged", netType);
Copy the code
  • forJsInterfaces(JScallAndroid)We need to annotate the methods we need to inject@JavascriptInterfaceIn order to expose the method, and then inject the class object containing the method,H5Need fromAndroidTo obtain the user’s account information natively, you can write:

First inject the H5JsStorage class object containing the corresponding method:

H5JsStorage h5JsStorage = new H5JsStorage(this, mUser);
webView.addJavascriptInterface(h5JsStorage, "h5JsStorage");
Copy the code

The statement of getUserAccountInfo is as follows:

public class H5JsStorage implements IH5JsStorage {  
    
    // ... 
    @JavascriptInterface
    public String getUserAccountInfo() {return String.format("{\"userAccount\":\"%s\", \"password\":\"%s\", \"userIncrId\":\"%s\", \"orgId\":\"%s\"}", mUser.getUserAccount(), mUser.getPassword(), mUser.getUserIncrId(), mUser.getOrgId());
    }
    
    // ...     
}
Copy the code

This is how H5 interacts with native interaction. The GitHub address is given at the end of the article.

3. WebView startup speed optimization, multi-module package automatic update

1. WebViewStart speed optimization

Let’s do an experiment to test the startup speed of an Activity that includes a WebView before and after optimization. Based on the Activity’s life cycle, record the initial time in the first line of onCreate and the end time in the last line of onStart. Then calculate the time difference as a reference to measure the start speed. The results are as follows:

/ / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / don't do any processing, Mi6 android 8.0 I/Main2Activity: onStart: total cost:150 ms I/Main2Activity: onStart: total cost:44 ms I/Main2Activity: onStart: total cost:33 ms I/Main2Activity: onStart: total cost:54 ms I/Main2Activity: onStart: total cost:35 ms I/Main2Activity: OnStart: total cost:34 ms I/Main2Activity: onStart: total cost:34 ms // Initialization time after optimization I/MyWebViewHolder: prepareWebView: total cost: 131 ms I/MyWebViewHolder: prepareWebView: total cost: 121 ms I/MyWebViewHolder: prepareWebView: total cost: 121 ms I/MyWebViewHolder: prepareWebView: total cost: 117 ms I/MyWebViewHolder: prepareWebView: total cost: 110 ms I/MyWebViewHolder: prepareWebView: total cost: 116 ms I/MyWebViewHolder: prepareWebView: total cost: 116 ms // Elapsed time I/Main2Activity: onStart: total cost:26 ms I/Main2Activity: onStart: total cost:20 ms I/Main2Activity: onStart: total cost:22 ms I/Main2Activity: onStart: total cost:17 ms I/Main2Activity: onStart: total cost:19 ms I/Main2Activity: onStart: Total cost: 21 ms / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / simulator android 9.0 I/Main2Activity: onStart: total cost:292 ms I/Main2Activity: onStart: total cost:50 ms I/Main2Activity: onStart: total cost:49 ms I/Main2Activity: onStart: total cost:54 ms I/Main2Activity: onStart: total cost:43 ms I/Main2Activity: onStart: total cost:47 ms I/Main2Activity: onStart: total cost:39 ms I/Main2Activity: onStart: Total cost:41 ms // Initialization time after optimization I/MyWebViewHolder: prepareWebView: Total cost: 177 ms I/MyWebViewHolder: prepareWebView: total cost: 169 ms I/MyWebViewHolder: prepareWebView: total cost: 183 ms I/MyWebViewHolder: prepareWebView: total cost: 159 ms // Elapsed time I/Main2Activity: onStart: total cost:40 ms I/Main2Activity: onStart: total cost:27 ms I/Main2Activity: onStart: total cost:34 ms I/Main2Activity: onStart: total cost:34 ms I/Main2Activity: onStart: total cost:33 ms I/Main2Activity: onStart: Total cost: 30 ms / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / MT6592 android 4.4 don't do deal with I/Main2Activity: onStart: total cost:141 ms I/Main2Activity: onStart: total cost:46 ms I/Main2Activity: onStart: total cost:43 ms I/Main2Activity: onStart: total cost:42 ms I/Main2Activity: onStart: total cost:44 ms I/Main2Activity: OnStart: total cost:46 ms // Initialization time after optimization I/MyWebViewHolder: prepareWebView: Total cost: 182 ms I/MyWebViewHolder: prepareWebView: total cost: 50 ms I/MyWebViewHolder: prepareWebView: total cost: 54 ms I/MyWebViewHolder: prepareWebView: total cost: 53 ms I/MyWebViewHolder: prepareWebView: total cost: 54 ms I/MyWebViewHolder: PrepareWebView: total cost: 56 ms // Time elapsed after I/Main2Activity: onStart: total cost:36 ms I/Main2Activity: onStart: total cost:34 ms I/Main2Activity: onStart: total cost:30 ms I/Main2Activity: onStart: total cost:31 ms I/Main2Activity: onStart: total cost:32 msCopy the code

According to the above results, the startup speed after optimization is 10~20 seconds faster than that before optimization, and the jitter is smaller. Notice that it contains a time difference called prepareWebView, so you’ll be smart enough to figure out what I’m doing with this optimization. Initialize the WebView in the right place and at the right time, and reuse the created instance.

/** * @author horseLai * CreatedAt 2018/12/10 10:11 * Desc: Used to hold an instance of MyWebView and reduce the overhead of recreating and destroying it every time * Update: */ public final class MyWebViewHolder { private static final String TAG ="MyWebViewHolder";

    private MyWebView mWebView;
    private static MyWebViewHolder sMyWebViewHolder;
    private View pageNoneNet;
     private boolean mShouldClearHistory = false;


    public boolean shouldClearHistory() {
        return mShouldClearHistory;
    }

    public void shouldClearHistory(boolean shouldClearHistory) {
        this.mShouldClearHistory = shouldClearHistory;
    }
    private MyWebViewHolder() {
    }

    public static MyWebViewHolder getHolder() {
        if(sMyWebViewHolder ! = null)return sMyWebViewHolder;

        synchronized (MyWebViewHolder.class) {
            if(sMyWebViewHolder == null) { sMyWebViewHolder = new MyWebViewHolder(); }}returnsMyWebViewHolder; Public void prepareWebView(context context) {long start = ** public void prepareWebView(context context) {long start = System.currentTimeMillis();if(mWebView ! = null)return;
        synchronized (this) {
            if (mWebView == null) {
                mWebView = new MyWebView(context);
            }
        }
        Log.i(TAG, "prepareWebView: total cost: " + (System.currentTimeMillis() - start) + " ms");
        Log.d(TAG, "prepare MyWebView OK...");
    }

    public MyWebView getMyWebView() { 
        return mWebView;
    }

    public void detach() {
        if(mWebView ! = null) { Log.d(TAG,"detach MyWebView, but not destroy...");
            ((ViewGroup) mWebView.getParent()).removeView(mWebView);
            mWebView.removeAllViews();
            mWebView.clearAnimation();
            mWebView.clearFormData();
            // mWebView.clearHistory();
            mShouldClearHistory = true;
            mWebView.getSettings().setJavaScriptEnabled(false);
        }
    }

    public void attach(ViewGroup parent, int index) {
        if(mWebView ! = null) { Log.d(TAG,"attach MyWebView, index of ViewGroup is "+ index); WebSettings settings = mWebView.getSettings(); / / do not add this configuration will be unable to load the display interface Settings. The setDomStorageEnabled (true);
            settings.setSupportZoom(false);
            settings.setJavaScriptEnabled(true);
            settings.setUseWideViewPort(true);

            mWebView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
            mWebView.setVerticalScrollBarEnabled(false);
            mWebView.setHorizontalScrollBarEnabled(false); FrameLayout = new FrameLayout(parent.getContext()); frameLayout.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); frameLayout.addView(mWebView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); pageNoneNet = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_null_net, frameLayout,false);
            frameLayout.addView(pageNoneNet);
            pageNoneNet.setVisibility(View.GONE);
            pageNoneNet.findViewById(R.id.btn_try).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { pageNoneNet.setVisibility(View.GONE); mWebView.reload(); }}); parent.addView(frameLayout, index); } } public voidshowNoneNetPage() {
        if(pageNoneNet ! = null) pageNoneNet.setVisibility(View.VISIBLE); } public voidhideNoneNetPage() {
        if(pageNoneNet ! = null) pageNoneNet.setVisibility(View.GONE); } public void attach(ViewGroup parent) { attach(parent, parent.getChildCount()); } public voiddestroy() {
        if(mWebView ! = null) { Log.d(TAG,"destroy MyWebView...");
            mWebView.destroy();
        }
    }

    public void pause() {
        if(mWebView ! = null) { Log.d(TAG,"pause MyWebView...");
            mWebView.onPause();
        }
    }

    public void resume() {
        if(mWebView ! = null) { Log.d(TAG,"resume MyWebView...");
            mWebView.onResume();
        }
    }

    public void removeJSInterfaces(String... names) {
        if (names == null || names.length == 0) return;
        for (String name : names) {
            Log.d(TAG,String.format("removeJSInterfaces:: %s ..", name)); mWebView.removeJavascriptInterface(name); }}}Copy the code

Then initialize in the appropriate place:

@Override
protected void onCreate(Bundle savedInstanceState) { 
    // ...
    MyWebViewHolder.getHolder().prepareWebView(this);
}
Copy the code

Add to the layout:

LinearLayout parent= findViewById(R.id.parent);
MyWebViewHolder.getHolder().attach(parent);
Copy the code

Unbind from the interface when onDestroy:

@Override
protected void onDestroy() {/ /... MyWebViewHolder.getHolder().detach(); }Copy the code

2. The multi-module package is automatically updated

The purpose of supporting automatic update of multiple modules is to facilitate update and maintenance and reduce the traffic expenditure caused by user upgrade. Each module package can be independent of each other and convenient for team development. You only need to agree the file directory with the front end.

Let’s take a look at the automatic update process of H5 module (complete update) :

The above is the complete update process of the module package, and you can also update the patch. The so-called patch update is that the downloaded update package only contains the files that need to be updated, so corresponding to the above process, it is the process of deleting the local old version files, and directly decompress and replace the corresponding files. This update method has the following advantages and disadvantages:

  1. Can greatly reduce the update of the user’s traffic consumption, and the speed is very fast.
  2. However, the front-end needs to extract the updated files explicitly, otherwise there will be problems, and the process may be tedious.
  3. If using something similar toVueJsThis template framework is written as an interface for compilationJSCode, and then just oneindex.htmlEntry, resulting in extraction and positioning cumbersome, and each compilation of the file name may be different, so it can not be usedpatchesThis way, can only be subcontracted, and then complete update.

H5ManagerSettings is an isolated class of H5Manager configuration information and irrelevant logic.

Build a common tool set

The logic of interaction and update in mixed development has been described above, and the toolset has been put on H5MixDevelopTools on Github for those who are interested, although I don’t include the JS interface and HTML interface here.

Practical problems encountered and solutions :(take writing H5 pages using VueJs as a template engine in the project as an example)

1. The interface cannot be loaded and blank is displayed. What should I do?

Solution: Add the following configuration to the WebView

mWebView.getSettings().setDomStorageEnabled(true);
Copy the code

2. What should I do if I always cannot find the defined interactive interface method during the joint investigation?

Reasons and Solutions: First, VueJs is obfuscating code by default, so if you run into this problem, manually configure it to turn off obfuscating (find out for yourself). If there is no confusion, but still can not find the corresponding method, what to do? My friends and I use the interface file as a component by placing it in components, and then attaching the interface method to the window object, as shown in the following example:

// Mount the method window.showtoast =function(msg){ UI.showToast(msg); } // Mount variables. Variables mounted at window can be referenced globally directly at window.userInfo = {name:"horseLai"}
Copy the code

3. Picture selection problem, how to select and preview pictures?

Let’s start with a specific scenario: For example, there is a comment function in our project, which is written with H5, and the number of comment pictures less than 3 can be selected for each comment and uploaded to the server with text.

When you find that does not call the system photo library and camera, nor does it display a preview image next to it, you may need these configurations:

    settings.setJavaScriptEnabled(true); 
    settings.setAllowFileAccess(true);
    settings.setAllowFileAccessFromFileURLs(true);
    settings.setAllowUniversalAccessFromFileURLs(true);
    settings.setAllowContentAccess(true);
Copy the code

Then there are two options for choosing an image:

  • By facsimileWebChromeClientc#onShowFileChooserandWebChromeClient#openFileChooser, butopenFileChooserMethods have become systemsApi, so it is not intuitive to find it, but, even if you do find it, you will find it is very difficult to adapt to different models. You can take a lookandroid-4-4-webview-file-chooser-not-openingAnd because I’m not calling gallery selection directly, I’m opening one firstBottomSheetDialogTo choose whether to take the image from the camera or from the library, which brings up the question,If I just turn it onBottomSheetDialogAnd then turn it off without making any choiceValueCallback#onReceiveValueSo if you pass the value, then<input>I can only open a popover once, and then I hit it again and it doesn’t work, whereas if I close it every timeBottomSheetDialogthroughValueCallback#onReceiveValueSend anull, then after two consecutive starts, there will be abnormal flash backWell, I’m going to skip this one. I’m going to go with the second option.
  • The second option is to set it up directlyJSInteractive interface, click the picture selection control after the call to establish a good native picture selection interface to take the picture, when we choose the picture inonActivityResultMethod to executeJSMethod to pass the local path to the imageJSHandling, well, by this point, we’re all familiar with the process. So how do you do that<img>Preview on, and how to upload pictures of this path as files.

Here is how we call the image path back to JS after the image is selected.

@param {Number} @param {Number}typeType: 0-> Gallery, 1-> Camera * @param {String} imgFilePath */ window.selectedImgFile = []; / / simulation < input > select file storage form, used to upload window. SelectedImgFileUrls = []; // Convert the image path to a path that <img> can preview window.onpictureresult =function (typeImgFilePath) {// Note here selecteDimgfile.push (new File([""], imgFilePath, {type:"image/*"}));
    selectedImgFileUrls.push({
        imgUrl: "file://" + imgFilePath
    });
}
Copy the code

SelectedImgFile above, selectedImgFileUrls these two variables mounted to the window, the two arrays can be directly in the global references, remember after use to empty, or it will affect the next time you use.

Well, it looks great, it’s great for selecting images and previewing, but pretty soon you realize that this is actually a BUG. What’s the BUG? ImgFilePath ([“”], imgFilePath, {type:”image/*”}) {new File([“”], imgFilePath, {type:”image/*”}); Because the first parameter [“”] is actually the image’s actual data (byte array), its length represents the size of the file, so the above can be previewed, but cannot be read directly from a local path to the file stream data, and thus cannot be uploaded successfully.

What to do? After thinking for a long time, I found myself stuck in the thinking of how JS could establish File and upload through a local path, so I communicated with my friends in front end and back end, and finally decided on the following solution: After selecting the image, the image will be encoded into Base64 string and then injected into JS for processing. After receiving the data, the JS side will bind the image data and upload it to the server. The server side will conduct Base64 decoding and save the local image.

So you can modify it a little bit like this:

window.selectedImgFile = []; window.selectedImgFileUrls = []; // Convert the image path to a path that <img> can preview window.onpictureresult =function (type, imgFilePath, base64Data) {
    selectedImgFile.push( base64Data );
    selectedImgFileUrls.push({
        imgUrl: "data:image/jpg; base64," + base64Data
    });
}
Copy the code

However, there may still be some problems, such as memory overflow, because the image itself may be very large, especially when the camera is used to take pictures directly, a picture may be 3~10M, so it is time-consuming to encode directly into the image itself, and the encoded string will be stored in the memory. Therefore, it is likely to cause memory overflow on the Android terminal. Therefore, you can consider compression before coding to reduce the probability of running out of memory.

Five, the summary

Based on the actual project, this paper introduces the implementation of JS and native interaction in the mixed development, and then tests the startup speed of WebView Activity with a small experiment, optimization, and then tests the startup speed after optimization, and then introduces the logic of H5 module update, and finally organizes a set of tools. You can check out H5MixDevelopTools if you are interested.

Using H5 hybrid development does speed things up, but the actual experience is mediocre and suitable for very speed-driven scenarios.