异步调用WebView#getDrawingCache()而不冻结UI线程

时间:2014-08-09 05:46:08

标签: android android-webview

我试图抓住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)

2 个答案:

答案 0 :(得分:5)

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

我们将创建一个由Canvas支持的Bitmap,然后询问WebView或持有WebView的容器(稍后将讨论为什么需要此功能) ),在这个画布上绘制本身。最后,Bitmap使用的Canvas将保留所需的屏幕截图。然后可以将其刷新到存储区。

我修改了您的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 - 对齐顶部&amp;剩下。要在滚动状态下捕获WebView,请将其放在容器中 - 比如说Framelayout。除了以下更改之外,该过程仍然类似:我们将在创建位图时使用FrameLayout's宽度和高度:

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

我们不会询问WebView,而是要求FrameLayout自我画出:

frameLayout.draw(c);

那应该这样做。

答案 1 :(得分:1)

这取决于您是尝试进行单次捕获还是连续捕获流。 在这两种情况下,屏幕捕获和位图存储都将在后台线程中完成,以避免冻结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;
    }
}