把圖片緩衝、手勢及OOM三個主題放在一起,是因為在Android應用開發過程中,這三個問題經常是聯絡在一起的。首先,預覽大圖需要支援手勢縮放,旋轉,平移等操作;其次,圖片在本地需要進行緩衝,避免頻繁訪問網路;最後,圖片(Bitmap)是Android中佔用記憶體的大戶,涉及高清大圖等處理時,記憶體佔用非常大,稍不謹慎,系統就會報OOM錯誤。
慶幸的是,這三個主題在Android開發中屬於比較普遍的問題,有很多針對於此的通用的開源解決方案。因此,本文主要說明筆者在開發過程中用到的一些第三方開源庫。主要內容如下:
1.Universal Image Loader、Picasso、Glide與Fresco的對比及使用
2.PhotoView、GestureImageView的原理及使用
3.leakcanry記憶體分析工具
--------------------------------------------------------------------------------
一、Universal Image Loader、Picasso、Glide與Fresco的對比及使用
Universal Image Loader(UIL)、Picasso、Glide與Fresco是Android中進行圖片載入的常用第三方庫,主要封裝了記憶體緩衝、磁碟緩衝、網路請求緩衝、線程池等方法,抽象了圖片載入的流程,很大程度避免了載入圖片引起的記憶體溢出,提高了圖片載入的效率。下圖是筆者近期從各個庫的github頁面查詢到的資訊:
需要說明的是:
•Imageloader是最早開源的圖片緩衝庫,目前作者已停止維護(11.27);
•Picasso的實際作者是Square的Jake Wharton,Android領域的絕對大牛;
•Glide是由Google員工開源的,在Google I/O 2014官方應用中推薦使用;
•Fresco的圖片載入不使用Java堆記憶體,而是匿名共用記憶體(Ashmem)。
附上各個庫的github地址:
Universal Image Loader:https://github.com/nostra13/Android-Universal-Image-Loader.git
Picasso:https://github.com/square/picasso.git
Glide:https://github.com/bumptech/glide.git
Fresco:https://github.com/facebook/fresco.git
這四個圖片緩衝庫的基本使用(HelloWorld)都可以通過一句代碼實現,分別如下:
UIL:
ImageLoader.getInstance().displayImage(url, imageView);
Picasso:
Picasso.with(context).load(url).into(imageView);
Glide:
Glide.with(context).load(url).into(imageView);
Fresco:
simpleDraweeView.setImageURI(uri);
細心的朋友可以看出,Picasso和Glide的API非常類似。事實上,這四個庫在實現的核心思想上都比較相似,可以抽象為以下五個模組:
1.RequestManager,主要負責請求產生和管理模組;
2.Engine,主要負責建立任務以及執行調度;
3.GetDataInterface,擷取資料的介面,主要用於從記憶體緩衝、磁碟緩衝以及網路等擷取圖片資料;
4.Displayer,主要用於顯示圖片,可能是對ImageView的封裝或者其他虛擬Displayer;
5.Processor,主要負責處理圖片,比如圖片的旋轉、壓縮以及截取等操作。
說一句題外話,掌握了各種開源庫的實現的核心思想後,會發現軟體工程的一個共同點,就是通過將流程形式化、抽象化,從而提高效率。不論是業務的效率,還是開發的效率,這或許也是軟體作為一門科學的核心思想。
ImageLoader的設計及優點
ImageLoader載入的流程如下圖。(需要申明:下面三張流程圖來自Trinea,尊重原作者著作權)
ImageLoader收到載入及顯示圖片的任務,ImageLoaderEngine分發任務,獲得圖片資料後,BitmapDisplayer 在ImageAware中顯示。
ImageLoader的有點:
•支援下載進度監聽
•可以在 View 滾動中暫停圖片載入,通過 PauseOnScrollListener 介面可以在 View 滾動中暫停圖片載入。
•預設實現多種記憶體緩衝演算法,這幾個圖片緩衝都可以配置緩衝演算法,不過 ImageLoader 預設實現了較多緩衝演算法,如 Size 最大先刪除、使用最少先刪除、最近最少使用、先進先刪除、時間最長先刪除等。
•支援本機快取檔案名稱規則定義
Picasso的設計及優點
Picasso的載入流程如下圖:
Picasso收到載入及顯示圖片的任務,Dispatcher 負責分發和處理,通過MemoryCache及Handler擷取圖片,通過PicassoDrawable顯示到Target中。
Picasso的優點:
•內建統計監控功能,支援圖片緩衝使用的監控,包括快取命中率、已使用記憶體大小、節省的流量等。
•支援優先順序處理,每次任務調度前會選擇優先順序高的任務,比如 App 頁面中 Banner 的優先順序高於 Icon 時就很適用。
•支援延遲到圖片尺寸計算完成載入,支援飛航模式、並發線程數根據網路類型而變,手機切換到飛航模式或網路類型變換時會自動調整線程池最大並發數,比如 wifi 最大並發為 4, 4g 為 3,3g 為 2。這裡 Picasso 根據網路類型來決定最大並發數,而不是 CPU 核心數。
•“無”本機快取,不是說沒有本機快取,而是 Picasso 自己沒有實現,交給了 Square 的另外一個網路程式庫 okhttp 去實現,這樣的好處是可以通過請求 Response Header 中的 Cache-Control 及 Expired 控製圖片的到期時間。
Glide的設計及優點
Glide的載入流程如下圖:
Glide 收到載入及顯示資源的任務,Engine 處理請求,通過Fetcher擷取資料,經Transformation 處理後交給Target顯示。
Glide的優點:
(1) 圖片緩衝->媒體緩衝
Glide 不僅是一個圖片緩衝,它支援 Gif、WebP、縮圖。甚至是 Video,所以更該當做一個媒體緩衝。
(2) 支援優先順序處理
(3) 與 Activity/Fragment 生命週期一致,支援 trimMemory
Glide 對每個 context 都保持一個 RequestManager,通過 FragmentTransaction 保持與 Activity/Fragment 生命週期一致,並且有對應的 trimMemory 介面實現可供調用。
(4) 支援 okhttp、Volley
Glide 預設通過 UrlConnection 擷取資料,可以配合 okhttp 或是 Volley 使用。實際 ImageLoader、Picasso 也都支援 okhttp、Volley。
(5) 記憶體友好
① Glide 的記憶體緩衝有個 active 的設計
從記憶體緩衝中取資料時,不像一般的實現用 get,而是用 remove,再將這個快取資料放到一個 value 為軟引用的 activeResources map 中,並計數引用數,在圖片載入完成後進行判斷,如果引用計數為空白則回收掉。
② 記憶體緩衝更小圖片
Glide 以 url、viewwidth、viewheight、螢幕的解析度等做為聯合 key,將處理後的圖片緩衝在記憶體緩衝中,而不是原始圖片以節省大小
③ 與 Activity/Fragment 生命週期一致,支援 trimMemory
④ 圖片預設使用預設 RGB565 而不是 ARGB888
雖然清晰度差些,但圖片更小,也可配置到 ARGB_888。
其他:Glide 可以通過 signature 或不使用本機快取支援 url 到期
關於Fresco
Fresco庫開源較晚,目前還沒有正式的1.0版本。但其功能比前三個庫都強大,比如:
•圖片儲存系統匿名共用記憶體Ashmem(Anonymous Shared Memory),並不分配Java堆記憶體,因此圖片載入不會引起堆記憶體抖動;
•JPEG映像流載入(先顯示映像輪廓,再慢慢載入清晰映像);
•更加完善的影像處理、顯示方式;
•JPEG映像本地(native)變換尺寸,避免OOM;
•……
關於系統匿名共用記憶體Ashmem,會在後續的一篇關於Android的記憶體使用量的文章中詳述,這裡僅作簡單介紹:
在Android系統裡面,Ashmem這個地區的記憶體並不屬於Java Heap,也不屬於Native Heap。當Ashmem中的某個記憶體空間像要被釋放時候,會通過系統調用unpin來告知。但實際上這塊記憶體空間的資料並沒有被真正的擦除。如果Android系統發現記憶體吃緊時,就會把unpin的記憶體空間利用起來去儲存所需的資料。而被unpin的記憶體空間,是可以被重新pin的,如果此時的該記憶體空間還沒有被其他人使用的話,就節省了重新往Ashmem重新寫入資料的過程了。所以,Ashmem這個工作原理是一種延遲釋放。
另外,學習Ashmem可以參考羅昇陽大師的部落格:
1.Android系統匿名共用記憶體Ashmem(Anonymous Shared Memory)簡要介紹和學習計劃
2.Android系統匿名共用記憶體Ashmem(Anonymous Shared Memory)驅動程式原始碼分析
3.Android系統匿名共用記憶體Ashmem(Anonymous Shared Memory)在進程間共用的原理分析
二、PhotoView、GestureImageView的原理及使用
需要使用上述第三方開源庫進圖片載入的一個典型情境是點擊查看大圖。大圖支援手勢縮放、旋轉、平移等操作,ImageView的手勢縮放,有很多種方法,絕大多數開源自訂縮放都是修改了ondraw函數來實現的。但是ImageView本身有scaleType屬性,通過設定android:scaleType="matrix" 可以輕鬆實現縮放功能。縮放的優點是實現起來簡單,同時因為沒有反覆調用ondraw函數,縮放過程中不會有閃爍現象。另外,需要注意的是,scaleType控製圖片的縮放方式,該圖片指的是資源而不是背景,換句話說,android:src="@drawable/ic_launcher",而非android:background="@drawable/ic_launcher"。
在github上可以找到很多開源的實現,這裡主要舉兩個例子進行簡單說明。
PhotoView地址:https://github.com/bm-x/PhotoView.git
GestureImageView地址:https://github.com/jasonpolites/gesture-imageview.git
PhotoView的介紹:
1.Gradle添加依賴(推薦)
dependencies {compile 'com.bm.photoview:library:1.3.6'}
(或者也可以將項目下載下來,將Info.java和PhotoView.java兩個檔案拷貝到你的項目中,不推薦)——這種方式適用於Eclipse。
2.xml添加
<com.bm.library.PhotoViewandroid:id="@+id/img"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerInside"android:src="@drawable/bitmap1" />
3.java代碼
PhotoView photoView = (PhotoView) findViewById(R.id.img);// 啟用圖片縮放功能photoView.enable();// 禁用圖片縮放功能 (預設為禁用,會跟普通的ImageView一樣,縮放功能需手動調用enable()啟用)photoView.disenable();// 擷取圖片資訊Info info = photoView.getInfo();// 從一張圖片資訊變化到現在的圖片,用於圖片點擊後放大瀏覽,具體使用可以參照demo的使用photoView.animaFrom(info);// 從現在的圖片變化到所給定的圖片資訊,用於圖片放大後點擊縮小到原來的位置,具體使用可以參照demo的使用photoView.animaTo(info,new Runnable() {@Overridepublic void run() {//動畫完成監聽}});// 擷取動畫期間int d = PhotoView.getDefaultAnimaDuring(); PhotoView實現的基本原理是在繼承於ImageView的PhotoView中採用了縮放Matrix及手勢監聽。public class PhotoView extends ImageView {……private Matrix mBaseMatrix = new Matrix();private Matrix mAnimaMatrix = new Matrix();private Matrix mSynthesisMatrix = new Matrix();private Matrix mTmpMatrix = new Matrix();private RotateGestureDetector mRotateDetector;private GestureDetector mDetector;private ScaleGestureDetector mScaleDetector;private OnClickListener mClickListener;private ScaleType mScaleType;……}
PhotoView的實現與上述原理基本一致,這裡不再贅述。對於自訂控制項的實現,後續文章會進行詳細的分析。
GestureImageView的簡介如下:
1.Configured as View in layout.xml
code:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:gesture-image="http://schemas.polites.com/android"android:layout_width="fill_parent"android:layout_height="fill_parent"><com.polites.android.GestureImageViewandroid:id="@+id/image"android:layout_width="fill_parent"android:layout_height="wrap_content"android:src="@drawable/image"gesture-image:min-scale="0.1"gesture-image:max-scale="10.0"gesture-image:strict="false"/></LinearLayout>
2.Configured Programmatically
code:
import com.polites.android.GestureImageView;import android.app.Activity;import android.os.Bundle;import android.view.ViewGroup;import android.widget.LinearLayout.LayoutParams;public class SampleActivity extends Activity {@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);GestureImageView view = new GestureImageView(this);view.setImageResource(R.drawable.image);view.setLayoutParams(params);ViewGroup layout = (ViewGroup) findViewById(R.id.layout);layout.addView(view);}}
原理基本同PhotoView一致,不再贅述。
三、OOM分析工具——LeakCanary
LeakCanary的介紹:
A memory leak detection library for Android and Java.
可見,LeakCanary主要用於檢測各種記憶體不能被GC,從而導致泄露的情況。
LeakCanary的地址 https://github.com/square/leakcanary.git
Demo地址:
https://github.com/liaohuqiu/leakcanary-demo.git(AS)
https://github.com/teffy/LeakcanarySample-Eclipse.git(Eclipse)
下面是demo中TestActivity中的TextView被靜態變數引用導致無法回收引起的記憶體泄露的截圖。
LeakCanary的使用較為簡單,首先添加依賴工程:
dependencies {debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'}
其次,在application的onCreate()方法中進行初始化。
public class ExampleApplication extends Application {@Override public void onCreate() {super.onCreate();LeakCanary.install(this);}}
經過這兩步之後就可以使用了。LeakCanary.install(this)會返回一個預定義的 RefWatcher,同時也會啟用一個ActivityRefWatcher,用於自動監控調用 Activity.onDestroy() 之後泄露的 activity。如果需要監聽fragment,則在fragment的onDestroy()方法進行註冊:
public abstract class BaseFragment extends Fragment {@Override public void onDestroy() {super.onDestroy();RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());refWatcher.watch(this);}}
當然,需要對某個變數進行監聽,直接對其進行watch即可。
RefWatcher refWatcher = {...};// We expect schrodingerCat to be gone soon (or not), let's watch it.refWatcher.watch(schrodingerCat);
需要注意的是,在eclipse中使用LeakCanary需要在AndroidManifest檔案中對堆佔用分析以及展示的Service進行申明:
<serviceandroid:name="com.squareup.leakcanary.internal.HeapAnalyzerService"android:enabled="false"android:process=":leakcanary" /><serviceandroid:name="com.squareup.leakcanary.DisplayLeakService"android:enabled="false" /><activityandroid:name="com.squareup.leakcanary.internal.DisplayLeakActivity"android:enabled="false"android:icon="@drawable/leak_canary_icon"android:label="@string/leak_canary_display_activity_label"android:taskAffinity="com.squareup.leakcanary"android:theme="@style/leak_canary_LeakCanary.Base" ><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity>
注意:HeapAnalyzerService採用了多進程android:process=":leakcanary"。
上述開源工具的使用都較為簡單,關於詳細使用,請參考其github地址。
四、一些雜亂的總結
記憶體泄露的常見原因:
•靜態對象:監聽器,廣播,webview;
•this$0:線程,定時器,Handler;
•系統:TextLine,IME,音頻
兜底回收記憶體:
Activity泄露會導致該Activity引用的Bitmap/DrawingCache等無法釋放,兜底回收是指對已泄露的Activity,嘗試回收其持有的資源。在onDestroy中從rootview開始,遞迴釋放所有子VIew涉及的圖片,背景,DrawingCache,監聽器等資源。
降低Runtime記憶體的方法:
1.減少bitmap佔用的記憶體:1)防止bitmap佔用資源過大,2.x系統開啟BitmapFactory.Options中的inNativeAlloc;4.x系統採用Facebook的fresco庫,將圖片資源放於native中。2)圖片按需載入,圖片的大小不應超過view的大小。3)統一的bitmap載入器:Picasso/Fresco。4)圖片存在像素浪費。
2.自身記憶體佔用監控:1)實現原理:通過Runtime擷取maxMemory,而totalMemory-freeMemory即為當前真正使用的dalvik記憶體。2)操作方式:定期檢查這個值,達到80%就去釋放各種cache資源(bitmap的cache)
3.使用多進程。對於webview,圖庫等,由於存在記憶體系統泄露,可以採用單獨的進程。