android内存优化之webview

提起android端的webview,它既是天使,又是魔鬼。

在混合型app中它是主角,一切由它呈现,如58同城,赶集网等;在另一些超级app中亦有它的影子,微信,qq,支付宝,没有一个超级app能少了它,既能展示最新最潮的实时资讯,又能扮演盘踞一方的全功能型网站,与native结合后又能扮演诸如公众号之内的应用等等,其能力可想而知。

webview在android端的演化可谓曲折,2015年google宣布不在支持4.4版本一下的webview[1],这意味着目前扔有近四分之一的Android用户因无法获得Google的支持而受到安全威胁。对于深度依赖webview的巨头开始自家的内核及上层sdk以求提升整体稳定性及其性能,腾讯x5内核是比较通用的一种.

目前大致的webview内存处理方式分为两类:

1.独立的web进程,与主进程隔开

这个方法被运用于类似qq,微信这样的超级app中,这也是解决任何webview内存问题屡试不爽的方法
对于封装的webactivity,在manifest.xml中设置

<activity android:name=".webview.WebViewActivity" android:launchMode="singleTop" android:process=":remote" android:screenOrientation="unspecified" />

然后在关闭webactivity时销毁进程

@Overrideprotected void onDestroy() {                
     super.onDestroy(); 
     System.exit(0);
}

关闭浏览器后便销毁整个进程,这样一般95%的情况下不会造成内存泄漏之类的问题,但这就涉及到android进程间通讯,比较不方便处理, 优劣参半,也是可选的一个方案.

2.封装过的webview

相比系统内置的webview的支持自2005年之后就没有了,而首推google的chrome。 腾讯的x5webview对h5的兼容性与稳定性与安全性逐渐凸显出来,并自成一系, 下面以使用x5webview为例做说明:

  • 首先使用webview的时候,不在xml里面声明,而是直接代码new个对象,传入application context防止activity引用滥用.
webView =  new BridgeWebView(getContext().getApplicationContext());
webFrameLayout.addView(webView, 0);

在使用了这个方式后,基本上90%的webview内存泄漏的问题便得以解决.

  • 而在android4.4版本以下,会出现android webview无法自动释放,如在fragment中,使用ondetach的释放webview是比较好的时机[2]
public void onDetach() {
    releaseWebViews();
    super.onDetach();
}
public synchronized void releaseWebViews() {
    if(webView != null) {
        try {
            if(webView.getParent() != null) {
                ((ViewGroup) webView.getParent()).removeView(webView);
            }
//                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
//this is causing the segfault occasionally below 4.2
            webView.destroy();
    //                }
        }catch (IllegalArgumentException e) {
            DLog.p(e);
        }
        RefWatcher refWatcher = FApplication.getRefWatcher();
        refWatcher.watch(webView);
        webView = null;
    }
}

其中webview自身的销毁代码如下:

@Override
public void destroy()
//flushMessageQueue();
    clearCache(true);
    clearFormData();
    clearMatches();
    clearSslPreferences();
    clearDisappearingChildren();
    clearHistory();
    //@Deprecated
    //clearView();
    clearAnimation();
    loadUrl("about:blank");
    removeAllViews();
    freeMemory();
    super.destroy();
}
  • 如果以上的方案还不管用,实时加入反射来清理webview的引用:
public void setConfigCallback(WindowManager windowManager) {
    try {
        Field field = WebView.class.getDeclaredField("mWebViewCore");
        field = field.getType().getDeclaredField("mBrowserFrame");
        field = field.getType().getDeclaredField("sConfigCallback");
        field.setAccessible(true);
        Object configCallback = field.get(null);
        if (null == configCallback) {
            return;
        }
        field = field.getType().getDeclaredField("mWindowManager");
        field.setAccessible(true);
        field.set(configCallback, windowManager);
    } catch(Exception e) {
    }
}

然后在activity中加入

public void onCreate(BundlesavedInstanceState){
    super.onCreate(savedInstanceState);
    setConfigCallback((WindowManager);
    getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
}
publicvoidonDestroy()
{
    setConfigCallback(null);
    super.onDestroy();
}
  • 对于application 级别的可能的misbehaving callbacks,加入
private static String[] misbehavingClasses = new String[]{
    "com.google.android.gms.ads",
    "com.android.org.chromium.android_webview.AwContents$AwComponentCallbacks",
};
public static boolean isMisbehavingCallBacks(String name){
    for(String s : misbehavingClasses){
        if(name.startsWith(s)){
            return true;
        }
    }
    return false;
}

然后重写applicaiton类记录这些callback, 并在适当的时机删掉:

@Override
public void registerComponentCallbacks(ComponentCallbacks callback) {
    super.registerComponentCallbacks(callback);
    ComponentCallbacksBehavioralAdjustmentToolIcs.INSTANCE.onComponentCallbacksRegistered(callback);
}

@Override
public void unregisterComponentCallbacks(ComponentCallbacks callback) {
    ComponentCallbacksBehavioralAdjustmentToolIcs.INSTANCE.onComponentCallbacksUnregistered(callback);
    super.unregisterComponentCallbacks(callback);
}

public void forceUnregisterComponentCallbacks() {
    ComponentCallbacksBehavioralAdjustmentToolIcs.INSTANCE.unregisterAll(this);
}


private static class ComponentCallbacksBehavioralAdjustmentToolIcs {
    private static final String TAG = "componentCallbacks";
    static ComponentCallbacksBehavioralAdjustmentToolIcs INSTANCE = new ComponentCallbacksBehavioralAdjustmentToolIcs();

    private WeakHashMap<ComponentCallbacks, ApplicationErrorReport.CrashInfo> mCallbacks = new WeakHashMap<>();
    private boolean mSuspended = false;

    public void onComponentCallbacksRegistered(ComponentCallbacks callback) {
        Throwable thr = new Throwable("Callback registered here.");
        ApplicationErrorReport.CrashInfo ci = new ApplicationErrorReport.CrashInfo(thr);

        if (FApplication.DEBUG) DLog.w(TAG, "registerComponentCallbacks: " + callback.getClass().getName(), thr);

        if (!mSuspended) {
            if (BugFix.isMisbehavingCallBacks(callback.getClass().getName())) {
                mCallbacks.put(callback, ci);
            }
            // TODO: other classes may still prove to be problematic?  For now, only watch for .gms.ads, since we know those are misbehaving
        } else {
            if (FApplication.DEBUG) DLog.e(TAG, "ComponentCallbacks was registered while tracking is suspended!");
        }
    }

    public void onComponentCallbacksUnregistered(ComponentCallbacks callback) {
        if (!mSuspended) {
            if (FApplication.DEBUG) {
                DLog.i(TAG, "unregisterComponentCallbacks: " + callback, new Throwable());
            }

            mCallbacks.remove(callback);
        }
    }

    public void unregisterAll(Context context) {
        mSuspended = true;
        for (Map.Entry<ComponentCallbacks, ApplicationErrorReport.CrashInfo> entry : mCallbacks.entrySet()) {
            ComponentCallbacks callback = entry.getKey();
            if (callback == null) continue;

            if (FApplication.DEBUG) {
                DLog.w(TAG, "Forcibly unregistering a misbehaving ComponentCallbacks: " + entry.getKey());
                DLog.w(TAG, entry.getValue().stackTrace);
            }

            try {
                context.unregisterComponentCallbacks(entry.getKey());
            } catch (Exception exc) {
                if (FApplication.DEBUG) DLog.e(TAG, "Unable to unregister ComponentCallbacks", exc);
            }
        }

        mCallbacks.clear();
        mSuspended = false;
    }
}
为方便webactivity的debug
  • applicationoncreate里面,我们加入
private static void enableStrictMode() {
StrictMode.ThreadPolicy.Builder threadPolicyBuilder =
    new StrictMode.ThreadPolicy.Builder()
    .detectAll()
    .penaltyLog();
    StrictMode.VmPolicy.Builder vmPolicyBuilder =
    new StrictMode.VmPolicy.Builder()
    .detectAll()
    .penaltyLog();
    threadPolicyBuilder.penaltyFlashScreen();
    vmPolicyBuilder.setClassInstanceLimit(WebActivity.class, 1);
    StrictMode.setThreadPolicy(threadPolicyBuilder.build());
    StrictMode.setVmPolicy(vmPolicyBuilder.build());
}
  • 在webview中,对于android4.4以上的版本,我们开启调试模式
if (FApplication.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true);
}
  • 最后不可缺少的是, leakcanary的加入来跟踪这些消耗内存的组件:
    webfragmentwebactivitywebview ondestory后加上
    RefWatcher refWatcher = FApplication.getRefWatcher();
    refWatcher.watch(obj);

总结: 如果你只是简单地用 webview 做呈现, 使用application context启动webview已经足够了,但如果你需要webview来播放视频,处理弹窗等复杂工作, 新建一个进程来处理会更可靠.


  1. Google将不再为Android 4.4之前版本提供WebView补丁

  2. this is how I fixed my WebView leak inside a fragment

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容