2019-07-27

Remember a WebView pit filling process – a bloodbath caused by newlines

Recently use WebView fell pit, and then difficult to climb pit experience feeling very deep, write out for reference.

demand

We have a web page that uses a lot of JS libraries. These libraries are quite large and basically unchanged. To improve performance, place these pages and JS libraries locally and load them. The modified data is retrieved from the server and then assembled with local HTML for display. The need is quite common.

implementation

Our web colleagues debug the HTML files. They package all the HTML, JS, CSS,image and resources together for us. We put these resources in assets directory (we can also create a subdirectory in Assets for them).

Android’s own WebView is used to load and display these pages. There are two methods loadUrl and loadDataWithBaseUrl.

1. LoadUrl (“file:///android_asset/xxxxxx.html”) this method is suitable for HTML does not need to change the situation, directly load display, save time and effort

2. LoadDataWithBaseUrl requires loading HTML content into memory first and then displaying it. In this way, you can modify the content of the HTML at will, and then modify the WebView to display, flexibility.

In the pit of

When testing, we encountered the same HTML file and loaded the display using loadUrl without any problems. But by loading the display with loadDataWithBaseUrl, the HTML content is not displayed

Climb the pit process

    1. Double check if loadDataWithBaseURL is used yes, baseUrl is ok. mWebView.loadDataWithBaseURL(“file:///android_asset/”, htmlContent, “text/html”, “utf-8”, null). Look on the Internet and see how other people use it, and look at other places in our project that use the loadDataWithBaseURL method, it’s pretty much the same. Then I wondered if there was something wrong with the htmlContent. I’m going to look at how to get a file from asset.
public static String getStringFromAssets(Context context,String fileName) { try { InputStreamReader inputReader = new InputStreamReader(context.getAssets().open(fileName)); BufferedReader bufReader = new BufferedReader(inputReader); String line; StringBuilder stringBuilder = new StringBuilder(); while ((line = bufReader.readLine()) ! = null) { stringBuilder.append(line); } return stringBuilder.toString(); } catch (Exception e) { e.printStackTrace(); } return ""; }Copy the code

There doesn’t seem to be anything wrong. I searched the use of this method in our project, and found that there are many other uses, and they have no problem using it. I’m not giving up. Look it up on the Internet. It’s all the same. I breakpoint debugging, and print out htmlContent, seems to be not too much of a problem, not here?

  • 2. Check other places in our project where loadDataWithBaseURL method is used. My usage is consistent with theirs. In the end, HOWEVER, I found that the HTML files I used were different from the HTML files they used: THE HTML files I used were more complex, with JS resource files, image files, and JAVASCRIPT code embedded in the HTML itself. My guess is embedded JS. I googled it to see if it was true. Finally found an article about this problem, but this post is a long time ago. Here’s what it says:

Javascript in the web page works fine using the loadUrl(URL) of the WebView. But after fetching the HTML content of the URL, using loadData or loadDataWithBaseURL, Javascript doesn’t work! Google a circle, it seems that this is a common problem, do not know the master here has a way to solve not?

Seems to prove my point. But this is a long time ago. I guess Google wouldn’t be that stupid. Then I saw some logs in logcat when I was debugging. A log similar to the following is printed when the web page is loaded

[INFO:CONSOLE(1)] “Uncaught SyntaxError: Unexpected identifier “

The page failed to parse THE JS file. I have looked at the JavaScript in the web page, and these are all correct according to reason, because there is no problem loading in loadUrl mode, and there is no problem opening directly in the browser on the computer. I deleted all these js and added a console.log(‘1111’) print statement. Run load again, no error!! Indicates that embedded JavaScript code is possible. Is there something wrong with our original JavaScript?

I put that part of JS into a new separate file. Because our HTML file itself also references other independent JS files. A run, normal load display! JS or the same JS, and it’s okay to put it in a file?

So I came to the conclusion at that time: * The parsing of embedded JavaScript code in a WebView is not quite the same as that of a standalone JS file, and maybe the parsing of embedded JavaScript code is a little more rigorous?

I started testing the idea by modifying the embedded JavaScript code. I’ve added console printing in a number of places to see where I get an error. Then I noticed that it looks like removing the comment makes the error report different.

Uncaught SyntaxError: Unexpected Token.

Uncaught SyntaxError: Unexpected end of input

I ended up removing all the comments and adding semicolons where they should be added. And it’s going to work. My preliminary conclusions are as follows:

If you need to embed JavaScript code in your HTML document, be very careful with JavaScript syntax. WebView has very strict requirements on the SYNTAX of JS code in this part, and errors are often reported when it cannot be recognized. Now find ① very attention to the expression after the semicolon, can not omit the semicolon!! ② Can not add annotations in JS casually. Check the syntax carefully and strictly follow the JS standard

I’ve also commented these conclusions into our code so that future generations don’t have to take advantage of them.

At this point, I load the HTML from the asset, replace some of the HTML strings (default data) with the data we fetched from the server (real user data), and then call the loadDataWithBaseURL method to display it. Perfect.

Now, some people are going to say, well, loadUrl is fine, so just use it. This is true, using loadUrl to implement my requirements, I actually tried this method, no problem. However, the implementation of loadUrl is not quite the same. You need to modify the JavaScript code. The HTML is also provided by the Web, which has been debugged with default data and can be displayed as soon as it is opened. With loadUrl, you can’t pre-modify the HTML file. You can only call the JS method in the WebView’s onPageFinished webClient callback after loading the HTML data.

1. You remove the window.onload method from JS so they don’t show 2. You add a js method that takes some arguments and then calls the render method (the old window.onload method) in that method. 3. In your Java code, call your new JS method in the WebView webClient callback method onPageFinished and pass in the parameters.

See, there’s quite a bit of work to be done here, as well as requiring client developers to understand JS code.

Using loadDataWithBaseURL, ideally, you don’t need to change the JS code, just tell the developer where the default data is, and when loading the HTML into memory, replace the default data with the real data using the string.replace method.

IOS end is implemented in this way, but Android side of the implementation of the time encountered a pit… I definitely want to keep both implementations consistent, so loadDataWithBaseURL is preferred. Of course, you can have the Web developers write it exactly as you want, and you won’t have to change as much…

The truth behind the

The problem was solved and the version was released. But is the truth of the matter that WebViews parse embedded JavaScript more rigorously? From the point of view of the phenomenon, comments are not supported, and you cannot omit the semicolon of the expression, which is obviously a line break problem. So I think the problem might really be getting files from asset. Look at the code is no obvious error, line by line read in, concatenated all returned. That’s how all the code on the Internet reads.

What about reading the entire file in at once? So I looked up the data and changed the code to read the entire file at once.

public static String getFileFromAssets(ontext context,String fileName) {
        try {
            InputStreamReader inputReader = new InputStreamReader(context.getAssets().open(fileName));
            BufferedReader bufReader = new BufferedReader(inputReader);

            StringWriter out = new StringWriter();
            copy(bufReader, out);
            return out.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    private static void copy(Reader in, Writer out) throws IOException {
        int c = -1;
        while((c = in.read()) != -1) {
            out.write(c);
        }
    }
Copy the code

There was nothing wrong! Logcat prints the same content as the HTML. There are no line breaks. I don’t care too much.

That just means there’s a problem with the bufreader.readline method. I googled it and said it automatically removes newlines. Is this the truth? Originally WebView has been a back pot man ah, and this pot back for many years 😂😂.

Let’s look at the declaration of the readLine method:

/**
     * Reads a line of text.  A line is considered to be terminated by any one
     * of a line feed ('\n'), a carriage return ('\r'), or a carriage return
     * followed immediately by a linefeed.
     *
     * @return     A String containing the contents of the line, not including
     *             any line-termination characters, or null if the end of the
     *             stream has been reached
     *
     * @exception  IOException  If an I/O error occurs
     *
     * @see java.nio.file.Files#readAllLines
     */
    public String readLine() throws IOException {
        return readLine(false);
    }
Copy the code

Return not includingany line-termination characters. Somebody else here said very clearly, vomiting blood…

If you’re familiar with Java, or with the readLine() method, you won’t have to mess around so much that you won’t fall into the trap.

I wonder why I haven’t noticed, this method has been used for so long, there are so many people on the Internet, and no one has any problems? I guess the main reason is that for the most part, not having a newline doesn’t matter. We usually put json or HTML files into assets and then read them and use them. It doesn’t matter if a JSON file has a newline! Most of our projects use it that way. If there is no JavaScript code embedded in the HTML, that’s fine! If you do have embedded JavaScript code, you can bypass it by using the loadUrl method, which I did earlier.

It’s only when the front is rigid that you’ll notice there’s a hole. ReadLine is a bad method, and it’s too confusing. And don’t trust the code copied online!! The lessons of blood.

Finally, a modified version of the correct way to read a file from assets is attached.

/** * Get the asset file, Public static String parseAssetsData(context context, parseAssetsData); String fileName) { StringBuilder stringBuilder = new StringBuilder(); InputStreamReader inputStreamReader = null; BufferedReader bfReader = null; try { AssetManager assetManager = context.getAssets(); inputStreamReader = new InputStreamReader(assetManager.open(fileName)); bfReader = new BufferedReader(inputStreamReader); String line; // Attention!! While ((line = bfreader.readline ()))! = null) { stringBuilder.append(line); stringBuilder.append("\n"); } } catch (Exception e) { e.printStackTrace(); } finally { closeStream(inputStreamReader); closeStream(bfReader); } return stringBuilder.toString(); } @param IO */ public static void closeStream(Closeable IO) {if (IO! = null) { try { io.close(); } catch (IOException e) { e.printStackTrace(); }}}Copy the code

If you find this article useful, please tip a small amount for a cup of coffee

Tagged: Android development WebView JavaScript

Comments