Android 內存管理認識與理解

前言:2018年三月,不得不再次跳槽.這次跳槽由於種種原因比以往都要急迫點。幸運的是正好趕上金三月,面試邀請還是很多的。基本每天都有幾家面試。但是面試十家左右,感覺到目前的處境,以及程序員就業的競爭環境。——艱難
學歷,技術,年齡,等各種尷尬問題凸現。

在移動設備開發中,內存管理是所有程序員面臨的一道難關。內存泄漏,內存溢出等問題又是Android面試中必問的問題。當然 關於此類問題,各個大佬都做出的相應回答範文,但是正因爲太在意這些回答範文,而對andorid內存管理機制原理似懂非懂,知其然而不知其所以然。

這裏是一些內存概念,我本身不是科班出身,所以就手打了一遍,加深印象。如果你是計算機出身,可以跳過這段介紹。同時也希望有一定計算基礎的同學幫忙指正。因爲看的比較雜,沒有系統的梳理。也沒有一個老師可以請教,一些概念都是自己總結,大家互相交流。

1、內存相關名詞介紹

地址寄存器:
用來保存當前CPU所訪問的內存單元的地址,由於在內存和CPU之間存在操作速度上的差異。所以必須使用地址寄存器來保持地址信息,一直到內存讀寫操作完成爲止。

RAM:
是一個讀寫存儲器,是程序運行時臨時存放數據的,是動態存放,每次開關機,內存都會清楚。相當於系統運行時的數據動態緩存區。

物理地址:
加載到內存地址寄存器中的內存“硬件內存”,是內存單元指向的地址。

邏輯地址:
由CPU控制生成的地址。是一個程序級別的概念。邏輯地址分配非常靈活,例如:在一個數組中,我可以通過邏輯地址的分配保證數組元素地址的連續性。當然邏輯地址最終要通過一定的方式映射到RAM中的物理地址上,而這個物理地址纔是元素存儲的真正地址,但是不一定是連續的。

虛擬內存:
是操作系統級別的概念,只計算機呈現出要比它實際內存量大的多的內存量。這裏又要引申出一個概念 交換空間。

交換空間:
在系統中運行的每個進程都要使用到內存,但是不是每個進程都需要時時刻刻使用系統分配的內存空間。當系統運行所需內存超過系統分配的內存空間時。內存會釋放某些進程所佔用但未使用的部分內存。 例如 火車運行在鐵軌上,在火車行駛過後的鐵軌鋪設在火車未行使的道路上,理論上或者只需要使用很少的鐵軌,就能行駛很長距離。

進程所擁有的內存空間指的是虛擬內存,虛擬地址/邏輯地址與進程息息相關。所以離開進程談虛擬內存沒有任何意義。

2、Android中的進程

Android是一個構建在linux系統上的開源移動開發平臺,在Android中,多數情況下每個程序都是在各自獨立的linux進程中運行。Android中分爲 native進程,java進程。

Native進程:是採用C/c++實現的,不包過Dalvik實例的Linux進程。
java進程: 實例化Dalvik虛擬機的linux進程

也就是說我平常開發程序是運行在java進程中。每個java進程會實例化一個dalvik 虛擬機。進程中的內存是JVM 分配與管理的)

JVM : 是一個虛構出來的計算機,是通過實際的計算機上 仿真模擬計算機功能來實現的,它有自己完善的 (虛擬)硬件架構,(處理器,堆棧,寄存器)使用java虛擬機程序就是爲了支持與操作系統無關,在任何系統中都可以運行的程序。——java語言的跨平臺特性。

DVM: 是google公司自己設計用於Andorid平臺的java虛擬機,也就是說 本質上。Dalivk也是一個java虛擬機,是Andoroid中java程序的運行基礎。(由java編譯流程得出(DVM+dx編譯器=JVM))

Dalvik可以允許多個instance 運行,也就是說每一個Android 的App是獨立跑在一個VM中.
一個應用,一個進程,一個Dalvik!

這裏寫圖片描述

2-1、Android中虛擬內存怎麼分配與管理?

1、分配機制:
爲每一個進程分配一個合理的內存大小,保證每一個進程都能夠正愁運行。

	詳細:Android爲每一個進程分配內存的時候,採用了彈性的分配方式,也就開始並不會一下子分配很多內存給每一個進程,而是給每一個進程分配一個“夠用”的量,這一個量是根據每一個設備實際的物理內存大小來決定的,這個時候Android又會爲每一個進程分配一些額外的內存大小。但是這些額外的內存並不是隨意分配的。也是有限額分配的。Andorid系統的宗旨是最大限度的讓更多的進程存活在內存中,因爲這樣的話,下一次用戶再啓動應用。不需要重新創建進程,只要恢復進程就可以了,

2、管理進程內存機制:
爲了使系統能夠正確決定在內存不足時,應該終止那個進程,Android根據每個進程中運行的組件以及組件的狀態把進程放入一個重要性分級中,進程的類型按照重要性排序分爲 前臺進程、可見進程、服務進程,後臺進程,空進程;

前臺進程:進程中有組件 例如Activity正與用戶進行交互  onpesume,或 service 中的一個回調正在運行等

可見進程:可以按照Activity生命週期的onstart理解,例如屏幕上的前臺進程是一個對話框,但是並沒有覆蓋全屏幕,可見進程中Activity於用戶來說是可見,單不能交互。

服務進程:進程中擁有的Service ,已經通過startService開啓了服務,雖然用戶無法看到進程中的具體組件,但是它們所做的事情確實用戶真正關心的(例如下載,播放歌曲)。

後臺進程:當前用戶看不到。進程組件onstop已經被調用。組件正常的生命週期已經走完。系統可以在任何時刻終止該進程,以確保其他優先級進程的內存需求。系統會把此類進程放在LRU列表中,以確保內存不足時 用戶最後一個看到進程,最後一個被銷燬。(LRU :最近最少使用算法)

空進程:進程沒有任何活動組件。保留此類進程是爲了確保用戶下次打開此類app,不用重新創建進程,這樣可以提升啓動速度。

3、進程中內存管理 ——這也是我們Android開發真正關心的內容

Java採用的GC(垃圾回收機制)進行內存管理Android 虛擬機的垃圾回收採用的是“根搜索算法”,GC 會從根節點(GC Roots)開始對heap進行遍歷,到最後,部分沒有直接或者間接引用到GCRoots節點的對象就是需要回收的垃圾,會被GC回收掉,而內存泄露出現的原因就是存在了無效的引用,導致本來需要被GC的對象沒有被回收。

垃圾回收算法相關
(1).可回收對象的判定
①.引用計數算法
給對象添加一個引用計數器,每當有一個地方引用它的時候,計數器的值就加1;當引用失效的時候,計數器的值就減1;任何時刻計數器爲0的對象是不可能再被引用的。
這種方法實現簡單,判斷效率也很高;但是該算法有一個致命的缺點就是難以解決對象相互引用的問題:試想有兩個對象,相互持有對方的引用,而沒有別的對象引用到這兩者,那麼這兩個對象就是無用的對象,理應被回收,但是由於他們互相持有對方的引用,因此他們的引用計數器不爲0,因此他們不能被回收。
②.可達性分析算法
爲了解決上面循環引用的問題,Java採用了一種全新的算法——可達性分析算法。這個算法的核心思想是,通過一系列稱爲“GC Roots”的對象作爲起始點,從這些結點開始向下搜索,搜索所走過的路徑成爲“引用鏈”,當一個對象到GC Roots沒有一個對象相連時,則證明此對象是不可用的(不可達)。

這裏寫圖片描述

無論是引用計數法還是可達性分析算法,判斷對象的存活與否都與“引用”有關。在JDK1.2之前,“引用”的解釋爲:如果reference類型的數據中儲存的數值代表的是另外一塊內存的起始地址,就稱這個數據代表着一個引用。在JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用。

強引用:就是指在程序代碼之中普遍存在的,類似於“Object obj = new Object();”這樣的引用,只要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象。

軟引用:用來描述一些還有用但並非必須的對象。對於軟引用關聯的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收的範圍,進行第二次回收——如果這次回收還沒有騰出足夠的內存,纔會內存溢出拋出異常。在JDK1.2之後,提供了SoftReference來實現軟引用。

弱引用:也是用來描述非必須對象的,但是他的強度比軟引用更弱一些。被弱引用引用的對象,只能生存到下一次GC之前,當GC發生時,無論無論當前內存是否足夠,都會回收掉被弱引用關聯的對象。JDK1.2之後,提供了WeakRefernce類來實現弱引用。

虛引用:是最弱的一種引用,一個對象有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個對象的實例。爲一個對象設置一個虛引用關聯的唯一目的就是能夠在這個對象被收集器回收的的時候收到一個系統的通知。

3-1、內存分配與內存存儲區域介紹:

棧 : 在執行方法時,方法一些內部變量的存儲都可以放在棧上面創建,方法執行結束時候這些存儲單元就會自動被註釋掉。棧內存包括分配的運算速度很快,
因爲在處理器裏面,容量有限,並且棧 是一塊連續的內存區域,大小是由操作系統決定,它先進後出,進出完全不會殘生碎片,運行效率高且穩定。

堆: 又稱動態內存,我們通常使用 new 來申請分配一個內存。GC會根據內存的使用情況,對堆內的垃圾進行回收,堆 內存是一塊不連續的內存區域。 如果頻繁new|remove
會造成大量的內存碎片,GC頻繁的回收導致內存抖動。會消耗我們的應用性能。

4、內存泄露、內存溢出、優化。

內存泄露:進程中的某些對象已經沒有使用的價值了,但是他們卻還可以直接或間接地被引用GCroot,當內存泄露過多的時候,再加上應用本身佔用的內存,時間長了就會導致內存溢出 oom.

4-1單例導致內存泄露

1.單例模式在Android開發中會經常用到,但是如果使用不當就會導致內存泄露。因爲單例的靜態特性使得它的生命週期同應用的生命週期一樣長,如果一個對象已經沒有用處了,但是單例還持有它的引用,那麼在整個應用程序的生命週期它都不能正常被回收,從而導致內存泄露。

public class AppSettings {
    private static AppSettings sInstance;
    private Context mContext;

    private AppSettings(Context context) {
        this.mContext = context;
        // 改進  爲了避免內存泄露,我可以獲取全局上下文
        this.mContext = context.getApplicationContext();
    }
    public static AppSettings getInstance(Context context) {
        //這裏我調用 getInstance 傳入參數(getActivity)
        if (sInstance == null) {
            sInstance = new AppSettings(context);
            // 靜態單利對象就持有了傳入activity對象。當前activity退出,內存並不會回收
            //(因爲sIntance作爲靜態單例(在應用程序的整個生命週期中存在)會繼續持有這個Activity的引用)
        }
        return sInstance;
    }
}

2、靜態變量導致內存泄露
靜態變量存儲在方法區,它的生命週期從類加載開始,到整個進程結束。一旦靜態變量初始化後,它所持有的引用只有等到進程結束纔會釋放。
比如下面這樣的情況,在Activity中爲了避免重複的創建info,將sInfo作爲靜態變量:

public class MainActivity2 extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        start();
    }
    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
        //即msg持有mHandler的引用,而mHandler是Activity的非靜態內部類實例,即mHandler持有Activity的引用,
        // 那麼我們就可以理解爲msg間接持有Activity的引用。msg被髮送後先放到消息隊列MessageQueue中,
        // 然後等待Looper的輪詢處理(MessageQueue和Looper都是與線程相關聯的,MessageQueue是Looper引用的成員變量,
        // 而Looper是保存在ThreadLocal中的)。那麼當Activity退出後,msg可能仍然存在於消息對列MessageQueue中未處理或者正在處理,
        // 那麼這樣就會導致Activity無法被回收,以致發生Activity的內存泄露。
}
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // 做相應邏輯
            }
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
        // 優化 在activity退出的時候,就把mHandler的回調和發送的消息給移除掉。
    }
}

3、開啓線程 或者IO流並沒有關閉Timer讀秒等引起的內存泄露。

4、屬性動畫造成內存泄露
動畫同樣是一個耗時任務,比如在Activity中啓動了屬性動畫(ObjectAnimator),但是在銷燬的時候,沒有調用cancle方法,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用Activity,這就造成Activity無法正常釋放。因此同樣要在Activity銷燬的時候cancel掉屬性動畫,避免發生內存泄漏。

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

4-2、內存溢出
當應用的heap 資源超過Dalvik虛擬機分配的內存時 就會內存溢出。
發生場景多集中於圖片加載,或者是列表拉去數據,沒有做分頁的情況下,一次拉去過去數據,申請內存大於dalvik 分配剩餘內存時。

關於處理大圖片
Android對於圖片編碼格式的不同,加載到手機佔用的內存大小也不一樣。

Android中RGB編碼格式
1、 RGB888
2、 RGB565
3、ARGB555
4、ARGB8888

Andorid中佔用的內存計算 圖片高度 * 圖片寬度 * 單位像素佔用內存
這裏的單位像素佔用內存就是根據編碼格式確定的。

Andorid BitmapConfig類中提供的 ARGB8888 ARGB4444 RGB565等常量,可以設置加載進入內存的圖片編碼格式,當然這樣計算也是不準備的,還要考慮圖片存放的目錄和手機屏幕密度的影響。

圖片實際大小與加載到imageview中所佔用的大小影響

一個imageview控件大小是 100*100;而加載的圖片是200的話,如果直接加載那麼部分內存是浪費掉的。
這裏我採用 bitmapFactory.options來加載所需的尺寸的圖片。這裏引用一個採樣率 inSampleSize
當 inSampleSize的值爲2時,加載的是原始大小的圖片1/2,inSampleSize的取值只能是2的倍數。如果外界傳給系統的inSampleSize不是2的倍數,例如3,那麼系統會向下去整並且選擇一個最接近2的指數來代替。

BitmapFactory類提供了四類方法:decodeFile、decodeResource、decodeStream、decodeByteArray,分別支持從文件系統,資源,輸入流,以及字節碼中加載出一個bitmap對象。其中decodeFile,decodeResource又間接調用了decodeStream方法。

// 壓縮圖片邊界,返回一個bitmap對象
    public static Bitmap decodeSampleBitmapForRrsource(Resources res, int resId, int reqwidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true; // 設置inJustDecodeBounds 爲true時,BitmapFactory會獲取bitmap的原始寬高但是不會去真實加載圖片
        BitmapFactory.decodeResource(res, resId, options);
        options.inSampleSize = calculateInSampleSize(options, reqwidth, reqHeight);
        // 計算出圖片採樣率之後,真正的去加載圖片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    // 計算需要壓縮圖片的採樣率
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqwidth, int reqHeight) {
        final int width = options.outWidth;
        final int height = options.outHeight;
        int insampleSize = 1;
        if (height > reqHeight || width > reqwidth) {
            final int halfwidth = width / 2;
            final int halfheight = height / 2;
            while ((halfwidth / insampleSize >= width) && (halfheight / insampleSize >= height)) {
                insampleSize *= 2;
            }
        }
        return 1;
    }

在開發中 對於圖片的處理遠遠沒有這麼簡單。這裏引申出andorid中的緩存策略,前面講解進程回收 ;LRU :最近最少使用算法。
它的核心思想是當緩存滿時,會優先淘汰那些最近最少使用的緩存對象,採用LRU算法的緩存由兩種緩存類,內存緩存:LruCache 本地緩存:DiskLruCache

LruCache 是android 3.1版本提供的一個緩存類。可以通過Support—v4 兼容。LruCache 內部採用一個LinkedHashMap以強引用的方式存儲外界的緩存對象。

DisLruCache 用於實現存儲設備緩存。 它不屬於android SDK的一部分。可以同過百度網址下載到源碼

當然以上我的都沒有使用過,關於圖片處理單方框架太多太多了。從最早期的 xutils-bitmaputils iamgeloader 到現在的Picasso Glide.

它們基本都是使用三級緩存策略:

什麼是三級緩存

網絡緩存, 不優先加載, 速度慢,浪費流量
本地緩存, 次優先加載, 速度快
內存緩存, 優先加載, 速度最快

三級緩存原理

首次加載 Android App 時,肯定要通過網絡交互來獲取圖片,之後我們可以將圖片保存至本地SD卡和內存中
之後運行 App 時,優先訪問內存中的圖片緩存,若內存中沒有,則加載本地SD卡中的圖片
總之,只在初次訪問新內容時,才通過網絡獲取圖片資源

站在巨人肩膀上看世界,參考或者拷貝以下博客。
Android內存管理分析總結
內存泄露優化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章