Fresco的內存機制
Fresco是Facebook出品的高性能圖片加載庫,采用了Ashmem匿名共享內存機制, 來解決圖片加載中的OOM問題。這里不對Fresco做深入分析,只關注Fresco在Android Bitmap的管理上采用了哪些黑科技。
Android的內存區域
Java Heap(Dalvik Heap),這部分的內存區域是由Dalvik虛擬機管理,通過Java中 new 關鍵字來申請一塊新內存。這塊區域的內存是由GC直接管理,能夠自動回收內存。這塊內存的大小會受到系統限制,當內存超過APP最大可用內存時會OOM
Native Heap,這部分內存區域是在C++中申請的,它不受限於APP的最大可用內存限制,而只是受限於設備的物理可用內存限制。它的缺點在於沒有自動回收機制,只能通過C++語法來釋放申請的內存
Ashmem(Android匿名共享內存),這部分內存類似於Native內存區,但是它是受Android系統底層管理的,當Android系統內存不足時,會回收Ashmem區域中狀態是 unpin 的對象內存塊,如果不希望對象被回收,可以通過 pin 來保護一個對象
Purgeable Bitmap
Ashmem一般在應用層中是無法直接訪問的,除了幾個特例之外。其中之一就是 decode bitmap ,我們可以通過設置 BitmapFactory.Optinons.inPurgeable = true 來創建一個 Purgeable Bitmap ,這樣decode出來的bitmap是在Ashmem內存中,GC無法自動回收它。當該Bitmap在被使用時會被 pin 住,使用完之后就 unpin ,這樣系統就可以在將來某一時間釋放這部分內存。
如果一個 unpinned 的bitmap在之后又要被使用,系統會運行時又將它重新decode,但是這個decode操作是發生在UI線程中的有可能會造成掉幀現象,因此改做法已經被Google廢棄掉,轉為鼓勵使用 inBitmap 來告知bitmap解碼器去嘗試使用已經存在的內存區域,新解碼的bitmap會嘗試去使用之前那張bitmap在heap中所占據的pixel data內存區域,而不是去問內存重新申請一塊區域來存放bitmap。利用這種特性,即使是上千張的圖片,也只會僅僅只需要占用屏幕所能夠顯示的圖片數量的內存大小。
但是使用 inBitmap 需要注意幾個限制條件:
在SDK 11 -> 18之間,重用的bitmap大小必須是一致的,例如給inBitmap賦值的圖片大小為100-100,那么新申請的bitmap必須也為100-100才能夠被重用。從SDK 19開始,新申請的bitmap大小必須小於或者等於已經賦值過的bitmap大小。 新申請的bitmap與舊的bitmap必須有相同的解碼格式,例如大家都是8888的,如果前面的bitmap是8888,那么就不能支持4444與565格式的bitmap了。 我們可以創建一個包含多種典型可重用bitmap的對象池,這樣后續的bitmap創建都能夠找到合適的“模板”去進行重用。
Pin Bitmap
為了讓inPurgeable的bitmap不被自動 unpinned ,可以通過使用jni函數 AndroidBitmap_lockPixels() 函數來強制 pin bitmap ,這樣我們就可以在bitmap被使用時不會被系統自動 unpinned ,從而也就避免了 unpinned 的bitmap在重新被使用時又會被重新decode而引起的掉幀問題。同樣的,Android也提供了 AndroidBitmap_unlockPixels() 來讓bitmap重新變為 unpinned 狀態,這樣系統在內存不足時就可自動回收這部分內存
參考文獻
Facebook tricks for image handling in Android
Introducing Fresco: A new image library for Android
------------------------------------------分割線-----------------------------------------------------------------
Fresco (Facebook圖片加載器)
Fresco是Facebook開源的一個圖片加載和管理庫, 同類型的開源庫市面有非常多,比如Picasso, Universal Image Loader, Glide, Volley.
而Fresco的最大特點在於,圖片不在Java Heap上分配內存! 對,你沒看錯,困擾許多開發很久的爆Java Heap拋出OutOfMemoryError的無解難題看到了曙光!
那到底Fresco都把圖片存到內存的那一片區域了呢?
答案是:Ashmem,匿名共享內存. 關於Ashmem的介紹,推薦看Android大牛羅升陽的這3篇技術Blog.
1. Android系統匿名共享內存Ashmem(Anonymous Shared Memory)簡要介紹和學習計划
2. Android系統匿名共享內存Ashmem(Anonymous Shared Memory)驅動程序源代碼分析
3. Android系統匿名共享內存Ashmem(Anonymous Shared Memory)在進程間共享的原理分析
Ashmem:
在Android系統里面,Ashmem這個區域的內存並不屬於Java Heap,也不屬於Native Heap.而Ashmem的使用,又有一點像Java的垃圾回收.
當Ashmem中的某個內存空間想要被釋放的時候,會通過系統調用unpin來告知, 但實際上這塊內存空間的數據並沒有被真正的擦除;
當Android系統發現內存吃緊時,就會把unpin的內存空間利用起來去存儲所需的數據;
而被unpin的內存空間,是可以被重新pin的,如果此時的該內存空間還沒有被其他人使用的話,就節省了重新往Ashmem重新寫入數據的過程了.
所以,Ashmem這個工作原理是一種延遲釋放.
Bitmap在Ashmem中的使用
Ashmem內存區域是不能被Java應用直接使用的,但這其中有一些例外,而Bitmap是其中一個.
BitmapFactory.Options = new BitmapFactory.Options(); options.inPurgeable = true; Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
Purgeable被設置成true以后,這個Bigmap就是保存在Ashmem內存區域中的,Java的垃圾回收是不能回收這篇區域的內存的.
當Android系統需要渲染這個Bitmap的時候,會調用pin,渲染完成后會調用unpin.而unpin后的內存空間表示能被其他人所使用.
如果被unpin的Bitmap需要重新渲染,系統會再次Decode這個Bitmap.而這個Decode的過程是在UI線程上完成的.所以Google后來廢棄了這個pureable的參數.
后來Google提供了另外一個Flag,叫inBitmap.很遺憾的是,知道Android4.4后,這個新的Flag才得到完善.而Fresco致力於實現一個包括Android2.3以及以上的Android系統都能完美工作的圖片加載管理開源庫,因此Fresco放棄了使用inBitmap的解決方案.
Fresco是如何利用Ashmem去給Bitmap分配和管理內存?
上面說到的pin和unpin兩個操作,對應的NDK調用是AndroidBitmap_lockPixels和unlockPixels.按照我們一慣認知,為了避免內存泄漏,這兩者必須成對出現.而Fresco為了避免Bitmap再次渲染而導致的在UI線程Decode的過程,偏偏不在渲染完成后調用unlockPixels.
這做后,Fresco需要自己去管理這塊內存區域,保證當這個Bitmap不再使用時,Ashmem的內存空間能被unpin.
而Fresco選擇在Bitmap離開屏幕可視范圍時候(onDetachWindow等時候),去做unpin.
Fresco還提供了哪些實用功能?
1. 加載和展示GIF,WebP;
2. 分別控制4個角的不同圓角;
3. 把圖片切成圓形;
4. focusCrop,從指定的focus位置做類似centerCrop的scaleType;
5. 圖片加載進度條,類似我們看新浪微博客戶端GIF圖片加載過程的那種進度,可自定義樣式;
6. 點擊加載失敗的圖片,可重新加載;
具體更多的能力提供,參考Fresco官方網頁.
對比Fresco和Picasso
package com.example.garena.myapplication; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import com.facebook.drawee.drawable.ScalingUtils; import com.facebook.drawee.view.SimpleDraweeView; import com.squareup.picasso.Picasso; import java.util.ArrayList; public class MainActivity extends ActionBarActivity { private static final ArrayList<String> URL_LIST = new ArrayList<>(); static { URL_LIST.add("http://upload.wikimedia.org/wikipedia/en/thumb/7/7a/Manchester_United_FC_crest.svg/1010px-Manchester_United_FC_crest.svg.png"); URL_LIST.add("http://www.newstylesports.com/wp-content/uploads/2014/09/Manchester-United-Salary-2014.jpg"); URL_LIST.add("http://www.businessofsoccer.com/wp-content/uploads/2013/06/Manchester-United-Logo-Full-HD-Wallpaper.jpg"); URL_LIST.add("http://createapk.com/project/2014/07/lightkretek/manchester-united-wallpapers/image/104267-manchester-united-wallpapers.jpg"); URL_LIST.add("http://i2.manchestereveningnews.co.uk/incoming/article339323.ece/alternates/s2197/Manchester-United.jpg"); URL_LIST.add("http://www.thedrum.com/uploads/drum_basic_article/156824/main_images/ManchesterUnitedLogo_0.png"); URL_LIST.add("http://www.manutd.com/~/media/7317FC0A0B9B4A8ABD47E29E8E47D5EA.ashx?w=2560&h=1600"); URL_LIST.add("http://niaje.com/wp-content/uploads/2015/02/manu.jpg"); URL_LIST.add("http://teamspictures.com/wp-content/uploads/2015/03/Manchester-united-Football-Club-Wallpaper.jpg"); URL_LIST.add("http://g.foolcdn.com/editorial/images/146745/manchester-united-stock_large.PNG"); URL_LIST.add("http://i2.mirror.co.uk/incoming/article4918329.ece/ALTERNATES/s1227b/Yeovil-v-Manchester-United.jpg"); URL_LIST.add("http://worldsoccertalk.com/wp-content/uploads/2014/01/manchester-united.jpg"); URL_LIST.add("http://i.dailymail.co.uk/i/pix/2014/09/03/1409773352229_wps_66_Cristiano_Ronaldo_of_Manc.jpg"); URL_LIST.add("http://i.guim.co.uk/static/w-620/h--/q-95/sys-images/Football/Pix/pictures/2014/10/5/1412513201728/Radamel-Falcao-celebrates-010.jpg"); URL_LIST.add("http://i.dailymail.co.uk/i/pix/2014/09/13/1410639943046_wps_18_MANCHESTER_ENGLAND_SEPTEM.jpg"); URL_LIST.add("http://www.manutd.com/sitecore/shell/~/media/CCE3892A29474CB7A126398691568B19.ashx?w=480&h=270&rgn=0,130,800,581"); URL_LIST.add("http://img.skysports.com/14/07/660x350/manchester-united-team-los-angeles-galaxy-la_3177246.jpg"); URL_LIST.add("http://i.dailymail.co.uk/i/pix/2014/08/02/1407019617274_wps_31_Manchester_United_defende.jpg"); URL_LIST.add("http://static.guim.co.uk/sys-images/Guardian/Pix/pictures/2013/11/27/1385582053011/Bayer-Leverkusen-v-Manche-002.jpg"); URL_LIST.add("http://www.simbasports.co.uk/wp-content/uploads/2013/08/Manchester-United.jpg"); URL_LIST.add("http://www.footballwood.com/wp-content/uploads/2014/07/Manchester-United-2014-pre-season-schedule-fixtures.jpg"); URL_LIST.add("https://lh5.googleusercontent.com/-HM991djPFX4/UTbiLMwiuNI/AAAAAAAAAFs/GFlG8v56TT0/w800-h800/Manchester%2BUnited%2Bin%2BNaruto%2BWallpaper.jpg"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); getSupportActionBar().hide(); ListView listView = (ListView) findViewById(R.id.fresco_list_view); Adapter adapter = new Adapter(this, URL_LIST); listView.setAdapter(adapter); adapter.notifyDataSetChanged(); }private static class Adapter extends BaseAdapter { private final ArrayList<String> mUrlList; private final LayoutInflater mInflater; private final Context mContext; public Adapter(Context context, ArrayList<String> urlList) { mUrlList = urlList; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mContext = context; } @Override public int getCount() { return mUrlList.size(); } @Override public String getItem(int position) { return mUrlList.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder; if (convertView == null) { convertView = mInflater.inflate(R.layout.fresco_item, parent, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } Uri uri = Uri.parse(getItem(position)); // viewHolder.draweeView.setImageURI(uri); // viewHolder.normalImageView.setVisibility(View.GONE); Picasso.with(mContext).load(uri).into(viewHolder.normalImageView); viewHolder.draweeView.setVisibility(View.GONE); return convertView; } private static class ViewHolder { private final SimpleDraweeView draweeView; private final ImageView normalImageView; public ViewHolder(View view) { draweeView = (SimpleDraweeView) view.findViewById(R.id.fresco_image_view); normalImageView = (ImageView) view.findViewById(R.id.normal_image_view); } } } }
上面是一個非常簡單的Activity,一個ListView展示一個列表的網絡圖片.
Android2.3.7,Fresco:
Android2.3.7,Picasso:
Android4.4.4,Fresco:
Android4.4.4,Picasso:
Android5.1,Fresco:
Android5.1,Picasso:
從上面的對比中發現:
1. Android2.3.7,Fresco反而內存占用比Picasso高.
2. Android4.4.4,Fresco完勝Picasso,無論怎么滾動ListView展示圖片,內存都是靜止的一條直線.
3. Android5.1,Fresco和Picasso不相伯仲.這是因為Fresco沒有使用Ashmem.
Fresco至於我的疑慮
最后,Fresco讓我疑慮的一點是,粗略查看了Fresco提供的能力,貌似木有發現有類似Picasso.pauseTag和resumeTag的API.
這兩個API對於scrolling的操作非常重要.當scrolling的時候,應該暫停圖片的加載的線程,當滾動停止時才恢復圖片加載的線程運行.