上篇博客咱們寫到了 Android 中內存泄漏的檢測以及相關案例,這篇咱們繼續來分析一下 Android 內存優化的相關內容。
上篇:Android 性能優化以內存泄漏檢測以及內存優化(上)。
中篇:Android 性能優化以內存泄漏檢測以及內存優化(中)。
下篇:Android 性能優化以內存泄漏檢測以及內存優化(下)。
轉載請註明出處:blog.csdn.net/self_study/…
對技術感興趣的同鞋加羣544645972一塊兒交流。javascript
上篇博客描述瞭如何檢測和處理內存泄漏,這種問題從某種意義上講是因爲代碼的錯誤致使的,可是也有一些是代碼沒有錯誤,可是咱們能夠經過不少方式去下降內存的佔用,使得應用的總體內存處於一個健康的水平,下面總結一下內存優化的幾個點:html
因爲圖片在應用中使用的較爲頻繁,並且圖片佔用的內存一般來講也比較大,舉個例子來講,如今正常的手機基本都在 1000W 像素左右的水平,較好的基本都在 1600W 像素,這時候拍出來的照片基本都在 34004600 這個水平,按照 ARGB_8888 的標準,一個像素 4 個字節,因此總共有 1600W\4=6400W 字節,總共 64M,也就是說會佔用 64M 的內存,而實際出來的 .png 圖片大小也就才 3M 左右,這是一個很是恐怖的數量,由於對於一個 2G 左右內存的手機來講,一個進程最大可用的內存可能也就在 100M+,一張圖片就可以佔用一半內存,這也就是爲何 decode 一個 bitmap 是發生 OOM 高頻的地方,因此在實際開發過程當中圖片的處理和內存佔用優化也是一個比較重要的地方。
Android中圖片有四種屬性,分別是:java
爲了找出在運行過程當中佔用內存很大的圖片,這個時候就能夠藉助上篇博客介紹到的 MAT 了,按照 Retained Heap 大小進行排序,找出佔用內存比較大的幾個對象,而後經過引用鏈找到持有它的地方,最後看可否有優化的地方。android
咱們通常將不一樣分辨率的圖片放置在不一樣的文件夾 hdpi/xhdpi/xxhdpi 下面進行適配,經過 android:background 來設置背景圖片或者使用 BitmapFactory.decodeResource() 方法的時候,圖片默認狀況下會進行縮放,在 Java 層實際調用的是 BitmapFactory 裏的 decodeResourceStream 方法:git
/** * Decode a new Bitmap from an InputStream. This InputStream was obtained from * resources, which we pass to be able to scale the bitmap accordingly. */
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}複製代碼
decodeResourceStream 在解析時會將 Bitmap 根據當前設備屏幕像素密度 densityDpi 的值進行縮放適配操做,使得解析出來的 Bitmap 與當前設備的分辨率匹配,達到一個最佳的顯示效果,上面也提到過,解析事後 Bitmap 的大小將比原始的大很多,關於 Bitmap 的詳細分析能夠看一下這篇博客:Android 開發繞不過的坑:你的 Bitmap 究竟佔多大內存?。
關於 Density、分辨率和相關 res 目錄的關係以下:github
DensityDpi | 分辨率 | res | Density |
---|---|---|---|
160dpi | 320 x 533 | mdpi | 1 |
240dpi | 460 x 800 | hdpi | 1.5 |
320dpi | 720 x 1280 | xhdpi | 2 |
480dpi | 1080 x 1920 | xxhdpi | 3 |
560dpi | 1440 x 2560 | xxxhdpi | 3.5 |
舉個例子來講一張 1920x1080 的圖片來講,若是放在 xhdpi 下面,那麼 xhdpi 設備將其轉成 bitmap 以後的大小是 1920x1080,而 xxhdpi 設備獲取的大小則是 2520x1418,大小約爲前者的 1.7 倍,這些內存對於移動設備來講已經算是比較大的差距。有一點須要提到的是新版本 Android Studio 已經使用 mipmap 來代替了,比起 drawable 官方的解釋是系統會在縮放上提供必定的性能優化:正則表達式
Mipmapping for drawables
Using a mipmap as the source for your bitmap or drawable is a simple way to provide a quality image and various image scales, which can be particularly useful if you expect your image to be scaled during an animation.
Android 4.2 (API level 17) added support for mipmaps in the Bitmap class—Android swaps the mip images in your Bitmap when you've supplied a mipmap source and have enabled setHasMipMap(). Now in Android 4.3, you can enable mipmaps for a BitmapDrawable object as well, by providing a mipmap asset and setting the android:mipMap attribute in a bitmap resource file or by calling hasMipMap().複製代碼
可是從用法來講和正常的 drawable 同樣。
系統也對圖片展現進行了相應的優化,對於相似在 xml 裏面直接經過 android:background 或者 android:src 設置的背景圖片,以 ImageView 爲例,最終會調用 ResourceImpl(低版本是 Resource) 類中的裏的 loadDrawable 方法,在這個方法中咱們能夠很清楚的看到系統針對相同的圖片使用享元模式構造了一個全局的緩存 DrawableCache 類的對象:數據庫
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;
}
}
.....
}複製代碼
DrawableCache 類繼承自 ThemedResourceCache 類,來看看這兩個相關類:數組
/** * Class which can be used to cache Drawable resources against a theme. */
class DrawableCache extends ThemedResourceCache<Drawable.ConstantState> {
......
}複製代碼
/** * Data structure used for caching data against themes. * * @param <T> type of data to cache */
abstract class ThemedResourceCache<T> {
private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
private LongSparseArray<WeakReference<T>> mUnthemedEntries;
private LongSparseArray<WeakReference<T>> mNullThemedEntries;
.....
}複製代碼
能夠看到這個類使用一個 ArrayMap 來存儲一個 Drawable 和這個 Drawable 對應的 Drawable.ConstantState 信息,相同的圖片對應相同的 Drawable.ConstantState,因此這就能夠保證在一些狀況下相同的圖片系統只須要保存一份,從而減小內存佔用。咱們從這裏能夠獲得一些啓示,若是咱們在某些會重複使用圖片的場景下,本身構造一個 Bitmap 緩存器,而後裏面保存 Bitmap 的 WeakReference,當使用的時候先去緩存裏面獲取,獲取不到再作解析的操做。緩存
BitmapFactory 在 decode 圖片的時候,能夠帶上一個 Options,這個不少人應該很熟悉,在 Options 中咱們能夠指定使用一些壓縮的功能:
要加載一張巨型的圖片,好比 20000*10000 分辨率的,這個時候全放進內存是徹底不可能的,直接會佔用 800M 內存,因此必需要用到上面說到的壓縮比,將其分辨率下降到和屏幕匹配,匹配以後若是還要去支持用戶的放大、縮小、左右滑動等操做,這時候就可使用 BitmapRegionDecoder 這個類去處理圖片了,具體的能夠去看看這篇博客:Android 高清加載巨圖方案 拒絕壓縮圖片,實現的原理就是分區域去加載,或者能夠去參考這個開源庫:WorldMap。
如今默認的圖片加載工具例如 Universal-ImageLoader 或者 Glide 都會使用一個 LruCache 來管理應用中的圖片緩存,通常緩衝池的大小設置爲應用可用內存的 1/8。
Android 系統自己內置了大量的資源,好比一些通用的字符串、顏色定義、經常使用 icon 圖片,還有些動畫和頁面樣式以及簡單佈局,若是沒有特別的要求,這些資源均可以在應用程序中直接引用。直接使用系統資源不只能夠在必定程度上減小內存的開銷,還能夠減小應用程序 APK 的體積:
<ListView
android:id="@+id/mylist"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>複製代碼
這裏咱們定義了一個 ListView,定義它的 id 是 "@+id/mylist",實際上,若是沒有特別的需求,就能夠利用系統定義的 ID,相似下面的樣子:
<ListView
android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>複製代碼
在 xml 文件中引用系統的 ID,只須要加上 「@android:」 前綴便可。若是是在Java代碼中使用系統資源,和使用本身的資源基本上是同樣的。不一樣的是,須要使用 android.R 類來使用系統的資源,而不是使用應用程序指定的 R 類。這裏若是要獲取 ListView 可使用 android.R.id.list 來獲取;
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium" />複製代碼
其中 android:textAppearance="?android:attr/textAppearanceMedium" 就是使用系統的 style,須要注意的是使用系統的 style 必須在想要使用的資源前面加 「?android:」 做爲前綴,而不是 「@android:」;
android:background ="@android:color/transparent"
。
上篇博客說到過頻繁的 GC 會形成內存的抖動,最終會致使內存當中存在不少內存碎片,雖然整體來講內存是可用的,可是當分配內存給一個大對象的時候,沒有一塊足夠大的連續區域能夠分配給這個對象就會形成 OOM,因此這個時候爲了減小內存抖動,須要去觀察 Memory Monitor,檢查應用的正常使用過程當中有沒有由於頻繁的內存分配和釋放致使鋸齒形狀的內存圖,若是有的話去檢查相關代碼,比較容易出現內存抖動的地方多是 convertView 沒有複用、頻繁拼接小的 String 字符串、在 for 循環中建立對象等等,找到問題所在,解決內存抖動。
ArrayMap 以及 SparseArray 是 Android 系統專門爲移動設備而定製的數據結構,用於在必定狀況下取代 HashMap 而達到節省內存的目的,對於 key 爲 int 的 HashMap 儘可能使用 SparceArray 替代(通常 Lint 也會提示開發者將其換成 SparceArray),大概能夠省30%的內存,而對於其餘類型,ArrayMap 對內存的節省實際並不明顯,10% 左右,可是數據量在 1000 以上時,查找速度可能會變慢,具體的能夠看看這篇博客:HashMap,ArrayMap,SparseArray源碼分析及性能對比。
最多見的例子就是當你要頻繁操做一個字符串時,使用 StringBuilder 代替 String。對於全部基本類型的組合:int 數組比 Integer 數組好,這也歸納了一個基本事實,兩個平行的 int 數組比 (int,int) 對象數組性能要好不少。整體來講,就是避免建立短命的臨時對象。減小對象的建立就能減小垃圾收集,進而減小對用戶體驗的影響。
Android 平臺上枚舉是比較爭議的,在較早的 Android 版本,使用枚舉會致使包過大,在某些狀況下使用枚舉甚至比直接使用 int 包的 size 大了 10 多倍。在 Stackoverflow 上也有不少的討論,大體意思是隨着虛擬機的優化,目前枚舉變量在 Android 平臺性能問題已經不大,而目前 Android 官方建議,使用枚舉變量仍是須要謹慎,由於枚舉變量可能比直接用 int 多使用 2 倍的內存,具體的能夠看看這個討論:Should I strictly avoid using enums on Android?
選擇系統類庫中的代碼而非本身重寫,第一個能夠節省少部份內存,第二個考慮到系統空閒時會用匯編代碼調用來替代系統類庫中方法,這可能比 JIT 中生成的等價的最好的 Java 代碼還要快:
雖然這或多或少有點渲染優化的味道,可是因爲 View 也是會佔用必定內存的,因此第一步是經過 Hierarchy Viewer 去去掉多餘的 View 層級,第二步是經過使用 ViewStub 去對一些能夠延遲加載的 View 作到使用時加載,必定程度上也能夠下降內存使用。
使用 Protocol Buffer 對數據進行壓縮(關於 Protocol Buffer 和其餘工具的對比,能夠看看這篇文章:thrift-protobuf-compare),Protocol Buffer 相比於 xml 能夠減小 30% 的內存使用量;慎用 SharedPreference,由於對於同一個 SP 有時候爲了讀取一個字段可能會將整個 xml 文件都加入內存,所以慎用 SP,或者能夠將一個大的 SP 分散爲幾個小的 SP;數據庫字段儘可能精簡,表設計合理,只讀取所須要的字段而不是整個結構都加載到內存當中。
有人以爲代碼多少與內存沒有關係,實際上會有那麼點關係,如今稍微大一點的項目動輒就是百萬行代碼以上,多 dex 也是常態,不只佔用 Rom 空間,實際上運行時候須要加載的 dex 也是會佔用內存的(幾 M),有時候爲了使用一些庫裏的某個功能函數就引入了整個龐大的庫是不太合適的,此時能夠考慮抽取必要部分;另外開啓 proguard 優化代碼,使用 Facebook redex 優化 dex(好像有很多坑)也是一種不錯的方式。
對於對象的重複使用來講,對象池模式和享元模式再合適不過了,具體的能夠去看看我博客裏面對於這兩個模式的介紹和使用。
咱們都知道 Android 用戶能夠隨意在不一樣的應用之間進行快速切換,系統爲了讓 Background 的應用可以迅速的切換到 Forground,每個 Background 的應用都會佔用必定的內存。Android 系統會根據當前的系統的內存使用狀況,在必定狀況下決定回收部分 Background 的應用內存,若是 Background 的應用從暫停狀態直接被恢復到 Forground,可以得到較快的恢復體驗,若是 Background 應用是從 Kill 狀態進行恢復,相比之下就顯得稍微有點慢:
使用多進程能夠把應用中的部分組件運行在單獨的進程當中,這樣能夠擴大應用的內存佔用範圍,可是這個技術必須謹慎使用,絕大多數應用都不該該貿然使用多進程,一方面是由於使用多進程會使得代碼邏輯更加複雜,另外若是使用不當,它可能反而會致使顯著的內存增長。當你的應用須要運行一個常駐後臺的任務,並且這個任務並不輕量,能夠考慮使用這個技術,一個典型的例子是建立一個能夠長時間後臺播放的 Music Player。若是整個應用都運行在一個進程中,當後臺播放的時候,前臺的那些 UI 資源也沒有辦法獲得釋放,相似這樣的應用能夠切分紅兩個進程:一個用來操做 UI,另一個給後臺的 Service。
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/luoshengyan…
blog.csdn.net/a396901990/…
mp.weixin.qq.com/s?__biz=MzA…
geek.csdn.net/news/detail…
www.jianshu.com/p/216b03c22…
zhuanlan.zhihu.com/p/25213586
joyrun.github.io/2016/08/08/…
www.cnblogs.com/larack/p/60…
source.android.com/devices/tec…
blog.csdn.net/high2011/ar…
gityuan.com/2015/10/03/…
www.ayqy.net/blog/androi…
developer.android.com/studio/prof…
zhuanlan.zhihu.com/p/26043999
www.csdn.net/article/201…