• Android内存优化14 内存泄漏常见情况5 特殊对象造成的内存泄漏 WebView内存泄漏


    WebView造成内存泄露

    关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。

    另外在查阅WebView内存泄露相关资料时看到这种情况:

    Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题(Android5.1之后)。

    最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。详细分析过程请参考这篇文章:WebView内存泄漏解决方法

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 先从父控件中移除WebView
        mWebViewContainer.removeView(mWebView);
        mWebView.stopLoading();
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.removeAllViews();
        mWebView.destroy();
    }


    前言:在项目的开发过程中,由于对内存要求较高,最近对应用的内存分析比较在意,前段时间监控图片内存,对Bitmap造成的内存泄漏进行了分析,并解决了问题。但是在图片内存泄漏之后,发现在访问网页的时候,webview竟然也会有内存泄漏,虽然内存占用很小,但是用户多次访问还是存在隐患。

    于是,开始对webview进行内存分析,发现webview下面的callback持有activity引用,造成webview内存无法释放,在网上也找了很多方法,但是webview.destory()等方法大都无法解决问题。

    最后看到一篇文章,才算明了出现这个bug的原因,按照作者的做法,确实解决了问题,安卓5.1和6.0系统都不存在内存泄漏问题。

    文章附下:



    销毁webview的方式

    mWebView.removeAllViews();
    /**、
    * 这里内存泄漏了,因为它的父容器在退出前没有被销毁,所以就会持有引用,内存泄漏
    * */
    // mWebView.destroy();

    改为

    在 Android 5.1 系统上,在项目中遇到一个WebView引起的问题,每打开一个带webview的界面,退出后,这个activity都不会被释放,activity的实例会被持有,由于我们项目中经常会用到浏览web页面的地方,可能引起内存积压,导致内存溢出的现象,所以这个问题还是比较严重的。

    问题分析

    使用Android Studio的内存monitor,得到了以下的内存分析,我打开了三个BookDetailActivity界面(都有webview),检查结果显示有3个activity泄漏,如下图所示:

    这个问题还是比较严重的,那么进一步看详细的信息,找出到底是哪里引起的内存泄漏,详情的reference tree如下图所示:

    从上图中可以看出,在第1层中的 TBReaderApplication 中的 mComponentCallbacks 成员变量,它是一个array list,它里面会持有住activity,引导关系是 mComponentCallbacks->AwContents->BaseWebView->BookDetailActivity, 代码在 Application 类里面,代码如下所示:

    public void registerComponentCallbacks(ComponentCallbacks callback) {
        synchronized (mComponentCallbacks) {
            mComponentCallbacks.add(callback);
        }
    }
    
    public void unregisterComponentCallbacks(ComponentCallbacks callback) {
        synchronized (mComponentCallbacks) {
            mComponentCallbacks.remove(callback);
        }
    }
    

    上面两个方法,会在 Context 基类中被调用,代码如下:

    /**
     * Add a new {@link ComponentCallbacks} to the base application of the
     * Context, which will be called at the same times as the ComponentCallbacks
     * methods of activities and other components are called.  Note that you
     * <em>must</em> be sure to use {@link #unregisterComponentCallbacks} when
     * appropriate in the future; this will not be removed for you.
     *
     * @param callback The interface to call.  This can be either a
     * {@link ComponentCallbacks} or {@link ComponentCallbacks2} interface.
     */
    public void registerComponentCallbacks(ComponentCallbacks callback) {
        getApplicationContext().registerComponentCallbacks(callback);
    }
    
    /**
     * Remove a {@link ComponentCallbacks} object that was previously registered
     * with {@link #registerComponentCallbacks(ComponentCallbacks)}.
     */
    public void unregisterComponentCallbacks(ComponentCallbacks callback) {
        getApplicationContext().unregisterComponentCallbacks(callback);
    }
    

    从第二张图我们已经知道,是webview引起的内存泄漏,而且能看到是在 org.chromium.android_webview.AwContents 类中,难道是这个类注册了component callbacks,但是未反注册?一般按系统设计,都会反注册的,最有可能的原因就是某些情况下导致不能正常反注册,不多说,read the fucking source。基于这个思路,我把chromium的源码下载下来,代码在这里 chromium_org(https://android.googlesource.com/platform/external/chromium_org/?spm=5176.100239.blogcont61612.7.j9EPtE

    然后找到 org.chromium.android_webview.AwContents 类,看看这两个方法 onAttachedToWindow 和 onDetachedFromWindow:

    @Override
    public void onAttachedToWindow() {
        if (isDestroyed()) return;
        if (mIsAttachedToWindow) {
            Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
            return;
        }
        mIsAttachedToWindow = true;
    
        mContentViewCore.onAttachedToWindow();
        nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),
                mContainerView.getHeight());
        updateHardwareAcceleratedFeaturesToggle();
    
        if (mComponentCallbacks != null) return;
        mComponentCallbacks = new AwComponentCallbacks();
        mContext.registerComponentCallbacks(mComponentCallbacks);
    }
    
    @Override
    public void onDetachedFromWindow() {
        if (isDestroyed()) return;
        if (!mIsAttachedToWindow) {
            Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
            return;
        }
        mIsAttachedToWindow = false;
        hideAutofillPopup();
        nativeOnDetachedFromWindow(mNativeAwContents);
    
        mContentViewCore.onDetachedFromWindow();
        updateHardwareAcceleratedFeaturesToggle();
    
        if (mComponentCallbacks != null) {
            mContext.unregisterComponentCallbacks(mComponentCallbacks);
            mComponentCallbacks = null;
        }
    
        mScrollAccessibilityHelper.removePostedCallbacks();
    }
    

    系统会在attach处detach进行注册和反注册component callback,注意到 onDetachedFromWindow() 方法的第一行,if (isDestroyed()) return;, 如果 isDestroyed() 返回 true 的话,那么后续的逻辑就不能正常走到,所以就不会执行unregister的操作,通过看代码,可以得到,调用主动调用 destroy()方法,会导致 isDestroyed() 返回 true。

    /**
     * Destroys this object and deletes its native counterpart.
     */
    public void destroy() {
        if (isDestroyed()) return;
        // If we are attached, we have to call native detach to clean up
        // hardware resources.
        if (mIsAttachedToWindow) {
            nativeOnDetachedFromWindow(mNativeAwContents);
        }
        mIsDestroyed = true;
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                destroyNatives();
            }
        });
    }
    

    一般情况下,我们的activity退出的时候,都会主动调用 WebView.destroy() 方法,经过分析,destroy()的执行时间在onDetachedFromWindow之前,所以就会导致不能正常进行unregister()。

    解决方案

    找到了原因后,解决方案也比较简单,核心思路就是让onDetachedFromWindow先走,那么在主动调用之前destroy(),把webview从它的parent上面移除掉。

    ViewParent parent = mWebView.getParent();
    if (parent != null) {
        ((ViewGroup) parent).removeView(mWebView);
    }
    
    mWebView.destroy();
    

    完整的代码如下:

    public void destroy() {
        if (mWebView != null) {
            // 如果先调用destroy()方法,则会命中if (isDestroyed()) return;这一行代码,需要先onDetachedFromWindow(),再
            // destory()
            ViewParent parent = mWebView.getParent();
            if (parent != null) {
                ((ViewGroup) parent).removeView(mWebView);
            }
    
            mWebView.stopLoading();
            // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
            mWebView.getSettings().setJavaScriptEnabled(false);
            mWebView.clearHistory();
            mWebView.clearView();
            mWebView.removeAllViews();
    
            try {
                mWebView.destroy();
            } catch (Throwable ex) {
    
            }
        }
    }
    

    Android 5.1之前的代码

    对比了5.1之前的代码,它是不会存在这样的问题的,以下是kitkat的代码,它少了一行 if (isDestroyed()) return;,有点不明白,为什么google在高版本把这一行代码加上。

    /**
     * @see android.view.View#onDetachedFromWindow()
     */
    public void onDetachedFromWindow() {
        mIsAttachedToWindow = false;
        hideAutofillPopup();
        if (mNativeAwContents != 0) {
            nativeOnDetachedFromWindow(mNativeAwContents);
        }
    
        mContentViewCore.onDetachedFromWindow();
    
        if (mComponentCallbacks != null) {
          mContainerView.getContext().unregisterComponentCallbacks(mComponentCallbacks);
          mComponentCallbacks = null;
        }
    
        if (mPendingDetachCleanupReferences != null) {
            for (int i = 0; i < mPendingDetachCleanupReferences.size(); ++i) {
                mPendingDetachCleanupReferences.get(i).cleanupNow();
            }
            mPendingDetachCleanupReferences = null;
        }
    }
    

    结束

    在开发过程中,还发现一个支付宝SDK的内存问题,也是因为这个原因,具体的类是 com.alipay.sdk.app.H5PayActivity,我们没办法,也想了一个不是办法的办法,在每个activity destroy时,去主动把 H5PayActivity 中的webview从它的parent中移除,但这个问题限制太多,不是特别好,但的确也能解决问题,方案如下:

    /**
     * 解决支付宝的 com.alipay.sdk.app.H5PayActivity 类引起的内存泄漏。
     *
     * <p>
     *     说明:<br>
     *         这个方法是通过监听H5PayActivity生命周期,获得实例后,通过反射将webview拿出来,从
     *         它的parent中移除。如果后续支付宝SDK官方修复了该问题,则我们不需要再做什么了,不管怎么
     *         说,这个方案都是非常恶心的解决方案,非常不推荐。同时,如果更新了支付宝SDK后,那么内部被混淆
     *         的字段名可能更改,所以该方案也无效了。
     * </p>
     *
     * @param activity
     */
    public static void resolveMemoryLeak(Activity activity) {
        if (activity == null) {
            return;
        }
    
        String className = activity.getClass().getCanonicalName();
        if (TextUtils.equals(className, "com.alipay.sdk.app.H5PayActivity")) {
            Object object = Reflect.on(activity).get("a");
    
            if (DEBUG) {
                LogUtils.e(TAG, "AlipayMemoryLeak.resolveMemoryLeak activity = " + className
                    + ",  field = " + object);
            }
    
            if (object instanceof WebView) {
                WebView webView = (WebView) object;
                ViewParent parent = webView.getParent();
                if (parent instanceof ViewGroup) {
                    ((ViewGroup) parent).removeView(webView);
                }
            }
        }
    }
  • 相关阅读:
    JavaCV音视频开发宝典:录制vp8和vp9编码的webm格式视频,以mp4转webm为例
    JavaCV音视频开发宝典:使用JavaCV采集windows系统声音并录制成mp3音频文件(FFmpeg采集windows系统声音)
    JavaCV音视频开发宝典:使用JavaCV和springBoot实现httpflv直播服务,无需流媒体服务,浏览器网页flv.js转封装方式播放rtsp,rtmp和桌面投屏实时画面
    linux用户用户组与ACL
    QUIC协议详解
    技术分享 | 黑盒测试方法论—等价类
    技术分享 | 这些常用测试平台,你们公司在用的是哪些呢?
    干货 | 测试专家(前阿里P8)聊测试职业发展常见瓶颈
    如何做好测试开发?| 破解测试人技术成长常见的 3 种错误思维!
    技术分享 | 黑盒测试方法论—因果图
  • 原文地址:https://www.cnblogs.com/ldq2016/p/8474172.html
Copyright © 2020-2023  润新知