http://my.oschina.net/ryanhoo/blog/88443
譯者:ryan
hoo
來源:https://developer.android.com/develop/index.html
譯者按: 在google最新的文檔中,提供了一系列含金量相當高的教程。因為種種原因而鮮為人知,真是可惜!ryan将會細心整理,将之翻譯成中文,希望對開發者有所幫助。
本系列是google關于展示大bitmap(位圖)的官方示範,可以有效的解決記憶體限制,更加有效的加載并顯示圖檔,同時避免讓人頭疼的oom(out of memory)。
-------------------------------------------------------------------------------------
譯文:
加載一個bitmap(位圖)到你的ui界面是非常簡單的,但是如果你要一次加載一大批,事情就變得複雜多了。在大多數的情況下(如listview、gridview或者viewpager這樣的元件),螢幕上的圖檔以及馬上要在滾動到螢幕上顯示的圖檔的總量,在本質上是不受限制的。
像這樣的元件在子視圖移出螢幕後會進行視圖回收,記憶體使用仍被保留。但假設你不保留任何長期存活的引用,垃圾回收器也會釋放你所加載的bitmap。這自然再好不過了,但是為了保持流暢且快速加載的ui,你要避免繼續在圖檔回到螢幕上的時候重新處理。使用記憶體和硬碟緩存通常能解決這個問題,使用緩存允許元件快速加載并處理圖檔。
這節課将帶你使用記憶體和硬碟緩存bitmap,以在加載多個bitmap的時候提升ui的響應性和流暢性。
使用記憶體緩存
以犧牲寶貴的應用記憶體為代價,記憶體緩存提供了快速的bitmap通路方式。lrucache類(可以在support
library中擷取并支援到api level 4以上,即1.6版本以上)是非常适合用作緩存bitmap任務的,它将最近被引用到的對象存儲在一個強引用的linkedhashmap中,并且在緩存超過了指定大小之後将最近不常使用的對象釋放掉。
注意:以前有一個非常流行的記憶體緩存實作是softreference(軟引用)或者weakreference(弱引用)的bitmap緩存方案,然而現在已經不推薦使用了。自android2.3版本(api
level 9)開始,垃圾回收器更着重于對軟/弱引用的回收,這使得上述的方案相當無效。此外,android 3.0(api level 11)之前的版本中,bitmap的備份資料直接存儲在本地記憶體中并以一種不可預測的方式從記憶體中釋放,很可能短暫性的引起程式超出記憶體限制而崩潰。
為了給lrucache選擇一個合适的大小,要考慮到很多原因,例如:
其他的activity(活動)和(或)程式都是很耗費記憶體的嗎?
螢幕上一次會顯示多少圖檔?有多少圖檔将在螢幕上顯示?
裝置的螢幕大小和密度是多少?一個超高清螢幕(xhdpi)的裝置如galaxy nexus,相比nexus s(hdpi)來說,緩存同樣數量的圖檔需要更大的緩存空間。
bitmap的尺寸、配置以及每張圖檔需要占用多少記憶體?
圖檔的通路是否頻繁?有些會比其他的更加被頻繁的通路到嗎?如果是這樣,也許你需要将某些圖檔一直保留在記憶體中,甚至需要多個lrucache對象配置設定給不同組的bitmap。
你能平衡圖檔的品質和數量麼?有的時候存儲大量低品質的圖檔更加有用,然後可以在背景任務中加載另一個高品質版本的圖檔。
對于設定緩存大小,并沒有适用于所有應用的規範,它取決于你在記憶體使用分析後給出的合适的解決方案。緩存空間太小并無益處,反而會引起額外的開銷,而太大了又可能再次引起java.lang.outofmemory異常或隻留下很小的空間給應用的其他程式運作。
這裡有一個設定bitmap的lrucache示例:
<code>01</code>
<code>private</code> <code>lrucache<string, bitmap> mmemorycache;</code>
<code>02</code>
<code>03</code>
<code>@override</code>
<code>04</code>
<code>protected</code> <code>void</code> <code>oncreate(bundle savedinstancestate) {</code>
<code>05</code>
<code> </code><code>...</code>
<code>06</code>
<code> </code><code>// get memory class of this device, exceeding this amount will throw an</code>
<code>07</code>
<code> </code><code>// outofmemory exception.</code>
<code>08</code>
<code> </code><code>final</code> <code>int</code> <code>memclass = ((activitymanager) context.getsystemservice(</code>
<code>09</code>
<code> </code><code>context.activity_service)).getmemoryclass();</code>
<code>10</code>
<code>11</code>
<code> </code><code>// use 1/8th of the available memory for this memory cache.</code>
<code>12</code>
<code> </code><code>final</code> <code>int</code> <code>cachesize = </code><code>1024</code> <code>* </code><code>1024</code> <code>* memclass / </code><code>8</code><code>;</code>
<code>13</code>
<code>14</code>
<code> </code><code>mmemorycache = </code><code>new</code> <code>lrucache<string, bitmap>(cachesize) {</code>
<code>15</code>
<code> </code><code>@override</code>
<code>16</code>
<code> </code><code>protected</code> <code>int</code> <code>sizeof(string key, bitmap bitmap) {</code>
<code>17</code>
<code> </code><code>// the cache size will be measured in bytes rather than number of items.</code>
<code>18</code>
<code> </code><code>return</code> <code>bitmap.getbytecount();</code>
<code>19</code>
<code> </code><code>}</code>
<code>20</code>
<code> </code><code>};</code>
<code>21</code>
<code>22</code>
<code>}</code>
<code>23</code>
<code>24</code>
<code>public</code> <code>void</code> <code>addbitmaptomemorycache(string key, bitmap bitmap) {</code>
<code>25</code>
<code> </code><code>if</code> <code>(getbitmapfrommemcache(key) == </code><code>null</code><code>) {</code>
<code>26</code>
<code> </code><code>mmemorycache.put(key, bitmap);</code>
<code>27</code>
<code> </code><code>}</code>
<code>28</code>
<code>29</code>
<code>30</code>
<code>public</code> <code>bitmap getbitmapfrommemcache(string key) {</code>
<code>31</code>
<code> </code><code>return</code> <code>mmemorycache.get(key);</code>
<code>32</code>
注意:在這個例子中,1/8的應用記憶體被配置設定給緩存。在一個普通的/hdpi裝置上最低也在4m左右(32/8)。一個分辨率為800*480的裝置上,全屏的填滿圖檔的gridview占用的記憶體約1.5m(800*480*4位元組),是以這個大小的記憶體可以緩存2.5頁左右的圖檔。
當加載一個bitmap到imageview中,先要檢查lrucache。如果有相應的資料,則立即用來更新imageview,否則将啟動背景線程來處理這個圖檔。
<code>public</code> <code>void</code> <code>loadbitmap(</code><code>int</code> <code>resid, imageview imageview) {</code>
<code> </code><code>final</code> <code>string imagekey = string.valueof(resid);</code>
<code> </code><code>final</code> <code>bitmap bitmap = getbitmapfrommemcache(imagekey);</code>
<code> </code><code>if</code> <code>(bitmap != </code><code>null</code><code>) {</code>
<code> </code><code>mimageview.setimagebitmap(bitmap);</code>
<code> </code><code>} </code><code>else</code> <code>{</code>
<code> </code><code>mimageview.setimageresource(r.drawable.image_placeholder);</code>
<code> </code><code>bitmapworkertask task = </code><code>new</code> <code>bitmapworkertask(mimageview);</code>
<code> </code><code>task.execute(resid);</code>
bitmapworkertask也需要更新記憶體中的資料:
<code>class</code> <code>bitmapworkertask </code><code>extends</code> <code>asynctask<integer, void, bitmap> {</code>
<code> </code><code>// decode image in background.</code>
<code> </code><code>@override</code>
<code> </code><code>protected</code> <code>bitmap doinbackground(integer... params) {</code>
<code> </code><code>final</code> <code>bitmap bitmap = decodesampledbitmapfromresource(</code>
<code> </code><code>getresources(), params[</code><code>0</code><code>], </code><code>100</code><code>, </code><code>100</code><code>));</code>
<code> </code><code>addbitmaptomemorycache(string.valueof(params[</code><code>0</code><code>]), bitmap);</code>
<code> </code><code>return</code> <code>bitmap;</code>
使用硬碟緩存
一個記憶體緩存對加速通路最近浏覽過的bitmap非常有幫助,但是你不能局限于記憶體中的可用圖檔。gridview這樣有着更大的資料集的元件可以很輕易消耗掉記憶體緩存。你的應用有可能在執行其他任務(如打電話)的時候被打斷,并且在背景的任務有可能被殺死或者緩存被釋放。一旦使用者重新聚焦(resume)到你的應用,你得再次處理每一張圖檔。
在這種情況下,硬碟緩存可以用來存儲bitmap并在圖檔被記憶體緩存釋放後減小圖檔加載的時間(次數)。當然,從硬碟加載圖檔比記憶體要慢,并且應該在背景線程進行,因為硬碟讀取的時間是不可預知的。
注意:如果通路圖檔的次數非常頻繁,那麼contentprovider可能更适合用來存儲緩存圖檔,例如image
gallery這樣的應用程式。
這個類中的示例代碼使用disklrucache(來自android源碼)實作。在示例代碼中,除了已有的記憶體緩存,還添加了硬碟緩存。
<code>private</code> <code>disklrucache mdisklrucache;</code>
<code>private</code> <code>final</code> <code>object mdiskcachelock = </code><code>new</code> <code>object();</code>
<code>private</code> <code>boolean</code> <code>mdiskcachestarting = </code><code>true</code><code>;</code>
<code>private</code> <code>static</code> <code>final</code> <code>int</code> <code>disk_cache_size = </code><code>1024</code> <code>* </code><code>1024</code> <code>* </code><code>10</code><code>; </code><code>// 10mb</code>
<code>private</code> <code>static</code> <code>final</code> <code>string disk_cache_subdir = </code><code>"thumbnails"</code><code>;</code>
<code> </code><code>// initialize memory cache</code>
<code> </code><code>// initialize disk cache on background thread</code>
<code> </code><code>file cachedir = getdiskcachedir(</code><code>this</code><code>, disk_cache_subdir);</code>
<code> </code><code>new</code> <code>initdiskcachetask().execute(cachedir);</code>
<code>class</code> <code>initdiskcachetask </code><code>extends</code> <code>asynctask<file, void, void> {</code>
<code> </code><code>protected</code> <code>void doinbackground(file... params) {</code>
<code> </code><code>synchronized</code> <code>(mdiskcachelock) {</code>
<code> </code><code>file cachedir = params[</code><code>0</code><code>];</code>
<code> </code><code>mdisklrucache = disklrucache.open(cachedir, disk_cache_size);</code>
<code> </code><code>mdiskcachestarting = </code><code>false</code><code>; </code><code>// finished initialization</code>
<code> </code><code>mdiskcachelock.notifyall(); </code><code>// wake any waiting threads</code>
<code> </code><code>return</code> <code>null</code><code>;</code>
<code>33</code>
<code>34</code>
<code>35</code>
<code>36</code>
<code> </code><code>final</code> <code>string imagekey = string.valueof(params[</code><code>0</code><code>]);</code>
<code>37</code>
<code>38</code>
<code> </code><code>// check disk cache in background thread</code>
<code>39</code>
<code> </code><code>bitmap bitmap = getbitmapfromdiskcache(imagekey);</code>
<code>40</code>
<code>41</code>
<code> </code><code>if</code> <code>(bitmap == </code><code>null</code><code>) { </code><code>// not found in disk cache</code>
<code>42</code>
<code> </code><code>// process as normal</code>
<code>43</code>
<code> </code><code>final</code> <code>bitmap bitmap = decodesampledbitmapfromresource(</code>
<code>44</code>
<code> </code><code>getresources(), params[</code><code>0</code><code>], </code><code>100</code><code>, </code><code>100</code><code>));</code>
<code>45</code>
<code>46</code>
<code>47</code>
<code> </code><code>// add final bitmap to caches</code>
<code>48</code>
<code> </code><code>addbitmaptocache(imagekey, bitmap);</code>
<code>49</code>
<code>50</code>
<code>51</code>
<code>52</code>
<code>53</code>
<code>54</code>
<code>55</code>
<code>public</code> <code>void</code> <code>addbitmaptocache(string key, bitmap bitmap) {</code>
<code>56</code>
<code> </code><code>// add to memory cache as before</code>
<code>57</code>
<code>58</code>
<code>59</code>
<code>60</code>
<code>61</code>
<code> </code><code>// also add to disk cache</code>
<code>62</code>
<code> </code><code>synchronized</code> <code>(mdiskcachelock) {</code>
<code>63</code>
<code> </code><code>if</code> <code>(mdisklrucache != </code><code>null</code> <code>&& mdisklrucache.get(key) == </code><code>null</code><code>) {</code>
<code>64</code>
<code> </code><code>mdisklrucache.put(key, bitmap);</code>
<code>65</code>
<code>66</code>
<code>67</code>
<code>68</code>
<code>69</code>
<code>public</code> <code>bitmap getbitmapfromdiskcache(string key) {</code>
<code>70</code>
<code>71</code>
<code> </code><code>// wait while disk cache is started from background thread</code>
<code>72</code>
<code> </code><code>while</code> <code>(mdiskcachestarting) {</code>
<code>73</code>
<code> </code><code>try</code> <code>{</code>
<code>74</code>
<code> </code><code>mdiskcachelock.wait();</code>
<code>75</code>
<code> </code><code>} </code><code>catch</code> <code>(interruptedexception e) {}</code>
<code>76</code>
<code>77</code>
<code> </code><code>if</code> <code>(mdisklrucache != </code><code>null</code><code>) {</code>
<code>78</code>
<code> </code><code>return</code> <code>mdisklrucache.get(key);</code>
<code>79</code>
<code>80</code>
<code>81</code>
<code> </code><code>return</code> <code>null</code><code>;</code>
<code>82</code>
<code>83</code>
<code>84</code>
<code>// creates a unique subdirectory of the designated app cache directory. tries to use external</code>
<code>85</code>
<code>// but if not mounted, falls back on internal storage.</code>
<code>86</code>
<code>public</code> <code>static</code> <code>file getdiskcachedir(context context, string uniquename) {</code>
<code>87</code>
<code> </code><code>// check if media is mounted or storage is built-in, if so, try and use external cache dir</code>
<code>88</code>
<code> </code><code>// otherwise use internal cache dir</code>
<code>89</code>
<code> </code><code>final</code> <code>string cachepath =</code>
<code>90</code>
<code> </code><code>environment.media_mounted.equals(environment.getexternalstoragestate()) ||</code>
<code>91</code>
<code> </code><code>!isexternalstorageremovable() ? getexternalcachedir(context).getpath() :</code>
<code>92</code>
<code> </code><code>context.getcachedir().getpath();</code>
<code>93</code>
<code>94</code>
<code> </code><code>return</code> <code>new</code> <code>file(cachepath + file.separator + uniquename);</code>
<code>95</code>
注意:即便是硬碟緩存初始化也需要硬碟操作,是以不應該在主線程執行。但是,這意味着硬碟緩存在初始化前就能被通路到。為了解決這個問題,在上面的實作中添加了一個鎖對象(lock
object),以確定在緩存被初始化之前應用無法通路硬碟緩存。
在ui線程中檢查記憶體緩存,相應的硬碟緩存檢查應在背景線程中進行。硬碟操作永遠不要在ui線程中發生。當圖檔處理完成後,最終的bitmap要被添加到記憶體緩存和硬碟緩存中,以便後續的使用。
處理配置更改
運作時的配置會發生變化,例如螢幕方向的改變,會導緻android銷毀并以新的配置重新啟動activity(關于此問題的更多資訊,請參閱handling
runtime changes)。為了讓使用者有着流暢而快速的體驗,你需要在配置發生改變的時候避免再次處理所有的圖檔。
幸運的是,你在“使用記憶體緩存”一節中為bitmap構造了很好的記憶體緩存。這些記憶體可以通過使用fragment傳遞到信的activity(活動)執行個體,這個fragment可以調用setretaininstance(true)方法保留下來。在activity(活動)被重新建立後,你可以在上面的fragment中通路到已經存在的緩存對象,使得圖檔能快加載并重新填充到imageview對象中。
下面是一個使用fragment将lrucache對象保留在配置更改中的示例:
<code> </code><code>retainfragment mretainfragment =</code>
<code> </code><code>retainfragment.findorcreateretainfragment(getfragmentmanager());</code>
<code> </code><code>mmemorycache = retainfragment.mretainedcache;</code>
<code> </code><code>if</code> <code>(mmemorycache == </code><code>null</code><code>) {</code>
<code> </code><code>mmemorycache = </code><code>new</code> <code>lrucache<string, bitmap>(cachesize) {</code>
<code> </code><code>... </code><code>// initialize cache here as usual</code>
<code> </code><code>mretainfragment.mretainedcache = mmemorycache;</code>
<code>class</code> <code>retainfragment </code><code>extends</code> <code>fragment {</code>
<code> </code><code>private</code> <code>static</code> <code>final</code> <code>string tag = </code><code>"retainfragment"</code><code>;</code>
<code> </code><code>public</code> <code>lrucache<string, bitmap> mretainedcache;</code>
<code> </code><code>public</code> <code>retainfragment() {}</code>
<code> </code><code>public</code> <code>static</code> <code>retainfragment findorcreateretainfragment(fragmentmanager fm) {</code>
<code> </code><code>retainfragment fragment = (retainfragment) fm.findfragmentbytag(tag);</code>
<code> </code><code>if</code> <code>(fragment == </code><code>null</code><code>) {</code>
<code> </code><code>fragment = </code><code>new</code> <code>retainfragment();</code>
<code> </code><code>return</code> <code>fragment;</code>
<code> </code><code>public</code> <code>void</code> <code>oncreate(bundle savedinstancestate) {</code>
<code> </code><code>super</code><code>.oncreate(savedinstancestate);</code>
<code> </code><code>setretaininstance(</code><code>true</code><code>);</code>
為了測試這個,可以在不适用fragment的情況下旋轉裝置螢幕。在保留緩存的情況下,你應該能發現填充圖檔到activity中幾乎是瞬間從記憶體中取出而沒有任何延遲的感覺。任何圖檔優先從記憶體緩存擷取,沒有的話再到硬碟緩存中找,如果都沒有,那就以普通方式加載圖檔。