Android 基本優化(一)

一、Context的使用

二、Activity活動的管理

三、多線程的管理

  `AsyncTask`、`HandlerThread`、`IntentService` 與 `ThreadPool` 分別適合的使用場景以及各自的使用注意事項。

四、更高效的 ArrayMap 容器,使用 Android 系統提供的特殊容器來避免自動裝箱,避免使用枚舉類型,

五、Bitmap

Bitmap是內存消耗大戶,絕大多數的OOM崩潰都是在操作Bitmap時產生的,下面來看看如何幾個處理圖片的方法:

  • 1、圖片顯示:

我們需要根據需求去加載圖片的大小
例如:在列表中僅用於預覽時加載縮略圖(thumbnails )。
只有當用戶點擊具體條目想看詳細信息的時候,這時另啓動一個fragment/activity/對話框等等,去顯示整個圖片

  • 2、圖片大小:

直接使用ImageView顯示bitmap會佔用較多資源,特別是圖片較大的時候,可能導致崩潰。
使用BitmapFactory.Options設置inSampleSize, 這樣做可以減少對系統資源的要求。
屬性值inSampleSize表示縮略圖大小爲原始圖片大小的幾分之一,即如果這個值爲2,則取出的縮略圖的寬和高都是原始圖片的1/2,圖片大小就爲原始大小的1/4。

BitmapFactory.Options bitmapFactoryOptions = new BitmapFactory.Options();  
bitmapFactoryOptions.inJustDecodeBounds = true;  
bitmapFactoryOptions.inSampleSize = 2;  
// 這裏一定要將其設置回false,因爲之前我們將其設置成了true    
// 設置inJustDecodeBounds爲true後,decodeFile並不分配空間,即,BitmapFactory解碼出來的Bitmap爲Null,但可計算出原始圖片的長度和寬度    
options.inJustDecodeBounds = false;  
Bitmap bmp = BitmapFactory.decodeFile(sourceBitmap, options);  
  • 3、圖片像素:

Android中圖片有四種屬性,分別是:
ALPHA_8:每個像素佔用1byte內存
ARGB_4444:每個像素佔用2byte內存
ARGB_8888:每個像素佔用4byte內存 (默認)
RGB_565:每個像素佔用2byte內存

Android默認的顏色模式爲ARGB_8888,這個顏色模式色彩最細膩,顯示質量最高。但同樣的,佔用的內存也最大。 所以在對圖片效果不是特別高的情況下使用RGB_565(565沒有透明度屬性),如下:

publicstaticBitmapreadBitMap(Contextcontext, intresId) {  
    BitmapFactory.Optionsopt = newBitmapFactory.Options();  
    opt.inPreferredConfig = Bitmap.Config.RGB_565;  
    opt.inPurgeable = true;  
    opt.inInputShareable = true;  
    //獲取資源圖片   
    InputStreamis = context.getResources().openRawResource(resId);  
    returnBitmapFactory.decodeStream(is, null, opt);  
}  
  • 4、圖片回收:

使用Bitmap過後,就需要及時的調用Bitmap.recycle()方法來釋放Bitmap佔用的內存空間,而不要等Android系統來進行釋放。
下面是釋放Bitmap的示例代碼片段。

// 先判斷是否已經回收  
if(bitmap != null && !bitmap.isRecycled()){  
    // 回收並且置爲null  
    bitmap.recycle();  
    bitmap = null;  
}  
System.gc();  
  • 5、捕獲異常:

經過上面這些優化後還會存在報OOM的風險,所以下面需要一道最後的關卡——捕獲OOM異常:

Bitmap bitmap = null;  
try {  
    // 實例化Bitmap  
    bitmap = BitmapFactory.decodeFile(path);  
} catch (OutOfMemoryError e) {  
    // 捕獲OutOfMemoryError,避免直接崩潰  
}  
if (bitmap == null) {  
    // 如果實例化失敗 返回默認的Bitmap對象  
    return defaultBitmapMap;  
}  

六、修改對象引用類型

  • 1、引用類型:

引用分爲四種級別,這四種級別由高到低依次爲:強引用>軟引用>弱引用>虛引用。

  • 強引用(strong reference)
    如:Object object=new Object()object就是一個強引用了。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。

  • 軟引用(SoftReference)
    只有內存不夠時纔回收,常用於緩存;當內存達到一個閥值,GC就會去回收它;

  • 弱引用(WeakReference)

弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。

  • 虛引用(PhantomReference)
    “虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

  • 2、軟引用和弱引用的應用實例:

注意:對於SoftReference(軟引用)或者WeakReference(弱引用)的Bitmap緩存方案,現在已經不推薦使用了。自Android2.3版本(API Level 9)開始,垃圾回收器更着重於對軟/弱引用的回收,所以下面的內容可以選擇忽略。

在Android應用的開發中,爲了防止內存溢出,在處理一些佔用內存大而且聲明週期較長的對象時候,可以儘量應用軟引用和弱引用技術。

下面以使用軟引用爲例來詳細說明(弱引用的使用方式與軟引用是類似的):

假設我們的應用會用到大量的默認圖片,而且這些圖片很多地方會用到。如果每次都去讀取圖片,由於讀取文件需要硬件操作,速度較慢,會導致性能較低。所以我們考慮將圖片緩存起來,需要的時候直接從內存中讀取。但是,由於圖片佔用內存空間比較大,緩存很多圖片需要很多的內存,就可能比較容易發生OutOfMemory異常。這時,我們可以考慮使用軟引用技術來避免這個問題發生。

首先定義一個HashMap,保存軟引用對象。

private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();  

再來定義一個方法,保存Bitmap的軟引用到HashMap。

public void addBitmapToCache(String path) {  
       // 強引用的Bitmap對象  
       Bitmap bitmap = BitmapFactory.decodeFile(path);  
       // 軟引用的Bitmap對象  
       SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);  
       // 添加該對象到Map中使其緩存  
       imageCache.put(path, softBitmap);  
   }  

獲取的時候,可以通過SoftReferenceget()方法得到Bitmap對象。

public Bitmap getBitmapByPath(String path) {  
        // 從緩存中取軟引用的Bitmap對象  
        SoftReference<Bitmap> softBitmap = imageCache.get(path);  
        // 判斷是否存在軟引用  
        if (softBitmap == null) {  
            return null;  
        }  
        // 取出Bitmap對象,如果由於內存不足Bitmap被回收,將取得空  
        Bitmap bitmap = softBitmap.get();  
        return bitmap;  
    }  

使用軟引用以後,在OutOfMemory異常發生之前,這些緩存的圖片資源的內存空間可以被釋放掉的,從而避免內存達到上限,避免Crash發生。
需要注意的是,在垃圾回收器對這個Java對象回收前,SoftReference類所提供的get方法會返回Java對象的強引用,一旦垃圾線程回收該Java對象之後,get方法將返回null。所以在獲取軟引用對象的代碼中,一定要判斷是否爲null,以免出現NullPointerException異常導致應用崩潰。

  • 3、到底什麼時候使用軟引用,什麼時候使用弱引用呢?

    個人認爲,如果只是想避免OutOfMemory異常的發生,則可以使用軟引用。如果對於應用的性能更在意,想盡快回收一些佔用內存比較大的對象,則可以使用弱引用。
    還有就是可以根據對象是否經常使用來判斷。如果該對象可能會經常使用的,就儘量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。
    另外,和弱引用功能類似的是WeakHashMap。WeakHashMap對於一個給定的鍵,其映射的存在並不阻止垃圾回收器對該鍵的回收,回收以後,其條目從映射中有效地移除。WeakHashMap使用ReferenceQueue實現的這種機制。

七、其他提示

  • 對常量使用static final修飾符

讓我們來看看這兩段在類前面的聲明:

static int intVal = 42;
static String strVal = “Hello, world!”;
編譯器會生成一個叫做clinit的初始化類的方法,當類第一次被使用的時候這個方法會被執行。方法會將42賦給intVal,然後把一個指向類中常量表 的引用賦給strVal。當以後要用到這些值的時候,會在成員變量表中查找到他們。 下面我們做些改進,使用“final”關鍵字:

static final int intVal = 42;
static final String strVal = “Hello, world!”;

現在,類不再需要clinit方法,因爲在成員變量初始化的時候,會將常量直接保存到類文件中。用到intVal的代碼被直接替換成42,而使用strVal的會指向一個字符串常量,而不是使用成員變量。

將一個方法或類聲明爲final不會帶來性能的提升,但是會幫助編譯器優化代碼。舉例說,如果編譯器知道一個getter方法不會被重載,那麼編譯器會對其採用內聯調用。

你也可以將本地變量聲明爲final,同樣,這也不會帶來性能的提升。使用“final”只能使本地變量看起來更清晰些(但是也有些時候這是必須的,比如在使用匿名內部類的時候)。

  • 靜態方法代替虛擬方法

如果不需要訪問某對象的字段,將方法設置爲靜態,調用會加速15%到20%。這也是一種好的做法,因爲你可以從方法聲明中看出調用該方法不需要更新此對象的狀態。

  • 減少不必要的全局變量

儘量避免static成員變量引用資源耗費過多的實例,比如Context

因爲Context的引用超過它本身的生命週期,會導致Context泄漏。所以儘量使用Application這種Context類型。 你可以通過調用Context.getApplicationContext()或 Activity.getApplication()輕鬆得到Application對象。

  • 避免創建不必要的對象

最常見的例子就是當你要頻繁操作一個字符串時,使用StringBuffer代替String。

對於所有所有基本類型的組合:int數組比Integer數組好,這也概括了一個基本事實,兩個平行的int數組比 (int,int)對象數組性能要好很多。

總體來說,就是避免創建短命的臨時對象。減少對象的創建就能減少垃圾收集,進而減少對用戶體驗的影響。

  • 避免內部Getters/Setters

在Android中,虛方法調用的代價比直接字段訪問高昂許多。通常根據面嚮對象語言的實踐,在公共接口中使用Getters和Setters是有道理的,但在一個字段經常被訪問的類中宜採用直接訪問。

  • 避免使用浮點數

通常的經驗是,在Android設備中,浮點數會比整型慢兩倍。

  • 使用實體類比接口好

假設你有一個HashMap對象,你可以將它聲明爲HashMap或者Map:

Map map1 = new HashMap();
HashMap map2 = new HashMap();
哪個更好呢?

按照傳統的觀點Map會更好些,因爲這樣你可以改變他的具體實現類,只要這個類繼承自Map接口。傳統的觀點對於傳統的程序是正確的,但是它並不適合嵌入式系統。調用一個接口的引用會比調用實體類的引用多花費一倍的時間。如果HashMap完全適合你的程序,那麼使用Map就沒有什麼價值。如果有些地方你不能確定,先避免使用Map,剩下的交給IDE提供的重構功能好了。(當然公共API是一個例外:一個好的API常常會犧牲一些性能)

  • 避免使用枚舉

枚舉變量非常方便,但不幸的是它會犧牲執行的速度和並大幅增加文件體積。

使用枚舉變量可以讓你的API更出色,並能提供編譯時的檢查。所以在通常的時候你毫無疑問應該爲公共API選擇枚舉變量。但是當性能方面有所限制的時候,你就應該避免這種做法了。

  • for循環

訪問成員變量比訪問本地變量慢得多,如下面一段代碼:
for(int i =0; i < this.mCount; i++) {}
永遠不要在for的第二個條件中調用任何方法,如下面一段代碼:
for(int i =0; i < this.getCount(); i++) {}
對上面兩個例子最好改爲:
int count = this.mCount; / int count = this.getCount();
for(int i =0; i < count; i++) {}

在java1.5中引入的for-each語法。編譯器會將對數組的引用和數組的長度保存到本地變量中,這對訪問數組元素非常好。 但是編譯器還會在每次循環中產生一個額外的對本地變量的存儲操作(如下面例子中的變量a),這樣會比普通循環多出4個字節,速度要稍微慢一些:
for (Foo a : mArray) {
sum += a.mSplat;
}

  • 瞭解並使用類庫

選擇Library中的代碼而非自己重寫,除了通常的那些原因外,考慮到系統空閒時會用匯編代碼調用來替代library方法,這可能比JIT中生成的等價的最好的Java代碼還要好。

當你在處理字串的時候,不要吝惜使用String.indexOf()String.lastIndexOf()等特殊實現的方法。這些方法都是使用C/C++實現的,比起Java循環快10到100倍。

System.arraycopy方法在有JITNexus One上,自行編碼的循環快9倍。

android.text.format包下的Formatter類,提供了IP地址轉換、文件大小轉換等方法;DateFormat類,提供了各種時間轉換,都是非常高效的方法。

TextUtils類,對於字符串處理Android爲我們提供了一個簡單實用的TextUtils類,如果處理比較簡單的內容不用去思考正則表達式不妨試試這個在android.text.TextUtils的類

高性能MemoryFile類,很多人抱怨Android處理底層I/O性能不是很理想,如果不想使用NDK則可以通過MemoryFile類實現高性能的文件讀寫操作。MemoryFile適用於哪些地方呢?對於I/O需要頻繁操作的,主要是和外部存儲相關的I/O操作,MemoryFile通過將 NAND或SD卡上的文件,分段映射到內存中進行修改處理,這樣就用高速的RAM代替了ROM或SD卡,性能自然提高不少,對於Android手機而言同時還減少了電量消耗。該類實現的功能不是很多,直接從Object上繼承,通過JNI的方式直接在C底層執行。

  • Reuse:

Reuse重用,減少內存消耗的重要手段之一。
核心思路就是將已經存在的內存資源重新使用而避免去創建新的,最典型的使用就是緩存(Cache)和池(Pool)。

  • Bitmap緩存:

Bitmap緩存分爲兩種:

一種是內存緩存,一種是硬盤緩存。

內存緩存(LruCache):

以犧牲寶貴的應用內存爲代價,內存緩存提供了快速的Bitmap訪問方式。系統提供的LruCache類是非常適合用作緩存Bitmap任務的,它將最近被引用到的對象存儲在一個強引用的LinkedHashMap中,並且在緩存超過了指定大小之後將最近不常使用的對象釋放掉。

注意:以前有一個非常流行的內存緩存實現是SoftReference(軟引用)或者WeakReference(弱引用)的Bitmap緩存方案,然而現在已經不推薦使用了。自Android2.3版本(API Level 9)開始,垃圾回收器更着重於對軟/弱引用的回收,這使得上述的方案相當無效。

硬盤緩存(DiskLruCache):

一個內存緩存對加速訪問最近瀏覽過的Bitmap非常有幫助,但是你不能侷限於內存中的可用圖片。GridView這樣有着更大的數據集的組件可以很輕易消耗掉內存緩存。你的應用有可能在執行其他任務(如打電話)的時候被打斷,並且在後臺的任務有可能被殺死或者緩存被釋放。一旦用戶重新聚焦(resume)到你的應用,你得再次處理每一張圖片。

在這種情況下,硬盤緩存可以用來存儲Bitmap並在圖片被內存緩存釋放後減小圖片加載的時間(次數)。當然,從硬盤加載圖片比內存要慢,並且應該在後臺線程進行,因爲硬盤讀取的時間是不可預知的。

注意:如果訪問圖片的次數非常頻繁,那麼ContentProvider可能更適合用來存儲緩存圖片,例如Image Gallery這樣的應用程序。

更多關於內存緩存和硬盤緩存的內容請看Google官方教程https://developer.android.com/develop/index.html

  • 圖片緩存的開源項目:

對於圖片的緩存現在都傾向於使用開源項目,這裏我列出幾個我搜到的:

  1. Android-Universal-Image-Loader 圖片緩存
    目前使用最廣泛的圖片緩存,支持主流圖片緩存的絕大多數特性。
    項目地址:https://github.com/nostra13/Android-Universal-Image-Loader

  2. picasso square開源的圖片緩存
    項目地址:https://github.com/square/picasso
    特點:(1)可以自動檢測adapter的重用並取消之前的下載
    (2)圖片變換
    (3)可以加載本地資源
    (4)可以設置佔位資源
    (5)支持debug模式

  3. ImageCache 圖片緩存,包含內存和Sdcard緩存
    項目地址:https://github.com/Trinea/AndroidCommon
    特點:
    (1)支持預取新圖片,支持等待隊列
    (2)包含二級緩存,可自定義文件名保存規則
    (3)可選擇多種緩存算法(FIFO、LIFO、LRU、MRU、LFU、MFU等13種)或自定義緩存算法
    (4)可方便的保存及初始化恢復數據
    (5)支持不同類型網絡處理
    (6)可根據系統配置初始化緩存等

  4. Android 網絡通信框架Volley
    項目地址:https://android.googlesource.com/platform/frameworks/volley
    我們在程序中需要和網絡通信的時候,大體使用的東西莫過於AsyncTaskLoader,HttpURLConnection,AsyncTask,HTTPClient(Apache)等,在2013年的Google I/O發佈了Volley。Volley是Android平臺上的網絡通信庫,能使網絡通信更快,更簡單,更健壯。
    特點:
    (1)JSON,圖像等的異步下載;
    (2)網絡請求的排序(scheduling)
    (3)網絡請求的優先級處理
    (4)緩存
    (5)多級別取消請求
    (6)和Activity和生命週期的聯動(Activity結束時同時取消所有網絡請
    求)


  • Adapter適配器

在Android中Adapter使用十分廣泛,特別是在list中。所以adapter是數據的 “集散地” ,所以對其進行內存優化是很有必要的。
下面算是一個標準的使用模版:
主要使用convertViewViewHolder來進行緩存處理

@Override  
public View getView(int position, View convertView, ViewGroup parent) {  
    ViewHolder vHolder = null;  
    //如果convertView對象爲空則創建新對象,不爲空則複用    
    if (convertView == null) {  
        convertView = inflater.inflate(..., null);  
        // 創建 ViewHodler 對象    
        vHolder = new ViewHolder();  
        vHolder.img= (ImageView) convertView.findViewById(...);  
        vHolder.tv= (TextView) convertView.findViewById(...);  
        // 將ViewHodler保存到Tag中(Tag可以接收Object類型對象,所以任何東西都可以保存在其中)  
        convertView.setTag(vHolder);  
    } else {  
        //當convertView不爲空時,通過getTag()得到View    
        vHolder = (ViewHolder) convertView.getTag();  
    }  
    // 給對象賦值,修改顯示的值    
    vHolder.img.setImageBitmap(...);  
    vHolder.tv.setText(...);  
    return convertView;  
}  
//將顯示的View 包裝成類    
static class ViewHolder {  
    TextView tv;  
    ImageView img;  
}  
  • 池(PooL)

    1. 對象池:
      對象池使用的基本思路是:將用過的對象保存起來,等下一次需要這種對象的時候,再拿出來重複使用,從而在一定程度上減少頻繁創建對象所造成的開銷。 並非所有對象都適合拿來池化――因爲維護對象池也要造成一定開銷。對生成時開銷不大的對象進行池化,反而可能會出現“維護對象池的開銷”大於“生成新對象的開銷”,從而使性能降低的情況。但是對於生成時開銷可觀的對象,池化技術就是提高性能的有效策略了。

    2. 線程池:
      線程池的基本思想還是一種對象池的思想,開闢一塊內存空間,裏面存放了衆多(未死亡)的線程,池中線程執行調度由池管理器來處理。當有線程任務時,從池中取一個,執行完成後線程對象歸池,這樣可以避免反覆創建線程對象所帶來的性能開銷,節省了系統的資源。
      比如:一個應用要和網絡打交道,有很多步驟需要訪問網絡,爲了不阻塞主線程,每個步驟都創建個線程,在線程中和網絡交互,用線程池就變的簡單,線程池是對線程的一種封裝,讓線程用起來更加簡便,只需要創一個線程池,把這些步驟像任務一樣放進線程池,在程序銷燬時只要調用線程池的銷燬函數即可。

java提供了ExecutorServiceExecutors類,我們可以應用它去建立線程池。
通常可以建立如下4種:

/** 每次只執行一個任務的線程池 */  
ExecutorService singleTaskExecutor=Executors.newSingleThreadExecutor();
/** 每次執行限定個數個任務的線程池 */  
ExecutorService limitedTaskExecutor=Executors.newFixedThreadPool(3);
/** 所有任務都一次性開始的線程池 */  
ExecutorService allTaskExecutor=Executors.newCachedThreadPool();
/** 創建一個可在指定時間裏執行任務的線程池,亦可重複執行 */  
ExecutorService scheduledTaskExecutor=Executors.newScheduledThreadPool(3);

更多關於線程池的內容我推薦這篇文章:http://www.xuanyusong.com/archives/2439
注意:
要根據情況適度使用緩存,因爲內存有限。
能保存路徑地址的就不要存放圖片數據,不經常使用的儘量不要緩存,不用時就清空。


  • OOM:

內存泄露可以引發很多的問題:

  1. 程序卡頓,響應速度慢(內存佔用高時JVM虛擬機會頻繁觸發GC)

  2. 莫名消失(當你的程序所佔內存越大,它在後臺的時候就越可能被幹掉。反之內存佔用越小,在後臺存在的時間就越長)

  3. 直接崩潰(OutOfMemoryError)


  • ANDROID內存面臨的問題:

    1. 有限的堆內存,原始只有16M

    2. 內存大小消耗等根據設備,操作系統等級,屏幕尺寸的不同而不同

    3. 程序不能直接控制

    4. 支持後臺多任務處理(multitasking)

    5. 運行在虛擬機之上

5R:

本文主要通過如下的5R方法來對ANDROID內存進行優化:

  1. Reckon(計算)
    首先需要知道你的app所消耗內存的情況,知己知彼才能百戰不殆

  2. Reduce(減少)
    消耗更少的資源

  3. Reuse(重用)
    當第一次使用完以後,儘量給其他的使用

  4. Review(檢查)
    回顧檢查你的程序,看看設計或代碼有什麼不合理的地方。

  5. Recycle(回收)
    回收資源

內存簡介,Reckon(計算):

關於內存簡介,和Reckon的內容請看:ANDROID內存優化(大彙總——上)

Reduce(減少) ,Reuse(重用):

關於Reduce,和Reuse的內容請看:ANDROID內存優化(大彙總——中)

Recycle(回收):

Recycle(回收),回收可以說是在內存使用中最重要的部分。因爲內存空間有限,無論你如何優化,如何節省內存總有用完的時候。而回收的意義就在於去清理和釋放那些已經閒置,廢棄不再使用的內存資源和內存空間。

因爲在Java中有垃圾回收(GC)機制,所以我們平時都不會太關注它,下面就來簡單的介紹一下回收機制:


  • 垃圾回收(GC):

Java垃圾回收器:

在C,C++或其他程序設計語言中,資源或內存都必須由程序員自行聲明產生和回收,否則其中的資源將消耗,造成資源的浪費甚至崩潰。但手工回收內存往往是一項複雜而艱鉅的工作。

於是,Java技術提供了一個系統級的線程,即垃圾收集器線程(Garbage Collection Thread),來跟蹤每一塊分配出去的內存空間,當Java 虛擬機(Java Virtual Machine)處於空閒循環時,垃圾收集器線程會自動檢查每一快分配出去的內存空間,然後自動回收每一快可以回收的無用的內存塊。

作用:

  1. 清除不用的對象來釋放內存:
    採用一種動態存儲管理技術,它自動地釋放不再被程序引用的對象,按照特定的垃圾收集算法來實現資源自動回收的功能。當一個對象不再被引用的時候,內存回收它佔領的空間,以便空間被後來的新對象使用。

  2. 消除堆內存空間的碎片:
    由於創建對象和垃圾收集器釋放丟棄對象所佔的內存空間,內存會出現碎片。碎片是分配給對象的內存塊之間的空閒內存洞。碎片整理將所佔用的堆內存移到堆的一端,JVM將整理出的內存分配給新的對象。

垃圾回收器優點:

  1. 減輕編程的負擔,提高效率:
    使程序員從手工回收內存空間的繁重工作中解脫了出來,因爲在沒有垃圾收集機制的時候,可能要花許多時間來解決一個難懂的存儲器問題。在用Java語言編程的時候,靠垃圾收集機制可大大縮短時間。

  2. 它保護程序的完整性:
    因此垃圾收集是Java語言安全性策略的一個重要部份。

垃圾回收器缺點:

  1. 佔用資源時間:
    Java虛擬機必須追蹤運行程序中有用的對象, 而且最終釋放沒用的對象。這一個過程需要花費處理器的時間。

  2. 不可預知:
    垃圾收集器線程雖然是作爲低優先級的線程運行,但在系統可用內存量過低的時候,它可能會突發地執行來挽救內存資源。當然其執行與否也是不可預知的。

  3. 不確定性:
    不能保證一個無用的對象一定會被垃圾收集器收集,也不能保證垃圾收集器在一段Java語言代碼中一定會執行。
    同樣也沒有辦法預知在一組均符合垃圾收集器收集標準的對象中,哪一個會被首先收集。

  4. 不可操作
    垃圾收集器不可以被強制執行,但程序員可以通過調用System. gc方法來建議執行垃圾收集器。

垃圾回收算法:

  1. 引用計數(Reference Counting)
    比較古老的回收算法。原理是此對象有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數爲0的對象。此算法最致命的是無法處理循環引用的問題。
  2. 標記-清除(Mark-Sweep)
    此算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此算法需要暫停整個應用,同時,會產生內存碎片。
  3. 複製(Copying)
    此算法把內存空間劃爲兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另外一個區域中。次算法每次只處理正在使用中的對象,因此複製成本比較小,同時複製過去以後還能進行相應的內存整理,不過出現“碎片”問題。當然,此算法的缺點也是很明顯的,就是需要兩倍內存空間。
  4. 標記-整理(Mark-Compact)
    此算法結合了 “標記-清除”和“複製”兩個算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,把清除未標記對象並且把存活對象 “壓縮”到堆的其中一塊,按順序排放。此算法避免了“標記-清除”的碎片問題,同時也避免了“複製”算法的空間問題。
  5. 增量收集(Incremental Collecting)
    實施垃圾回收算法,即:在應用進行的同時進行垃圾回收。不知道什麼原因JDK5.0中的收集器沒有使用這種算法的。
  6. 分代(Generational Collecting)
    基於對對象生命週期分析後得出的垃圾回收算法。把對象分爲年青代、年老代、持久代,對不同生命週期的對象使用不同的算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此算法的。

  • finalize():

每一個對象都有一個finalize方法,這個方法是從Object類繼承來的。

當垃圾回收確定不存在對該對象的更多引用時,由對象的垃圾回收器調用此方法。

Java 技術允許使用finalize方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。一旦垃圾回收器準備好釋放對象佔用的空間,將首先調用其finalize()方法,並且在下一次垃圾回收動作發生時,纔會真正回收對象佔用的內存。
簡單的說finalize方法是在垃圾收集器刪除對象之前對這個對象調用的

  • System.gc():

我們可以調用System.gc方法,建議虛擬機進行垃圾回收工作(注意,是建議,但虛擬機會不會這樣幹,我們也無法預知!)

下面來看一個例子來了解finalize()和System.gc()的使用:

public class TestGC {  
    public TestGC() {}  

    //當垃圾回收器確定不存在對該對象的更多引用時,由對象的垃圾回收器調用此方法。  
    protected void finalize() {  
        System.out.println("我已經被垃圾回收器回收了...");  
    }  

    public static void main(String [] args) {  
        TestGC gc = new TestGC();  
        gc = null;    
        // 建議虛擬機進行垃圾回收工作  
        System.gc();  
    }  
}  

如上面的例子所示,大家可以猜猜重寫的finalize方法會不會執行?

答案是:不一定!

因爲無論是設置gc的引用爲null還是調用System.gc()方法都只是”建議”垃圾回收器進行垃圾回收,但是最終所有權還在垃圾回收器手中,它會不會進行回收我們無法預知!

  • 垃圾回收面試題:

最後通過網上找到的3道面試題來結束垃圾回收的內容。

面試題一:
1.fobj = new Object ( ) ;
2.fobj. Method ( ) ;
3.fobj = new Object ( ) ;
4.fobj. Method ( ) ;

問:這段代碼中,第幾行的fobj 符合垃圾收集器的收集標準?
答:第3行。因爲第3行的fobj被賦了新值,產生了一個新的對象,即換了一塊新的內存空間,也相當於爲第1行中的fobj賦了null值。這種類型的題是最簡單的。

面試題二:
1.Object sobj = new Object ( ) ;
2.Object sobj = null ;
3.Object sobj = new Object ( ) ;
4.sobj = new Object ( ) ;
問:這段代碼中,第幾行的內存空間符合垃圾收集器的收集標準?
答:第2行和第4行。因爲第2行爲sobj賦值爲null,所以在此第1行的sobj符合垃圾收集器的收集標準。而第4行相當於爲sobj賦值爲null,所以在此第3行的sobj也符合垃圾收集器的收集標準。

如果有一個對象的句柄a,且你把a作爲某個構造器的參數,即 new Constructor ( a )的時候,即使你給a賦值爲null,a也不符合垃圾收集器的收集標準。直到由上面構造器構造的新對象被賦空值時,a纔可以被垃圾收集器收集。

面試題三:
1.Object aobj = new Object ( ) ;
2.Object bobj = new Object ( ) ;
3.Object cobj = new Object ( ) ;
4.aobj = bobj;
5.aobj = cobj;
6.cobj = null;
7.aobj = null;
問:這段代碼中,第幾行的內存空間符合垃圾收集器的收集標準?
答:第4,7行。注意這類題型是認證考試中可能遇到的最難題型了。
行1-3:分別創建了Object類的三個對象:aobj,bobj,cobj
行4:此時對象aobj的句柄指向bobj,原來aojb指向的對象已經沒有任何引用或變量指向,這時,就符合回收標準。
行5:此時對象aobj的句柄指向cobj,所以該行的執行不能使aobj符合垃圾收集器的收集標準。
行6:此時仍沒有任何一個對象符合垃圾收集器的收集標準。
行7:對象cobj符合了垃圾收集器的收集標準,因爲cobj的句柄指向單一的地址空間。在第6行的時候,cobj已經被賦值爲null,但由cobj同時還指向了aobj(第5行),所以此時cobj並不符合垃圾收集器的收集標準。而在第7行,aobj所指向的地址空間也被賦予了空值null,這就說明了,由cobj所指向的地址空間已經被完全地賦予了空值。所以此時cobj最終符合了垃圾收集器的收集標準。 但對於aobj和bobj,仍然無法判斷其是否符合收集標準。

總之,在Java語言中,判斷一塊內存空間是否符合垃圾收集器收集的標準只有兩個:
1.給對象賦予了空值null,以下再沒有調用過。
2.給對象賦予了新值,既重新分配了內存空間。

最後再次提醒一下,一塊內存空間符合了垃圾收集器的收集標準,並不意味着這塊內存空間就一定會被垃圾收集器收集。


  • 資源的回收:

剛纔講了一堆理論的東西,下面來點實際能用上的,資源的回收:

Thread(線程)回收:

線程中涉及的任何東西GC都不能回收(Anything reachable by a thread cannot be GC’d ),所以線程很容易造成內存泄露。

如下面代碼所示:

Thread t = new Thread() {  
    public void run() {  
        while (true) {  
            try {  
                Thread.sleep(1000);  
                System.out.println("thread is running...");  
            } catch (InterruptedException e) {  

            }  
        }  
    }  
};  
t.start();  
t = null;  
System.gc();  

如上在線程t中每間隔一秒輸出一段話,然後將線程設置爲null並且調用System.gc方法。

最後的結果是線程並不會被回收,它會一直的運行下去。

因爲運行中的線程是稱之爲垃圾回收根(GC Roots)對象的一種,不會被垃圾回收。當垃圾回收器判斷一個對象是否可達,總是使用垃圾回收根對象作爲參考點。

  • Cursor(遊標)回收:

Cursor是Android查詢數據後得到的一個管理數據集合的類,在使用結束以後。應該保證Cursor佔用的內存被及時的釋放掉,而不是等待GC來處理。並且Android明顯是傾向於編程者手動的將Cursor close掉,因爲在源代碼中我們發現,如果等到垃圾回收器來回收時,會給用戶以錯誤提示。

所以我們使用Cursor的方式一般如下:

Cursor cursor = null;  
try {  
    cursor = mContext.getContentResolver().query(uri,null, null,null,null);  
    if(cursor != null) {  
        cursor.moveToFirst();  
        //do something  
    }  
} catch (Exception e) {  
    e.printStackTrace();  
} finally {  
    if (cursor != null) {  
        cursor.close();  
    }  
}  

有一種情況下,我們不能直接將Cursor關閉掉,這就是在CursorAdapter中應用的情況,但是注意,CursorAdapter在Acivity結束時並沒有自動的將Cursor關閉掉,因此,你需要在onDestroy函數中,手動關閉。

@Override    
protected void onDestroy() {          
    if (mAdapter != null && mAdapter.getCurosr() != null) {    
        mAdapter.getCursor().close();    
    }    
    super.onDestroy();     
}    
  • Receiver(接收器)回收

調用registerReceiver()後未調用unregisterReceiver()
當我們Activity中使用了registerReceiver()方法註冊了BroadcastReceiver,一定要在Activity的生命週期內調用unregisterReceiver()方法取消註冊
也就是說registerReceiver()unregisterReceiver()方法一定要成對出現,通常我們可以重寫Activity的onDestory()方法:

@Override    
protected void onDestroy() {    
      this.unregisterReceiver(receiver);    
      super.onDestroy();    
}    
  • Stream/File(流/文件)回收:

主要針對各種流,文件資源等等如:

InputStream/OutputStream,SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O,Bitmap圖片等操作等都應該記得顯示關閉。
和之前介紹的Cursor道理類似,就不多說了。

  • Review:

Review(回顧,檢查),大家都知道Code Review的重要性。而這裏我說的Review和Code Review差不多,主要目的就是檢查代碼中存在的不合理和可以改進的地方,當然這個Review需要大家自己來做啦。

  • Code Review(代碼檢查):

Code Review主要檢查代碼中存在的一些不合理或可以改進優化的地方,大家可以參考之前寫的Reduce,Reuse和Recycle都是側重講解這方面的。

  • UI Review(視圖檢查):

Android對於視圖中控件的佈局渲染等會消耗很多的資源和內存,所以這部分也是我們需要注意的。

  1. 減少視圖層級:
    減少視圖層級可以有效的減少內存消耗,因爲視圖是一個樹形結構,每次刷新和渲染都會遍歷一次。

    hierarchyviewer:
    想要減少視圖層級首先就需要知道視圖層級,所以下面介紹一個SDK中自帶的一個非常好用的工具hierarchyviewer。
    你可以在下面的地址找到它:your sdk path\sdk\tools
    如上圖大家可以看到,hierarchyviewer可以非常清楚的看到當前視圖的層級結構,並且可以查看視圖的執行效率(視圖上的小圓點,綠色表示流暢,黃色和紅色次之),所以我們可以很方便的查看哪些view可能會影響我們的性能從而去進一步優化它。
    hierarchyviewer還提供另外一種列表式的查看方式,可以查看詳細的屏幕畫面,具體到像素級別的問題都可以通過它發現。

  2. ViewStub標籤
    此標籤可以使UI在特殊情況下,直觀效果類似於設置View的不可見性,但是其更大的意義在於被這個標籤所包裹的Views在默認狀態下不會佔用任何內存空間。

  3. include標籤
    可以通過這個標籤直接加載外部的xml到當前結構中,是複用UI資源的常用標籤。

  4. merge標籤
    它在優化UI結構時起到很重要的作用。目的是通過刪減多餘或者額外的層級,從而優化整個Android Layout的結構。

(注意:靈活運用以上3個標籤可以有效減少視圖層級,具體使用大家可以上網搜搜)


佈局用Java代碼比寫在XML中快

一般情況下對於Android程序佈局往往使用XML文件來編寫,這樣可以提高開發效率,但是考慮到代碼的安全性以及執行效率,可以通過Java代碼執行創建,雖然Android編譯過的XML是二進制的,但是加載XML解析器的效率對於資源佔用還是比較大的,Java處理效率比XML快得多,但是對於一個複雜界面的編寫,可能需要一些套嵌考慮,如果你思維靈活的話,使用Java代碼來佈局你的Android應用程序是一個更好的方法。

重用系統資源:

1.利用系統定義的id
比如我們有一個定義ListView的xml文件,一般的,我們會寫類似下面的代碼片段。

<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來獲取。

2.利用系統的圖片資源

這樣做的好處,一個是美工不需要重複的做一份已有的圖片了,可以節約不少工時;另一個是能保證我們的應用程序的風格與系統一致。

3.利用系統的字符串資源

如果使用系統的字符串,默認就已經支持多語言環境了。如上述代碼,直接使用了@android:string/yes和@android:string/no,在簡體中文環境下會顯示“確定”和“取消”,在英文環境下會顯示“OK”和“Cancel”。

4.利用系統的Style

假設佈局文件中有一個TextView,用來顯示窗口的標題,使用中等大小字體。可以使用下面的代碼片段來定義TextView的Style。

<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:”。

5.利用系統的顏色定義

除了上述的各種系統資源以外,還可以使用系統定義好的顏色。在項目中最常用的,就是透明色的使用。

android:background ="@android:color/transparent"  

除了上面介紹的以外還有很多其他Android系統本身自帶的資源,它們在應用中都可以直接使用。具體的,可以進入android-sdk的相應文件夾中去查看。例如:可以進入androidsdk \platforms\android-8\data\res,裏面的系統資源就一覽無餘了。

開發者需要花一些時間去熟悉這些資源,特別是圖片資源和各種Style資源,這樣在開發過程中,能重用的儘量重用,而且有時候使用系統提供的效果可能會更好。

其他小tips:

  1. 分辨率適配-ldpi,-mdpi, -hdpi配置不同精度資源,系統會根據設備自適應,包括drawable, layout,style等不同資源。
  2. 儘量使用dp(density independent pixel)開發,不用px(pixel)。
  3. 多用wrap_content, match_parent
  4. 永遠不要使用AbsoluteLayout
  5. 使用9patch(通過~/tools/draw9patch.bat啓動應用程序),png格式
  6. 將Acitivity中的Window的背景圖設置爲空。
    getWindow().setBackgroundDrawable(null);android的默認背景是不是爲空。
  7. View中設置緩存屬性.setDrawingCache爲true。

  • Desgin Review(設計檢查):

Desgin Review主要側重檢查一下程序的設計是否合理,包括框架的設計,界面的設計,邏輯的設計(其實這些東西開發之前就應該想好了)。

  1. 框架設計:
    是否定義了自己的Activity和fragment等常用控件的基類去避免進行重複的工作
    是否有完善的異常處理機制,即使真的出現OOM也不會直接崩潰導致直接退出程序

  2. 界面設計:
    1.在視圖中加載你所需要的,而不是你所擁有。因爲用戶不可能同時看到所有東西。最典型的例子就是ListView中的滑動加載。
    2.如果數據特別大,此時應該暗示用戶去點擊加載,而不是直接加載。
    3.合理運用分屏,轉屏等,它是個雙刃劍,因爲它即可以使程序更加美觀功能更加完善,但也相應增加了資源開銷。

  3. 邏輯設計:
    避免子類直接去控制父類中內容,可以使用監聽等方式去解決

七、Activity生命週期

這裏寫圖片描述

1.典型情況下的生命週期分析

2.異常情況下的生命週期分析

  1. 情況1:資源相關的系統配置發生改變導致Activity被殺死並重新創建

如:以圖片爲例,我們爲了兼容不同的設備,可能需要在其他目錄放置不同圖片,這樣當應用程序啓動時,系統就會根據當前設備的情況去加載適合的圖片資源,比如說橫屏手機和豎屏手機會拿到兩張不同的圖片(設定了landscape或portrait狀態的圖片),若當前Activity處於豎屏狀態,如果突然旋轉屏幕,由於系統配置發生了改變,在默認情況下,Activity就會被銷燬並且重新創建,當然我們可以阻止系統重新創建我們的Activity。
默認情況下,當系統配置發生改變後,Activity就會被銷燬並重新創建,其生命週期如下:
這裏寫圖片描述

當系統配置發生改變後,Activity會被銷燬,其onPauseonStoponDestroy均會被調用,同時由於Activity是在異常情況下終止的,系統會調用onSaveInstanceState來保存當前Activity的狀態。這個方法的調用時機是在onStop之前,它和onPause沒有既定的時許關係,可能之前可能之後調用。需要強調的一點是,這個方法只會出現在Activity被異常終止的情況下,正常情況下系統不會回調這個方法。當Activity被重新創建後,系統會調用onRestoreInstanceState,並且把Activity銷燬時onSaveInstanceState方法所保存的Bundle對象作爲參數同時傳遞給onRestoreInstanceStateonCreate方法。因此,我們可以通過onRestoreInstanceStateonCreate方法來判斷Acitivity是否被重建了,如果被重建了,那麼我們就可以取出之前保存的數據並恢復,從時許上來說,onRestoreInstanceState的調用時機在onStart之後。
同時,在onSaveInstanceStateonRestoreInstanceState方法中,系統自動爲我們做了一定的恢復工作,系統會默認爲我們保存當前Activity的視圖結構,並且在Activity重啓後爲我們恢復這些數據,比如文本框中用戶輸入的數據、ListView滾動的位置等,這些View相關的狀態系統都能夠默認爲我們恢復(詳細瞭解自己查找資料)。
2. 情況2:資源內存不足導致低優先級的Activity被殺死

Activity按照優先級從高到低,可以分爲如下三種:
(1)前臺Activity——正在和用戶交互的Activity,優先級最高。
(2)可見但非前臺Activity——比如Activity中彈出了一個對話框,導致Activity可見但是位於後臺無法和用戶直接交互。
(3)後臺Activity——已經被暫停的Activity,比如執行了onStop,優先級最低。

當系統內存不足時,系統就會按照上述優先級去殺死目標Acitity所在的進程,並在後續通過onSaveInstanceStateonRestoreInstanceState來存儲和恢復數據。如果一個進程中沒有四大組件在執行,那麼這個進程將 很快被系統殺死,因此,一些後臺工作不適合脫離四大組件而獨自運行在後臺中,這樣進程很容易被殺死。比較好的方法是將後臺工作放入Service中從而保證進程有一定的優先級,這樣就不會輕易地被系統殺死。

上面分析了系統的數據存儲和恢復機制,我們知道,當系統配置發生改變後,Activity會重新創建,那麼有沒有辦法不重新創建呢?答案是有的,接下來我們就來分析這個問題。系統配置中有很多內容,如果當某項內容發生改變後,我們不想系統重新創建Activity,可以給Activity指定configChanges屬性。比如不想讓Activity在屏幕旋轉的時候重新創建,就可以給configChanges屬性添加orientation這個值,如下所示:
android:configChanges="orientation"

如果我們想指定多個值,可以用“|”連接起來。系統配置中所含的項目很多,下面介紹每個項目的含義,如下圖所示:

這裏寫圖片描述

從上表可以知道,如果我們沒有在Activity的configChanges屬性中指定該選項的話,當配置發生改變就會導致Activity重新創建。上面表格中的項目很多,但是我們常用的只有localeorientationkeyboardHidden這三個選項,其他很少使用。需要注意的是screenSizesmallestScreenSize,它們兩個比較特殊,它們的行爲和編譯選項有關,但和運行環境無關(爲了防止旋轉屏幕時Activity重啓,除了orientation,我們還要加上screenSize,原因上面的表格裏有說明)。

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