【问题标题】:Call WebView#getDrawingCache() asynchrnously without freezing UI thread异步调用 WebView#getDrawingCache() 而不冻结 UI 线程
【发布时间】:2014-08-17 20:52:53
【问题描述】:

我正在尝试获取 WebView 的屏幕截图,但是,WebView#getDrawingCache(true) 是一个昂贵的调用并且会冻结 UI 线程。

尽管如此,任何将其移至 AsyncTask 或单独线程的尝试都会导致“必须在 UI 线程上调用所有 WebView 方法”错误。

是否有解决方法,这样我就可以在后台线程中处理 WebView#getDrawingCache(true) 而不会冻结 UI 线程?

这是一个崩溃的代码示例

public class TouchTask extends AsyncTask<Object, Void, Void> {
    Runnable completionCallback;

    @Override
    protected Void doInBackground(Object... objects) {
        Integer id = (Integer) objects[0];
        completionCallback = (Runnable) objects[1];
        touch(id);
        return null;
    }

    @Override
    protected void onPostExecute(Void res) {
        super.onPostExecute(res);
        completionCallback.run();
    }
}

/*
Updates the webview's bitmap in cache
 */
public Pair<String, Bitmap> touch(Integer id) {
    CustomWebView webView = getWebView(id);
    String title = webView.getTitle();
    Bitmap screenie = getScreenshotFromWebView(webView);

    Pair<String, Bitmap> res = new Pair<String, Bitmap>(title, screenie);
    bitmapCache.put(id, res);
    return res;
}

public void touchAsync(Integer id, Runnable callback) {
    new TouchTask().execute(id, callback);
}

public Bitmap getScreenshotFromWebView(WebView webView) {
    webView.setDrawingCacheEnabled(true);
    webView.buildDrawingCache(true);
    Bitmap screenieBitmap = null;
    if ((webView.getDrawingCache(true) != null) && (!webView.getDrawingCache(true).isRecycled())) {
        screenieBitmap = webView.getDrawingCache(false);
        screenieBitmap = compressScreenshot(webView.getDrawingCache(false));
    }
    return screenieBitmap;
}

public Bitmap compressScreenshot(Bitmap screenie) {
    Display display = activity.getWindowManager().getDefaultDisplay();
    Point size = new Point();
    display.getSize(size);
    int width = size.x;
    int height = size.y;
    return Bitmap.createScaledBitmap(screenie, width / 3, height / 3, true);
}

下面是运行代码:

new TouchTask().execute(id, callback);

这是错误

java.lang.RuntimeException: An error occured while executing doInBackground()
            at android.os.AsyncTask$3.done(AsyncTask.java:300)
            at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:355)
            at java.util.concurrent.FutureTask.setException(FutureTask.java:222)
            at java.util.concurrent.FutureTask.run(FutureTask.java:242)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:818)
     Caused by: java.lang.RuntimeException: java.lang.Throwable: A WebView method was called on thread 'AsyncTask #3'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {16dba051} called on null, FYI main Looper is Looper (main, tid 1) {16dba051})
            at android.webkit.WebView.checkThread(WebView.java:2185)
            at android.webkit.WebView.getTitle(WebView.java:1321)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager.touch(TabManager.java:53)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:145)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:138)
            at android.os.AsyncTask$2.call(AsyncTask.java:288)
            at java.util.concurrent.FutureTask.run(FutureTask.java:237)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:818)
     Caused by: java.lang.Throwable: A WebView method was called on thread 'AsyncTask #3'. All WebView methods must be called on the same thread. (Expected Looper Looper (main, tid 1) {16dba051} called on null, FYI main Looper is Looper (main, tid 1) {16dba051})
            at android.webkit.WebView.checkThread(WebView.java:2175)
            at android.webkit.WebView.getTitle(WebView.java:1321)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager.touch(TabManager.java:53)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:145)
            at com.nubelacorp.javelin.activities.helpers.browseractivity.TabManager$TouchTask.doInBackground(TabManager.java:138)
            at android.os.AsyncTask$2.call(AsyncTask.java:288)
            at java.util.concurrent.FutureTask.run(FutureTask.java:237)
            at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:231)
            at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
            at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
            at java.lang.Thread.run(Thread.java:818)

【问题讨论】:

  • 你可以在非UI线程中调用getDrawingCache(true)
  • @pskink 你确定吗?我相信我试过了,我得到了一个错误
  • 我试过了,我没有收到错误
  • @pskink 你能展示你使用的代码吗?我用一个崩溃的异步任务示例代码更新了我的问题。
  • 这很好,旧线程覆盖了 run() 方法...

标签: android android-webview


【解决方案1】:

要从后台线程获取屏幕截图,您必须使用不同的方法。

我们将创建一个由Bitmap 支持的Canvas,然后让WebView 或包含WebView 的容器(稍后将讨论您为什么需要它)来绘制自己 在此画布上。最后,Canvas 使用的Bitmap 将保留所需的屏幕截图。然后可以将其刷新到存储中。

我已经修改了您的AsyncTask 来代表基本实现。目前,它只显示Bitmap 是在doInBackground(...) 方法内部生成的——在主线程之外。

public class TouchTask extends AsyncTask<Object, Void, Bitmap> {

    @Override
    protected Bitmap doInBackground(Object... objects) {
        // Create a new Bitmap with dimensions of the WebView
        Bitmap b = Bitmap.createBitmap(webView.getWidth(), 
                            webView.getHeight(), Bitmap.Config.ARGB_8888);

        // Canvas that is backed by the `Bitmap` and used by webView to draw on
        Canvas c = new Canvas(b);

        // WebView draws itself
        webView.draw(c);

        // Return bitmap
        // OR: Compress the bitmap here itself
        // No need to do that in `onPostExecute(...)`
        return b;
    }

    // There's no need to actually do anything inside `onPostExecute(..)`
    // Compressing and saving the bitmap to sdcard can be handled
    // in `doInBackground(...)` itself
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);

        if (bitmap != null) {
            FileOutputStream fos;
            try {
                fos = new FileOutputStream(new File("/sdcard/webview_screenshot.png"));
                bitmap.compress(CompressFormat.PNG, 100, fos);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

在测试的时候,我也发现这个方法比使用getDrawingCache()花费的时间更少。我还没有试图找到原因。

...or the container holding the WebView(will discuss why you would need this later)...

考虑滚动WebView 的场景。如果您使用上述方法,获得的屏幕截图将不准确 - 屏幕截图将显示WebView,就好像滚动是0 - 上下对齐。即使在滚动状态下也要捕获WebView,请将其放在一个容器中——比如Framelayout。该过程保持相似,除了以下更改:我们将在创建位图时使用FrameLayout's 的宽度和高度:

Bitmap b = Bitmap.createBitmap(frameLayout.getWidth(), 
                            frameLayout.getHeight(), Bitmap.Config.ARGB_8888);

我们不会要求WebView,而是要求FrameLayout 自己绘制:

frameLayout.draw(c);

应该这样做。

【讨论】:

  • 这实际上仍然不起作用(在 android L 上)。以下是异常: 原因:android.view.ViewRootImpl$CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能触摸其视图。
  • @nubela 我稍后会自己尝试一下。另一个建议:您是否尝试过在主线程上对 getDrawingCache 进行基准测试?我已经看到canvas 方法工作得非常快——你的 ui 线程不会保持很长时间。我会回复你的。
  • 尚未在主线程上,但我会试一试。我发现这个 webview 主线程唯一的限制非常愚蠢。每次我想用 draw 做任何事情时,我都必须冻结 UI。
  • @Vikram,您给出的示例有效,但屏幕截图只是空白填充黑色。我该如何解决这个问题?
【解决方案2】:

这取决于您是尝试进行单次捕获还是连续捕获流。 在这两种情况下,屏幕捕获和位图存储都将在后台线程中完成,以避免冻结 Android 主线程。

第一个 AsyncTask 可用于截取和保存单个屏幕截图:

public class SingleScreenshotTask extends AsyncTask<Void, Void, Bitmap> {
    View view;
    Context context;

    public SingleScreenshotTask(View view) {
        this.view = view;
        this.context = this.view.getContext().getApplicationContext();
    }

    @Override
    protected Bitmap doInBackground(Void... params) {
        Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        view.draw(canvas);
        saveBitmap(context, bitmap);
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        super.onPostExecute(result);
        // do something more on the UI thread?
    }

}

第二个可以用来连续保存截图:(看看Bitmap和Canvas是如何重用内存效率的)

public class ContinuousScreenshotTask extends AsyncTask<Void, Bitmap, Void> {
    View view;
    Context context;
    Bitmap bitmap;
    Canvas canvas;

    public ContinuousScreenshotTask(View view) {
        this.view = view;
        this.context = this.view.getContext().getApplicationContext();
        bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
        canvas = new Canvas(bitmap);
    }

    @Override
    protected Void doInBackground(Void... params) {
        while (!isCancelled()) {
            try {
                // some timeout to avoid cpu overload
                Thread.sleep(500);
            } catch (InterruptedException e) {
                return null;
            }
            view.draw(canvas);
            saveBitmap(context, bitmap);
        }
        return null;
    }

    @Override
    protected void onProgressUpdate(Bitmap... values) {
        super.onProgressUpdate(values);
        // do something more on the UI thread?
    }
}

两个 AsyncTask 都使用这种方法将 Bitmap 保存到内部存储:

public static void saveBitmap(Context context, Bitmap bitmap) {
    FileOutputStream out;
    try {
        out = context.openFileOutput(System.currentTimeMillis() + ".png", Context.MODE_PRIVATE);
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
        out.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

要执行这些任务,您可以执行以下操作:

View yourViewToCapture;
new ScreenShotTask(yourViewToCapture).execute();

View yourViewToCapture;
ContinuousScreenshotTask continuousScreenShotTask;

public void toggle() {
    if (continuousScreenShotTask == null) {
        continuousScreenShotTask = new ContinuousScreenshotTask(webview);
        continuousScreenShotTask.execute();
    } else {
        continuousScreenShotTask.cancel(true);
        continuousScreenShotTask = null;
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-12-24
    • 2016-05-12
    • 1970-01-01
    • 2017-08-22
    • 1970-01-01
    • 2021-12-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多