2015年8月10日 星期一

顯示圖片所遭遇的OOM...「原來要用WebView」

一個APP在Android上可以使用的記憶體空間最大為24MB。

但一張圖片轉成Bitmap「物件」後,實際大小會是檔案的四倍以上!

也就是說如果圖片有N張、總大小為4MB,那它轉為Bitmap物件後在記憶體中吃掉將近16MB的大小。

這時候如果加上APP本身其他功能在記憶體中佔的大小,很容易會發生「Out Of Memory」的Error!



或許有人會想:但Bitmap物件在Android APP中無法顯示或作用,需要丟給ImageView之類的UI元件後才會顯示,或許這些UI元件並不會保留Bitmap物件、會將它快速丟入Garbage回收列中。

如果是這樣...對一張已經轉給ImageView顯示的Bitmap進行recycle()指令應該會成功才對!但實際上log訊息卻跳出錯誤碼「不能對一張ImageView正在執行顯示的Bitmap執行recycle()」。



也就是說...

顯示圖片要遭遇的問題並不單單是「將圖檔從resource狀態轉成Bitmap的過程」,轉成Bitmap後存在記憶體中的實際大小本身就是問題的根源。



先解釋一下關於:將圖檔從resource狀態轉成Bitmap的過程...

Bitmap物件並沒有建構子,必須使用內建的static型態參數createBitmap()來產生,或是使用BitmapFactory物件的static型態參數decodeStream。

關於後者,顧名思義就是使用Stream來將resource檔轉成InputStream,然後........(不知道怎麼形容這個過程,反正就是接收Stream資料串就是了。Stream的資料串吐出資料的「開啟」機制一直是我覺得這個物件很「謎」的地方。大家可以直接點連結參考一下怎麼用Stream轉成Bitmap物件。)(連結一)

ImageView有setImageResource()的功能,但這功能本身也是將圖檔從resource索引值轉出成Bitmap物件,它的過程和會遭遇的問題跟上面在討論的並沒有不同。

網路上常見的「標準」解決方法(連結一),其實僅僅只是將圖檔製作縮圖,取得一個比較小的Bitmap物件。



但...「挑戰」之所以是「挑戰」,就是因為永遠會有人、會有狀況來測驗標準/穩定方法的極限!

如果APP的程序中存在著一次要顯示/讀取多張大尺寸Bitmap,則這個方法(連結一)的穩定性會變得跟紙糊的沒兩樣!

大家用動態的流程想像一下...

假設我用縮放尺寸A來讀取的圖片1,然後將圖片1設給ImageView1,接著我要用相同的縮放尺寸A來讀取圖片2.......抱歉,OOM了!因為記憶體的環境已經跟讀取圖片1時的環境不同了!而這樣的環境已經無法用縮放尺寸A來讀取圖片2。



我曾經試過動態的變化縮放尺寸,讓每一張圖片都有「適合」自己的數值。

結果圖片的縮放品質會開始無底線的探底!

如果圖片是「不太講究品質的小icon」,那OOM的問題根本從一開始就不會發生!也就是說會發生OOM,一定都是解析度要講究、內容必須要可以讓使用者精確閱覽的圖片!



所以用Stream來縮放圖片(連結一)雖然符合Java解決問題的精神原則,但實際上根本沒有解決問題!

(其他還有一些「幻燈片/跑馬燈切換」的方式來展示多張圖片,但這都是從根本上限制「APP設計」,碰到「不行!我們就是要同時展示多張圖片!」時,依賴這種技巧等於讓程式設計師陷入一個叫天天不應、求地地不靈的死局。)


真正治標又治本的方式,就是製作一個存在手機端的小網頁,然後將圖片插入這個網頁中,在轉到WebView上顯示!(連結二)



不要聽到WebView就啞然失笑,「用WebAPP?效能不是很差嗎?」

這個方法除了圖片的顯示以外,其他都還是Java Code,效能等同於Native APP!



只是要注意一些小細節:

1.不要使用String.format來組成Html Tag。(執行後,經常會跳出Error,告訴我「Html少了個「"」。)
2.連結二中是Asset資源圖檔的版本,res中的resource資源檔有不同的file路徑寫法。(連結三)

3.如果有辦法精確控制WebView大小會更好,也可以輕鬆顯示多張圖片。(我有自己的秘訣,但.....解釋起來挺複雜的。如果能夠從「用Java動態設置」為方向去思考,會發現不難。)

2015年8月6日 星期四

關於Thread多線程

過去幾個禮拜在做「在Google Map上記錄使用者行走路徑」的功能...

(基於業務需求,不能附上詳細的程式碼。)

製作的過程中碰到最重要的幾個問題:

1.GPS的精準度和效率。
2.如何在行走過程中,開啟和關閉地圖,(地圖只是記錄行走路徑的附屬功能,可以獨立開啟跟關閉,)但又不會影響紀錄的效率和整體操作的流暢性。
3.紀錄行走的路徑資料量吃光了記憶體。

關於1,結果有點無解。

關於3,用即時的SQL讀寫來解決。

關於2,解決的過程中刷新了我對Thread的應用和設計心得.......




Thread的意義在於「不要讓單一項工作/任務吃光所有CPU的運算資源」,所以它會建議設計師在迴圈或程式碼中埋下Thread.sleep,來讓目前的工作/任務先暫時停止工作、把結果保留下來,然後處理其他工作。



我個人接觸Thread的初體驗是遊戲設計中的「畫面更新」功能。

畫面更新速率要穩定!(不穩定可以視為Thread設計失敗!)

但這是「畫面更新」的要點,而不是「Thread設計」的基本技巧,導致我一直以來在Thread的設計上,都會將Thread設計得過於僵硬強勢!──Thread結構都類似「畫面更新」的「無止盡的執行下去、直到程式關閉為止。」(這樣的Thread執行或許很穩定,但卻很難關掉、很難即刻反映APP的生命週期或開關。)

我想這是因為遊戲中都會將要運算和更新的資料量控制在一個穩定的範圍內,例如敵人數量要穩定、圖層數量要穩定。

所以不管是遊戲數值的運算、或畫面的更新,這兩個Thread的運算效率通常都會很穩定。(偶爾會有「爆擊」、「連續技」、「超大範圍攻擊」等事件發生,.......能否處理好就看工程師的功力了。)



但這次在更新地圖路徑的繪圖需求上,卻是完全彈性的!

資料量從一開始可能十筆二十筆,在五分鐘後會爆增到一百筆兩百筆,再十分鐘後又會增加到八百筆九百筆。

資料的量非常不穩定,所以「遊戲軟體」中「畫面更新」的Thread設計原則必然不適用!

將「數據運算」和「畫面更新」做成兩個完全平行、且穩定持續運作的Thread並不適合在這個運算資料量一直增加的設計中。



所以兩個Thread要從平行改為主從式設計。

一個Thread為穩定持續運作,另一個則被前面的Thread動態的生成啟動或關閉,...但在我這次的工作中,只有啟動、不需要關閉。(「畫面更新」Thread不需要循環執行,單一的功能執行完、則整個Thread關閉丟入記憶體回收列中.....,下次要更新畫面時,重啟一個Thread。)

這還是需要一些參數上的設計,例如「畫面更新開始」和「畫面更新結束」,畫面更新如果開始,就主Thread就不會再發出畫面更新的Thread,直到畫面更新結束為止。

(其實這是可以同一個參數解決的。)



這樣設計後,畫面的更新效率或許不太理想,時不時「使用者當下位置」跟「路徑線」會有點差距.......

但APP的整體操作完全不受影響。