在不冻结UI线程的情况下asynchronous调用WebView#getDrawingCache()

我试图抓住一个WebView的截图,但是,WebView#getDrawingCache(true)是一个昂贵的调用,并冻结了UI线程。

尽pipe如此,任何试图将其移至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) 

Solutions Collecting From Web of "在不冻结UI线程的情况下asynchronous调用WebView#getDrawingCache()"

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

我们将创build一个由Bitmap支持的Canvas ,然后询问WebView ,或持有WebView的容器(将在稍后讨论为什么需要这样做),以便在此canvas上绘制自己 。 最后, Canvas使用的Bitmap将保存所需的屏幕截图。 它可以被冲洗到存储。

我修改了你的AsyncTask来代表裸露的实现。 目前,它只显示Bitmap正在doInBackground(...)方法内生成 – closures主线程。

 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(); } } } } 

在testing的同时,我也发现这个方法比使用getDrawingCache()花费的时间less。 我还没有试图find原因。

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

考虑WebView滚动时的场景。 如果使用上述方法,则获得的屏幕截图将不准确 – 屏幕截图将显示WebView ,就好像滚动为0 – 顶部和左侧alignment。 即使在滚动状态下也要捕获WebView ,把它放在一个容器里 – 比如说Framelayout 。 除了以下更改,该过程保持相似:创build位图时,我们将使用FrameLayout's宽度和高度:

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

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

 frameLayout.draw(c); 

这应该做到这一点。

这取决于你是否试图采取一个单一的捕获,或连续的捕获stream。 在这两种情况下,屏幕截图和位图存储都将在后台线程中完成,以避免冻结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? } } 

第二个可以用来连续保存截图:(看看如何重新使用位图和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; } }