preface

There has always been user feedback in the APP WebView page white screen, including myself a few times. On how to detect the screen, to browse the Internet, he sorted out a plan. Roughly divided into the following steps:

  1. Capture the content of the current screen to obtain the Bitmap
  2. Determine whether the Bitmap is white
  3. Do the corresponding processing for the white screen

The WebView screenshots

Timing of screenshots

WebView has the following functions:

public void setWebViewClient(WebViewClient client) {}
Copy the code

Into the android refs. Its. The WebViewClient has the following functions:

   /**
     * Notify the host application that a page has finished loading. This method
     * is called only for main frame. Receiving an {@code 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 {@link WebView#postVisualStateCallback} and wait for the supplied
     * callback to be triggered.
     *
     * @param view The WebView that is initiating the callback.
     * @param url The url of the page.
     */
    public void onPageFinished(WebView view, String url) {}
Copy the code

We can prepare screenshots in onPageFinished with the following code:

   // There is a bug in the X5 SDK that causes onPageFinished to call back twice when progress is 100
   private final ArrayList<String> completedPageCache = new ArrayList<>();

   @Override
   public void onPageFinished(WebView webView, String url) {
       // Enter onPageFinished several times, the last time the progress was 100
       if (completedPageCache.contains(url)) return;
       
       // The progress is 100
       if (webView.getProgress() > 99) {
           completedPageCache.add(url);
           // Start the screenshot
           new WebWhiteChecker(WebActivity.this, webView, url).startCheck();
       }
       super.onPageFinished(webView, url);
    }
Copy the code

OnPageFinished is actually called multiple times, each time with a different progress, but the final progress is 100. Webview.getprogress () > 99 Make sure the progress reaches 100 before you start taking screenshots.

CompletedPageCache is used to store the address of a web page that has been taken a screenshot. Because we are using the Tencent X5 SDK, we will actually call twice when the progress is 100. Here to do a heavy, really pit!

The X5 WebView class names are almost identical to the native ones, and the related apis above are all generic.

To capture

Go directly to the code:

  fun startCheck(a){ webView? .postDelayed({try {
              // The Activity is not in the destroyed state
              if(! activity.isDestroyed && ! activity.isFinishing) { webView.x5WebViewExtension? .let {// Take half of the size here, otherwise it might be OOM
                      val bitmap = Bitmap.createBitmap(webView.width / 2, webView.height / 2, Bitmap.Config.ARGB_8888)
                      // This must be set to 0.5f, which is the same as the scale of the bitmap above, otherwise the screenshot will not be taken
                      it.snapshotVisible(bitmap, false.false.false.false.0.5 f.0.5 f) {
                            // Start the test
                            checkOnSubThread(bitmap)
                        }
                    }
                }
            } catch (e: Exception) {
                L.e(e)
            }
        }, 1000)}Copy the code

PostDelayed set a delay of 1 second because some pages in the APP are full of images and the background is white when the images are not loaded, so no delay would result in a white background being truncated (1 second is my own experiment, I could have set it larger).

When createBitmap was created, the width and height of the WebView were selected to be half of that of the WebView. In addition, it can also improve the following scanning efficiency, after all, the amount is small.

SnapshotVisible is X5 SDK provided screenshot method, simple and easy to use! Native WebView how screenshots, you can search! I haven’t tried it yet

Bitmap detection

Bitmap has a getPixel function:

    /**
     * Returns the {@link Color} at the specified location. Throws an exception
     * if x or y are out of bounds (negative or >= to the width or height
     * respectively). The returned color is a non-premultiplied ARGB value in
     * the {@link ColorSpace.Named#SRGB sRGB} color space.
     *
     * @paramx The x coordinate (0... width-1) of the pixel to return *@paramy The y coordinate (0... height-1) of the pixel to return *@return     The argb {@link Color} at the specified coordinate
     * @throws IllegalArgumentException if x, y exceed the bitmap's bounds
     * @throws IllegalStateException if the bitmap's config is {@link Config#HARDWARE}
     */
    @ColorInt
    public int getPixel(int x, int y) {
        checkRecycled("Can't call getPixel() on a recycled bitmap");
        checkHardware("unable to getPixel(), "
                + "pixel access is not supported on Config#HARDWARE bitmaps");
        checkPixelAccess(x, y);
        return nativeGetPixel(mNativePtr, x, y);
    }
Copy the code

This method returnsa non-premultiplied ARGB valueWith respect to my limited ability, I don’t know what this is, I have a guess but I can’t talk nonsense! There’s a great god who knows.

Throws IllegalStateException if the Bitmap’s config is {@link Config#HARDWARE} An exception is thrown if the Bitmap is of the HARDWARE type. You can search for the HARDWARE type.

For x, y passes, look directly at the following calls:

   // Count the white dots
   private var whitePixelCount = 0

   private fun checkOnSubThread(bitmap: Bitmap) {
       // Asynchronous thread execution
       RxSchedulers.scheduleWorkerIo {
           val width = bitmap.width
           val height = bitmap.height

           for (x in 0 until width) {
               for (y in 0 until height) {
                   if (bitmap.getPixel(x, y) == -1) {// It is white
                       whitePixelCount++
                   }
               }
           }

           if (whitePixelCount > 0) {
               val rate = whitePixelCount * 100f / width / height
               // Here you can compare the set upper limit and then do the processing
            }
            bitmap.recycle()
        }
    }

Copy the code

First, make sure that the detection process is performed on the child thread, otherwise you get the idea!

We interpret the width and height of the Bitmap as a two-dimensional array, and pass x(with) and y(height) directly to getPixel through a two-layer loop. Then you can get the color value of each pixel.

Bitmap.getpixel (x, y) == -1// bitmap.getPixel(x, y) == -1// bitmap.getPixel(x, y) == -1// bitmap.getPixel(x, y) == -1// bitmap.getPixel(x, y) == -1 Professional knowledge is not enough, can only go to the extreme, their own do not have what the web test out (other color value can also test out the specific value).

In fact you putColor.WHITEWhen I print it out, it’s also minus 1. Of course, strictly speaking, that doesn’t prove itgetPixelThe -1 is white!

For each determination of whiteness, the whitePixelCount is increased by one and then divided by the width * height of the Bitmap to get the proportion of whiteness.

Let’s try Microsoft’s Bing https://cn.bing.com/ :

The test results are as follows:

WebWhiteChecker: white rate = 4.73251
Copy the code

In actual application, the threshold set by our APP is 95%, that is to say, if the proportion of white exceeds the threshold, it is considered as a white screen.

The condition of the dark mode needs to be studied. In the actual test, although the background is black, the proportion of white is actually the same as the normal mode!

supplement

There is also a function in Bitmap:

    @NonNull
    public Color getColor(int x, int y) {
        checkRecycled("Can't call getColor() on a recycled bitmap");
        checkHardware("unable to getColor(), "
                + "pixel access is not supported on Config#HARDWARE bitmaps");
        checkPixelAccess(x, y);

        final ColorSpace cs = getColorSpace();
        if (cs.equals(ColorSpace.get(ColorSpace.Named.SRGB))) {
            return Color.valueOf(nativeGetPixel(mNativePtr, x, y));
        }
        // The returned value is in kRGBA_F16_SkColorType, which is packed as
        // four half-floats, r,g,b,a.
        long rgba = nativeGetColor(mNativePtr, x, y);
        float r = Half.toFloat((short) ((rgba >>  0) & 0xffff));
        float g = Half.toFloat((short) ((rgba >> 16) & 0xffff));
        float b = Half.toFloat((short) ((rgba >> 32) & 0xffff));
        float a = Half.toFloat((short) ((rgba >> 48) & 0xffff));

        // Skia may draw outside of the numerical range of the colorSpace.
        // Clamp to get an expected value.
        return Color.valueOf(clamp(r, cs, 0), clamp(g, cs, 1), clamp(b, cs, 2), a, cs);
    }
Copy the code

Return an android.graphics.color object directly. Why not use this function?

  1. becauseRequiresApiLimits, see below:

em… You need Android Q

  1. For it is better thangetPixelMore than a layer of Color value into the Color object process, involving the full graph two-dimensional array traversal, efficiency must be poor!

If you want to print the Color object that getColor gets in white, I’ll post it here:

Color(1.0.1.0.1.0.1.0, sRGB IEC61966-2.1)
Copy the code

em… I can’t read it. I think it’s white

White during processing

What do I do after I detect a white screen?

According to the actual situation, we started from the operation and maintenance, and checked the WebView request situation when the white screen, and found that the resource interface returned 204–No Content No new document, the browser should continue to display the original document.

Our WebView Settings use the default cache policy:

// Set the cache mode
WebSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
Copy the code

In the case of 204, we used the local cache. We guess there was a problem with the cache.

So our APP performs the task of clearing the WebView cache after the page containing the WebView exits:

 public static void clearDataAndCache(@NonNull WebView webView) {
      // Clear the cache
      webView.clearCache(true);
      webView.clearFormData();
      webView.clearHistory();
 }
Copy the code

Clearing the cache proved to be effective in most cases.

So what is the reason for the white screen? You can refer to the quality optimization of today’s headlines.

On Android, we use our own WebView kernel, and there are some weird holes.

WebView will read the file template when it is running. If another thread updates the template file at the same time, the template loading problem will occur. Therefore, it is necessary to ensure that the atomicity Render stuck when the template is loaded. Internal rendering will also appear in rare cases of the problem of Render jam, but in the detail page of the overall user level, even if only one in 100,000, it is a relatively big problem for users, at this time we will do white screen monitoring from the business to retry

Of course, regardless of iOS and Android, the WebView loading logic is quite complex, sometimes no retries will succeed, at this time we will directly downgrade to the loading line details page, priority to ensure the user experience.

We use Tencent’s X5, the headline is that they are self-developed, may not be as mature as the native WebView, white screen when we can also downgrade operations.

Finally, if you know any other reasons for the white screen, I hope you can share!