Android開發筆記——圖片緩存、手勢及OOM分析


      把圖片緩存、手勢及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頁面查詢到的信息:

圖片1

     需要說明的是:

  • 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,尊重原作者版權)

image

      ImageLoader收到加載及顯示圖片的任務,ImageLoaderEngine分發任務,獲得圖片數據后,BitmapDisplayer 在ImageAware中顯示。

ImageLoader的有點:

  • 支持下載進度監聽
  • 可以在 View 滾動中暫停圖片加載,通過 PauseOnScrollListener 接口可以在 View 滾動中暫停圖片加載。
  • 默認實現多種內存緩存算法,這幾個圖片緩存都可以配置緩存算法,不過 ImageLoader 默認實現了較多緩存算法,如 Size 最大先刪除、使用最少先刪除、最近最少使用、先進先刪除、時間最長先刪除等。
  • 支持本地緩存文件名規則定義

 

     Picasso的設計及優點

     Picasso的加載流程如下圖:

image

     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的加載流程如下圖:

image

     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系統匿名共享內存AshmemAnonymous Shared Memory)簡要介紹和學習計划
  2. Android系統匿名共享內存AshmemAnonymous Shared Memory)驅動程序源代碼分析
  3. Android系統匿名共享內存AshmemAnonymous 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.PhotoView
     android: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() {
       @Override
       public 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:

<LinearLayout
    xmlns: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.GestureImageView
        android: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 {
    @Override
    public 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被靜態變量引用導致無法回收引起的內存泄露的截圖。

device-2016-01-07-162003

      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進行申明:

        <service
            android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
            android:enabled="false"
            android:process=":leakcanary" />
        <service
            android:name="com.squareup.leakcanary.DisplayLeakService"
            android:enabled="false" />

        <activity
            android: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,輸入法,音頻

      兜底回收內存:

      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,圖庫等,由於存在內存系統泄露,可以采用單獨的進程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM