记一次WebView填坑过程--由换行符引发的血案

2019-07-27

记一次WebView填坑过程--由换行符引发的血案

最近使用WebView掉坑了,然后艰难爬坑经历感触很深,写出来大家借鉴一下。

需求

我们有个网页需要用到很多js库,这些库比较大,而且基本上是不变的。为了提高性能,将这些网页和JS库放到本地,进行加载。变的数据从服务器获取,然后跟本地的HTML组装后显示。这种需求还是挺普遍的。

实现方式

由web的同事调试好HTML文件,他们把所有的HTML,js,css,image和资源一起打包给我们 我们把这些资源放到assets目录下(也可以专门在assets再建一个子目录来放)

采用Android自带的WebView来加载和显示这些网页。有两个方法loadUrlloadDataWithBaseUrl方法。

1.loadUrl("file:///android_asset/xxxxxx.html")这种方式适合HTML不需要改变的情况,直接加载展示,省时省力

2.loadDataWithBaseUrl方式需要先把HTML内容现在到内存,然后再展示。这种方式可以随意修改HTML里的内容,修改好再交由WebView展示,灵活性强。

遇到的坑

测试的时候,我们遇到同一个HTML文件,通过loadUrl方式加载展示,没有任何问题。但是通过loadDataWithBaseUrl加载展示,HTML的内容死活展示不出来

爬坑过程

    1. 反复确认loadDataWithBaseURL的用法有没有用对,baseUrl参数没有问题。 mWebView.loadDataWithBaseURL("file:///android_asset/", htmlContent, "text/html", "utf-8", null). 网上搜资料看看别人的用法,还有查看我们工程里其他用了loadDataWithBaseURL方法的地方,都是差不多的。然后我怀疑是htmlContent有问题,是不是读取的时候出错了。 我去看从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 "";
    }

看上去并没有什么问题呀。我搜了下这个方法在我们工程里的使用,发现还有很多使用的地方,他们使用都没问题。我不放弃,到网上再搜一下。全是同样的方法。我断点调试,并且把htmlContent打印出来,好像没有太大的问题呀,不是这里的问题?

  • 2.查看我们工程里其他用了loadDataWithBaseURL方法的地方,我的用法跟他们一致。不过最终我发现我用的HTML文件跟他们用的HTML文件有不同的地方:我使用的HTML文件中 更复杂一些,在文件里引用JS资源文件,图片文件,在HTML里本身内嵌了JS代码。我猜测是内嵌JS导致的。我网上搜资料,看看是不是这样的。终于搜到一篇文章说这个问题,但是这个帖子是很久以前的。下面是它的原话:

使用WebView的loadUrl(url),网页中的Javascript运行正常。但是获取url的html内容后,使用loadData或者loadDataWithBaseURL之后,Javascript就不起作用了!google了一圈,似乎这是普遍问题,不知道这里的高手有法子解决不?

貌似验证了我的想法。但是这是很久以前的帖子呀。我想Google改不会那么傻吧。后来我调试时在logcat里看到一些日志。加载网页时打印了类似下面的日志

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

说明网页在解析JS的时候报错了。 我看了下网页中JavaScript,按道理的这些都是正确的,因为采用loadUrl方式加载没问题,直接在电脑上浏览器打开也没有问题。我把这些js都删掉,只加了一条console.log('1111')的打印语句。再次运行加载,没报错了!!说明内嵌JavaScript代码是可行的。那就是我们原本的JavaScript有问题了??

我把那部分JS放到一个新的单独的文件里。因为我们的HTML文件本身也引用了其他的独立JS文件。一运行,正常加载显示!JS还是那个JS,换到文件里就没问题了?

所以我那时得出结论: * 内嵌JavaScript代码在WebView的解析跟独立JS文件不太一样,可能内嵌JavaScript代码的解析要严格一些?

我开始验证这个想法,去修改内嵌JavaScript代码。 我在很多地方加了console打印,看看执行到哪里报错。然后我发现貌似去掉了注释,报错就不一样了。

Uncaught SyntaxError: Unexpected token。

Uncaught SyntaxError: Unexpected end of input

最后我把注释都去掉,把该加分号的地方都加上。就能正常运行了。 我初步得出以下结论:

需要HTML文档里内嵌JavaScript代码的话,要非常注意JavaScript的语法问题。WebView对这部分的JS代码语法要求非常严,经常发生识别不了而报错的情况。 现在发现①非常注意表达式后面要加分号,不能省略分号!!②不能在JS里随便加注释。要仔细检查语法,严格遵循JS标准

我把这些结论还加到了我们代码的注释了,以免后人采坑。

到这里问题解决了,我把HTML从asset中加载出来,再HTML字符串中的一些字符串(默认数据)替换成我们从服务器取到的数据(真实用户数据),然后调用loadDataWithBaseURL方法展示。完美。

到这里有些人会说loadUrl方法不是没问题吗,直接用它不就行了,折腾那么多干嘛。确实是如此,用loadUrl方法能实现我的需求,实际上我也试用了这个方法,没问题。 不过采用loadUrl的方式,实现起来不太一样。需要修改JavaScript代码。这些HTML是有web同时提供的,他们都是已经默认数据直接调试好了,一打开就能展示。采用loadUrl的方法,你不能预先修改HTML文件,只能在加载完HTML数据之后,在WebView的webclient回调方法onPageFinished里去调用JS方法。

就是说:

  • 1.你将JS里的window.onload方法去掉,让他们先不展示
  • 2.你新增一个js方法,这方法接收一些参数,然后在这个方法里调用渲染方法(即原来的window.onload里的方法)。
  • 3.在你Java代码里,在WebView的webclient回调方法onPageFinished里去调用你新增的JS方法,把参数都传入进去。

看到没有,这里要修改的东西还蛮多,还要要求客户端开发人员懂JS代码。

采用loadDataWithBaseURL方式,理想的情况下,不需要改JS代码,直接告诉开发人员默认数据在哪里,把HTML加载进内存的时候,通过String.replace方法,把默认数据替换为真实数据,就可以了。

iOS端就是这么实现的,但是Android这边实现的时候就遇到了坑。。。。 我肯定想两端实现方式保持一致,所有优先采用loadDataWithBaseURL方式。当然,你也可以让web开发人员按你的要求写好,那你也不用改动那么多。。。

背后真相

问题解决了,版本也发了。但问题的真相真的是WebView解析内嵌JavaScript比较严格吗?从现象来看,不支持注释,不能省略表达式的分号,这明显是换行问题。 所以我想问题可能真的出现在从asset里读取文件地方。看代码就是没有明显错误,一行一行的读取进来,拼接好全部返回。网上查到的所有代码都是这么读的。

那一次性读取整个文件进来呢?所有我找了下资料,将代码改成一次性读取整个文件。

public static String getStringFromAssets(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);
        }
    }

果然什么问题都没有!看logcat打印出来的内容也是跟HTML里的内容一模一样,之前方式打印出来的没有换行并没有太过在意!

那只能说明bufReader.readLine的方法有问题,我网上搜了下,说这个方法会自动去掉换行。原来真相是这个么??原来WebView一直是个背锅侠呀,而且这个锅背了好多年😂😂。

我们来看看时间的真凶readLine方法的声明:

/**
     * 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);
    }

注意看return的声明not includingany line-termination characters. 人家这里说得很清楚嘛,吐血了。。。

如果你对Java很熟悉,或者对readLine()方法熟悉,你就不用折腾那么多,就不会掉坑里了。

我在想为什么一直没发现呢,这个方法用了这么久,网上也这么多人用,就没人遇到问题?我猜主要原因是大部分情况下,没有换行符也没有什么影响。我们一般都是把json或HTML文件放assets里,然后读取出来使用。json文件有没有换行根本不影响!我们工程里大部分都是这种用法。如果HTML里没有内嵌JavaScript代码也没有问题!如果真有内嵌JavaScript代码了,也可以通过采用loadUrl方法来绕过,就是我之前的做法那样。

只有正面刚,才会发现这里有个坑。readLine这个方法不好,太有迷惑性了。 还有就是不要随便相信网上拷来的代码!!!这些血的教训。

最后附上修正版的正确的从assets里读取文件的方法。

 /**
     * 获取asset的文件,转化成String
     *
     * @param context
     * @param fileName
     * @return
     */
    public static String parseAssetsData(Context context, 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;
            //注意!! readLine()返回的内容已经把换行符去掉,所以要补上
            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();
            }
        }
    }

如果你觉得这篇文章有用,请打赏小钱喝杯咖啡^_^ 打赏

Category: 技术 Tagged: Android开发 WebView JavaScript

Comments