2015年2月6日 星期五

這些年在Android上,我看過的網路傳輸並接收資料的模組架構,還有Intent、SingleTon模組、和Adapter機制

【我不是要寫文教大家怎麼去POST/GET資料,也不是要自以為是的跟大家分享Socket經驗(雖然我很想......),我是想要分享我在設計Android APP時,設計「接收資料並呈獻到畫面上給使用者看」的過程中遭遇到的種種狀況。

這文對工程師意義可能不大,因為裡頭會談到一些Android架構的限制,工程師可能比較好奇怎麼凌駕這些限制,但我個人選擇和這些限制和平共處,──所以我的經驗和意見可能是不中看也不中用,但對非技術底的人來說可能比較有價值,例如設計或PM,因為在構思功能和流程時,不需要太多知識與經驗也知道要避開這些現象。】



Android的問題在於除了「Activity生命週期」的幾個階段被底層呼叫後,畫面一旦生成就不能隨便更動。要更動就必須要經由Handler、BroadCastReciever、或另起Activity。不然就必須要使用本來就可隨意更動的SurfaceView。

所以當畫面原本設計目的就是「不時要被網路資料更動」時,...注意!網路資料進出的時間點本來就不可能會等APP的「生命週期」,結果不是造成APP掛點,不然就是畫面跟實際接收到的資訊有落差。(前者是因為在生命周期外更動畫面,後者是因為生命周期外接收到的資訊不會反映在畫面上。)

要處理這個,我見到的兩大派作法:
1.大量使用Adapter,將資訊裝入Adapter中後進行簡單的「自動更新」。(給非技術底的:如果簡單描述Adapter,它就是個會針對資訊的內容快速產生畫面的工具,例如一百筆記錄了連絡資料的JSON。它有些限制,例如每筆資料間要最低限度的共通點、針對每筆資料產生的畫面通常也只能操做該筆資料...等。說它簡單,因為要啟動它的「自動更新」很簡單,而且「自動更新」的內容要多複雜就可以有多複雜,幾乎可以完全無視起動的時機點──因為系統會自動幫APP管理。)
2.在上一個Activity中使用一個Thread確保網路資料都已經接收完畢後,再啟動新的Activity、並直接使用剛接收到的網路資訊來決定畫面內容。(給非技術底的:這些字數完全無法描繪它實際上需要做的事情。)

第一種是Google在設計Android基本功能時常用的手法,不管是GMail、Google Play、或一些管理功能這類進行「系統控制」的「APP」。(對!一樣是給非技術底的:那些都是APP,Android底層是Linux,本來是只有文字的東西而已,凡是圖形介面、包含解鎖畫面都是種APP。)連FaceBook也是使用這樣的方式製作最新、最穩定的APP版本。(注意!這真的是最穩定,而且可能是最完美的APP型態。)

第二種在許多客製化打造的APP中經常使用。例如時不時接收網路傳來的JSON資料,然後將它分散到畫面上的數塊文字訊息欄中,或是變換背景顏色,甚至野心很大的更動它的佈局。



關於第一種,參考GMail、Google Play、還有FaceBook就可以知道這種設計方式的最大特徵。它們的畫面並不單調,程式不管是使用者體驗或功能都可以維持一定的穩定順暢。反觀第二種,就很考驗設計者和QC的耐心了!

以下是個範例客題:如果畫面被要求動態的、隨機的產生無數個欄位,每個欄位的內容功能都要隨網路資訊而有不同變化,而且網路資料的量、規則都無法預估.......

如果是第一種設計法,只要確保Adapter有呈現每種資料的能力,而網路接收資料正確、傳遞給Adapter的順序也如網路資料預估.......然後要求系統去自行「更新」即可。不管重新接收多少次資料、刪除單筆、插入多筆.........,都可以維持這個簡單的線性流程。(操作資料......操作一次、操作兩次、操作三次......操作N次,然後要求Adapter更新內容,一定可以精準的看到結果。)

如果是第二種設計法,就要先把畫面維持一個空白的大框架,然後接收完網路資料後開始數算數量,然後預估會需要在大框架中塞入幾個欄位,接著就開始逐一生成欄位。........

聽起來沒什麼、兩種差異不大,問題是用第一種方案中一行程式碼就可以做到的事情,為何要像第二種方案這樣寫數十、甚至數百行程式碼呢?

很多專案遲遲無法收尾,就是因為工程師被迫使用第二種。最後不管他們寫再多程式碼,都會發生「為何第一次新增條目時不會當機,刪除後新增就當機了,你的新增功能真的有做好嗎?」這類的Bug。因為第一種方法強制要求APP的畫面一律跟資料看齊,而第二種方式其實是寫很多修改畫面的程式碼,然後隨時判斷資料內容來決定要使用哪段程式碼修改畫面,只是這些方法即使相似處很多,但不是「永遠不夠用」,不然就是「會打架、會矛盾、會互扯後腿」。



另一個問題是我身為一個半路出家的技術狂最感興趣的現象。就是程式的「穩定壽命會縮短」。(以下所說其實已經是種理論性的天馬行空胡亂講...只是因為真的發生過,所以我寫下來。)

Activity是Android切割畫面與功能分界的一種機制,意義就有點像網路的「頁面」。

即使是同一個Activity之間,也無法直接交換資料,需要經過一些比較繁瑣的方式去設計它。

例如Intent,即使預設目的並不是要夾帶「畫面資料」,但很多人都會選擇這樣設計它。將下一個畫面的「標題」、「背景」...等資訊夾在Intent中,發送給下一個Activity。聰明的人、經驗夠的人都一定會經歷到Intent沒有忠實的把所有資訊傳給下個Activity的現象,例如前十筆資料都有帶到,後十筆資料就不見了。(這可能是因為Intent是用C/C++實做的,它並不是完美的物件導向定義出來的物件,即使它有嚴謹的建構子,但它其實會被轉換成非物件的型態在Activity和系統間傳遞,轉換回物件時,資料就消失在記憶體中了。──歡迎技術底更強的人吐嘲!)

所以老手應該都會使用Intent以外的方法傳遞資料。最常見的就是SingleTon。

所謂的SingleTon,就是在記憶體中畫置一塊區域,存放一個物件,然後這個物件中有存放資料的功能。因為這個物件嚴格說起來是「人人(任何Activity)都可以摸到裡面,所以只要知道這個物件存在的都可以往裡面塞資料、取資料」。

如果看過我其它文章的人就知道:使用SingleTon的人不見得很懂物件導向,他們只是覺得短短幾行程式碼可以有這個功用,就把它拿來用吧!

不管是SingleTon,或任何一種第二類資料傳輸法,理論上都會有個風險,就是記憶體會不夠用、記憶體管理會出錯。

不懂電腦原理的人一定會質疑「記憶體需求不都開好、規畫好,怎麼會不夠用?不夠用不能改良嗎?」

事實是:資料在記憶體中不會自己判斷自己是不是「已經不被需要了」,必須要程式要求它「請從記憶體中消失」,或是設計者關機斷電。一般來說,C/C++程式設計師都要自己管理這個動作,自己產生的、自己主動讀取的取用的資料,都要自己下指令、寫程式把它們清除;但Java/Android工程師不是如此,只要操作一種類似「索引值」的東西,系統內的某個特定程式就會定時按照索引值來清除記憶體內不要的資料。

不是開玩笑,真的有一支專門的系統程式負責這個工作。

但某些因素讓這支程式工作起來不太精準,經常會發生已經被排入要被清除的資料,清除程式竟然認為自己不可以清除它,但也不知道該拿它怎辦.........(或許是「索引值」管理程式出問題?或其他?)當這些「不知道該怎辦的資料」在記憶體中越堆越多,最終的後果就是程式效能越跑越慢、甚至當機。(所以重開機通常可以讓Android效能恢復,可見這個現象不是神話或笑話。)

可是「因為Android和Java的共通性」這種話講起來是很讓人心虛的。重點是:程式的動作越多、程式碼越多、管理調配的動作越多,就越容易產生這些「不知道該怎辦的資料」。譬如不用Adapter來產生動態畫面的APP在刷新畫面無數次後,最終的命運就是記憶體被那些「淘汰」的畫面元件吃光光.....(被丟掉的畫面變成「不知道該怎辦的資料」。)

使用第一種方法並不會保障「不知道該怎辦的資料」絕不會產生。

但是.......舉例來說,如果使用Intent傳遞資料,結果可能會造成資料傳遞完、Intent的一部份變成「不知道該怎辦的資料」,如果大量、頻繁使用Intent,結果就是造成APP從開始到「因為記憶體被這些資料吃光而無法正常運作」的周期縮短。

SingleTon也如此。

重點在於:第二類方法總是會造成「特定Activity需要的資料必須要在其他Activity中產生,並且使用額外物件管理或傳遞,處理跟接收本身也要產生額外的物件」,──附帶一提,所謂的物件也是種「資料」。這些物件越多,也就表示資料越多......

所以在單一Activity當下接收並管理資料,怎麼看都是王道。



寫這些,是因為公司某前輩離職前寫的程式開始被客戶抱怨「使用久了、操作頻繁了,就會當機」。

大老闆正在四處找更資深的工程師來解這個問題,我雖然知道解法、而且在別家公司也成功處理過類似的狀況,但.........先安靜.........