本文內(nèi)容,主題是透過應(yīng)用程序來分析Android系統(tǒng)的設(shè)計原理與構(gòu)架。我們先會簡單介紹一下Android里的應(yīng)用程序編程,然后以這些應(yīng)用程序在運行環(huán)境上的需求來分析出,為什么我們的Android系統(tǒng)需要今天這樣的設(shè)計方案,這樣的設(shè)計會有怎樣的意義, Android究竟是基于怎樣的考慮才變成今天的這個樣子,所以本文更多的分析Android應(yīng)用程序設(shè)計背后的思想,品味良好架構(gòu)設(shè)計的魅力。分五次連載完成,第一部分是最簡單的部分,解析Android應(yīng)用程序的開發(fā)流程。 特別聲明:本系列文章LiAnLab.org著作權(quán)所有,轉(zhuǎn)載請注明出處。作者系LiAnLab.org資深A(yù)ndroid技術(shù)顧問吳赫老師。本系列文章交流與討論:@宋寶華Barry
1. Android應(yīng)用程序在目前Android大紅大紫的情況下,很多人對編寫Android應(yīng)用程序已經(jīng)有了足夠深入的了解。即便是沒有充分的認(rèn)識,在現(xiàn)在Android手機(jī)已經(jīng)相當(dāng)普及的情況下,大家至少也會知道Android的應(yīng)用程序會是一個以.apk為后綴名的文件(在Windows系統(tǒng)里,還會是一個帶可愛機(jī)器人圖標(biāo)的文件)。那這個apk包又有什么樣的含義呢? 如果您正在使用Linux操作系統(tǒng),可以使用命令file命令來查看這一文件的類型。比如我們下載了一個Sample.apk的文件,則使用下面的命令:
對,沒有看錯,只一個簡單的zip文件。要是做過Java開發(fā)的人,可以對這種格式很親切,因為傳說中的.jar、.war格式,都是Zip壓縮格式的文件。我們可繼續(xù)使用unzip命令將這一文件解壓(或是任何的解壓工具,zip是人類歷史是最會古老最為普及的壓縮格式之一,幾乎所有壓縮工具都支持)。通過解壓,我們就得到了下面的文件內(nèi)容:
到這里,我們就可以看到一個Android應(yīng)用程序結(jié)構(gòu)其實是異常簡單的。這五部分內(nèi)容(其中META-INF和res是目錄,其他是文件)除了META-INF是這一.apk文件的校驗信息,resources.arsc是資源的索引文件,其他三部分則構(gòu)成了Android應(yīng)用程序的全部。 T AndroidManifest.xml,這是每個Android應(yīng)用程序包的配置文件,這里會保存應(yīng)用程序名字、作者、所實現(xiàn)的功能、以及一些權(quán)限驗證信息。但很可惜,在編譯完成的.apk文件里,這些文件都被編譯成了二進(jìn)制版本,我們暫時沒有辦法看到內(nèi)容,后面我們可以再看看具體的內(nèi)容。 T classes.dex,這則是Android應(yīng)用程序?qū)崿F(xiàn)的邏輯部分,也就是通過Java編程寫出來而被編譯過的代碼。這種特殊的格式,是Android里特定可執(zhí)行格式,是可由Dalvik虛擬機(jī)所執(zhí)行的代碼,這部分內(nèi)容我們也會在后續(xù)的介紹Dalvik虛擬機(jī)的章節(jié)里介紹。 T res,這一目錄里則保存了Android所有圖形界面設(shè)計相關(guān)的內(nèi)容,比如界面應(yīng)該長成什么樣子、支持哪些語言顯示等等。 從一個android應(yīng)用程序的包文件內(nèi)容,我們可以看到android應(yīng)用程序的特點,這也是Android編程上的一些特征: 1 簡單:最終生成的結(jié)果是如些簡單的三種組成,則他們的編程上也不會有太大的困難性。這并不是說Android系統(tǒng)里無法實現(xiàn)很復(fù)雜的應(yīng)用程序,事實上Android系統(tǒng)擁有世界上僅次于iOS的應(yīng)用程序生態(tài)環(huán)境,也擁有復(fù)雜的辦公軟件、大型3D游戲。而只是說,如果要實現(xiàn)和構(gòu)成同樣的邏輯,它必然會擁有其他格式混雜的系統(tǒng)更簡化的編程模式。 2 Java操作系統(tǒng):既然我們編譯得到的結(jié)果,classes.dex文件,是用于Java虛擬機(jī)(雖然是Dalvik虛擬機(jī),但實際上這一虛擬機(jī)只是一種特定的Java解析器和虛擬機(jī)執(zhí)行環(huán)境 )解析執(zhí)行的,于是我們也可以猜想到,我們的Android系統(tǒng),必然是一個Java操作系統(tǒng)。我們在后面會解釋,如果把Android系統(tǒng)直接看成Linux內(nèi)核和Java語言組合到一起的操作系統(tǒng)很不準(zhǔn)確,但事實上Android,也還是Java操作系統(tǒng),Java是唯一的系統(tǒng)入口。 使用MVC設(shè)計模式:所謂的MVC,就是Model,View,Controller的首字母組合起來的一種設(shè)計模式,主要思想就是把顯示與邏輯實現(xiàn)分離。Model用于保存上下文狀態(tài)、View用于顯示、而Controller則是用于處理用戶交互。三者之間有著如下圖所示的交互模型,交互只到Controller,而顯示更新只通過View進(jìn)行,這兩者再與Model交換界面狀態(tài)信息:
在現(xiàn)代的圖形交互相關(guān)的設(shè)計里,MVC幾乎是在圖形交互處理上的不二選擇,這樣系統(tǒng)設(shè)計包括一些J2EE的應(yīng)用服務(wù)器框架,最受歡迎的Firefox瀏覽器,iOS,MacOSX等等。這些使用MVC模式的最顯著特點就是顯示與邏輯分離,在Android應(yīng)用程序里我們看到了用于邏輯實現(xiàn)的classes.dex,也看到用于顯示的res,于是我們也可以猜想到在UI上便肯定會使用MVC設(shè)計模式。 當(dāng)然,所謂的Android應(yīng)用程序編程,不會只有這些內(nèi)容。到目前為止,我們也只是分析.apk文件,于是我們可以回過頭來看看Android應(yīng)用被編譯出來的過程。
2. Android編程從編程角度來說,Android應(yīng)用程序編程幾乎只與Java相關(guān),而Java平臺本身是出了名跨平臺利器,理論上來說,所有Java環(huán)境里使用的編程工具、IDE工具,皆可用于Android的編程。Android SDK環(huán)境里提供的編程工具,是基于標(biāo)準(zhǔn)的Java編譯工具ant的,但事實上,一些大型的Android軟件工程,更傾向于使用Maven這樣的并行化編譯工具(maven.apache.org)。如果以前有過Java編程經(jīng)驗,會知道Java環(huán)境里的圖形化IDE(Integrated Development Environment)工具,并非只有Eclipse一種,實際上Java的官方IDE是NetBeans,而商用化的Java大型項目開發(fā)者,也可能會比較鐘意于使用IntelliJ,而從底層開發(fā)角度來說,可能使用vim是更合適的選擇,可以靈活地在C/C++與Java代碼之間進(jìn)行切換??偠灾瑤缀跛械腏ava環(huán)境的編程工具都可以用于Android編程。 對于這些工具呢,熟悉工具的使用是件好事,所謂“磨刀不誤砍柴工”,為將來提升效率,這是件好事。但是要磨刀過多,柴沒砍著,轉(zhuǎn)型成“磨刀工”了。如果過多地在這些編程工具上糾結(jié)嘗試,反而忽視了所編代碼的本身,這倒會舍本逐末。 我們既然是研究Android編程,這時僅說明兩種Android官方提供的編程方法:使用Android SDK工具包編程,或是使用Eclipse + ADT插件編程。 2.1 使用Android SDK工具包在Android開發(fā)過程中,如果Eclipse環(huán)境不可得的情況下,可以直接使用SDK來創(chuàng)建應(yīng)用程序工程。首先需要安裝某一個版本的Android SDK開發(fā)包,這個工具包可以到http://developer./sdk/index.html這個網(wǎng)址去下載,根據(jù)開發(fā)所用的主機(jī)是Windows、Linux還是MacOS X(MacOS僅支持Intel芯片,不支持之前的PowerPC芯片),下載對應(yīng)的.zip文件,比如android-sdk_r19-linux.zip。下載完成后,解壓到一個固定的目錄,我們這里假定是通過環(huán)境變量$ANDROID_SDK_PATH指定的目錄。 下載的SDK包,默認(rèn)是沒有Android開發(fā)環(huán)境支持的,需要通過tools目錄里的一個android工具來下載相應(yīng)的SDK版本以用于開發(fā)。我們通過運行$ANDROID_SDK_PATH/tools/android會得到如下的界面:
在上面的安裝界面里選擇不同的開發(fā)工具包,其中Tools里包含一些開發(fā)用的工具,如我們的SDK包,實際上也會在這一界面里進(jìn)行更新。而對于不同的Android版本,1.5到4.1,我們必須選擇下載某個SDK版本來進(jìn)行開發(fā)。而下載完之后的版本信息,我們既可以在這一圖形界面里看到,也可以通過命令行來查看。
通過android list targets列出來的信息,可以用于后續(xù)的開發(fā)之用,比如對于不同的target,最后得到了id:1、id:2這樣的信息,則可以被用于應(yīng)用程序工程的創(chuàng)建。而細(xì)心一點的讀者會看到同一個4.1版本的SDK,實際可分為”android-16”和"Google Inc.:Google APIs:16",這樣的分界也還有有意義的,”android-16”用于“純”的android 4.1版的應(yīng)用程序開發(fā),而“Google Inc.:Google APIs:16”則加入了Google的開發(fā)包。 配置好環(huán)境之后,如果我們需要創(chuàng)建Android應(yīng)用程序。tools/android這個工具,同時也具備可以創(chuàng)建Android應(yīng)用程序工程的能力。我們輸入:
這樣我們就在hello目錄里創(chuàng)建了一個Android的應(yīng)用程序,名字是Hello,使用API16(Android 4.1的API版本),包名是org.lianlab.hello,而默認(rèn)會被執(zhí)行到的Activity,會是叫Helloworld的Activity類。 掌握Android工具的一些使用方法也是有意義的,比如當(dāng)我們的Eclipse工程被破壞的情況下,我們依然可以手工修復(fù)這一Android應(yīng)用程序工程?;蚴切枰薷脑摴こ痰腁PI版本的話,可以使用下面的命令: $ANDROID_SDK_PATH/tools/android updateproject -t 2 -p . 在這個工程里,如果我們不加任何修改,會生成一個應(yīng)用程序,這個應(yīng)用程序運行的效果是生成一個黑色的圖形界面,打印出一行"Hello World, Helloworld"。如果我們需要對這一工程進(jìn)行編譯等操作的話,剩下的事情就屬于標(biāo)準(zhǔn)的Java編譯了,標(biāo)準(zhǔn)的Java編譯,使用的是ant(ant.apache.org)編譯工具。我們先改變當(dāng)前目錄到hello,然后就可以通過” ant –projecthelp”來查看可以被執(zhí)行的Android編譯工程,
但如果只是編譯,我們可以使用antdebug生成Debug的.apk文件,這時生成的文件,會被放到bin/Hello-debug.apk。此時生成的Hello-debug.apk,已經(jīng)直接可以安裝到Android設(shè)備上進(jìn)行測試運行。我們也可以使用ant release來生成一個bin/Hello-release-unsigned.apk,而這時的.apk文件,則需要通過jarsigner對文件進(jìn)行驗證才能進(jìn)行安裝。 通過antdebug這一編譯腳本,我們可以看到詳細(xì)的編譯過程。我們可以看到,一個Android的工程,最后會是通過如圖所示的方式生成最后的.apk文件。
把一個Android的源代碼工程編譯成.apk的Android應(yīng)用程序,其過程如下: 1) 所有的資源文件,都會被aapt進(jìn)行處理。所有的XML文件,都會被aapt解析成二進(jìn)制格式,準(zhǔn)確地說,這樣的二進(jìn)制格式,是可以被直接映射到內(nèi)存里的二進(jìn)制樹。做過XML相關(guān)開發(fā)的工程師,都會知道,XML的驗證與解析是非常消耗時間與內(nèi)存的,而通過編譯時進(jìn)行XML解析,則節(jié)省了運行時的開銷。當(dāng)然解析的結(jié)果最后會被aapt通過一個R.java保存一個二進(jìn)制樹的索引,編程時可通過這個R.java文件進(jìn)行XML的訪問。aapt會處理所有的資源文件,也就是Java代碼之外的任何靜態(tài)性文件,這樣處理既保證了資源文件間的互相索引得到了驗證,也確保了R.java可以索引到這個應(yīng)用程序里所有的資源。 2) 所有的Java文件,都會被JDK里的javac工具編譯成bin目錄下按源代碼包結(jié)構(gòu)組織的.class文件(.class是標(biāo)準(zhǔn)的Java可解析執(zhí)行的格式),比如我們這個例子里生成的bin/classes/org/lianlab/hello/*.class文件。然后這些文件,會通過SDK里提供的一個dx工具轉(zhuǎn)換成classes.dex文件。這一文件,就是會被Dalvik虛擬機(jī)所解析執(zhí)行的 3) 最后我們得到的編譯過的二進(jìn)制資源文件和classes.dex可執(zhí)行文件,會通過一個apkbuilder工具,通過zip壓縮算法打包到一個文件里,生成了我們所常見的.apk文件。 4) 最后,.apk文件,會通過jarsigner工具進(jìn)行校驗,這一校驗值會需要一個數(shù)字簽名。如果我們申請了Android開發(fā)者賬號,這一數(shù)字簽名就是Android所分發(fā)的那個數(shù)字證書;如果沒有,我們則使用debug模式,使用本地生成的一個隨機(jī)的數(shù)字證書,這一文件位于~/.android/debug.keystore。 雖然我們只是下載了SDK,通過一行腳本創(chuàng)建了Android應(yīng)用程序工程,通過另一行完成了編譯。但也許還是會被認(rèn)為過于麻煩,因為需要進(jìn)行字符界面的操作,而且這種開發(fā)方式也不是常用的方式,在Java環(huán)境下,我們有Eclipse可用。我們可以使用Eclipse的圖形化開發(fā)工具,配合ADT插件使用。 2.2 使用Eclipse+ADT插件在Android環(huán)境里可以使用Java世界里幾乎一切的開發(fā)工具,比如NetBeans等,但Eclipse是Android官方標(biāo)準(zhǔn)的開發(fā)方式。使用Eclipse開發(fā),前面提到的開發(fā)所需SDK版本下載,也是必須的,然后還需要在Eclipse環(huán)境里加裝ADT插件,Android Development Toolkit。 我們在Eclipse的菜單里,選擇”Help” à “Install New Software…”,然后在彈出的對話框里的Workwith:輸入ADT的發(fā)布地址:https://dl-ssl.google.com/android.eclipse,回車,則會得到下面的軟件列表。選擇Select All,將這些插件全都裝上,則得到了可用于Android應(yīng)用程序開發(fā)的環(huán)境。
這里還需要指定SDK的地址,Windows或是Linux里,會是在菜單“Window” à “Preferences”,在MacOS里,則會是”Eclipse” à“Preferences” 。在彈出的對話框里,選擇Android,然后填入Android SDK所保存的位置。
點擊OK之后,則可以進(jìn)行Android開發(fā)了。選擇”File” à “New”à “Project” à “Android”,在Eclipse 3.x版本里,會是“Android Project”,在Eclipse 4.x版本里,會是“Android Application Project”。如果我們需要創(chuàng)建跟前面字符界面下一模一樣的應(yīng)用程序工程,則在彈出的創(chuàng)建應(yīng)用程序?qū)υ捒蚶锾钊肴缦碌膬?nèi)容:
然后我們選擇Next,一直到彈出最后界面提示,讓我們選擇默認(rèn)Activity的名字,最后點擊”Finish”,我們就得到一個Android應(yīng)用程序工程,同時在Eclipse環(huán)境里,我們既可以通過圖形化界面編輯Java代碼,也可以通過圖形化的界面編輯工具來繪制圖形界面。 (注意: 如果Android工程本身比較龐大,則最好將Eclipse里的內(nèi)存相關(guān)的配置改大。在Windows和Linux里,是修改eclipse里的eclipse.ini文件,而在MacOS里,則是修改Eclipse.app/Contents/MacOS/eclipse.ini。一般會將如下的值加大成兩倍:
我們得到工程目錄,在Eclipse環(huán)境里會是如下圖所示的組織方式。代碼雖然是使用一模一樣的編譯方式,唯一的改變是,我們不再需要使用腳本來完成編譯,我們可以直接使用Eclipse的”Project”à“Build project”來完成編譯過程。如果我們使用默認(rèn)設(shè)置,則代碼是使用自動編譯的,我們的每次修改都會觸發(fā)增量式的編譯。
我們從這些android編程的過程,看不出來android跟別的Java編程模式有什么區(qū)別,倒至少驗證了我們前面對android編程特點的猜想,就是很簡單。如果同樣我們使用Eclipse開發(fā)Java的圖形界面程序,需要大量地時間去熟悉API,而在Android這里學(xué)習(xí)的曲線被大大降低,如果我們只是要畫幾個界面,建立起簡單的交互,我們幾乎無須學(xué)習(xí)編程。 而從上面的步驟,我們大概也可以得到Android開發(fā)的另一個好處,就是極大的跨平臺性,它的開發(fā)流程里除了JDK沒有提及任何的第三方環(huán)境需求,于是這樣的開發(fā)環(huán)境,肯定可以在各種不同的平臺執(zhí)行。這也是Android上進(jìn)行開發(fā)的好處之一,跨平臺,支持Windows,Linux與MacOS三種。 我們再來看一個,我們剛才創(chuàng)建的這個工程里,我們怎么樣進(jìn)行下一步的開發(fā)。在Android開發(fā)里,決定我們應(yīng)用程序表現(xiàn)的,也就是我們從一個.apk文件里看到的,我們實際上只需要: T 修改AndroidManifest.xml文件。AndroidManifest.xml是Android應(yīng)用程序的主控文件,類型于Windows里的注冊表,我們通過它來配置我們的應(yīng)用程序與系統(tǒng)相關(guān)的一些屬性。 T 修改UI顯示。在Android世界里,我們可以把UI編程與Java編程分開對待,處理UI控件的語言,我們可以叫它UI語言,或是layout語言,因為它們總是以layout類型的資源文件作為主入口的。Android編程里嚴(yán)格地貫徹MVC的設(shè)計思路,使我們得到了一個好處,就是我們的UI跟要實現(xiàn)的邏輯沒有任何必然聯(lián)系,我們可先去調(diào)整好UI顯示,而UI顯示后臺的實現(xiàn)邏輯,則可以在后續(xù)的步驟里完成。 T 改寫處理邏輯的Java代碼。也就是我們MVC里的Controller與Model部分,這些部分的內(nèi)容,如果與UI沒有直接交互,則我們可以放心大膽的改寫,存在交互的部分,比如處理按鈕的點擊,取回輸入框里的文字等。作為一個定位于拓展能力要求最高的智能手機(jī)操作系統(tǒng),android肯定不會只實現(xiàn)畫畫界面而已,會有強(qiáng)大的可開發(fā)能力,在android系統(tǒng)里,我們可以開發(fā)企業(yè)級應(yīng)用,大型游戲,以及完整的Office應(yīng)用。 無論是通過tools/android工具生成的Android源代碼目錄,還是通過Eclipse來生成的Android源代碼工程,都需要進(jìn)一步去自定義這個步驟來完成一個Android應(yīng)用程序。當(dāng)然,還有一種特殊的情況就是,這個源代碼工程并非直接是一個Android應(yīng)用程序,只是Unit Test工程或是庫文件工作,并不直接使用.apk文件,這里則可能后續(xù)的編程工作會變得不同。我們這里是分析Android應(yīng)用程序,于是后面分別來看應(yīng)用程序編程里的這三部分的工作如何進(jìn)行。 2.3 AndroidManifest.xml先來看看AndroidManifest.xml文件,一般出于方便,這一文件有可能也被稱為manifest文件。像我們前面的例子里的創(chuàng)建的Android工程,得到的AndroidManifest.xml文件就很簡單:
作為xml文件,所有的<>都會是成對的,比如我們看到的<manifest></manifest>,這被稱為標(biāo)簽(Tag)。標(biāo)簽可以包含子標(biāo)簽,從而可以形成樹型的結(jié)點關(guān)系。如果沒有子標(biāo)簽,則我們也可以使用</>來進(jìn)行標(biāo)識,比如我們上面看到的<actionandroid:name=”android.intent.action.MAIN” />。 <manifest>,是主標(biāo)簽,每個文件只會有一個,這是定義該應(yīng)用程序?qū)傩缘闹魅肟?,包含?yīng)用程序的一切信息,比如我們的例子里定義了xml的命名空間,這個應(yīng)用程序的包名,以及版本信息。 <application>,則是用于定義應(yīng)用程序?qū)傩缘臉?biāo)簽,理論上可以有多個,但多個不具有意義,一般我們一個應(yīng)用程序只會有一個,在這個標(biāo)簽里我們可以定義圖標(biāo),應(yīng)用程序顯示出來的名字等。在這一標(biāo)簽里定義的屬性一般也只是輔助性的。 <activity>,這是用來定義界面交互的信息。我們在稍后一點的內(nèi)容介紹Android編程細(xì)節(jié)時會描述到這些信息,這一標(biāo)簽里的屬性定義會決定應(yīng)用程序可顯示效果。比如在啟動界面里的顯示出來的名字,使用什么樣的圖標(biāo)等。 <intent-filter>,這一標(biāo)簽則用來控制應(yīng)用程序的能力的,比如該圖形界面可以完成什么樣的功能。我們這里的處理比較簡單,我們只是能夠讓這個應(yīng)用程序的HelloWorld可以被支持點擊到執(zhí)行。 從這個最簡單的AndroidManifest.xml文件里,我們可以看到Android執(zhí)行的另一個特點,就是可配置性強(qiáng)。它跟別的編程模型很不一樣的地方是,它沒有編程式規(guī)定的main()函數(shù)或是方法,而應(yīng)用程序的表現(xiàn)出來的形態(tài),完全取決于<activity>字段是如何定義它的。 2.4 圖形界面(res/layout/main.xml)我們可以再來看android的UI構(gòu)成。UI也是基于XML的,是通過一種layout的資源引入到系統(tǒng)里的。在我們前面看到的最簡單的例子里,我們會得到的圖形界面是res/layout/main.xml,在這一文件里,我們會看到打開顯示,并在顯示區(qū)域里打印出Hello World, Helloworld。
在這個圖形界面的示例里,我們可以看到,這樣的圖形編程方式,比如傳統(tǒng)的方式里要學(xué)習(xí)大量的API要方便得多。 <LinearLayout>,這一標(biāo)簽會決定應(yīng)用程序如何在界面里擺放相應(yīng)的控件 <TextView>,則是用于顯示字符串的圖形控件 使用這種XML構(gòu)成的UI界面,是MVC設(shè)計的附屬產(chǎn)品,但更大的好處是,有了標(biāo)準(zhǔn)化的XML結(jié)構(gòu),就可以創(chuàng)建可以用來畫界面的IDE工具。一流的系統(tǒng)提供工具,讓設(shè)計師來設(shè)計界面、工程師來邏輯,這樣生產(chǎn)出來的軟件產(chǎn)品顯示效果與用戶體驗會更佳,比如iOS;二流的系統(tǒng),界面與邏輯都由工程師來完成,在這種系統(tǒng)上開發(fā)出來的軟件,不光界面不好看,用戶體驗也會不好。我們比如在Eclipse里的工程里查看,我們會發(fā)現(xiàn),我們打開res/layout/main.xml,會自動彈出來下面的窗口,讓我們有機(jī)會使圖形工具來操作界面。
在上面IDE工具里,左邊是控件列表,中間是進(jìn)行繪制的工作區(qū),右邊會是控件一些微調(diào)窗口。一般我們可以從左邊控制列表里選擇合適的控件,拖到中間的工作區(qū)來組織界面,原則上的順序是layout à 復(fù)合控件 à 簡單控件。中間區(qū)域可以上面還有選擇項用于控制顯示屬性,在工作區(qū)域里我們可以進(jìn)一步對界面進(jìn)行微調(diào),也可以選擇控件點擊左鍵,于是會出來上下文菜單來操作控件的屬性。到于右邊的操作界面,上部分則是整個界面構(gòu)成的樹形結(jié)構(gòu),而下部分則是當(dāng)我們選擇了某個界面元素時,會顯示上下文的屬性。最后,我們還可以在底部的Graphic Layout與main.xml進(jìn)行圖形操作界面與源代碼編輯兩種操作方式的切換。 有了這種工具,就有可能實現(xiàn)設(shè)計師與工程師合作來構(gòu)建出美觀與交互性更好的Android應(yīng)用程序。但可惜的是,Android的這套UI設(shè)計工具,太過于編程化,而且由于版本變動頻繁的原因,非常復(fù)雜化,一般設(shè)計師可能也不太容易學(xué)好。更重要的一點,Android存在碎片化,屏幕尺寸與顯示精度差異性非常大,使實現(xiàn)像素級精度的界面有技術(shù)上的困難。也這是Android上應(yīng)用程序不如iOS上漂亮的原因之一。但這種設(shè)計至少也增強(qiáng)了界面上的可設(shè)計性,使Android應(yīng)用程序在觀感上也有不俗表現(xiàn)。 我們可以再回過頭來看看應(yīng)用程序工程里的res目錄,res目錄里包含了Android應(yīng)用程序里的可使用的資源,而資源文件本身是可以索引的,比如layout會引用drawable與values里的資源。對于我們例子里使用的<TextView … android:text="Hello World,Helloworld" …>,我們可以使用資源來進(jìn)行引用,<TextView …android:text=” @string/hello_string” …>,然后在res/values/strings.xml里加入hello_string的定義。
從通過這種方式,我們可以看另外一些特點,就是Android應(yīng)用程序在多界面、多環(huán)境下的自適應(yīng)性。對于上面的字符串修改的例子,我們?nèi)绻裣旅娴氖纠h(huán)境那樣定義了res/layout-zh/strings.xml,并提供hello_string的定義:
最后,得到的應(yīng)用程序,在英文環(huán)境里會顯示‘Hello world!’,而如果系統(tǒng)當(dāng)前的語言環(huán)境是中文的話,就會顯示成‘歡迎使用!’。這種自適應(yīng)方式,則是不需要我們進(jìn)行編程的,系統(tǒng)會自動完成對這些顯示屬性的適配。 當(dāng)然,這時可能會有人提疑問,如果這時是韓文或是日文環(huán)境會出現(xiàn)什么情況呢?在Android里,如果不能完成相應(yīng)的適配,就會使用默認(rèn)值,比如即使是我們創(chuàng)建了res/values-zh/strings.xml資源,在資源沒有定義我們需要使用的字符串,這時會使用英文顯示。不管如何,Android提供自適應(yīng)顯示效果,但也保證總是不是會出錯。 這些也體現(xiàn)出,一旦Android應(yīng)用程序?qū)懗鰜?,如果對多語言環(huán)境不滿意,這時,我們完全可以把.apk按zip格式解開,然后加入新的資源文件定義,再把文件重新打包,也就達(dá)到了我們可能會需要的漢化的效果。
在res目錄里,我們看到,對于同一種類型的資源,比如drawable、values,都可以在后面加一個后綴,像mdpi,hdpi, ldpi是用于適配分辨率的,zh是用來適配語言環(huán)境的,large則是用來適配屏幕大小的。對于這種顯示上的自適應(yīng)需求,我們可以直接在Eclipse里通過創(chuàng)建Android XML文件里得到相應(yīng)的提示,也可以參考http://developer./training/basics/supporting-devices/查看具體的使用方式。 于是,透過資源文件,我們進(jìn)一步驗證了我們對于Android MVC的猜想,在Android應(yīng)用程序設(shè)計里,也跟iOS類似,可以實現(xiàn)界面與邏輯完全分離。而另一點,就是Android應(yīng)用程序天然具備屏幕自適應(yīng)的能力,這一方面帶來的影響是Android應(yīng)用程序天生具備很強(qiáng)的適應(yīng)性,另一方面的影響是Android里實現(xiàn)像素精度顯示的應(yīng)用程序是比較困難的,維護(hù)的代價很高。 我們可以再通過應(yīng)用程序的代碼部分來看看應(yīng)用程序是如何將顯示與邏輯進(jìn)行綁定的。 2.5 Java編程(src/org/lianlab/hello/HelloWorld.java)在Android編程里,實現(xiàn)應(yīng)用程序的執(zhí)行邏輯,幾乎就是純粹的Java編程。但在編程上,由于Android的特殊性,這種Java編程也還是被定制過的。 我們看到我們例子里的源代碼,如果寫過Java代碼,看到這樣的源代碼存放方式,就可以了解到Android為什么被稱為Java操作系統(tǒng)的原因了,像這種方式,就是標(biāo)準(zhǔn)的Java編程了。事實上,在Android的代碼被轉(zhuǎn)義成Dalvik代碼之前,Android編程都可被看成標(biāo)準(zhǔn)的Java編程。我們來看這個HelloWorld.java的源代碼。
代碼結(jié)構(gòu)很簡單,我們所謂的HelloWorld,就是繼承了Activity的基類,然后再覆蓋了Acitivity基于的onCreate()方法。 Activity類,是Android系統(tǒng)設(shè)計思路里的很重要的一部分,所有與界面交互相關(guān)的操作類都是Activity,是MVC框架里的Controller部分。那Model部分由誰來提供呢?這是由Android系統(tǒng)層,也就是Framework來提供的功能。當(dāng)界面失去焦點時,當(dāng)界面完全變得不可見時,這些都屬于Framework層才會知道的狀態(tài),F(xiàn)ramework會記錄下這些狀態(tài)變更的信息,然后再回調(diào)到Activity類提供的相應(yīng)狀態(tài)的回調(diào)方法。關(guān)于Activity我們后面來詳細(xì)說明,而見到Activity類的最簡單構(gòu)成,我們大體上就可以形成Android世界里的完整MVC框架構(gòu)成完整印象了。 我們繼承了Activity類之后,就會覆蓋其onCreate()回調(diào)方法。這里我們使用了”@Override”標(biāo)識,這是一種Java語言里的Annotation(代碼注釋)技術(shù),相當(dāng)于C語言里的pragma,用于告訴編譯器一些相應(yīng)參數(shù)。我們的Override則告訴javac編譯器,下面的方法在構(gòu)建對象時會覆蓋掉父類方法,從而提高構(gòu)建效率。Activity類里提供的onXXX()系列的都可以使用這種方法進(jìn)行覆蓋,從而來實現(xiàn)自定義的方法,切入到Android應(yīng)用程序不同狀態(tài)下的自定義實現(xiàn)。 我們覆蓋掉的onCreate()方法,使用了一個參數(shù),savedInstanceState,這個參數(shù)的類型是Bundle。Bundle是構(gòu)建在Android的Binder IPC之上的一種特殊數(shù)據(jù)結(jié)構(gòu),用于實現(xiàn)普通Java代碼里的Serialization/Deserializaiton功能,序列化與反序列化功能。在Java代碼里,我們?nèi)绻枰4嬉恍?yīng)用程序的上下文,如果是字符串或是數(shù)據(jù)值等原始類型,則可以直接寫到文件里,下次執(zhí)行時再把它讀出來就可以了。但假設(shè)我們需要保存的是一個對象,比如是界面的某個狀態(tài)點,像下面的這樣的數(shù)據(jù)結(jié)構(gòu):
我們按這種類似的格式寫到文件里,當(dāng)再讀取出來時,我們就可以新建一個ViewState對象,再使用這些保存過的值對這一對象進(jìn)行初始化。這樣就可以實現(xiàn)對象的保存與恢復(fù),這是我們onCreate()方法里使用Bundle做序列化操作的主要目的,我們的Activity會有不同生存周期,當(dāng)我們有可能需要在進(jìn)程退出后再次恢復(fù)現(xiàn)象時,我們就會在退出前將上下文環(huán)境保存到一個onSavedInstance的Bundle對象里,而在onCreate()將顯示的上下文恢復(fù)成退出時的狀態(tài)。 而另一個必須要使用Bundle的理由是,我們的Activity與實現(xiàn)Activity管理的Framework功能部件ActivityManager,是構(gòu)建在不同進(jìn)程空間上的,Activity將運行在自己獨立的進(jìn)程空間里,而Framework則是運行在另一個系統(tǒng)級進(jìn)程SystemServer之上。我們的Bundle是一種進(jìn)行過序列化操作的對象,于是相應(yīng)的操作是系統(tǒng)進(jìn)程會觸發(fā)Activity的進(jìn)行onCreate()回調(diào)操作,而同時會轉(zhuǎn)回一個上下文環(huán)境的Bundle,可將Activity恢復(fù)到系統(tǒng)指定的某種圖形界面狀態(tài)。Bundle也可能為空,比如Activity是第一個被啟動的情況下,這個空的onSavedInstance則會被忽略掉。 我們進(jìn)入到onCreate()方法之后,第一行便是
從字面上看,這種方式相當(dāng)于我們繼承了父類方法,然后又回調(diào)到父類的onCreate()來進(jìn)行處理。這種方式貌似很怪,但這是設(shè)計模式(Design Pattern)里鼎鼎大名的一種,叫IoC ( Inversion of Control)。通過這樣的設(shè)計模式,我們可以同時提供可維護(hù)性與可調(diào)試性,我們可以在通過覆蓋的方法提供功能更豐富的子類,實際上每次調(diào)用子類的onCreate()方法,都將調(diào)用到各個Activity拓展類的onCreate()方法。而這個方法一旦進(jìn)入,又會回調(diào)到父類的onCreate()方法,在父類的onCreate()方法里,我們可以提供更多針對諸多子類的通用功能(比如啟動時顯示的上下文狀態(tài)的恢復(fù),關(guān)閉時一些清理性工作),以及在這里面插入調(diào)試代碼。 然后,我們可以加載顯示部分的代碼的UI,
這一行,就會使我們想要顯示的圖形界面被輸出到屏幕上。我們可以隨意地修改我們的main.xml文件,從而使setContentView()之后顯示出來的內(nèi)容隨之發(fā)生變化。當(dāng)然,作為XML的UI,最終是在內(nèi)存里構(gòu)成的樹形結(jié)構(gòu),我們也可以在調(diào)用setContentView()之前通過編程來修改這個樹形結(jié)構(gòu),于是也可以改變顯示效果。 到目前為止,我們也只是實現(xiàn)了將內(nèi)容顯示到屏幕上,而沒有實現(xiàn)交互的功能。如果要實現(xiàn)交互的功能,我們也只需要很簡單的代碼就可以做到,我們可以將HelloWorld.java改成如下的內(nèi)容,從而使用我們的”Hello world”字符串可以響應(yīng)點擊事件:
我們使用Activity類的findViewById()方法,則可以找到任何被R.java所索引起來的資源定義。我們在這里使用了R.id.textView1作為參數(shù),是因為我們在main.xml就是這么定義TextView標(biāo)簽的:android:id="@+id/textView1"。 而我們找到字段之后,會調(diào)用TextView對象的setOnClickListener()方法,給TextView注冊一個onClickListener對象。這樣的對象,是我們在Android世界里遇到的第二次設(shè)計模式的使用(事實上Android的實現(xiàn)幾乎使用到所有的Java世界里的通用設(shè)計模式),Listener本身也會被作為Observer設(shè)計模式的一種別稱,主要是用于實現(xiàn)被動調(diào)用邏輯,比如事件回饋。 Observer(Listener)設(shè)計模式的思路,跟我們數(shù)據(jù)庫里使用到的Trigger功能類似,我們可對需要跟蹤的數(shù)據(jù)操作設(shè)置一個Trigger,當(dāng)這類數(shù)據(jù)操作進(jìn)行時,就會觸發(fā)數(shù)據(jù)庫自動地執(zhí)行某些操作代碼。而Observer(Listener)模式也是類似的,監(jiān)聽端通過注冊O(shè)bserver來處理事件的回調(diào),而真正的事件觸發(fā)者則是Observer,它的工作就是循環(huán)監(jiān)聽事件,然后再調(diào)用相應(yīng)監(jiān)聽端的回調(diào)。 這樣的設(shè)計,跟效率沒有必然聯(lián)系,太可以更大程度地降低設(shè)計上的復(fù)雜度,同時提高設(shè)計的靈活性。一般Observer作為接口類,被監(jiān)聽則會定位成具體的Subject,真正的事件處理,則是通過實現(xiàn)某個Observer接口來實現(xiàn)的。對于固定的事件,Subject對象與Observer接口是無須變動的,而Observer的具體實現(xiàn)則可以很靈活地被改變與擴(kuò)充。如下圖所示:
如果我們對于監(jiān)聽事件部分的處理,也希望能加入這樣的靈活性,于是我們可以繼續(xù)抽象,將Subject泛化成一個Observable接口,然后可以再提供不同的Observable接口的實現(xiàn)來設(shè)計相應(yīng)的事件觸發(fā)端。
針對于我們的Android里的OnClickListener對象,則是什么情況呢?其實不光是OnClickListener,在Android里所有的事件回調(diào),都有類似于Observer的設(shè)計技巧,這樣的回調(diào)有OnLongClickListener,OnTouchListener,OnKeyListener,OnContextMenuListener,以及OnSetOnFocusChangeListener等。但Android在使用設(shè)計模式時很簡潔,并不過大地提供靈活性,這樣可以保證性能,也可以減小出錯的概率(基本上所有的設(shè)計復(fù)雜到難以理解的系統(tǒng),可維護(hù)性遠(yuǎn)比簡單易懂但設(shè)計粗糙的系統(tǒng)更差,因為大部分情況下人的智商也是有限的資源)。于是,從OnClickLister的角度,我們可以得到下圖所示的對象結(jié)構(gòu)。
Click事件的觸發(fā)源是Touch事件,而當(dāng)前View的Touch事件在屬于點擊事件的情況下,會生成一個performClick的Runnable對象(可交由Thread對象來運行其run()回調(diào)方法)。在這個Runnable對象的run()方法里會調(diào)用注冊過的OnClickListener對象的OnClick()方法,也就是圖示中的mOnClickListener::onClick()。當(dāng)這個對象被post()操作發(fā)送到主線程時(作為Message發(fā)送給UI線程的Hander進(jìn)行處理),我們覆蓋過的OnClick()回調(diào)方法就由主線程執(zhí)行到了。 我們注冊的Click處理,只有簡單的一行,finish(),也就是通過點擊事件,我們會將當(dāng)前的Activity關(guān)閉掉。如果我們覺得這樣不過癮,我們也可通過這次點擊觸發(fā)另一個界面的執(zhí)行,比如直接搜索這個字符串。這樣的改動代碼量很小,首先,我們需要在HelloWorld.java的頭部引入所需要的Java包,
然后可以將我們的OnClick()方法改寫成啟動一個搜索的網(wǎng)頁界面,查找這個字符串,而當(dāng)前界面的Activity則退出。這時,我們新的OnClick()方法則會變成這個樣子:
但是可能還是無法解決我們對于Android應(yīng)用程序與Java環(huán)境的區(qū)別的疑問: T Android有所謂的MVC,將代碼與顯示處理分享,但這并非是標(biāo)準(zhǔn)Java虛擬機(jī)環(huán)境做不到。一些J2EE的軟件框架也有類似的特征。 T AndroidManifest.xml與On*系列回調(diào),這樣的機(jī)制在JAVA ME也有,JAVA ME也是使用類似的機(jī)制來運行的,難道Android是JAVA ME的加強(qiáng)版? T 至于Listener模式的使用,眾所周知,Java是幾乎所有高級設(shè)計模式的實驗田,早就在使用Listener這樣模式在處理輸入處理。唯一不同的是ClickListener,難道Android也像是可愛的觸摸版Ubuntu手機(jī)一樣,只在是桌面Java界面的基礎(chǔ)加入了觸摸支持? T Activity從目前的使用上看,不就是窗口(Window)嗎?Android開發(fā)者本就有喜歡取些古怪名字的嗜好,是不是他們只是標(biāo)新立異地取了個Activity的名字? 對于類似這樣的疑問,則是從代碼層面看不清楚了,我們得回歸到Android的設(shè)計思想這一層面來分析,Android應(yīng)用程序的執(zhí)行環(huán)境是如何與眾不同的。 不過,我們可以從最后的那行Activity調(diào)用另一個Activity的例子里看出一些端倪,在這次調(diào)用里,我們并沒有顯式地創(chuàng)建新的Activity,如果從代碼直接去猜含義的話,我們只是發(fā)出了個執(zhí)行某種操作的請求,而這個請求并沒有指定有誰來完成。這就是Android編程思想的基礎(chǔ),一種全開放的“無界化”編程模型。 |
|