Android - 传递按钮实例时避免 AsyncTask 中的内存泄漏

Android - avoiding memory leak in AsyncTask when passing a button instance

我有一个扩展 AsyncTask 的 class。调用时,此任务会将视频下载到内部存储器,然后更新进度指示器。任务完成后,它会将下载按钮更改为已下载按钮(我使用的是 abdularis AndroidButtonProgress)。

程序运行良好,但我有一个下载按钮字段,它被突出显示为内存泄漏:

public class DownloadHandler extends AsyncTask<Object, Integer, String> {

    private DownloadButtonProgress downloadButton; // This field leaks a context object

    private WeakReference<Context> context;

    Episode episode;

    int totalSize;


    public DownloadHandler(Context context) {
        this.context = new WeakReference<> (context);
    }

    @Override
    protected String doInBackground(Object... params) {
        episode = (Episode) params[0];
        Context context = (Context) params[1];
        downloadButton = (DownloadButtonProgress) params[2];

        String urlString = "https://path.to.video.mp4";

        try {
            URL url = new URL(urlString);

            URLConnection ucon = url.openConnection();
            ucon.setReadTimeout(5000);
            ucon.setConnectTimeout(10000);
            totalSize = ucon.getContentLength();

            InputStream is = ucon.getInputStream();
            BufferedInputStream inStream = new BufferedInputStream(is, 1024 * 5);

            String fileName = episode.getFilename() + ".mp4";
            File file = new File(String.valueOf(context.getFilesDir()) + fileName);

            if (file.exists()) {
                file.delete();
            }
            file.createNewFile();

            FileOutputStream outStream = new FileOutputStream(file);
            byte[] buff = new byte[5 * 1024];

            int len;
            long total = 0;
            while ((len = inStream.read(buff)) != -1) {
                total += len;
                if (totalSize > 0) {
                    publishProgress((int) (total * 100 / totalSize));
                }
                outStream.write(buff, 0, len);
            }

            outStream.flush();
            outStream.close();
            inStream.close(); 

            return "Downloaded";

        } catch (Exception e) {
            e.printStackTrace();
            return "Not downloaded";
        }
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        int downloadedPercentage = progress[0];
        downloadButton.setCurrentProgress(downloadedPercentage);
    }

    @Override
    protected void onPostExecute(String result) {
        if (!result.equals("Downloaded")) {
            Log.d(TAG, "onPostExecute: ERROR");
        } else {
            downloadButton.setFinish();

            // Save to Room (this is why I pass context as a weak reference)
            AppDatabase db = AppDatabase.getDbInstance(context.get().getApplicationContext());
            // ....
        }

    }
}

当我从片段中调用 DownloadHandler 时,我是这样做的:

DownloadHandler downloadTask = new DownloadHandler(getActivity());
downloadTask.execute(episode, getActivity(), downloadButton);

我在执行方法中传递了下载按钮,但我需要它对 DownloadHandler class(onProgressUpdate()、onPostExecute())中的其他方法可用,所以我将其设为场.

我尝试在构造函数中将其作为弱引用传递给上下文,但出现错误提示我无法将 downloadButton 转换为 WeakReference。

我怎样才能使下载处理程序中的所有方法都可以使用 downloadButton,同时避免内存泄漏?

您应该将下载按钮作为构造函数依赖项传递,并将其包装在弱引用中,就像您对上下文所做的那样。

我认为它可能抛出了一个 ClassCastException 因为你试图从 doInBackground() 强制转换它并且来自你的 AsyncTask 主机的下载按钮是对查看。

对现有代码稍作修改应该可以正常工作:

public class DownloadHandler extends AsyncTask<Object, Integer, String> {

    private WeakReference<DownloadButtonProgress> downloadButton;

    private WeakReference<Context> context;

    Episode episode;

    int totalSize;


    public DownloadHandler(Context context, DownloadButtonProgress button) {
        this.context = new WeakReference<> (context);
        this.downloadButton = new WeakReference<>(button)
    }

    @Override
    protected String doInBackground(Object... params) {
        episode = (Episode) params[0];

        String urlString = "https://path.to.video.mp4";

        try {
            URL url = new URL(urlString);

            URLConnection ucon = url.openConnection();
            ucon.setReadTimeout(5000);
            ucon.setConnectTimeout(10000);
            totalSize = ucon.getContentLength();

            InputStream is = ucon.getInputStream();
            BufferedInputStream inStream = new BufferedInputStream(is, 1024 * 5);

            String fileName = episode.getFilename() + ".mp4";
            File file = new File(String.valueOf(context.get().getFilesDir()) + fileName);

            if (file.exists()) {
                file.delete();
            }
            file.createNewFile();

            FileOutputStream outStream = new FileOutputStream(file);
            byte[] buff = new byte[5 * 1024];

            int len;
            long total = 0;
            while ((len = inStream.read(buff)) != -1) {
                total += len;
                if (totalSize > 0) {
                    publishProgress((int) (total * 100 / totalSize));
                }
                outStream.write(buff, 0, len);
            }

            outStream.flush();
            outStream.close();
            inStream.close(); 

            return "Downloaded";

        } catch (Exception e) {
            e.printStackTrace();
            return "Not downloaded";
        }
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        int downloadedPercentage = progress[0];
        if (downloadButton.get() != null) {
            downloadButton.get().setCurrentProgress(downloadedPercentage);
        }
    }

    @Override
    protected void onPostExecute(String result) {
        if (!result.equals("Downloaded")) {
            Log.d(TAG, "onPostExecute: ERROR");
        } else {
            if (downloadButton.get() != null) {
                downloadButton.get().setFinish();
            }

            // Save to Room (this is why I pass context as a weak reference)
            AppDatabase db = AppDatabase.getDbInstance(context.get().getApplicationContext());
            // ....
        }
    }
}

现在您可以像这样调用它(注意在 doInBackground 中使用上下文的弱引用):

DownloadHandler downloadTask = new DownloadHandler(getActivity(), downloadButton);
downloadTask.execute(episode);

话虽如此,它仍然不够整洁,因为您需要进行所有空检查,这会造成很多可读性差的问题,因此为了避免泄漏,请确保您使用 AsyncTask#cancel() API 在 activity 被销毁时取消任何正在进行的任务,然后你可以从你的实现中删除所有弱引用(假设 activity 重新创建再次处理状态)

另外,在 运行 中,您可能想查看更好的异步 APIs,例如协程或 RxJava,因为 AsyncTask 已被弃用。