Android當記憶體監控到閾值時應該怎麼辦?

DK_BurNIng發表於2019-03-04

我們啟動一個空殼app,所謂空殼app 就是裡面實際上啥都沒有,一張圖片都沒有的app,在我的小米Note 4x上看看記憶體佔用多少

Android當記憶體監控到閾值時應該怎麼辦?

我曹 怎麼會有 36mb? wtf? 這個graphics裡面幾乎都是bitmap,為何這裡bitmap佔用記憶體這麼多呀?

我明明一張圖都沒載入啊。

開啟記憶體dump,匯出以後 用hprof工具轉換成mat可以解析的格式:

Android當記憶體監控到閾值時應該怎麼辦?

最後用mat開啟,搜下bitmap 看看是啥?

Android當記憶體監控到閾值時應該怎麼辦?

嗯?怎麼有這麼多bitmap物件?我沒載入過任何圖片啊?

查檢視都是誰引用的?

Android當記憶體監控到閾值時應該怎麼辦?

去原始碼裡面查查這是個sPreloadedDrawables啥東西?

既然是跟資源有關的東西 肯定繞不開ResourcesImpl這個類了,很快就能搜到

Android當記憶體監控到閾值時應該怎麼辦?

實際上解釋起來,就是android系統啟動的時候肯定是先啟動zygote程式,有了這個程式以後,其他app程式啟動 就只要fork他即可。所以實際上當zygote程式啟動的時候會把一些系統的資源優先載入到記憶體裡,注意 這個framework本身包含的資源是很多的,但是隻有很有限的資源才有資格到這預載入記憶體裡面

所以當我們zygote程式啟動完畢以後,這個程式的記憶體裡面就有這些 系統資源在裡面了,我們這裡只關注bitmap資源 也就是drawable,然後其他app程式啟動的時候既然是fork了zygote程式,自然而然的這裡 也會包含這些圖片資源。

這就是為什麼我們一個空殼app也會載入那麼多bitmap在記憶體裡的原因,這樣做的好處當然是如果你的app用到了這些預載入 的資源,那麼可以省略decode的過程,直接從記憶體裡面取,速度快,缺點就是這個東西佔用的記憶體著實有一些大了。。。 動不動20mb左右的空間。

我們來看下系統loadDrawable的過程就可以加深這個理解了

@Nullable
    Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
            boolean useCache) throws NotFoundException {
        try {
            if (TRACE_FOR_PRELOAD) {
                // Log only framework resources
                if ((id >>> 24) == 0x1) {
                    final String name = getResourceName(id);
                    if (name != null) {
                        Log.d("PreloadDrawable", name);
                    }
                }
            }

            final boolean isColorDrawable;
            final DrawableCache caches;
            final long key;
            if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                    && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                isColorDrawable = true;
                caches = mColorDrawableCache;
                key = value.data;
            } else {
                isColorDrawable = false;
                caches = mDrawableCache;
                key = (((long) value.assetCookie) << 32) | value.data;
            }

            // First, check whether we have a cached version of this drawable
            // that was inflated against the specified theme. Skip the cache if
            // we're currently preloading or we're not using the cache.
            if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
                    return cachedDrawable;
                }
            }

            // Next, check preloaded drawables. Preloaded drawables may contain
            // unresolved theme attributes.
            final Drawable.ConstantState cs;
            if (isColorDrawable) {
                cs = sPreloadedColorDrawables.get(key);
            } else {
                //這裡看到沒有 取系統快取了
                cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
            }
            //取不到就重新decode
            Drawable dr;
            if (cs != null) {
                dr = cs.newDrawable(wrapper);
            } else if (isColorDrawable) {
                dr = new ColorDrawable(value.data);
            } else {
                dr = loadDrawableForCookie(wrapper, value, id, null);
            }

            // Determine if the drawable has unresolved theme attributes. If it
            // does, we'll need to apply a theme and store it in a theme-specific
            // cache.
            final boolean canApplyTheme = dr != null && dr.canApplyTheme();
            if (canApplyTheme && theme != null) {
                dr = dr.mutate();
                dr.applyTheme(theme);
                dr.clearMutated();
            }

            // If we were able to obtain a drawable, store it in the appropriate
            // cache: preload, not themed, null theme, or theme-specific. Don't
            // pollute the cache with drawables loaded from a foreign density.
            if (dr != null && useCache) {
                dr.setChangingConfigurations(value.changingConfigurations);
                cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
            }

            return dr;
        } catch (Exception e) {
            String name;
            try {
                name = getResourceName(id);
            } catch (NotFoundException e2) {
                name = "(missing name)";
            }

            // The target drawable might fail to load for any number of
            // reasons, but we always want to include the resource name.
            // Since the client already expects this method to throw a
            // NotFoundException, just throw one of those.
            final NotFoundException nfe = new NotFoundException("Drawable " + name
                    + " with resource ID #0x" + Integer.toHexString(id), e);
            nfe.setStackTrace(new StackTraceElement[0]);
            throw nfe;
        }
    }
複製程式碼

所以這裡就給我們提供了一種優化思路:

當我們的app剛開始執行時,記憶體富餘空間很大,所以我們可以不關心這個系統快取帶來的問題,但是如果當我們發現 記憶體壓力變大的時候,就可以考慮在合適的時機,手動的釋放掉這20mb左右的系統快取了,釋放的方法也很簡單 反射clear一下即可:

Resources resource = getApplicationContext().getResources();
        try {
            //注意這個地方 有些rom會有獨特的名字,需要你們線上監控到不同rom以後 遍歷下他們的field看看這個變數
            //的名字是什麼 才能hook成功
            Field field = Resources.class.getDeclaredField("sPreloadedDrawables");
            field.setAccessible(true);
            LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables = (LongSparseArray<Drawable.ConstantState>[]) field
                    .get(resource);
            for (LongSparseArray<Drawable.ConstantState> s : sPreloadedDrawables) {
                s.clear();
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
複製程式碼

當然這樣的開銷也有弊病,就是如果你釋放掉這部分記憶體以後,如果要載入這些資源,那麼會多一步decode的過程。

當然現在外掛框架很多了,很多人都對resources這部分有了解,其實這個地方我們完全可以利用動態代理的方法 hook掉 系統的這個取快取的sPreloadedDrawables 過程,讓他們取 我們自己圖片載入框架裡的快取即可。 但是注意這個hook的過程要考慮不同rom的相容性的地方就更多了,有興趣的同學可以自行研究下。

相關文章