理解Android命名規(guī)范
最近我一直在回答學生的以及StackOverflow上的問題,比如Activity里面應該寫什么、如何在需要Context的時候獲得它、如何在UI線程上進行異步任務以及為什么要用Fragment。這些問題歸根結底都是在問:“我如何完成這些被Android搞得很麻煩的事情?”
不可避免的,大多數答案所提供的方法都是***黑客色彩的,雖然技術上是可行的,但不應當遵從。這些問題表現出的是對Android框架中某些類的根本誤解,正確的回答應該是:你誤解了這個類的用途,本來這個類就不是干這個事兒的!
我甚至還看到過相關的開源框架,在解決某個因為框架誤用才產生的問題。以Infograph for Robospice為例,Robospice的基礎假設是:“AsyncTask有一個大問題:它和Activity的生命周期結合不夠緊密.”這個假設無疑是正確的,但這本來就不是個問題,這是AsyncTask自身的特點。任何異步的任務都不應該綁定在某個Activity或其生命周期上。這個框架的存在會導致開發(fā)者誤用Activity類。
我一直想對這些誤用問題給出一個統(tǒng)一的回答,可是想不出來,直到這周我重新閱讀了Robert Martin的《代碼整潔之道》中有關命名規(guī)范的章節(jié)。當我讀到“添加有意義的語境”部分時,我意識到,我們通過每個Android組件的名字就可以清晰地理解其功能。下面讓我們探討有關process(進程)、thread(線程)、application(應用)、activity(活動)、task(任務)、Fragment(碎片)與context(上下文)的命名。讀完之后你將對這些組件的職責有更準確的理解,并能發(fā)現我們?yōu)槭裁丛谡`用這些組件。
PROCESS
在書中“使用解決方案領域名稱”部分里,Martin寫到:
記住,只有程序員才會讀你的代碼。所以,盡管用那些計算機科學術語、算法名、模式名、數學術語吧。 |
Process(進程)的名字就是這么來的。在Android系統(tǒng)中,Process只不過是一個普通的計算進程,這是計算機所關心的東西,使用者自然不會關心它,事實上開發(fā)者也很少使用到這個概念。當我們談論進程、多進程實現與進程間通信時,你會想到這些在計算機內是如何完成的。
在Android中,每個Application(應用)會在自己的Linux線程中開啟,當系統(tǒng)需要回收內存時,最終會殺掉一個進程。進程的五個狀態(tài)包括:forground(前臺)、visible(課件)、service(服務)、background(后臺)和empty(空)。所以簡單來說,一個進程包括指令集和內存空間,而且是可被系統(tǒng)殺死的單元。
TASK
在書中“使用源自所涉問題領域的名稱”部分中,Martin寫到:
如果不能用程序員熟悉的術語來給手頭的工作命名,就采用從所涉問題領域而來的名稱吧。至少,負責維護代碼的程序員就能去請教領域專家了……與所涉問題領域更為貼近的代碼,應當采用源自問題領域的名稱。 |
Android設備所解決的最基本的問題是什么?它們能幫助人們完成任務。這看起來異常簡單,但想想過去在智能手機剛被發(fā)明的時候人們是如何憧憬的。誰也不知道未來人們能用手機完成什么事情,但是開發(fā)者所開發(fā)的工具根本上都是為了幫助人們完成任務的,也就是task(任務)。任務是以人為中心的術語,它包含未知數目的步驟,但有一個核心的主題。在日常生活中有哪些任務實例呢?打掃房間、開車上班、逛超市,這些都是任務。人們在生活中可能要完成許多任務,但在同一時刻只能進行一項任務。人可以開始或停止做任務,也可以在任務間切換。
Android中也是這樣。使用者在點擊應用圖標時開始一個task,可以在最近任務界面中看到最近的task,可以將task暫?;蛑匦麻_始,甚至可以通過在最近任務界面中移除某task來完全銷毀掉這個task。最重要的是,使用者只能同時與一個task進行交互。
ACTIVITY
就像task一樣,activity是問題領域中的術語。每個task包含了一個或多個activity。當一個人在進行任務時,他需要完成許多活動。比如打掃房間時,一個人可能正在疊衣服,當他疊完衣服時,他要開始清理浴缸。在完成一件活動前,他可以暫停并跳轉到當前任務下的另一個活動中。
當一個人在任務間切換時,他需要先暫停***個任務中的一個活動,然后開始第二個任務中的某個活動。比如當一個人從打掃房間轉到開車上班時,他需要先停止疊衣服的活動,并開始走向他的車。
雖然人可以在多個活動間切換,他同時只能進行一個活動。沒有人可以同時疊衣服和擦馬桶,如果有人嘗試那么畫面一定頗為娛樂。就算他在活動之間來回跳,也要線性地進行任務。
Android中Activity也類似,Activity是一個面向使用者的概念,它描述了使用者正在做的事情。一個Task包含了一個或多個Activity,但使用者只能同時與一個Activity交互,這個Activity通過占據整個屏幕來獲得使用者所有的注意力。使用者在進行任務切換的同時會停止與啟動Activity,每個Task都會記住用戶正在使用哪個Activity,這樣通過回退鍵回退時就會回到正確的Activity。
Activity的概念是基于使用者的,所以它負責向使用者展示數據并響應輸入,也正因此,它不應該進行任何幕后操作,比如數據庫讀寫、網絡請求與大量計算。這些代碼模塊都應該與Activity解耦,并離開用戶的視野。
只有當對使用者產生直接影響的變化發(fā)生時,計算機才會操作Activity生命周期,這些變化叫做配置信息改變,比如設備旋轉。
這也是為什么我們不應該在設備旋轉時讓AsyncTask存貨。Robospice看起來幫助了想在Activity中啟動AsyncTask的開發(fā)者。其實,這個類庫本不應這么做,因為Android中有更簡潔的架構模式來處理這些情況。
Fragment
Activity大到一定程度后就需要分割成更小的組件,現在暫時我們還沒有用于表示這個小組件的術語。這些Activity的部件是面向用戶的,所以我們不能去找解決方案領域的名稱。我們可能會想到“Sub Activity(子活動)”、“Component(組件)”、“Part(部分)”或是“Partial Activity(部分活動)”這些名稱,但是這些不免有誤導信息。Martin在“避免誤導”部分中講:
程序員必須避免留下掩藏代碼本意的錯誤線索。應當避免使用與本意相悖的詞。 |
“sub Activity(子活動)”可能會被誤解為Activity的子類。”Component(組件)”和”Part(部分)”都太模糊。“Partial Activity(部分活動)”隱含意是一個”部分活動”是不夠的,我們需要多個這種組件來構建一個Activity。說實話,我猜當內部人員在設計框架時,對于這個組件的命名的爭論最為激烈。開發(fā)者最終選擇“Fragment(碎片)”這個詞,我也想不出更好的詞了。
就像Activity一樣,Fragment的目的一樣是完成任務,但是尺度則小了一些。比如“擦浴缸”與“打掃廚房”這兩件活動,都需要以“準備清潔用具”這個碎片為開始。所以“準備清潔用具”這個碎片是可以在多個活動中使用的,而且不清楚整體的活動到底是擦浴缸還是打掃廚房。但比如“給花澆水”這類很小的活動,它本身就不需要再分碎片。
有些碎片很小很小,它們可以同時進行。比如“擦浴室瓷磚“和”擦浴室玻璃“是可以同時完成的,雖然它們是不同的碎片。
當使用者停止了一個任務或是任務中的一個活動時,自動就會停止一個碎片。一個人可以在多個碎片間切換,而不會停止活動。例如,一個人可以停止“準備清潔用具”并開始“往桶中灌水”,但他一直在進行”拖地”這項活動。
Android中的Fragment也是這個道理。一個Activity可以包含零個或多個Fragment,兩個Activity可以包含同一個Fragment的不同實例。如果一個Fragment足夠小,而且和另外的Fragment緊密相關,它們可以同時被顯示,比如master/detail design pattern(主-從視圖)。
使用者可以以任何順序在Fragment間切換。開發(fā)者甚至可以讓用戶通過回退鍵在多個Fragment間回退。當使用者停止或暫停一個Activity時,Activity也會停止或暫停它的Fragment。
THREAD
Thread(線程),就像Process(進程)一樣,也是解決方案領域的名稱。就像Process一樣,thread也只是一個計算機科學概念的基礎實現。Android沒有把事情搞得太復雜,直接使用了Java的thread。Android中的多數Thread都是Java Thread。Java Thread表現很好,Android只需要繼承Thread一次就能實現HandlerThread。
就像Process一樣,Thread是一個面向計算機的概念。使用Thread是為了同時完成多件事。就像計算機科學中線程的概念一樣,多個Thread實例可以存在于一個Process中。
使用者永遠不關心每個線程上正在發(fā)生什么,或是到底有多少個線程在運行。開發(fā)者使用Thread是為了告訴計算機干的再快一點。因為開發(fā)者是與計算機交談,需要像計算機一樣思考。所以涉及線程的問題一般都更難理解與debug,開發(fā)者需要考慮計算機的時間,而不是人類的時間,開發(fā)者需要思考有關內存訪問的問題,以及要不要限制某些組件只能在規(guī)定時間訪問規(guī)定內存空間。Thread之一,main thread又稱UI Thread,負責監(jiān)聽使用者輸入并與使用者交互。這個線程不應該進行任何后臺操作。所有的Activity都存活在這個線程中,并完成其所有工作。每個應用開啟時都只有一個線程,至于是否需要多個線程則是開發(fā)者所要考慮的問題。HandlerThread是進行后臺任務的一個很好的選項。
APPLICATION
就像Process和Thread一樣,Application(應用)也是解決方案領域的名稱,也是一個基本的計算機科學概念。一個Application是一個幫助使用者完成(多個)任務的軟件。但是,Application已經成為使用者與開發(fā)者所共同熟知的概念,所以有時它會帶來一些面向使用者的問題。
銀行和超市是物理意義上的Application(應用),它們被創(chuàng)建出來的目的就是幫助人們完成任務。超市的某些部分是直接與人交互的,但有些部分是在后臺幫助超市實現功能,比如倉庫和會計室。
許多應用可以組合成一個任務來幫助使用者完成更多的事情,即使這些應用是不同的人寫的。如果使用者正在進行烤蛋糕的任務,那么他可以去超市來進行“購買原材料”的活動,然后回家繼續(xù)烤蛋糕。只要有人在與一個應用交互,這個應用實例就存在。
開啟一個應用可以說是開啟一項任務的一部分。比如我們要開始做打掃房間這項任務,隱含意就是房間已經存在。當一個應用不再被使用時,它會被關閉掉。當所有的人都完成了超市中要完成的任務,超市就會關門。當一個人完成了要在家做的所有任務時,他就會把所有東西都關掉。
Android中的Application也是這個道理。啟動一個Task,進而啟動一個Activity的一部分就是啟動一個Application。一般來說,開發(fā)者只需要創(chuàng)建一個Application實例就可以服務所有的Activity,當用戶完成了所有的Activity,Android可能就會銷毀Application。
在進行一項任務時,使用者可以轉換到另一個Application來幫助他完成附加的活動,比如通過相機應用,使用者可以啟動郵箱應用來發(fā)送有圖片附件的郵箱。這兩個應用是可以由不同的開發(fā)者開發(fā)的,但是都屬于一個任務。
如果使用者在郵箱應用中強制停止當前任務,系統(tǒng)會停止郵箱應用中的Activity。如果使用者只是暫停并繼續(xù)當前任務,系統(tǒng)會立即重新啟動郵箱應用中的Activity。
如果使用者直接啟動郵箱應用,系統(tǒng)會開啟一個新的任務。相機任務中屬于郵箱應用的Activity不受新創(chuàng)建的郵箱任務影響。也就是說,同一個Activity的不同實例可以在不同的任務中生存。
即使使用者沒有與任何Activity進行交互,一個Application還是可以存在,比如當一個Application只在進行后臺任務,沒有Activity被展示,Application一樣可以生存。
CONTEXT
有一篇來自Dave Smith的博客很好地解釋了不同種類的Context(上下文)以及它們的功能。但什么是Context呢?很簡單。Context就是Context(上下文)。
在我們的Android基礎系列課程中,我將Context比作一個深入操作系統(tǒng)的鉤子。因為Activity需要系統(tǒng)來啟動,你需要使用Context來要求系統(tǒng)啟動一個Activity)。因為系統(tǒng)可以填充布局,你需要通過Context來要求系統(tǒng)填充布局。因為系統(tǒng)可以發(fā)送廣播,你可以通過Context來發(fā)送廣播或是注冊BroadcastReceiver(廣播接收器)。因為系統(tǒng)提供系統(tǒng)服務,你可以通過Context來訪問系統(tǒng)服務。
但是這些事情并不能隨處完成。比如在Service中,要求系統(tǒng)展示對話框或是啟動Activity是毫無意義的。再比如在Application中,系統(tǒng)甚至都不知道當前Activity的theme是什么,所以讓系統(tǒng)填充布局是毫無意義的。所以當你要求系統(tǒng)來完成任務時,你需要告訴系統(tǒng)你是在哪種Context(上下文)里想要做這些事情。當系統(tǒng)認定你想要做的事情對于當前上下文是合理的。Context的名字就是這么來的。就像Task和Activity一樣,Context屬于問題領域的名稱。
Context有許多子類,但我們主要會用到Application、Activity和Service。這幾個組件都描述了不同的上下文,可以做不同的事情。Dave Smith的文章詳細講述了它們都能干什么。
Context是暫時的,隨著時間變化會發(fā)生改變。當處理有關Context的問題時,我總是告訴自己:這個Context可以用于操作這個類,可是不一定能操作其他類。內存泄漏的一大來源就是到處傳遞Context并讓其他對象持有其引用。如果這里的Context是一個Activity,那么Activity在生命周期結束后也不會被回收,所以到處傳遞Context并不是一個好主意,我們不應該這么做。
當你的確需要傳遞Context對象時,要確保這個Context對象適用范圍越大越好,一般來說我們應該傳遞一個Application Context。當你傳遞Context時,不要假設傳遞過來的人考慮過你的需求,也不要假設傳過來的就是正確的Context。
- public void doSomething(Context context) {
- // 不要假設傳過來的就是Application
- mContext = context;
- // 永遠要手動獲取Application Context
- mContext = context.getApplicationContext();
- ...
- }
所以呢?
明白了這些名字是哪里來的又有什么用呢?我經??吹綄W生的問題是如下格式:我怎么讓X完成Y?他們其實應該先問問自己:X應該完成Y嗎?為了回答這個問題,他們需要問:X的單一職責是什么?對于所有的Android框架中的組件,其職責就體現在命名中。這個名稱可以映射到問題領域或是解決方案領域的名臣很高。解決方案領域的名稱,如Process、Thread和Application,是開發(fā)者所理解與熟知的。問題領域的術語如task、activity、context和fragment則只當我們在描述現實生活中的問題時才可以被理解。
所以當你下次想問如何在Activity中完成異步任務,或是如何通過Context對象獲取系統(tǒng)組件,或是Process(進程)、Thread(線程)與Task(任務)之間的區(qū)別是什么時,看看名字就知道答案了。
這篇文章將在Android N推出時被推翻,因為多個Activity可以同時存在于一塊屏幕上。
這么說好像很傻。