|
本文內容,主題是透過應用程序來分析Android系統的設計原理與構架。我們先會簡單介紹一下Android裡的應用程序編程,然後以這些應用程序在運行環境上的需求來分析出,為什麼我們的Android系統需要今天這樣的設計方案,這樣的設計會有怎樣的意義, Android究竟是基於怎樣的考慮才變成今天的這個樣子,所以本文更多的分析Android應用程序設計背後的思想,品味良好架構設計的魅力。分五次連載完成,第一部分是最簡單的部分,解析Android應用程序的開發流程。
特別聲明:本系列文章LiAnLab.org著作權所有,轉載請註明出處。作者系LiAnLab.org資深Android技術顧問吳赫老師。本系列文章交流與討論:@宋寶華Barry
1. Android應用程序在目前Android大紅大紫的情況下,很多人對編寫Android應用程序已經有了足夠深入的瞭解。即便是沒有充分的認識,在現在Android手機已經相當普及的情況下,大家至少也會知道Android的應用程序會是一個以.apk為後綴名的文件(在Windows系統裡,還會是一個帶可愛機器人圖標的文件)。那這個apk包又有什麼樣的含義呢?
如果您正在使用Linux操作系統,可以使用命令file命令來查看這一文件的類型。比如我們下載了一個Sample.apk的文件,則使用下面的命令:- $file Sample.apk
- Sample.apk: Zip archive data, at least v1.0 to extract
複製代碼 $file Sample.apkSample.apk: Zip archive data, at least v1.0 to extract對,沒有看錯,只一個簡單的zip文件。要是做過Java開發的人,可以對這種格式很親切,因為傳說中的.jar、.war格式,都是Zip壓縮格式的文件。我們可繼續使用unzip命令將這一文件解壓(或是任何的解壓工具,zip是人類歷史是最會古老最為普及的壓縮格式之一,幾乎所有壓縮工具都支持)。通過解壓,我們就得到了下面的文件內容:- AndroidManifest.xml,
- classes.dex,
- resources.arsc,
- META-INF,
- res,
複製代碼 到這裡,我們就可以看到一個Android應用程序結構其實是異常簡單的。這五部分內容(其中META-INF和res是目錄,其他是文件)除了META-INF是這一.apk文件的校驗信息,resources.arsc是資源的索引文件,其他三部分則構成了Android應用程序的全部。
T AndroidManifest.xml,這是每個Android應用程序包的配置文件,這裡會保存應用程序名字、作者、所實現的功能、以及一些權限驗證信息。但很可惜,在編譯完成的.apk文件裡,這些文件都被編譯成了二進製版本,我們暫時沒有辦法看到內容,後面我們可以再看看具體的內容。
T classes.dex,這則是Android應用程序實現的邏輯部分,也就是通過Java編程寫出來而被編譯過的代碼。這種特殊的格式,是Android裡特定可執行格式,是可由Dalvik虛擬機所執行的代碼,這部分內容我們也會在後續的介紹Dalvik虛擬機的章節裡介紹。
T res,這一目錄裡則保存了Android所有圖形界面設計相關的內容,比如界面應該長成什麼樣子、支持哪些語言顯示等等。
從一個android應用程序的包文件內容,我們可以看到android應用程序的特點,這也是Android編程上的一些特徵:
1 簡單:最終生成的結果是如些簡單的三種組成,則他們的編程上也不會有太大的困難性。這並不是說Android系統裡無法實現很複雜的應用程序,事實上Android系統擁有世界上僅次於iOS的應用程序生態環境,也擁有複雜的辦公軟件、大型3D遊戲。而只是說,如果要實現和構成同樣的邏輯,它必然會擁有其他格式混雜的系統更簡化的編程模式。
2 Java操作系統:既然我們編譯得到的結果,classes.dex文件,是用於Java虛擬機(雖然是Dalvik虛擬機,但實際上這一虛擬機只是一種特定的Java解析器和虛擬機執行環境 )解析執行的,於是我們也可以猜想到,我們的Android系統,必然是一個Java操作系統。我們在後面會解釋,如果把Android系統直接看成Linux內核和Java語言組合到一起的操作系統很不準確,但事實上Android,也還是Java操作系統,Java是唯一的系統入口。
使用MVC設計模式:所謂的MVC,就是Model,View,Controller的首字母組合起來的一種設計模式,主要思想就是把顯示與邏輯實現分離。Model用於保存上下文狀態、View用於顯示、而Controller則是用於處理用戶交互。三者之間有著如下圖所示的交互模型,交互只到Controller,而顯示更新只通過View進行,這兩者再與Model交換界面狀態信息:
在現代的圖形交互相關的設計裡,MVC幾乎是在圖形交互處理上的不二選擇,這樣系統設計包括一些J2EE的應用服務器框架,最受歡迎的Firefox瀏覽器,iOS,MacOSX等等。這些使用MVC模式的最顯著特點就是顯示與邏輯分離,在Android應用程序裡我們看到了用於邏輯實現的classes.dex,也看到用於顯示的res,於是我們也可以猜想到在UI上便肯定會使用MVC設計模式。
當然,所謂的Android應用程序編程,不會只有這些內容。到目前為止,我們也只是分析.apk文件,於是我們可以回過頭來看看Android應用被編譯出來的過程。
2. Android編程從編程角度來說,Android應用程序編程幾乎只與Java相關,而Java平台本身是出了名跨平台利器,理論上來說,所有Java環境裡使用的編程工具、IDE工具,皆可用於Android的編程。Android SDK環境裡提供的編程工具,是基於標準的Java編譯工具ant的,但事實上,一些大型的Android軟件工程,更傾向於使用Maven這樣的並行化編譯工具(maven.apache.org)。如果以前有過Java編程經驗,會知道Java環境裡的圖形化IDE(Integrated Development Environment)工具,並非只有Eclipse一種,實際上Java的官方IDE是NetBeans,而商用化的Java大型項目開發者,也可能會比較鍾意於使用IntelliJ,而從底層開發角度來說,可能使用vim是更合適的選擇,可以靈活地在C/C++與Java代碼之間進行切換。總而言之,幾乎所有的Java環境的編程工具都可以用於Android編程。
對於這些工具呢,熟悉工具的使用是件好事,所謂「磨刀不誤砍柴工」,為將來提升效率,這是件好事。但是要磨刀過多,柴沒砍著,轉型成「磨刀工」了。如果過多地在這些編程工具上糾結嘗試,反而忽視了所編代碼的本身,這倒會捨本逐末。
我們既然是研究Android編程,這時僅說明兩種Android官方提供的編程方法:使用Android SDK工具包編程,或是使用Eclipse + ADT插件編程。
2.1 使用Android SDK工具包在Android開發過程中,如果Eclipse環境不可得的情況下,可以直接使用SDK來創建應用程序工程。首先需要安裝某一個版本的Android SDK開發包,這個工具包可以到http://developer.android.com/sdk/index.html這個網址去下載,根據開發所用的主機是Windows、Linux還是MacOS X(MacOS僅支持Intel芯片,不支持之前的PowerPC芯片),下載對應的.zip文件,比如android-sdk_r19-linux.zip。下載完成後,解壓到一個固定的目錄,我們這裡假定是通過環境變量$ANDROID_SDK_PATH指定的目錄。
下載的SDK包,默認是沒有Android開發環境支持的,需要通過tools目錄裡的一個android工具來下載相應的SDK版本以用於開發。我們通過運行$ANDROID_SDK_PATH/tools/android會得到如下的界面: 在上面的安裝界面裡選擇不同的開發工具包,其中Tools裡包含一些開發用的工具,如我們的SDK包,實際上也會在這一界面裡進行更新。而對於不同的Android版本,1.5到4.1,我們必須選擇下載某個SDK版本來進行開發。而下載完之後的版本信息,我們既可以在這一圖形界面裡看到,也可以通過命令行來查看。- $ANDROID_SDK_PATH/tools/android list targets
- id: 1 or "android-16"
- Name: Android 4.1
- Type: Platform
- API level: 16
- Revision: 1
- Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in
- ABIs : armeabi-v7a
- ----------
- id: 2 or "Google Inc.:Google APIs:16"
- Name: Google APIs
- Type: Add-On
- Vendor: Google Inc.
- Revision: 1
- Description: Android + Google APIs
- Based on Android 4.1 (API level 16)
- Libraries:
- * com.google.android.media.effects (effects.jar)
- Collection of video effects
- * com.android.future.usb.accessory (usb.jar)
- API for USB Accessories
- * com.google.android.maps (maps.jar)
- API for Google Maps
- Skins: WVGA854, WQVGA400, WSVGA, WXGA800-7in, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800
- ABIs : armeabi-v7a
複製代碼 通過android list targets列出來的信息,可以用於後續的開發之用,比如對於不同的target,最後得到了id:1、id:2這樣的信息,則可以被用於應用程序工程的創建。而細心一點的讀者會看到同一個4.1版本的SDK,實際可分為」android-16」和"Google Inc.:Google APIs:16",這樣的分界也還有有意義的,」android-16」用於「純」的android 4.1版的應用程序開發,而「Google Inc.:Google APIs:16」則加入了Google的開發包。
配置好環境之後,如果我們需要創建Android應用程序。tools/android這個工具,同時也具備可以創建Android應用程序工程的能力。我們輸入:- $ANDROID_SDK_PATH/tools/android create project -n Hello -t 1 -k org.lianlab.hello -a Helloworld -p hello
複製代碼 這樣我們就在hello目錄裡創建了一個Android的應用程序,名字是Hello,使用API16(Android 4.1的API版本),包名是org.lianlab.hello,而默認會被執行到的Activity,會是叫Helloworld的Activity類。
掌握Android工具的一些使用方法也是有意義的,比如當我們的Eclipse工程被破壞的情況下,我們依然可以手工修復這一Android應用程序工程。或是需要修改該工程的API版本的話,可以使用下面的命令:
$ANDROID_SDK_PATH/tools/android updateproject -t 2 -p .
在這個工程裡,如果我們不加任何修改,會生成一個應用程序,這個應用程序運行的效果是生成一個黑色的圖形界面,打印出一行"Hello World, Helloworld"。如果我們需要對這一工程進行編譯等操作的話,剩下的事情就屬於標準的Java編譯了,標準的Java編譯,使用的是ant(ant.apache.org)編譯工具。我們先改變當前目錄到hello,然後就可以通過」 ant –projecthelp」來查看可以被執行的Android編譯工程,- $ ant -projecthelp
- Buildfile: /Users/wuhe/android/workspace/NotePad/bin/tmp/hello/build.xml
- Main targets:
- clean Removes output files created by other targets.
- debug Builds the application and signs it with a debug key.
- install Installs the newly build package. Must be used in conjunction with a build target (debug/release/instrument). If the application was previously installed, the application is reinstalled if the signature matches.
- installd Installs (only) the debug package.
- installi Installs (only) the instrumented package.
- installr Installs (only) the release package.
- installt Installs (only) the test and tested packages.
- instrument Builds an instrumented packaged.
- release Builds the application in release mode.
- test Runs tests from the package defined in test.package property
- uninstall Uninstalls the application from a running emulator or device.
- Default target: help
複製代碼 但如果只是編譯,我們可以使用antdebug生成Debug的.apk文件,這時生成的文件,會被放到bin/Hello-debug.apk。此時生成的Hello-debug.apk,已經直接可以安裝到Android設備上進行測試運行。我們也可以使用ant release來生成一個bin/Hello-release-unsigned.apk,而這時的.apk文件,則需要通過jarsigner對文件進行驗證才能進行安裝。
通過antdebug這一編譯腳本,我們可以看到詳細的編譯過程。我們可以看到,一個Android的工程,最後會是通過如圖所示的方式生成最後的.apk文件。
把一個Android的源代碼工程編譯成.apk的Android應用程序,其過程如下:
1) 所有的資源文件,都會被aapt進行處理。所有的XML文件,都會被aapt解析成二進制格式,準確地說,這樣的二進制格式,是可以被直接映射到內存裡的二進制樹。做過XML相關開發的工程師,都會知道,XML的驗證與解析是非常消耗時間與內存的,而通過編譯時進行XML解析,則節省了運行時的開銷。當然解析的結果最後會被aapt通過一個R.java保存一個二進制樹的索引,編程時可通過這個R.java文件進行XML的訪問。aapt會處理所有的資源文件,也就是Java代碼之外的任何靜態性文件,這樣處理既保證了資源文件間的互相索引得到了驗證,也確保了R.java可以索引到這個應用程序裡所有的資源。
2) 所有的Java文件,都會被JDK裡的javac工具編譯成bin目錄下按源代碼包結構組織的.class文件(.class是標準的Java可解析執行的格式),比如我們這個例子裡生成的bin/classes/org/lianlab/hello/*.class文件。然後這些文件,會通過SDK裡提供的一個dx工具轉換成classes.dex文件。這一文件,就是會被Dalvik虛擬機所解析執行的
3) 最後我們得到的編譯過的二進制資源文件和classes.dex可執行文件,會通過一個apkbuilder工具,通過zip壓縮算法打包到一個文件裡,生成了我們所常見的.apk文件。
4) 最後,.apk文件,會通過jarsigner工具進行校驗,這一校驗值會需要一個數字簽名。如果我們申請了Android開發者帳號,這一數字簽名就是Android所分發的那個數字證書;如果沒有,我們則使用debug模式,使用本地生成的一個隨機的數字證書,這一文件位於~/.android/debug.keystore。
雖然我們只是下載了SDK,通過一行腳本創建了Android應用程序工程,通過另一行完成了編譯。但也許還是會被認為過於麻煩,因為需要進行字符界面的操作,而且這種開發方式也不是常用的方式,在Java環境下,我們有Eclipse可用。我們可以使用Eclipse的圖形化開發工具,配合ADT插件使用。
2.2 使用Eclipse+ADT插件在Android環境裡可以使用Java世界裡幾乎一切的開發工具,比如NetBeans等,但Eclipse是Android官方標準的開發方式。使用Eclipse開發,前面提到的開發所需SDK版本下載,也是必須的,然後還需要在Eclipse環境裡加裝ADT插件,Android Development Toolkit。
我們在Eclipse的菜單裡,選擇」Help」 a 「Install New Software…」,然後在彈出的對話框裡的Workwith:輸入ADT的發佈地址:https://dl-ssl.google.com/android.eclipse,回車,則會得到下面的軟件列表。選擇Select All,將這些插件全都裝上,則得到了可用於Android應用程序開發的環境。
這裡還需要指定SDK的地址,Windows或是Linux裡,會是在菜單「Window」 a 「Preferences」,在MacOS裡,則會是」Eclipse」 a「Preferences」 。在彈出的對話框裡,選擇Android,然後填入Android SDK所保存的位置。
點擊OK之後,則可以進行Android開發了。選擇」File」 a 「New」a 「Project」 a 「Android」,在Eclipse 3.x版本裡,會是「Android Project」,在Eclipse 4.x版本裡,會是「Android Application Project」。如果我們需要創建跟前面字符界面下一模一樣的應用程序工程,則在彈出的創建應用程序對話框裡填入如下的內容:

然後我們選擇Next,一直到彈出最後界面提示,讓我們選擇默認Activity的名字,最後點擊」Finish」,我們就得到一個Android應用程序工程,同時在Eclipse環境裡,我們既可以通過圖形化界面編輯Java代碼,也可以通過圖形化的界面編輯工具來繪製圖形界面。
(注意: 如果Android工程本身比較龐大,則最好將Eclipse裡的內存相關的配置改大。在Windows和Linux裡,是修改eclipse裡的eclipse.ini文件,而在MacOS裡,則是修改Eclipse.app/Contents/MacOS/eclipse.ini。一般會將如下的值加大成兩倍:- --launcher.XXMaxPermSize
- 512m
- -vmargs
- -Xms80m
- -Xmx1024m
- )
複製代碼 我們得到工程目錄,在Eclipse環境裡會是如下圖所示的組織方式。代碼雖然是使用一模一樣的編譯方式,唯一的改變是,我們不再需要使用腳本來完成編譯,我們可以直接使用Eclipse的」Project」a「Build project」來完成編譯過程。如果我們使用默認設置,則代碼是使用自動編譯的,我們的每次修改都會觸發增量式的編譯。
我們從這些android編程的過程,看不出來android跟別的Java編程模式有什麼區別,倒至少驗證了我們前面對android編程特點的猜想,就是很簡單。如果同樣我們使用Eclipse開發Java的圖形界面程序,需要大量地時間去熟悉API,而在Android這裡學習的曲線被大大降低,如果我們只是要畫幾個界面,建立起簡單的交互,我們幾乎無須學習編程。
而從上面的步驟,我們大概也可以得到Android開發的另一個好處,就是極大的跨平台性,它的開發流程裡除了JDK沒有提及任何的第三方環境需求,於是這樣的開發環境,肯定可以在各種不同的平台執行。這也是Android上進行開發的好處之一,跨平台,支持Windows,Linux與MacOS三種。
我們再來看一個,我們剛才創建的這個工程裡,我們怎麼樣進行下一步的開發。在Android開發裡,決定我們應用程序表現的,也就是我們從一個.apk文件裡看到的,我們實際上只需要:
T 修改AndroidManifest.xml文件。AndroidManifest.xml是Android應用程序的主控文件,類型於Windows裡的註冊表,我們通過它來配置我們的應用程序與系統相關的一些屬性。
T 修改UI顯示。在Android世界裡,我們可以把UI編程與Java編程分開對待,處理UI控件的語言,我們可以叫它UI語言,或是layout語言,因為它們總是以layout類型的資源文件作為主入口的。Android編程裡嚴格地貫徹MVC的設計思路,使我們得到了一個好處,就是我們的UI跟要實現的邏輯沒有任何必然聯繫,我們可先去調整好UI顯示,而UI顯示後台的實現邏輯,則可以在後續的步驟裡完成。
T 改寫處理邏輯的Java代碼。也就是我們MVC裡的Controller與Model部分,這些部分的內容,如果與UI沒有直接交互,則我們可以放心大膽的改寫,存在交互的部分,比如處理按鈕的點擊,取回輸入框裡的文字等。作為一個定位於拓展能力要求最高的智能手機操作系統,android肯定不會只實現畫畫界面而已,會有強大的可開發能力,在android系統裡,我們可以開發企業級應用,大型遊戲,以及完整的Office應用。
無論是通過tools/android工具生成的Android源代碼目錄,還是通過Eclipse來生成的Android源代碼工程,都需要進一步去自定義這個步驟來完成一個Android應用程序。當然,還有一種特殊的情況就是,這個源代碼工程並非直接是一個Android應用程序,只是Unit Test工程或是庫文件工作,並不直接使用.apk文件,這裡則可能後續的編程工作會變得不同。我們這裡是分析Android應用程序,於是後面分別來看應用程序編程裡的這三部分的工作如何進行。
2.3 AndroidManifest.xml先來看看AndroidManifest.xml文件,一般出於方便,這一文件有可能也被稱為manifest文件。像我們前面的例子裡的創建的Android工程,得到的AndroidManifest.xml文件就很簡單:- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="org.lianlab.hello"
- android:versionCode="1"
- android:versionName="1.0">
- <application android:label="@string/app_name">
- <activity android:name=".Helloworld"
- android:label="@string/app_name">
- <intent-filter>
- <action android:name="android.intent.action.MAIN" />
- <category android:name="android.intent.category.LAUNCHER" />
- </intent-filter>
- </activity>
- </application>
- </manifest>
複製代碼 作為xml文件,所有的<>都會是成對的,比如我們看到的<manifest></manifest>,這被稱為標籤(Tag)。標籤可以包含子標籤,從而可以形成樹型的結點關係。如果沒有子標籤,則我們也可以使用</>來進行標識,比如我們上面看到的<actionandroid:name=」android.intent.action.MAIN」 />。
<manifest>,是主標籤,每個文件只會有一個,這是定義該應用程序屬性的主入口,包含應用程序的一切信息,比如我們的例子裡定義了xml的命名空間,這個應用程序的包名,以及版本信息。
<application>,則是用於定義應用程序屬性的標籤,理論上可以有多個,但多個不具有意義,一般我們一個應用程序只會有一個,在這個標籤裡我們可以定義圖標,應用程序顯示出來的名字等。在這一標籤裡定義的屬性一般也只是輔助性的。
<activity>,這是用來定義界面交互的信息。我們在稍後一點的內容介紹Android編程細節時會描述到這些信息,這一標籤裡的屬性定義會決定應用程序可顯示效果。比如在啟動界面裡的顯示出來的名字,使用什麼樣的圖標等。
<intent-filter>,這一標籤則用來控制應用程序的能力的,比如該圖形界面可以完成什麼樣的功能。我們這裡的處理比較簡單,我們只是能夠讓這個應用程序的HelloWorld可以被支持點擊到執行。
從這個最簡單的AndroidManifest.xml文件裡,我們可以看到Android執行的另一個特點,就是可配置性強。它跟別的編程模型很不一樣的地方是,它沒有編程式規定的main()函數或是方法,而應用程序的表現出來的形態,完全取決於<activity>字段是如何定義它的。
2.4 圖形界面(res/layout/main.xml)我們可以再來看android的UI構成。UI也是基於XML的,是通過一種layout的資源引入到系統裡的。在我們前面看到的最簡單的例子裡,我們會得到的圖形界面是res/layout/main.xml,在這一文件裡,我們會看到打開顯示,並在顯示區域裡打印出Hello World, Helloworld。- <?xml version="1.0" encoding="utf-8"?>
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent" >
- <TextView
- android:id="@+id/textView1"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerInParent="true"
- android:padding="@dimen/padding_medium"
- android:text="@string/hello_world"
- tools:context=".HelloWorld" />
- </RelativeLayout>
複製代碼 在這個圖形界面的示例裡,我們可以看到,這樣的圖形編程方式,比如傳統的方式裡要學習大量的API要方便得多。
<LinearLayout>,這一標籤會決定應用程序如何在界面裡擺放相應的控件
<TextView>,則是用於顯示字符串的圖形控件
使用這種XML構成的UI界面,是MVC設計的附屬產品,但更大的好處是,有了標準化的XML結構,就可以創建可以用來畫界面的IDE工具。一流的系統提供工具,讓設計師來設計界面、工程師來邏輯,這樣生產出來的軟件產品顯示效果與用戶體驗會更佳,比如iOS;二流的系統,界面與邏輯都由工程師來完成,在這種系統上開發出來的軟件,不光界面不好看,用戶體驗也會不好。我們比如在Eclipse裡的工程裡查看,我們會發現,我們打開res/layout/main.xml,會自動彈出來下面的窗口,讓我們有機會使圖形工具來操作界面。
在上面IDE工具裡,左邊是控件列表,中間是進行繪製的工作區,右邊會是控件一些微調窗口。一般我們可以從左邊控制列表裡選擇合適的控件,拖到中間的工作區來組織界面,原則上的順序是layout a 復合控件 a 簡單控件。中間區域可以上面還有選擇項用於控制顯示屬性,在工作區域裡我們可以進一步對界面進行微調,也可以選擇控件點擊左鍵,於是會出來上下文菜單來操作控件的屬性。到於右邊的操作界面,上部分則是整個界面構成的樹形結構,而下部分則是當我們選擇了某個界面元素時,會顯示上下文的屬性。最後,我們還可以在底部的Graphic Layout與main.xml進行圖形操作界面與源代碼編輯兩種操作方式的切換。
有了這種工具,就有可能實現設計師與工程師合作來構建出美觀與交互性更好的Android應用程序。但可惜的是,Android的這套UI設計工具,太過於編程化,而且由於版本變動頻繁的原因,非常複雜化,一般設計師可能也不太容易學好。更重要的一點,Android存在碎片化,屏幕尺寸與顯示精度差異性非常大,使實現像素級精度的界面有技術上的困難。也這是Android上應用程序不如iOS上漂亮的原因之一。但這種設計至少也增強了界面上的可設計性,使Android應用程序在觀感上也有不俗表現。
我們可以再回過頭來看看應用程序工程裡的res目錄,res目錄裡包含了Android應用程序裡的可使用的資源,而資源文件本身是可以索引的,比如layout會引用drawable與values裡的資源。對於我們例子裡使用的<TextView … android:text="Hello World,Helloworld" …>,我們可以使用資源來進行引用,<TextView …android:text=」 @string/hello_string」 …>,然後在res/values/strings.xml裡加入hello_string的定義。- <span style="line-height: 1.5; background-color: rgb(255, 255, 255);"><div class="blockcode"><blockquote><string name="hello_world">Hello world!</string>
複製代碼從通過這種方式,我們可以看另外一些特點,就是Android應用程序在多界面、多環境下的自適應性。對於上面的字符串修改的例子,我們如果像下面的示例環境那樣定義了res/layout-zh/strings.xml,並提供hello_string的定義: - <span style="line-height: 1.5; background-color: rgb(255, 255, 255);"><div class="blockcode"><blockquote> <string name="hello_world">欢迎使用!</string>
複製代碼 最後,得到的應用程序,在英文環境裡會顯示『Hello world!』,而如果系統當前的語言環境是中文的話,就會顯示成『歡迎使用!』。這種自適應方式,則是不需要我們進行編程的,系統會自動完成對這些顯示屬性的適配。當然,這時可能會有人提疑問,如果這時是韓文或是日文環境會出現什麼情況呢?在Android裡,如果不能完成相應的適配,就會使用默認值,比如即使是我們創建了res/values-zh/strings.xml資源,在資源沒有定義我們需要使用的字符串,這時會使用英文顯示。不管如何,Android提供自適應顯示效果,但也保證總是不是會出錯。
這些也體現出,一旦Android應用程序寫出來,如果對多語言環境不滿意,這時,我們完全可以把.apk按zip格式解開,然後加入新的資源文件定義,再把文件重新打包,也就達到了我們可能會需要的漢化的效果。
在res目錄裡,我們看到,對於同一種類型的資源,比如drawable、values,都可以在後面加一個後綴,像mdpi,hdpi, ldpi是用於適配分辨率的,zh是用來適配語言環境的,large則是用來適配屏幕大小的。對於這種顯示上的自適應需求,我們可以直接在Eclipse裡通過創建Android XML文件裡得到相應的提示,也可以參考http://developer.android.com/training/basics/supporting-devices/
於是,透過資源文件,我們進一步驗證了我們對於Android MVC的猜想,在Android應用程序設計裡,也跟iOS類似,可以實現界面與邏輯完全分離。而另一點,就是Android應用程序天然具備屏幕自適應的能力,這一方面帶來的影響是Android應用程序天生具備很強的適應性,另一方面的影響是Android裡實現像素精度顯示的應用程序是比較困難的,維護的代價很高。
我們可以再通過應用程序的代碼部分來看看應用程序是如何將顯示與邏輯進行綁定的。
2.5 Java編程(src/org/lianlab/hello/HelloWorld.java)在Android編程裡,實現應用程序的執行邏輯,幾乎就是純粹的Java編程。但在編程上,由於Android的特殊性,這種Java編程也還是被定制過的。
我們看到我們例子裡的源代碼,如果寫過Java代碼,看到這樣的源代碼存放方式,就可以瞭解到Android為什麼被稱為Java操作系統的原因了,像這種方式,就是標準的Java編程了。事實上,在Android的代碼被轉義成Dalvik代碼之前,Android編程都可被看成標準的Java編程。我們來看這個HelloWorld.java的源代碼。- package org.lianlab.hello;
- import android.os.Bundle;
- import android.app.Activity;
- public class HelloWorld extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- }
- }
複製代碼 代碼結構很簡單,我們所謂的HelloWorld,就是繼承了Activity的基類,然後再覆蓋了Acitivity基於的onCreate()方法。
Activity類,是Android系統設計思路裡的很重要的一部分,所有與界面交互相關的操作類都是Activity,是MVC框架裡的Controller部分。那Model部分由誰來提供呢?這是由Android系統層,也就是Framework來提供的功能。當界面失去焦點時,當界面完全變得不可見時,這些都屬於Framework層才會知道的狀態,Framework會記錄下這些狀態變更的信息,然後再回調到Activity類提供的相應狀態的回調方法。關於Activity我們後面來詳細說明,而見到Activity類的最簡單構成,我們大體上就可以形成Android世界裡的完整MVC框架構成完整印象了。
我們繼承了Activity類之後,就會覆蓋其onCreate()回調方法。這裡我們使用了」@Override」標識,這是一種Java語言裡的Annotation(代碼註釋)技術,相當於C語言裡的pragma,用於告訴編譯器一些相應參數。我們的Override則告訴javac編譯器,下面的方法在構建對像時會覆蓋掉父類方法,從而提高構建效率。Activity類裡提供的onXXX()系列的都可以使用這種方法進行覆蓋,從而來實現自定義的方法,切入到Android應用程序不同狀態下的自定義實現。
我們覆蓋掉的onCreate()方法,使用了一個參數,savedInstanceState,這個參數的類型是Bundle。Bundle是構建在Android的Binder IPC之上的一種特殊數據結構,用於實現普通Java代碼裡的Serialization/Deserializaiton功能,序列化與反序列化功能。在Java代碼裡,我們如果需要保存一些應用程序的上下文,如果是字符串或是數據值等原始類型,則可以直接寫到文件裡,下次執行時再把它讀出來就可以了。但假設我們需要保存的是一個對象,比如是界面的某個狀態點,像下面的這樣的數據結構:- class ViewState {
- public int focusViewID;
- public Long layoutParams ;
- public String textEdited;
- …
- }
複製代碼 這時,我們就無法存取這樣的結構了,因為這樣的對象只是內存裡的一些標識,存進時是一個進程上下文環境,取回來時會是另一種,就會出錯。為了實現這樣的功能,就需要序列化與反序列化,我們讀寫時都不再是以對像為單位,而是以類似於如下結構的一種字典類型的結構,最後進行操作的是一個個的鍵值對, ViewState[『focusViewID』]的值會是valueOfViewID,一個整形值。- ‘ViewState’ {
- ‘focusViewID’: valueOfViewID,
- ‘LayoutParams’:valueOfLayoutParams,
- ‘textEdited’: ‘User input’,
- }
複製代碼 我們按這種類似的格式寫到文件裡,當再讀取出來時,我們就可以新建一個ViewState對象,再使用這些保存過的值對這一對像進行初始化。這樣就可以實現對象的保存與恢復,這是我們onCreate()方法裡使用Bundle做序列化操作的主要目的,我們的Activity會有不同生存週期,當我們有可能需要在進程退出後再次恢復現象時,我們就會在退出前將上下文環境保存到一個onSavedInstance的Bundle對像裡,而在onCreate()將顯示的上下文恢復成退出時的狀態。
而另一個必須要使用Bundle的理由是,我們的Activity與實現Activity管理的Framework功能部件ActivityManager,是構建在不同進程空間上的,Activity將運行在自己獨立的進程空間裡,而Framework則是運行在另一個系統級進程SystemServer之上。我們的Bundle是一種進行過序列化操作的對象,於是相應的操作是系統進程會觸發Activity的進行onCreate()回調操作,而同時會轉回一個上下文環境的Bundle,可將Activity恢復到系統指定的某種圖形界面狀態。Bundle也可能為空,比如Activity是第一個被啟動的情況下,這個空的onSavedInstance則會被忽略掉。
我們進入到onCreate()方法之後,第一行便是- super.onCreate(savedInstanceState);
複製代碼 從字面上看,這種方式相當於我們繼承了父類方法,然後又回調到父類的onCreate()來進行處理。這種方式貌似很怪,但這是設計模式(Design Pattern)裡鼎鼎大名的一種,叫IoC ( Inversion of Control)。通過這樣的設計模式,我們可以同時提供可維護性與可調試性,我們可以在通過覆蓋的方法提供功能更豐富的子類,實際上每次調用子類的onCreate()方法,都將調用到各個Activity拓展類的onCreate()方法。而這個方法一旦進入,又會回調到父類的onCreate()方法,在父類的onCreate()方法裡,我們可以提供更多針對諸多子類的通用功能(比如啟動時顯示的上下文狀態的恢復,關閉時一些清理性工作),以及在這裡面插入調試代碼。
然後,我們可以加載顯示部分的代碼的UI,- setContentView(R.layout.main);
複製代碼 這一行,就會使我們想要顯示的圖形界面被輸出到屏幕上。我們可以隨意地修改我們的main.xml文件,從而使setContentView()之後顯示出來的內容隨之發生變化。當然,作為XML的UI,最終是在內存裡構成的樹形結構,我們也可以在調用setContentView()之前通過編程來修改這個樹形結構,於是也可以改變顯示效果。
到目前為止,我們也只是實現了將內容顯示到屏幕上,而沒有實現交互的功能。如果要實現交互的功能,我們也只需要很簡單的代碼就可以做到,我們可以將HelloWorld.java改成如下的內容,從而使用我們的」Hello world」字符串可以響應點擊事件:- package org.lianlab.hello;
- import android.os.Bundle;
- import android.app.Activity;
- import android.view.View;
- import android.view.View.OnClickListener;
- import android.widget.TextView;
- public class HelloWorld extends Activity {
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- ((TextView)findViewById(R.id.textView1)).setOnClickListener(
- new OnClickListener() {
- @Override
- public void onClick(View v) {
- finish();
- }
- });
- }
- }
複製代碼 我們使用Activity類的findViewById()方法,則可以找到任何被R.java所索引起來的資源定義。我們在這裡使用了R.id.textView1作為參數,是因為我們在main.xml就是這麼定義TextView標籤的:android:id="@+id/textView1"。
而我們找到字段之後,會調用TextView對象的setOnClickListener()方法,給TextView註冊一個onClickListener對象。這樣的對象,是我們在Android世界裡遇到的第二次設計模式的使用(事實上Android的實現幾乎使用到所有的Java世界裡的通用設計模式),Listener本身也會被作為Observer設計模式的一種別稱,主要是用於實現被動調用邏輯,比如事件回饋。
Observer(Listener)設計模式的思路,跟我們數據庫裡使用到的Trigger功能類似,我們可對需要跟蹤的數據操作設置一個Trigger,當這類數據操作進行時,就會觸發數據庫自動地執行某些操作代碼。而Observer(Listener)模式也是類似的,監聽端通過註冊Observer來處理事件的回調,而真正的事件觸發者則是Observer,它的工作就是循環監聽事件,然後再調用相應監聽端的回調。
這樣的設計,跟效率沒有必然聯繫,太可以更大程度地降低設計上的複雜度,同時提高設計的靈活性。一般Observer作為接口類,被監聽則會定位成具體的Subject,真正的事件處理,則是通過實現某個Observer接口來實現的。對於固定的事件,Subject對象與Observer接口是無須變動的,而Observer的具體實現則可以很靈活地被改變與擴充。如下圖所示: 如果我們對於監聽事件部分的處理,也希望能加入這樣的靈活性,於是我們可以繼續抽像,將Subject泛化成一個Observable接口,然後可以再提供不同的Observable接口的實現來設計相應的事件觸發端。
針對於我們的Android裡的OnClickListener對象,則是什麼情況呢?其實不光是OnClickListener,在Android裡所有的事件回調,都有類似於Observer的設計技巧,這樣的回調有OnLongClickListener,OnTouchListener,OnKeyListener,OnContextMenuListener,以及OnSetOnFocusChangeListener等。但Android在使用設計模式時很簡潔,並不過大地提供靈活性,這樣可以保證性能,也可以減小出錯的概率(基本上所有的設計複雜到難以理解的系統,可維護性遠比簡單易懂但設計粗糙的系統更差,因為大部分情況下人的智商也是有限的資源)。於是,從OnClickLister的角度,我們可以得到下圖所示的對象結構。
Click事件的觸發源是Touch事件,而當前View的Touch事件在屬於點擊事件的情況下,會生成一個performClick的Runnable對像(可交由Thread對像來運行其run()回調方法)。在這個Runnable對象的run()方法裡會調用註冊過的OnClickListener對象的OnClick()方法,也就是圖示中的mOnClickListener::onClick()。當這個對象被post()操作發送到主線程時(作為Message發送給UI線程的Hander進行處理),我們覆蓋過的OnClick()回調方法就由主線程執行到了。
我們註冊的Click處理,只有簡單的一行,finish(),也就是通過點擊事件,我們會將當前的Activity關閉掉。如果我們覺得這樣不過癮,我們也可通過這次點擊觸發另一個界面的執行,比如直接搜索這個字符串。這樣的改動代碼量很小,首先,我們需要在HelloWorld.java的頭部引入所需要的Java包,- import android.app.SearchManager;
- import android.content.Intent;
複製代碼 然後可以將我們的OnClick()方法改寫成啟動一個搜索的網頁界面,查找這個字符串,而當前界面的Activity則退出。這時,我們新的OnClick()方法則會變成這個樣子:- public void onClick(View v) {
- Intent query = new Intent(Intent.ACTION_WEB_SEARCH);
- query.putExtra(SearchManager.QUERY,
- ((TextView)v).getText());
- startActivity(query);
- finish();
- }
複製代碼 但是可能還是無法解決我們對於Android應用程序與Java環境的區別的疑問:
T Android有所謂的MVC,將代碼與顯示處理分享,但這並非是標準Java虛擬機環境做不到。一些J2EE的軟件框架也有類似的特徵。
T AndroidManifest.xml與On*系列回調,這樣的機制在JAVA ME也有,JAVA ME也是使用類似的機制來運行的,難道Android是JAVA ME的加強版?
T 至於Listener模式的使用,眾所周知,Java是幾乎所有高級設計模式的實驗田,早就在使用Listener這樣模式在處理輸入處理。唯一不同的是ClickListener,難道Android也像是可愛的觸摸版Ubuntu手機一樣,只在是桌面Java界面的基礎加入了觸摸支持?
T Activity從目前的使用上看,不就是窗口(Window)嗎?Android開發者本就有喜歡取些古怪名字的嗜好,是不是他們只是標新立異地取了個Activity的名字?
對於類似這樣的疑問,則是從代碼層面看不清楚了,我們得回歸到Android的設計思想這一層面來分析,Android應用程序的執行環境是如何與眾不同的。
不過,我們可以從最後的那行Activity調用另一個Activity的例子裡看出一些端倪,在這次調用裡,我們並沒有顯式地創建新的Activity,如果從代碼直接去猜含義的話,我們只是發出了個執行某種操作的請求,而這個請求並沒有指定有誰來完成。這就是Android編程思想的基礎,一種全開放的「無界化」編程模型。
|
|