Android應用程序開發以及背後的設計思想深度剖析


Android應用程序開發以及背後的設計思想深度剖析
 

作者:admin,發佈於2012-11-6

 

本文內容,主題是透過應用程序來分析Android系統的設計原理與構架。我們先會簡單介紹一下Android裏的應用程序編程,然後以這些應用程 序在運行環境上的需求來分析出,爲什麼我們的Android系統需要今天這樣的設計方案,這樣的設計會有怎樣的意義, Android究竟是基於怎樣的考慮才變成今天的這個樣子,所以本文更多的分析Android應用程序設計背後的思想,品味良好架構設計的魅力。分五次連 載完成,第一部分是最簡單的部分,解析Android應用程序的開發流程。

Android應用程序開發以及背後的設計思想深度剖析 1

Android應用程序開發以及背後的設計思想深度剖析 2

Android應用程序開發以及背後的設計思想深度剖析 3

Android應用程序開發以及背後的設計思想深度剖析 4

Android應用程序開發以及背後的設計思想深度剖析 5

Android應用程序開發以及背後的設計思想深度剖析 1

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  

對,沒有看錯,只一個簡單的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應用程序的全部。

  • AndroidManifest.xml,這是每個Android應用程序包的配置文件,這裏會保存應用程序名字、作者、所實現的功能、以及一些權限驗證信息。但很可惜,在編譯完成的.apk文件裏,這些文件都被編譯成了二進制版本,我們暫時沒有辦法看到內容,後面我們可以再看看具體的內容。
  • classes.dex,這則是Android應用程序實現的邏輯部分,也就是通過Java編程寫出來而被編譯過的代碼。這種特殊的格式,是Android裏特定可執行格式,是可由Dalvik虛擬機所執行的代碼,這部分內容我們也會在後續的介紹Dalvik虛擬機的章節裏介紹。
  • 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” à “Install New Software…”,然後在彈出的對話框裏的Workwith:輸入ADT的發佈地址:https://dl-ssl.google.com/android.eclipse,回車,則會得到下面的軟件列表。選擇Select All,將這些插件全都裝上,則得到了可用於Android應用程序開發的環境。

這裏還需要指定SDK的地址,Windows或是Linux裏,會是在菜單“Window” à “Preferences”,在MacOS裏,則會是”Eclipse” à“Preferences” 。在彈出的對話框裏,選擇Android,然後填入Android SDK所保存的位置。

點擊OK之後,則可以進行Android開發了。選擇”File” à “New”à “Project” à “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”à“Build project”來完成編譯過程。如果我們使用默認設置,則代碼是使用自動編譯的,我們的每次修改都會觸發增量式的編譯。

我們從這些android編程的過程,看不出來android跟別的Java編程模式有什麼區別,倒至少驗證了我們前面對android編程特點的 猜想,就是很簡單。如果同樣我們使用Eclipse開發Java的圖形界面程序,需要大量地時間去熟悉API,而在Android這裏學習的曲線被大大降 低,如果我們只是要畫幾個界面,建立起簡單的交互,我們幾乎無須學習編程。

而從上面的步驟,我們大概也可以得到Android開發的另一個好處,就是極大的跨平臺性,它的開發流程裏除了JDK沒有提及任何的第三方環境需 求,於是這樣的開發環境,肯定可以在各種不同的平臺執行。這也是Android上進行開發的好處之一,跨平臺,支持Windows,Linux與 MacOS三種。

我們再來看一個,我們剛纔創建的這個工程裏,我們怎麼樣進行下一步的開發。在Android開發裏,決定我們應用程序表現的,也就是我們從一個.apk文件裏看到的,我們實際上只需要:

  • 修改AndroidManifest.xml文件。AndroidManifest.xml是Android應用程序的主控文件,類型於Windows裏的註冊表,我們通過它來配置我們的應用程序與系統相關的一些屬性。
  • 修改UI顯示。在Android世界裏,我們可以把UI編程與Java編程分開對待,處理UI控件 的語言,我們可以叫它UI語言,或是layout語言,因爲它們總是以layout類型的資源文件作爲主入口的。Android編程裏嚴格地貫徹MVC的 設計思路,使我們得到了一個好處,就是我們的UI跟要實現的邏輯沒有任何必然聯繫,我們可先去調整好UI顯示,而UI顯示後臺的實現邏輯,則可以在後續的 步驟裏完成。
  • 改寫處理邏輯的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>  

<div id="chart" style="width:360px;height:200px"/>
</body>
</html> 

作爲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 à 複合控件 à 簡單控件。中間區域可以上面還有選擇項用於控制顯示屬性,在工作區域裏我們可以進一步對界面進行微調,也可以選擇控件點擊左鍵,於是會出來上下文菜單來操作控件的屬性。到於右邊的操作界面,上部分則是整個界面構成的樹形結構,而下部分則是當我們選擇了某個界面元素時,會顯示上下文的屬性。最後,我們還可以在底部的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的定義。

.<string name="hello_world">Hello world!</string>

從通過這種方式,我們可以看另外一些特點,就是Android應用程序在多界面、多環境下的自適應性。對於上面的字符串修改的例子,我們如果像下面的示例環境那樣定義了res/layout-zh/strings.xml,並提供hello_string的定義:

<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環境的區別的疑問:

  • Android有所謂的MVC,將代碼與顯示處理分享,但這並非是標準Java虛擬機環境做不到。一些J2EE的軟件框架也有類似的特徵
  • AndroidManifest.xml與On*系列回調,這樣的機制在JAVA ME也有,JAVA ME也是使用類似的機制來運行的,難道Android是JAVA ME的加強版?
  • 至於Listener模式的使用,衆所周知,Java是幾乎所有高級設計模式的實驗田,早就在使用Listener這樣模式在處理輸入處理。唯一不同的是ClickListener,難道Android也像是可愛的觸摸版Ubuntu手機一樣,只在是桌面Java界面的基礎加入了觸摸支持?
  • Activity從目前的使用上看,不就是窗口(Window)嗎?Android開發者本就有喜歡取些古怪名字的嗜好,是不是他們只是標新立異地取了個Activity的名字?

對於類似這樣的疑問,則是從代碼層面看不清楚了,我們得迴歸到Android的設計思想這一層面來分析,Android應用程序的執行環境是如何與衆不同的。

不過,我們可以從最後的那行Activity調用另一個Activity的例子裏看出一些端倪,在這次調用裏,我們並沒有顯式地創建新的Activity,如果從代碼直接去猜含義的話,我們只是發出了個執行某種操作的請求,而這個請求並沒有指定有誰來完成。這就是Android編程思想的基礎,一種全開放的“無界化”編程模型。

Android應用程序開發以及背後的設計思想深度剖析(2)

Android的系統設計,與別的智能手機操作系統有很大區別,甚至在以往的任何操作系統裏,很難找到像Android這樣進行全面地系統級創新的操作系統。從創新層面上來說,Android編程上的思想和支持這種應用程序運行環境的系統,這種理念本身就是一種大膽的創新。

整個Android系統,實際主要目的,就是打造一個功能共享的世界。

功能共享最重要的交互,於是Android創造出一種Intent和IntentFilter配合的低耦合的交互模型,Intent只是一種描述要完成什麼工作跨進程的結構體,而最終如何解析這些Intent並完成其響應,是由IntentFilter來進行換算,最終是由用戶來決定如何完成。

而在Intent這種超級交互消息之上,Android進一步把應用程序的實現邏輯拆分成多種特殊的實現:

  • Activity:帶顯示與交互能力的部分
  • Service:不帶顯示與交互能力的部分
  • Content Provider:在功能交互之外,提供數據交互能力的部分
  • Broadcast Receiver:用來處理廣播交互的部分

這四種功能上的拆分,也體現了Android設計者在設計上抽象思緒能力,即便是隨着Android迅猛發展,目前已經到了4.1這麼功能豐富、用戶體驗良好的狀態,我們編程也還是與這四種功能組件打交道,可以滿足我們任何的編程時所需要的任何行爲。

而這四種基本組件組成部分,使Android應用程序反倒成了一個“空殼子”。靜態上看,應用程序只是一種包裝這些功能的容器;從運行態來看,所謂的應用程序,也只是承載某些功能的進程。

1.1 所謂的Android應用程序

我們從前面的例子中看到,無論是編寫的代碼,還是最後生成的.apk文件,都是沒有所謂的應用程序的。應用程序本身是一種虛無的概念,只是一種以zip格式進行壓縮的一個文件,一種容器而已。

如我們前面的Helloworld的例子裏所看到的那樣,其實一個應用程序裏最重要的一個配置文件就是AndroidManifest.xml文件。一個最簡單的項目,除了基本的代碼與UI資源,也會需要有個AndroidManifest.xml文件。甚至一些極端一點的例子,我們去市場上下載一些什麼主題包、插件包、權限包之類的.apk文件,解壓開,這時可以發現這樣的.apk文件裏,連代碼都沒有,只有一些圖片之類的文件。

於是,我們可以得到Android裏關於應用程序的第一個印象,作爲Android應用程序的載體,.apk文件只是一種進行包裝與傳輸的格式,而每個.apk文件必然包含一個AndroidManifest.xml文件,由這一文件來描述該.apk文件提供的內容。當然,我們在稍後會看到,這一文件裏,還會包含一些權限控制的信息。

我們可以給我們的應用程序創建兩個一模一樣的圖形界面,直接從我們的前面的HelloWorld開始下手,比如將HelloWorld.java在Eclipse裏拷貝到HelloAgain.java(這樣可以減少改代碼的麻煩)。這時可以得到兩個界面的應用程序,然後我們再把我們的AndoridManifest.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">
    <applicationandroid: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>
        <activity android:name=".HelloAgain"
                  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>

這時我們編譯、安裝到Android設備(或者是虛擬機)裏,這時再打開主界面查看安裝過的應用程序,這時是不是發生了什麼很奇怪的現象?這時界面上會出現兩個叫Helloworld的應用程序。我們這時如果在設備裏去“設置”à “應用程序”,我們仍只看到一個應用程序。

通過對AndroidManifest.xml的小惡作劇,我們可以看到Android應用程序的第二個特點,就是沒有所謂的主入口(即我們點擊的圖標時觸發的執行效果)。應用程序在安裝完成後,只是通過AndroidManifest.xml來決定在系統上應該表現成什麼樣子。

如果希望應用程序可以表現不如此變態,這時,我們可以回到AndroidManifest.xml裏,把<activityandroid:name=".HelloAgain"…這個標籤裏的<intent-filter>標籤刪掉,這時應用程序的表現就正常了。

到目前爲止,我們就已經接觸了android編程裏的兩個概念,一個是Activity,另一個是Intent(而我們AndroidManifest.xml文件裏的Intent Filter實際是輔助Intent的)。Android畢竟是種圖形界面的編程環境,我們常見的應用程序裏,可能絕大部分只會與這兩種概念打交道。而兩者的概念組合,就很容易體現出Android應用程序在編程上的“無界化”思想。

1.2 Android世界裏的共享

作爲一個智能手機操作系統,其用戶可能在功能上有各種各樣的功能組合。比如最簡單的打電話,則後續動作會有保存聯繫人,同時需要給聯繫人拍照做來電大頭貼。又比如需要來了個短信通信用戶到某個地方幹什麼事情,這時,用戶需要打開地圖,搜索一下地址,然後還有可能需要定位到那個位置。

用戶在主界面裏點擊相應功能的應用程序之後,就可能有非常多的功能性的組合,因爲用戶的想法是不可預估的。我們當然也可以限制用戶當前菜單下可以幹什麼事情,但這樣就失去了智能系統的意義。

我們也可以假設用戶都會按一個“Home”鍵回到主界面,這時原來的執行的程序就會被鎖定當前狀態,用戶重新打開另外一個應用程序,操作完再按“Home”鍵可以退回到原來的應用程序。通過這種“應用程序”到“Home”到“應用程序”的循環,我們也可以達到我們想要達到的目的。但這時,出於交互性的考慮,我們也還是需要有限地提供一些交互手段,比如“短信”應用程序裏包含地址信息,一點擊可以直接打開“地圖”進行後續操作,但這些有限交互是可以在系統設計階段被固化。這時,我們是不是就得到了我們想要的能夠應付用戶任何操作組合的系統?是的,恭喜您,您得到了iPhone的設計思路。但此時的用戶交互流程則被改變成這個樣子:

這種解決問題的辦法也不是不可以,但需要很固化的設計,應用程序的行爲比較受限。雖然通過橫掃全世界的iPhone證明了這樣的設計可能是比較合乎用戶體驗之道的(不容易出錯),但這樣的解決思路從系統設計角度來看,並不是很靈活。另外一個麻煩是必須要有蘋果級設計功底的“Home”鍵,山寨貨則用不了多久就會因爲鍵盤失靈而失效。當然即使蘋果級設計,iPhone裏的“Home”鍵還是會失效,於是又不得不在屏幕上加上觸摸的Home手勢。

作爲開源系統的Android,當然不可能基於iOS的交互思路來解決問題,何這種交互時多了一步不停要回到主界面這一步。在Android的設計裏,最重要的是能夠解決一個應用程序之間進行交互的問題,然後可以實現我們想要在Android系統裏完成某種操作時,可以享受從一路順暢完成的快感。

Android的解決之道,則是將傳統意義上的應用程序,細化成一個個完成某項功能的部分,這種功能部分,在Android世界裏被稱爲Activity。Activity都應該被設計成可以獨立地被執行以解決某個問題,當它完成或是用戶選擇退出執行時,又會自動跳回到調用這一Activity的界面,當然這時跳回的位置肯定是另外一個Activity。當然,在一個Android系統裏有可能存在無限多的Activity,在他們進行跳轉切換時,我們就需要一種很靈活的消息傳輸機制(因爲我們必須兼容系統裏所有可能的互相調用的情況)。而且這種傳輸機制還必須能夠跨進程,不然,我們所有的涉及Activity互相調用部分都必須在同一進程裏完成。於是,Android系統裏又有了Intent,用於解決交互通信。

這樣的編程模型也需要有一定前提,那就是我們Application概念必須被弱化,我們不能有main函數入口(如果系統執行依賴main作入口,則不能實現Activity之間互相調用了,所有的Activity執行之前,必須先通過main入口來初始化環境)。出於這樣的設計,所以Application必須只是一個容器,將各種不同的Activity實現包裝起來加載到系統裏。

當然,將功能拆分成一個個的單一功能界面之後,我們需要有種機制可以將用戶一路點擊過去歷史記錄下來,當用戶處理完時,可以退回到他們之前操作過的界面,這次就可以由多個應用程序組合出像是在用同一個應用程序的效果。有了Activity,有了Activity之間起到調用作用的Intent,這時所有界面間操作變得有點像是函數調用一樣,於是我們可以找函數調用時的基本數據結構—棧來幫忙,發生調用時,需要退出的Activity及其狀態壓棧,當從調用退出時則進行棧的彈出操作,這時我們的Activity管理就演變成如下圖所示的簡單棧管理。

有了這樣的概念,於是我們響應用戶點擊操作的問題便迎刃而解,我們在設計應用程序時,不再是設計一個複雜的功能實現,而是實現一組完成單項功能的實現,也就是Activity。然後這些Activity,只會通過用戶點擊來驅動它們之間是如何進行交互的。比如,我們前面看到的地圖、搜索、定位三個功能,雖然它都會被包裝到同一個地圖的應用程序裏,但在實現上會是地圖、搜索、定位三個不同的Activity。

因爲現在我們的界面上的互相調用,已經變成了一種函數式的調用,這樣,整個手機上的功能都被切分成各個單一的小功能,而真正要在Android系統上完整地實現某複雜個操作,則會提交由用戶的點擊來組合生成。這樣的複雜功能,則已經不是一個編程上的概念了,在Android系統裏,這種需要完成什麼事情的操作被抽象成一個虛擬的概念Task。比如我們前面提到的打電話加拍大頭貼的操作組合,就構成一個Task,這一Task需要由Launcher.apk,Contacts.apk,Gallery.apk來協同完成

如果我們有兩個能夠提供同樣功能的Activity,這種執行模式的靈活性表現得會更加明顯。比如中間打電話的功能,我們系統裏有三個Activit(CallScreen, SipPhone, Dialer)都可以完成電話呼叫的功能,這時執行上的路徑則會有三種可能性,會在進行跳轉時彈出聖對話框由用戶來選擇:

通過Activity的這種可以動態被用戶選擇的特點,當用戶對某一功能不滿意時,完全就有可能通過下載另一個能實現這種功能的應用程序進行替換,甚至可以自己寫一個。事實上,Android系統裏除了系統狀態條與鎖屏界面之後,沒有任何的不可被替換的功能,這也是Android設備總是會長得千奇百怪的原因之一。

到這時,我們就可以看到Activity之所以會不被稱爲Window的原因,它也是單個界面或是MVC裏的Controller實現部分那麼簡單,Activity這個名字代表的是某種單一交互功能上的實現。這種功能的實現將在系統裏通過Intent串接起來,構成了一個在功能上具備極大可拓展性的系統。基於這樣的特點,Android也就被稱作是“無邊界”系統,因爲它在功能上延展不再受限於系統的能力,而只受限於智商與創意。

這就是Android世界裏的功能共享。

在這種功能共享模型之下,可能還是會有一些微調的需求:

1. 我們有一些情況下不宜使用這種棧式Activity管理,比如我們寫一個需要註冊的應用程序,註冊完開始使用,然後再按退出,我們又會一步步退回到註冊填個人信息的界面,而不合理地完全退出。這樣可能不合適。這時,我們可以使用Intent的Flag參數, 加上Activity的Affinity屬性進行組合控制。

2. 如果不停地跳出對話框讓用戶選,用戶會崩潰掉。當然,用戶可以在選擇時點選一個“始終”的默認選擇,這時下次就會使用默認的Activity處理某種操作。但還是有可能會不合理地使用跨.apk文件裏使用Activity,造成性能上的開銷,這時,我們也可以在執行下次Activity執行操作時進行強制性地指定。

當然,我們通過Activity這種概念還需要另外一個前提,這就是Android會有別於傳統操作系統的前提,那就是單窗口。想像一下,在多窗口環境下,我們的棧式管理Activity在進行跳轉和返回時將會構成多大的災難啊。好在使用電容屏的設備,單窗口是天生的需求。由於手指觸摸的精度非常低,無法點準過小的按鈕,比如窗口上的關閉按鈕,如果將這些按鈕放大,又造成了屏幕顯示空間上的浪費。iPhone帶來的“後PC時代”革命,最重要的一點就是使用“返祖”式的單窗口顯示。

這種怪異的操作方式,實際上在我們生活中也有類似的例子,就比如說我們的動態網頁。動態網頁,特別是HTML5構建的網絡應用程序,其操作模式,就是可以在不同的鏈接裏不斷地點擊下去,如果不是彈出新窗口,我們始終還可以退回到發起這一連串點擊的起始頁面。Android應用程序,XML構成的UI語言的作用跟Html頁面類似,而Java構建的Activity就相當與網頁交互中使用的JavaScript,有了這樣的相似性,Android編程環境可以說是最接近HTML5的一種編程環境了,但可惜不能像HTML5那樣可以跨平臺。

我們解析了能完成單一功能的Activty,這時還需要了解Intent,就像是我們瞭解過了函數實現原理,我們還需要掌握函數之間的參數傳遞。當然,一般在介紹編程的思路里,會結合起來說明,或是先說明參數傳遞。但Android環境裏有點特殊性,一是Intent是一種能夠實現跨進程調用的信息傳遞機制,二是Intent在消息傳遞上又很靈活,有一定的動態性。Intent不光服務於Activity之間的調用,還會用於一些不直接與界面打交道的邏輯實現部分,比如我們後面將提到的Service,Broadcast Receiver,以及 Notification。

1.3 Intent與Intent Filter

Intent,英文原意就是要“幹什麼”的意思,之所以取這個名字,也是因爲在Android系統裏,Intent所起到的作用就是用來指明下一步具體是做什麼,具體是不是執行,由誰來執行,則會由根據當前的系統狀態(能不能解析這個Intent請求)來決定。這不只是簡簡單單地發個消息而已,而是一種更安全的、更加鬆散的消息機制。

在一個Intent消息對象裏,共有六個成員(並不都是必須賦值的,只要一個Intent對象能夠被解析,就會得以執行,否則就會會被捨棄):

成員 類型 說明 示例
ComponentName String 用於定義誰將處理這一Intent。它由一個Activity的具體實現的全名(加上包名)來指定 org.lianlab.hello.HelloActivity
Action String 用於定義這一動作是做什麼,可以被拓展自定義類型 ACTION_CALL 開始通話.
ACTION_EDIT 進行編輯.
Data String 用一個URI來指定Intent的操作對象,因爲URI一般會包含種類信息,於是這個值也可能被用作MIME設別。 “content://contacts/people/1”
指定聯繫列表中的第一個
Category String 用來進一步明確什麼樣的可執行實體將處理這一Intent。是可選項,也可多選。 CATEGORY_HOME 主界面應用程序
CATEGORY_LAUNCHER 可在主界面裏被點擊
Type String 用來指定特定的MIME類型 "video/*"
視頻
Extra Bundle 用來傳遞額外的數據傳遞,前面我們也介紹了Bundle是一種key:value配對的字典類型,於是Extra裏可以轉遞複雜的數據 putExtra("sms_body", "some text");
發短信時指定內容
Flags
int 預定義一系列用來控制Intent行爲的屬性值  

在這個成員變量裏,最能體現靈活性的就是Component,如果指定了這個值,則我們在通過startActivity()方法來發送Intent時,就會自動啓動Component指定的Activity。如果沒有指定,則會由系統來選擇一個能夠處理這一Intent的Activity來執行,這時就引入了Intent Filter的概念。

Intent Filter在Android裏是一種類似於Windows裏的註冊表一樣的東西,雖然我們也可以通過編程來進行Intent Filter的控制,但一般情況下,我們只在AndroidManifest.xml文件裏進行定義,對它一個<intent-filter>的標籤進行指定。應用程序在安裝過程中,它的AndroidManifest.xml會被系統掃描並彙總到系統環境裏,這時<intent-filter>也會被導入。當Activity發送出來的Intent,沒有指定Component時,系統就會通過<intent-filter>找到合適的處理對象,如果只有一個或是用戶設置了默認項,則啓動這個功能部件來完成任務;如果有多個<intent-filter>匹配同時用戶又沒有指定默認項,則會彈出對話框讓用戶選擇。當然,默認項也會隨着系統裏新增了同一Intent匹配項而失效,用戶也可以通過“設置”à “應用程序”來取消默認值。

在AndroidManifest.xml裏面定義<intent-filter>很簡單,就是通過指定Intent對象的Action,Data,Type,和Category這四個成員變量來指定。比如:

       <activityandroid:name=".PlayerActivity"android:label="@string/app_name"
                       android:configChanges="orientation" >
            <intent-filter>
                <actionandroid:name="android.intent.action.MAIN" />
                <categoryandroid:name="android.intent.category.LAUNCHER" />
            </intent-filter>
                                    <intent-filter>
                                      <actionandroid:name="android.intent.action.VIEW" />
                                      <categoryandroid:name="android.intent.category.DEFAULT" />
                                      <dataandroid:scheme="file" />
                                    </intent-filter>
...
</activity>

當我們的某個Activity,發送了一個Intent,其Action是”android.intent.action.VIEW”,data又是以file:///開始的URI指定的內容(也就是文件類型),這時上面例子裏的PlayerActivity就會成爲播放時的候選項。

我們可以繼續修改我們前面的HelloWorld的例子,我們新建一個Intent,將Action設成”android.intent.action.VIEW”(可以通過Intent.ACTION_VIEW這個預變量來轉義),data使用某個文件“file:///sd-ext/Movies/test.mp4”,這時就會匹配到我們上面的<intent-filter>定義:

    public void onClick(Viewv) {
       Intent request = new Intent(Intent.ACTION_VIEW);
       request.putData(“file:///sd-ext/Movies/test.mp4”);
       startActivity(request);
       finish();
   }    

當然,我們並不一定需要代碼來進行這樣的測試,我們也可以使用設備上的am命令來完成。要完成與上面的點擊操作一樣的功能,也可以通過adb來執行這條命令:

$adb shell am start –aandroid.intent.action.VIEW –d file:///sd-ext/Movies/test.mp4

在我們具體寫代碼過程中,我們可以根據需求來定義我們所需要的<intent-filter>,可以將過濾規則寫得很細,也可以寫得很粗,讓我們的Activity有更多地被執行到的機會。在這些規則裏,可能最重要的規則,就是我們前面也示範過的:

 <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> 

這兩行<intent-filter>規則,將使用我們的應用程序可以被主界面所收集,使用戶可以在主界面裏點擊運行這個Activity。

有了Activity,我們就可以構建基於功能共享而實現所謂應用程序,而有了Intent,使用我們在共享時所受的限制可以變得更小。而且,由於是簡單化的單窗口模式,再加上一些在性能設計上的精細設計,於是我們的Android系統便有了良好的人機交互體驗。

1.4 編程角度的應用程序

光有Activity與Intent,並不是Android應用程序編程時的全部。應用程序除了有人機交互界面之外,有可能還需要使用到一些不直接與人交互而在後臺長期運行操作;我們還需要有某種機制,能夠提供數據共享,並且在數據共享時能使用統一的訪問機制;最後,我們可能還需要處理以廣播方式發送的消息,廣播與Intent不同之處在於一到多的方式傳播,同時消息只在某個時間段內有效。

事實上,我們的Android編程,是被包裝成四個不同的類型,同時通過Intent將這些類包裝起來,以解決我們上面提到的,在編寫圖形應用程序裏可能遇到的問題的:

  • Intent: 全局性的、鬆散的消息傳遞機制
  • Activity:帶圖形界面的,可以與用戶進行交互的邏輯實現。
  • Service: 不帶圖形界面的,不直接與用戶交互的代碼。一般會被用於在後臺做些什麼事情,比如監聽網絡、下載、拷貝文件等。(這可能是一般的Android工程師覺得沒有必要實現的部分,筆者在講解Android應用程序相關的課程時,就常有問及,Actiivity會進入到後臺,然後有可能被殺死掉,這樣的問題如何解決?實際上Activity只解決交互,需要在後臺時還需要繼續執行的代碼,需要用Service來實現。只自己可訪問的Service,可以使用簡單的本地Service,而需要提供給別的進程來訪問的情況下,我們需要通過AIDL編寫Remote Service。Service的實現,我們在後臺再詳細說明,因爲Android系統的核心Framework,本身就是由大量這樣的Remove Service來組成的。)
  • Content Provider: 提供數據層共享,以CRUD(Create Read Update Delete)方式進行數據訪問來統一化數據讀寫指口一種模型。如果使用了Sqlite做後臺的數據支持(實際上相當於應用程序MVC模型裏的Model部分被Sqlite延展開來),我們可以通過ContentProvider來各系統內的其他部分提供數據源,當然系統本身也給我們提供了大量這樣的ContentProvider,像Setting裏的設置的值、聯繫列表、多媒體文件掃描結果等。(這種數據層上的共享機制,也是應用程序編程上需要加強的技巧之一,因爲有了Content Provider,我們則有可能使用Cursor式進行訪問,這時我們就可以使用CursorAdapter來自動化地處理數據源。)
  • Broadcast Receiver:處理廣播類消息的監聽器,從而可以給應用程序提供廣播式的信息處理,同時也提供系統消息的廣播式分發。比如,Android會將一些系統事件廣播出來,像電話振鈴、電量狀態變化、網絡狀態變化等,我們需要能夠處理這樣的事件,電話振鈴時我們寫的多媒體播放器就應該靜音、電量過低時需要保存狀態等。對我們應用程序而言,廣播方式也是一種很好的通信機制,我們不需要寫一個循環通知所有的Activity、Service我們狀態發生了改變,而只需要發一個廣播,則所有關心這一事件的部分都可以收到。

這些功能實體,都是我們通過Java代碼根據不同的基類(Service、 ContentProvider、BroadcastReceiver)派生出相應的子類,再加以具體實現。這樣的功能實現不需要自己去創建這個對象,會通過AndroidManifest.xml裏的定義,由系統按執行的需要自動創建。有了這些不同的功能實體,我們最後的應用程序,實際上就成了這個樣子:

而我們的所有代碼,從運行態行爲來看,都不再是直接的互相調用關係,而是全部都通過Intent來進行彼此之間的交互。而這樣的交互,也不再是傳統式地自己陪自己玩,而是會進入到一個大的功能集合體時,提供功能給系統內其他應用程序所使用,而自己也會調用其他部分的代碼。

當然,隨着Android版本變更,Android系統又新增了一些新的概念,比如針對多窗口功能的Fragment、針對於使用異步機制操作Cursor的Loader等。但萬變不離其宗,這些Android的核心原理則一直如此。

總結一下Android編程思想,我們就會知道其實在Android整個生態環境最重要的元素,應用程序,反倒是Android編程上最不重要的。所謂的Android編程,就是要通過編寫一個個的Activity、Service、Content Provider、Broadcast Receiver實現,通過功能上的共享與數據上的共享進一步豐富用戶可用的功能。當用戶可以通過Market或自己下載取得我們封裝到.apk文件裏的實現之後,這些功能就會無縫地被Intent整合到了一起。

從Android這個編程原則來看,我們可以看到,如果我們使用某種Java執行環境,將Android應用程序的這些組成部分的支持都加入進來,我們也可以得到一個Android兼容的環境。的確如此,已經有人在打這方面的主意,有將Android環境移植到Windows環境裏的BlueStack商業解決方案,也有號稱兼容Android應用程序,通過一個類似於JAVA ME的虛擬機環境來支持Android應用程序的BlackBerryOS。但我們可以再來看看Android的真正的支持環境,我們可以看到,Android有其獨特的特性,也不是那麼容易被取代

Android應用程序開發以及背後的設計思想深度剖析(3)

我們接下來從安全性,性能,功能,可移植性的角度分別分析Android系統爲應用程序提供的支撐。

1. 支撐應用程序的Android系統

分析一個系統的構成,可以有多個出發點。從不同出發點,我們可從不同側面分析這個系統的設計,以及爲什麼要這樣設計:

  • 從系統結構出發,Android系統給我們的感覺就是一種簡潔的,分層式的構架實現,從這種分層式的構架實現角度,我們可以理解這個系統是如何被組織到一起。
  • 從系統的運行態角度出發,我們又可以從Android的整個啓動過程裏做了哪些工作,系統是由哪些運行態的組成部分來構造起來的。
  • 從源代碼的結構出發,我們可以通過Android的源代碼,來分析Android系統具體實現。設計最終都會落實到代碼,通過代碼,我們可以逆向地看出來這個系統是如何構建起來的。

這種不同的分析角度,通常也包含着不同的用意,一般是在Android開發裏從事不同的方向所導向的最方便的一種選擇。比如,作爲底層移植者而言,大部分都是BSP(Board Support Package)工程師的工作,比較接近硬件底層,一般就會暴力一點,從Linux啓動開始分析,得到運行態的Android系統概念。作爲高級的軟件設計師,看這個系統,可能更多的會是從系統結構出發,來了解這一系統設計上的技巧與原理。而更多的學習者或是愛好者,則會是直接從源代碼來着手,一行行代碼跟,最後也會理解到Android的系統的構架。

我們前面粗淺地介紹了Android的編程,並非只是浪費篇張,而是作爲我們這裏獨特的分析角度的一種鋪墊。我們的角度,由會先看應用程序,然後回過頭來再分析Android系統是如何支持應用程序運行的,如何通過精巧設計來完成基於功能來共享的Android系統的。這種角度可能更接近逆向工程,但這時我們也能瞭解到Android系統構架的各種組織部分,而且更容易理解爲什麼Android系統,會是這樣看上去有點怪的樣子。

作爲Android這樣的系統,或者是任何的智能手持設備上的操作系統,其實最大挑戰並非功能有多好、性能有多好,而首先面臨是安全性問題。桌面操作系統、服務器操作系統,在設計上的本質都是要儘可能提供一種多人共享化的系統,而所謂的智能手機操作系統,包含了個人的隱私信息,需要在保護這些隱私信息條件下來提供儘可能多的功能,這對安全性要求是一個很大的挑戰。特別像Android這樣,設計思路本就是用戶會隨時隨地地下載更多應用程序到系統裏,相當於是添加了更多功能到系統裏,這時就必須解決安全性問題。

以安全性角度出發,Android必然會引入一些特殊的設計,而這些設計帶來的一個直接後果就是會帶來一些連鎖性的設計上的難題:即要保持足夠的性能、同時又要控制功耗;即要功能豐富,同時又需要靈活的權限控制;需要儘可能實現簡單,但同時又需要提供靈活性。所有這些後續問題的解決,就構成了我們的Android系統。

1.1 Android應用程序運行環境所帶來的需求

我們在實現類似於Android應用程序的支撐環境時,必然會遇到一些比較棘手的問題,當我們能夠解決這些問題時,我們就得到了我們想要的Android系統。這類問題有:

  • 安全性:作爲一個功能可靈活拓展的系統,都將面臨安全性的挑戰。特別是Android又是一個開源操作系統,它不能像iOS那樣保持封閉來保持一定的安全性,又不能過於嚴格地限制開發者自由來達到安全性目的的,而且由於是嵌入式設備,還不能過於複雜,複雜則執行效率不夠。於是,安全性是Android這套運行機制最大的挑戰。
  • 性能:如果Android系統僅僅只是能夠提供桌面上的Java Applet,或是老式手機上的JAVA ME那樣的性能,則Android系統則會毫無吸引力。而我們的安全性設計,總會帶來一定的性能上的開銷,這時可能會導致Android系統的表現還不如標準的Java環境。
  • 跨進程交互:因爲有了Intent與Activity,我們的系統可能隨時隨地都在交互,而且會是跨進程的交互,我們需要了解Android裏獨特的進程間通信的方式。
  • 功耗控制:所有使用電池供電的設備,都天生地有強烈的功耗控制的需求。隨着處理器的能力加強,這時功耗會變得得更大,提供合理的功耗控制是一種天生地需求。
  • 功能:如前面所說,我們提供了安全性系統,又不能限制應用程序在使用上的需求,我們應該儘可能多地提供系統裏有的硬件資源,系統能夠提供的軟件層功能。
  • 可移植性:作爲一個開源的野心勃勃的智能手機操作系統,Android必須在可移植性上,甚至是跨平臺上都要表現良好。只有這樣,纔會有更多廠商樂意生產Android設備,開發者會提供更多應用程序,從而像今天這樣形成良性循環的Android生態環境。

我們再來針對這些應用程序所必須解決的問題,一個個地看Android是如何解決的。

1.2 安全性設計

安全性是軟件系統永恆的主題,其緊迫程度與功能的可拓展性成正比。越是可以靈活拓展的系統,越是需要一種強大的安全控制機制。世界上最安全的系統,就是一坨廢鐵,因爲永遠不可能有新功能加入,於是絕對安全。如果我們可以在其上編寫程序,則需要提供一定程度的安全控制,這時程序有好有壞,也有可能出錯。如果我們的軟件,會通過互聯網這樣的渠道可以獲得,則這種安全上需求會更強烈,因爲各種各樣的邪惡用意都有可能存在。大體上說,安全性控制會有四種需求:

  • 應用程序絕對不能對系統造成破壞。作爲一個系統,它的首要目標當然是共享給運行於其上的應用程序以各種系統級的功能。但如果這些應用程序,如果可以通過某種渠道對這個共享的系統造成破壞,這樣的系統去運行程序就沒有意義,因爲這時系統過於脆弱。
  • 應用程序之間,最好不能互相干擾。如果我們的應用程序,互相之間可以破壞對方的數據,則也不會有很好的可用性,因爲這時單個的應用程序也還是脆弱的。
  • 應用程序與系統,應用程序之間,應該提供共享的能力。在安全性機制下,我們也還是需要提供手段,讓應用程序與系統層之間、應用程序之間可以交互。
  • 還需要權限控制。我們可以通過權限來保護系統,一些非法的代碼在沒有權限的情況就無法造成破壞。在給不同應用程序提供系統層功能、提供共享時,應用程序有權限才能執行,沒有權限則會拒絕應用程序的訪問。

解決這類安全性需求的辦法,有繁有簡,像Android這樣追求簡潔,當然會使更簡潔的方案,同時這套方案得非常可靠。於是Android的運行模型就參考了久經40年考驗的單內核操作系統的安全模型。

爲了解釋得更清楚一點,我們先來從單內核操作系統開始說起。

在計算機開始出現的原始時期,我們的計算機是沒有所謂操作系統的。即使是我們的PC,也是由DOS開始,這樣的所謂操作系統,實際上也只是把一些常用的庫封閉起來,可以支持一個字符的操作界面,運行完一個任務會退回到操作界面,然後才能再運行下一個。這樣的操作系統性能不高,在做一些耗時操作則必須等待。

於是,大家又給CPU的中斷控制入手,一些固定的中斷源(比如時鐘中斷)會打斷CPU操作而強制讓CPU進入一段中斷處理代碼,在這種中斷處理代碼里加入一些代碼跳轉執行的邏輯,就允許代碼可以有多種執行序列。這樣的代碼序列被抽象成任務,而這種修改過的中斷處理代碼則被稱爲調度器,這樣得到的操作系統就叫多任務操作系統,因爲這些操作系統上運行的代碼像是有多個任務在並行一樣。

這種模型實現簡單,同時所謂的任務調度只是一次代碼跳轉,開銷也小,實際上我們今天也在廣泛地用它,比如大部分的實時操作系統。但在這種模式裏有個很致命的缺陷,就是任務間的內存是共享的,這就跟我們想達到的安全性機制不符,應用程序會有可能互相破壞。這是目前大家在實時操作系統做開發的一個通病,90%的比較有歷史的實時系統裏,大量使用全局變量(因爲內存是可以共享訪問的),幾乎到了無法維護的程度了。大部分情況下,決定代碼質量的,並非框架設計,而是寫代碼的人。當系統允許犯使用全局變量的錯誤,大家就會隔三差五的因爲不小心使用到,而累積到最後,就會是一大坨無法再維護的全局變量。

於是,在改進的操作系統裏,不但讓每個任務有獨立的代碼執行序列,同時也給它們虛擬出來獨立的內存空間。這時,系統裏的每個任務執行的時候,它自己會以爲整個世界裏只有它存在,於是很開心地執行,想怎麼玩就怎麼玩,就算把自己玩掛掉,對系統裏的其他執行的任務完全沒有影響,因爲這時每個任務所用的內存都是虛擬出來互相獨立的空間。當然,此時還會有一些共享的需求,比如訪問硬件,訪問一些所有任務都共享的數據,還有可能需要多個任務之間進行通信。這時,就再設立一種特權模式,在這種模式裏則使用同一份內存,來完成這種共享的需求。任務要使用這種模式,必須通過一層特殊的系統調用來進入。在Unix系列的操作系統裏,我們這裏的任務(task)被稱爲進程,給每個進程分配的獨立的內存區域,被稱爲用戶空間,而進程間特權模式下共享的那段空間,被稱爲內核空間。

特權模式裏的代碼經過精心設計,確保執行時不出錯,這時就完善了我們前面提到的安全性模型。

有了多任務的支持後,特別是歷史上計算機曾經極度昂貴,這時大家覺得只一個人佔着使用,有了多任務也很浪費,希望可以讓多人共享使用。這時,每個可以使用計算機的人,都分配一個標籤,可以在操作系統裏通過這個系統認可的標籤來運行任務。這時因爲所執行的任務都互相獨立的,加入標籤之後,這些任務在執行時也以不同標籤爲標識,這樣就可以根據標籤來進行權限的判斷,比如用戶創建的文件可以允許其他人操作,也可以不允許其他人操作。這種標籤通常只是一個整形值,被稱爲用戶ID(User ID,簡稱uid)。而用戶ID在權限上會有一定的共性,可以被組織成羣組,比如所有人是一個羣組、負責服務器維護的是一個羣組等等。於是可以在用戶ID基礎上進一步得一個羣組ID(Group ID,簡稱gid)的概念,用於有組織的共享。每個世界都需要超人,有了多用戶的操作系統,都必須要有一個超人可以求我們於水火,於是在uid/gid體系裏,就把uid與gid都爲0的用戶作爲超人,可以完成系統內任何操作,這一用戶也一般稱爲root。

基於調度器與內存空間獨立的設計,使我們得到了安全的多任務支持;而基於uid/gid的權限控制,使我們得到了多用戶支持。支持多用戶與多任務的操作系統,則是Unix系統。我們的Linux內核也屬於Unix系統的一種變種。在4、5年前,我們談及代碼時總是會說Unix/Linux系統的什麼什麼。除了性能上和功能上的細緻差異,Linux與其他Unix系統幾乎沒有區別(當然實現上差異很大)。只不過近年來Linux的表現實在太過威猛,以至於於我們每次不單獨把Linux提出來講,就顯得我們沒有表現出來滔滔江水般的崇拜之情。

等一下,難道我們這是在普及操作系統的基礎知識?

稍安勿躁,我們的這些操作系統知識在Android環境還是很有幫助的。Android系統是借用的Linux內核,於是這個原則性的東西是有效的。而更重要的是,Android的應用程序管理模型,與Unix系統進程模型有極大的相似性,更容易說明問題。

首先,我們的應用程序,不是需要一種安全的執行環境嗎?這時,Linux的進程就提供了良好的支持。假如所有的應用程序,都會以非root的uid權限運行(這時就應用程序就不會有權限去破壞系統級功能),又擁有各自獨立的進程空間,還可以通過系統調用來進行共享,這時,我們的安全性需求基本上就得到滿足了。當然,這時的Android系統,在構架上跟普通的嵌入式Linux方案沒有任何區別。

然後,我們再來看uid/gid。在傳統的服務器環境裏,Linux系統裏的uid/gid是一把利器,可以讓成千上萬的用戶登錄到上面,共享這臺機器上的服務,因爲它本就是服務器。即便是我們的PC(個人計算機),我們也有可能使用uid來使多個人可以安全地共享這臺機器。但是,放到嵌入式平臺,試想一下,咱們的手機,會不會也有多人共享使用?這是不可能的,手機是個私人性的設備。於是,uid在手機上便是廢物了。

而傳統Linux運行環境裏的另外一個問題是共享,作爲環境,是提供儘可能友好的環境,讓用戶可以共享這臺機器上的資源。比如文件系統,每個用戶共享機器裏的一切文件系統,除了唯一個私人目錄環境/home之外,所有用戶都可以共享一切文件環境。而無論是PC還是服務器,並不完全控制應用的訪問環境的,比如應用程序都是想上網就上網。這些在一個更私人化的嵌入式執行環境裏則會是災難性地,想像一下您的手機,您下載的應用程序,想上網就上網,想打電話就打電話,所有的隱私信息,就泄露得一乾二淨了。

在純正Linux環境裏做嵌入式系統,我們就面臨了更精細的權限控制的問題,需要通過權限來限制使用系統裏的資源的共享。我們也可以使用很複雜的方案,Linux本身完全是可以提供軍用級安全能力的,有selinux這個幾乎可以控制一切的方案。但這樣的方案實現起來很複雜,而且我們需要考慮到傳統Linux環境是需要一個管理員的,如果我們提供一套智能手機方案,同時還要爲這臺手機配置一個Linux系統管理員,這就太搞笑了。

在Android的設計裏,既然面臨這樣的挑戰,於是系統就採取了一種簡單有效的方案,這種方案就是基於uid/gid的“沙盒”(Sandbox)。既然uid/gid在嵌入式系統裏是個廢物,於是我們可以進行廢物利用。應用程序在安裝到系統裏時,則給每個機程都分配一個獨立的uid,如果有共享的需求,則給需要共享的應用程序分配同樣的gid,應用程序在執行時,我們都使用這種uid/gid來執行應用程序,則可以根據uid/gid來控制應用程序的能力。應用程序被安裝到系統後,它就不再使用完整的根目錄,只給它一個特定目錄作爲它的根目錄,每個應用程序的根目錄,不再是系統的/目錄,也是每個應用程序都不一樣的。讓每個應用程序使用不同根目錄環境,技術上倒不復雜,我們Linux上很多交叉編譯工具都是這樣方式運行的,這樣可以讓編譯環境完全與主機環境隔離,不會因爲主機環境裏提供的庫文件或是頭文件而造成編譯成功,但無法運行。

同時,當應用程序發生系統級請求時,都會根據uid/gid來決定它有什麼權限,有怎麼樣的權限。

這樣的模型就更加符合手機平臺的需求了,應用程序都以獨立的進程執行,這時應用程序無論寫得多糟糕多邪惡,它只能進行自殘,而不能破壞其他應用程序的執行環境。而它們的文件系統都是互相隔離的,則這種文件系統上有可能互相破壞的潛在風險也被解決掉了。這時,我們只需要通過一層庫文件,或是在內核里加一層機制可以讓所有系統功能的使用都需要經過uid/gid進行權限控制就可以了。

這時,造成的另一個問題是,如果是使用這樣的沙盒模型,則庫文件與資源,每個進程的私有空間裏都需要一份。另外,加入權限驗證到庫文件裏,或是到內核裏,都不是軟件工程上的合理選擇:庫文件的方式,需要引入一層權限驗證到每個應用程序,在通用性上會帶來挑戰;而修改Linux內核,則是下下策,核心組件是最不應該改動的,有可能影響內核的正常工作,也可能造成Android對內核的依賴,如果哪天我們不想使用Linux內核了怎麼辦?

假如,我們把Android核心層的功能,抽出來做成一個獨立的進程,這些就迎刃而解了。這個(或是可有多個)核心進程,運行在設備真實的根目錄(/)環境裏,也會有高於用戶態的權限。這時,應用程序可以使用一個最小化的根目錄,只需要應用程序執行所需要最基本環境,而對系統級的請求,這些應用程序都可以發出請求到核心進程來完成。這時,我們就可以在覈心進程裏進行權限控制了。

這樣,是不是就完美了?首先這種設計沒有依賴性,如果我們哪天把Linux內核幹掉,換用FreeBSD或是iOS使用的Darwin內核,這種設計也將是有效的(也就是我們有可能在iOS系統裏兼容Android應用程序)。而跟我們Unix的進程實現模型作對比的話,是不是有點熟悉?我們可以將應用程序視爲進程,核心進程視爲內核層,它們之類的通訊方式,則是用戶態發生的一層System Call層。是的,這時,我們相當於在用戶態環境又抽象出一套操作系統層。

但是,這跟我們前面介紹的Android環境好像是對不上號,我們的Android應用程序不是Java寫的嗎?這是安全性設計裏更高一級的方案,Java是一種基於虛擬機解析運行的環境,也常被稱爲託管環境(Hosted),因爲需要執行的邏輯並不直接與機器打交道,於是更安全。可惜的是,我們的Android系統從2.3之後,就可以使用一種叫Native Activity的邏輯實體,可以使用C++代碼來寫應用程序(主要用於編寫遊戲),這會一定程度上影響到這種託管機制帶來的安全性。但問題並不是很嚴重,我們在後面的內容會看到,實際上,Native Activity最終還是需要通過JNI調用,才能訪問到Android裏的系統功能,因爲這部分是由Java來實現的。

我們再來看看,真實的使用Java的Android系統。

從Java誕生之日起,就給予開發者無限的期望,這種語言天生具備的各種特點,曾在一段時間裏被認爲可以取代其他任何編程語言。

  • 跨平臺。Java是一種翻譯型語言,則具備了“編寫一次,到處運行”的能力,天生可以跨平臺。
  • 純面向對象。Java語言是一種高級語言,是完全面向對象的語言,不能使用指針、機器指令等底層技術,同時還帶自動垃圾回收機制,這樣可以使用代碼更健壯,編程更容易。
  • 重用性。由於純面向對象,Java語言的重用性很高,絕大部分源代碼幾乎不需要修改就可以直接使用。由於Java語言的易用性,很多開發的原型系統,都基於Java開發,幾乎所有的設計模式都在Java環境裏先實現。更因爲Java的歷史悠久,這種編程資源的積累,使它的重用性優勢更加明顯。
  • 安全的虛擬機。Java是基於虛擬機環境,虛擬機環境實際上是一種通過程序模擬出來的機器執行環境,更安全。所謂執行的代碼,只是程序所能理解的一種僞代碼,而且代碼代碼執行的最壞情況,也就僅能破壞虛擬機環境,完全影響不到運行的實際機器。Java虛擬機這樣的執行環境,一般被稱爲託管(Hosted)編程環境,可以進一步將執行代碼的潛在破壞能力限制到一個進程範圍內,就是像PC上的虛擬機,再怎麼威猛的病毒,最多也只是破壞了虛擬機的執行程序,完全影響不到實際的機器,像.Net,Java等都有這樣的加強的健壯性。
  • 性能。我們並不總是需要再編譯執行的,上次翻譯出來的代碼,我們也可以緩衝起來,下次支持調用機器代碼,這樣,Java的執行效率跟實際機器代碼的效率相關不大。因爲虛擬機是軟件,我們可以在虛擬機環境裏追蹤代碼的執行歷史,在這種基礎上,可以更容易進行虛擬機裏面代碼的執行狀況分析,甚至加入自動化優化,甚至可以超過真實機器的執行效率。比如,在Java環境裏,我們執行過一次handler.post(msgTouch),當下次通過msgTouch這個Object作爲參數來執行handler.post()方法時,我們完全不需要再執行一次,我們已經可以得到其執行的結果,我們只需要把結果操作再做一次。對於一些大運算量而重複性又比較高的代碼,我們的執行效率會得到成倍地提升。這種技術是運行態的優化技術,叫JIT(Just In Time)優化,在實際機器上是很難實現的,而幾乎所有使用虛擬機環境的編程語言都支持。

所有的Java語言在編程上的優勢,都使它可以成爲Android的首選編程環境,這跟WebOS選擇JavaScript、WindowsPhone選擇.Net都是同樣的道理。比如安全性,如果我們上面描述的執行模型是純Java的,則其安全性得到進一步提升。

但是,純Java環境不要說在嵌入式平臺上,就是在PC環境裏,也是以緩慢淡定著稱的,Java的自優化能力與處理器能力、內存大小成正比。使用純Java寫嵌入式方案,最終達到的結果也就只會是奇慢無比的JAVA ME。另外,Java是使用商業授權的語言,無論是在以前它的創建者Sun公司,還是現在已經收購Sun公司的Oracle,對Java一貫會收取不低的商業授權費用,一旦基於純粹Java環境來構建系統,最後肯定會造成Android系統不再是免費的午餐。既然Android所需要的環境只是Java語言本身,原始的Java虛擬機的授權又難以免費,這就迫使Android的開發者,開發出來另一套Java虛擬機環境,也就是我們的Dalvik虛擬機。

於是,我們基於多進程模型的系統構架,出於跨平臺、安全、編程的簡易性等多方面的原因,使我們得到的Android的設計方案成爲下面的這個新的樣子:核心進程這部分的實現我們還沒分析到,但應用程序此時在引入Java環境之後,都變成了通過Dalvik虛擬機所管理起來的更受限的環境,於是更安全。

而在Java環境裏,一個Java程序的主入口實際上還是傳統的main()入口的方式,而以main()方法作爲主入口,則意味着編程時,特別是圖形界面編程,需要用戶更多地考慮如何實現,如何進行交互。整個Java環境,全都是由一個個的有一定生存週期的對象組合而成,任何一個對象裏存在的static屬性的main()方法,則可以在Java環境裏作爲一個程序的主入口得到執行。如果使用標準Java編程,則我們的圖形界面編程將複雜得多,比如我們下面的使用Swing編程寫出來的,跟我們前面的Helloworld類似的例子:

importjavax.swing.JFrame;
importjavax.swing.JButton;
importjavax.swing.JOptionPane;
importjava.awt.event.ActionListener;
importjava.awt.event.ActionEvent;
publicclass Swing {
   publicstaticvoid main(String[] args) {
      JFrame frame = newJFrame("Hello Swing");
      JButton button = newJButton("Click Me");
      button.addActionListener(newActionListener() {
         publicvoidactionPerformed(ActionEvent event) {
            JOptionPane.showMessageDialog(null,
            String.format("<html>Hello from <b>Java</b><br/>" +
            "Button %s pressed", event.getActionCommand()));
         }
      });
      frame.getContentPane().add(button);
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.pack();
      frame.setVisible(true);
   }
}

這種方式裏,我們這個Helloworld的例子運行起來,會需要另外有窗口管理器來維護應用程序的狀態。通過main()進入之後,會有很大的實現上的自由度,過大的自由會導致最終編寫出來的應用程序,在代碼實現角度就已經會是千奇百怪的,同時,也加大了開發者在編程上的負擔。

而Android的編程上,應該繞開這些Java標準化編程上的侷限性,應用程序的行爲模式應該被規範化,同時也需要減小應用程序開發時的開銷。於是,我們的Android環境裏,Activity、Service、Content Provider、Broadcast Receiver,不光有共享功能的作用,實際上還起到了減少編程工作量的作用。大部分情況下,我們的通用行爲,已經被基類所實現,編程時只是需要將基類的功能按需求進行拓展。而從完全性的角度,我們也限制了應用程序的自由度,不再是從一個main()方法進入來實現所有的邏輯,而是僅能拓展一些特定的回調點,比如Activity的onStart(),onStop()等。從應用程序的角度來看,實際上編程是這個樣子的:

這種實現的技巧,對於Android系統來說,雖然應用程序會通過自己的classes.dex來實現一些各自不同的功能實現,但對於Android系統來說,這些不同實現都是土頭土腦的一個樣子,可以方便管理。有了這種方便管理之後,實際又能達到靈活的目的,比如我們去修改這種應用程序的管理框架時,應用程序則完全不需要改變,自然而然被管理起來。對於Android的應用程序,則通過這種抽象的工作,大規模地減小了工作量,如果我們很懶不願意浪費時間寫垃圾代碼,我們都可以借用Android內部已經提供的實現,而所謂的借用實際上是什麼都不用幹,由系統來自動完成。而這些具體的生命週期,我們在後面的內容裏再討論它們的意義。

引入了Java對Android來說,帶來的好處是明顯的,不說可移植性。就是對於安全性設計而言,就已經進一步強化了沙盒式的開發模型。但也還有懸而未決的問題,比如性能,比如說功耗控制。我們先來看Android的性能解決之道。

1.3 跨進程通信

從Android的“沙盒”模型,我們可以知道,這種多進程模型,必須需要一種某種通過在多個進程之間進行消息互通的機制。與其他系統不同的是,Android對於這種多進程之間通信的要求會更高。在Android世界裏,出於功能共享的需求,進程間的交互隨時都在進行,所以必然需要一種性能很高的解決方案。由於Android是使用Java語言作爲運行環境的,這樣的情況下,通訊機制就需要跟Java環境結合,可以提供面向對象的訪問方式,並且最好能夠與自動化垃圾回收整合到一起。

出於這些設計上的考慮,Android最終在選擇跨進程通信方式時使用了一種叫Binder的通信機制,絕大部分系統的運行,以及系統與應用程序的交互,都是通過Binder通信機制來完成的。但Android系統裏並不只有Binder,實際上,在與已有的解決方案進行整合的過程中,Android也可能會使用其他的跨進程通信機制,比如進程管理上會使用Signal,在處理3G Modem時會使用Socket通信,以及爲了使用BlueZ這套比較成熟的藍牙解決方案,也被迫使用Dbus。

任何一種計算環境,都會有實現的功能部件之間進行交互的需求。如果是統一的地址空間,這時解決方式可以簡單粗暴,比如通過讀寫全局變量等來完成。如果是各部件之間的地址空間互相獨立,這就會是多進程的操作系統環境,我們就需要某種在兩個進程空間之間傳遞消息的能力,這就是跨進程通信。一般在多進程操作系統裏,這都被稱爲進程間通信(Inter-Process Communication,IPC)。

在傳統的操作系統環境裏,IPC進程並非必須的選項,只是一種支持多進程環境設計的一種輔助手段。而由於這種非強制性的原因,IPC機制在長期的操作系統發展歷史被約定俗成的固化下來,會包括信號(Signal)、信號量(Semaphore)、管道(PIPE)、共享內存(Share Memory)、消息隊列(Message Queue)、套接字(Socket)等。

在這些IPC機制裏,對系統唯一的必須選項是信號。要是研究過系統的進程調度方向的,都知道信號不光是進程之間提供簡單消息傳遞的一種機制,同時也被用於進程管理。比如在Linux的進程管理框架裏,會通過SIGCHLD(Signal No. 20)的信號來管理父子進程,通過SIGKILL(Signal No. 9)來關閉進程,以及SIGSEGV(Signal No. 11)來觸發段錯誤。所以對於進程管理而言,信號同時也是一種進程管理機制,像SIGKILL,在內核進行進程調度時會立即處理,不進入到用戶態,也無法進行被屏蔽。既然構建於Linux內核之上,Android必然會使用到信號。

這些常用IPC機制構造出一種靈活的支持環境,可以讓多進程軟件的設計者可以靈活地選擇。但問題是過於靈活也是毛病,這樣的靈活機制也不能對上層交互消息作任何假設,只能作爲進程間交互的一種最裸的手段,在上層傳輸還需要進行封裝。每個多進程軟件在設計裏會自己設計一套內部通訊的方案,在與其他軟件有交互時再吵架協商出一套通用的交互方案,最後才能組合出一套整個操作系統級別的通訊機制。比如我們的Linux桌面環境裏,Firefox有自己的一套IPC機制、Gnome有自己的一套通過Gconf構建的IPC機制,OpenOffice又有另一套。這些軟件剛開始只關注自己的實現和改進時,這種IPC不統一的缺陷還不是那麼的嚴重,到後來需要協同工作時,由於IPC不統一造成的無法共同協作的問題就進一步嚴重起來,於是又通過一番痛苦的標準化過程,又形成了Linux環境裏統一的IPC機制—Dbus。

前車之鑑,後事之師,既然以前的Linux解決方案會因爲IPC機制不統一造成了缺陷,於是就不能重蹈覆轍了。於是Android權衡了設計上的需求,在性能、面向對象、以進程爲單位、可以自動進行垃圾回收的多方位的需求,於是選用了一種叫Binder的通信機制。Binder源自於Palm公司開源出來的一套IPC機制OpenBinder,正如名字上所看到的,Binder比OpenBinder拼寫簡化了一些,也是OpenBinder的一種簡化版。

OpenBinder本用於構建傳統的操作系統BeOS的系統級消息傳遞機制的,在BeOS退出歷史舞臺之後,又被Palm收購用於Palm的編程環境。但對於Android來說,OpenBinder的設計過於複雜,它本質是非常接近微軟的COM通信機制全功能級IPC,在Binder體系裏,可用於包裝系統裏的一切對象,同時也具備像CORBA那樣的可以綁定到多種語言支持環境裏,甚至它裏面還有shell環境支持!這對Android來講就有點殺雞用牛刀了。於是Android簡化了OpenBinder,只借用其內核驅動部分,上層則重新進行封裝,於是得到我們常說的Binder。從學習角度而言,我們只需要理解與分析Binder,也不應該被一般誤導性的說明去研究OpenBinder,OpenBinder絕大部分功能是在Android裏碰不到的,而Android裏的Binder實現與應用的完整源代碼,總共也沒幾行,分析起來更容易一些。

Binder構建於Linux內核裏的一個叫binder的驅動之上,系統裏所有涉及Binder通信的部分,都通過與/dev/binder的設備驅動交互來得到信息互通的功能。而binder本身只是一種借用內存作後端的“僞驅動”,並不對應到硬件,而只是作用於一段內存區域。通過這個binder驅動,最終在Android系統裏得到了進程間通信所需要的特點:

  • 高性能:基於Binder的通信全都使用ioctl來進行通信,做過實時系統的人都會知道,ioctl則會繞開文件系統緩衝,達到實時交互的目的。同時,基於Binder傳遞的數據,binder可以靈活地選用不同的機制傳遞數據,兩次複製(用於傳遞小數據量),一次複製(用於ServiceManager),零複製(通過IMemory對象共享,數據只在內核空間裏存在一份,用戶態進行內存映射),於是在效率上靈活性都可以很高。
  • 面向對象:與傳統的僅提供底層通信能力的IPC不同,Android系統是一種面向對象式開發的系統,於是需要更高層的具備面向對象的抽象能力的IPC機制。使用底層IPC加以封裝也不是不可以,像Mozilla這種設計也可以解決問題,但Android是作爲操作系統開發的,沒必要產生這樣的開銷。而使用Binder之後,就得到了天然的面向對象的IPC,在設計上與實現上都得到了簡化。使用Binder進行通信異常簡單,只需要通過直接或是間接的方式繼承IBinder基類即可。
  • 綁定到Dalvik虛擬機:一般的系統設計裏使用IPC,都是先將就底層,再設計更面向高層的IPC接口。還拿Mozilla來作例子的話,Mozilla裏的IPC先是提供C/C++編程接口,然後再綁定到其他高級語言,像Java、JavaScript、Python。而Android裏的Binder則不同,更多地是面向Java的,直接通過JNI綁定到Java層的Dalvik虛擬機,先滿足Java層的編程需求,然後再考慮到C/C++。使用C/C++語言來對Binder進行編程,更像是在Java底層的hack層,無論Binder怎麼被擴展,其服務對象都是Java層的,從這個意義上來說,Android的Binder,也是面向Java的。
  • 自動垃圾回收:在傳統的IPC機制裏,垃圾回收或者說內存回收,都是IPC編程框架之外的,IPC本身只負責通信,並不管理內存。而Android裏使用Binder也不一樣,因爲它是面向對象式的,於是更容易使用基於對象引用計數的內存管理。在使用Binder時,我們可能會經常遇到sp<>,wp<>這樣的模板類,這種模板則是直接服務於垃圾回收的,以Java語言的Soft Reference、Weak Reference來管理對象的生存週期。
  • 簡單靈活:與傳統IPC相比,或是標準OpenBinder實現相比,Binder都具備了實現更簡單靈活的特點。Binder在消息傳遞機制之上,附加了一定的內存管理功能,大大簡化了編程,同時在實現上又非常簡單,大家可以看到frameworks/base/libs/binder下的實現,也沒有幾行代碼,而對於驅動來說,也僅有一個drivers/stage/android/binder.c一個文件而已。這種簡單實現也給上層使用上提供了更多靈活性。
  • 面向進程:除了Signal之外,傳統IPC機制幾乎沒有辦法獲取訪問者(也就是進程)相關的信息,而只以內核態的文件描述符爲單位進行通信。但Binder在設計裏,就是以進程爲單位的,所有的信息在傳遞過程裏都以PID作爲分發的基礎,這時也爲Android的多進程模型提供了便捷性。在維護進程之間通信狀態時,Binder底層本身是可以得到進程是否已經出錯掛掉等信息。Binder這種特性也間接提供了一定的進程調度的能力,處於Binder通信過程裏的進程,在沒有Binder通信發生時,實際一直會處於休眠狀態,並不會盲目運行。
  • 安全:我們前面分析過基於進程空間的“沙盒”模型,它是基於uid/gid爲基礎的單進程模型。如果我們的Binder提供的傳遞機制也是以進程爲單位進行通信,這時這種進程間通信的模型也可以得以強化,進程運行時的uid/gid也會在Binder通信時被用於權限判斷。

當然,Android系統並非依賴Binder處理全部邏輯,在特殊性況下也會使用到其他的IPC機制,比如我們前面提到的RIL與BlueZ。在與進程管理模型相適配時,Android也會使用到信號,關閉應用程序是通過簡單的SIGKILL,還會在某些代碼調試部分的實現裏使用SIGUSR1、SIGUSR2等。但對於Android整個系統的核心實現而言,都會通過Binder來組織系統內部的消息傳遞與處理。應用程序之間的萬能信息Intent,實際上底層是使用的Binder,而應用程序要訪問到系統裏的功能,則會是使用Binder封裝出來的Remote Service。整個系統的基本交互圖如下所示:

在binder之上,framework裏會把binder通信的基本操作封裝到libbinder庫(frameworks/base/libs/binder)實現裏,當然libbinder則會通過JNI綁定到Dalvik虛擬機的執行環境。應用程序App1與App2之間進行通信,只會通過Binder之上再包裝出來的Intent來完成具體的交互。同時,在Android的系統實現的,我們提供的系統級功能,大部分會是Java實現的Remote Service,有一小部分是使用的C/C++實現的Native Service,而所有的這些Service,會通過一個叫ServiceManager的進程來進行管理,不論是Java實現的Service還是NativeService,都將使用addService()註冊到ServiceManager裏。當任何一個應用程序需要訪問系統級功能時,由會通過調用ServiceManager的getService方法取回一個系統級Service的實例,然後再與這些Service進行交互。

圖中我們可以看到,實線代表交互,虛線代表基於Binder的遠程交互,從上圖中我們也可以看出,實際上,系統裏基本上都不會有多個功能實現間的直接調用,所有的可執行部分,都只是通過libbinder封裝庫來與binder設備進行通信,而通信的結果就是實現了虛線所示的跨進程間的調用。當然,在代碼裏是看出來這些貓膩的,我們代碼上可能大部分都貌似是直接調用的,比如WIFI訪問:

mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
intwifiApState = mWifiManager.getWifiApState();
if (isChecked&& ((wifiApState == WifiManager.WIFI_AP_STATE_ENABLING) ||
(wifiApState == WifiManager.WIFI_AP_STATE_ENABLED))) {
mWifiManager.setWifiApEnabled(null, false);
} 

我們在代碼調用上根本看不到有Binder存在,就像是在當前進程裏調用了某個方法一樣。在這代碼示例裏,我們只是通過getSystemService()取回一個Context.WIFI_SERVICE的實例,然後就通過這一實例來訪問gitWifiApState()方法,但在底層實現上,getSystemService()與getWifiApState()都是運行另一個進程空間裏的代碼。這就是Android在實現上的厲害之處,雖然是簡單的封裝,但使我們的代碼具備了強大跨進程的功能。

而這些所謂的Service,跟我們在應用程序編程裏看到的Service基本上是一樣的,唯一的區別是會通過addService()加載到ServiceManager的管理框架裏,於是所有的進程都可以共享這種Service。在addService()成功之後,應用程序或是系統的其他部分,不需要再通過Intent來調用Service,而是可以直接通過getService()取回被調用的實例,然後直接進行跨進程調用。當然,Service概念的引入也給系統設計帶來了方便,這些Service,即可以以獨立進程的方式運行,也可以以線程方式被託管到另一個進程。在Android世界裏,這樣的技巧被反覆使用,一般情況下,都會將系統級的Service以線程方式運行在SystemServer這個系統進程裏,但如果有功能上或是穩定性上的考慮,則有可能以獨立的進程來執行Service,比如MediaServer、StageFlinger就都是這樣的例子。但對於調用它們的上層來說,都是透明的,無論哪種方式都沒有區別。

對於我們應用程序,我們最關心的可能還是Intent。其實,如何回到前面我們提及過的onSavedInstance這種序列化對象,我們就可以瞭解到Intent這種神奇機制是如何實現的了。我們前面說明了,其實onSavedInstance的類型是Bundle,是一個用於恢復Activity上下文現場的用於序列化處理的特殊對象,而這種所謂序列化,就是將其轉換成字典類型的結構,以key對應value的方式進行保存與讀取。Bundle類可以處理一些Java原始支持的數據類型,像String,Int等,可以使只使用這些數據類型的對象可以被序列化(字典化),於是可以將這樣的Bundle對象保存起來或是在跨進程間傳遞。同時,Bundle裏可以把另一個Bundle對象作爲其屬性變量,於是通過Bundle實際上可以描述所有對象,而且是以一種進程無關機器無關的狀態描述,這種結構天生就可以跨進程。

像我們的這例子裏,我們可以通過Bundle來描述Bundle_Master對象,但因爲Bundle_Master裏又包含另一個對象,於是使用Bundle_Key4來描述。這些在編程上則不是直接使用Bundle對象進行進程間的數據傳遞,因爲需要使用Binder來傳遞對象,於是我們又會得到一個Parcel類,把字典結構的Bundle對象,轉換成基於Binder IPC進制的Parcel對象。Parcel對象,實際上也沒有什麼特殊性,只是提供一種跨進程交互時的簡便性罷了,它內部使用一個Bundle來存儲中間結果,同時會通過Binder來標識訪問的活躍狀態,然後提供writeToParcel()與readFromParcel()兩個讀寫的接口方法也提供中間數據的讀寫。在使用Parcel進行封裝時,可以通過實現Parcelable接口來實現。最後得到的Parcel對象如下所示:

如上所示,實現了一個Parcel接口之後,我們得到的某個Parcelable的類,內部除了自己定義的屬性與方法之外,還需要提供一個static final的CREATOR對象。Static final表明,所有這些Parcelable的對象(比如我們例子裏的Intent),都會共享同一CREATOR對象來初始化對象,或是對象的數組。此時便提供了對象初始化方法的跨進程性。進一步需要提供兩個接口方法,readFromParcel()與writeToParcel()兩個方法,則此時對象就可以通過這兩個接口來進行不同上下文環境裏的讀寫,也就是對象本身可被“自動”序列化了。當然,對於Bundle內保存的中間數據,有可能也需要單個的讀寫,所以也會提供readValue()與writeValue()方法來進行內部屬性的讀寫,嚴格地說是Getter/Setter。我們這裏是以Intent爲例子說明的,這我們就可以看到Intent的起源,其實Intent也只是通過Binder來拓展出來的Bundle序列化對象而已。

在我們Intent對象裏包含的信息裏,絕大部分是Java的基本類型,比如Action,Data都是String,於是大部分情況下我們都是直接Parcelable的接口操作即可完成共享。而唯一需要進一步使用Parcel的,是Extras屬性,通過Extras可以傳遞極其複雜的對象爲參數,於是Extras便成爲Intent的屬性裏唯一會被Parcel進一步包裝的部分。

通過Binder可以很高效而且安全實現數據的傳遞,於是我們整個Android世界便毫無顧忌地使用Intent在多進程的環境裏運行,Intent的發送與處理,實際上一直處理多進程的交互環境裏,用戶本質上並沒有進程內與跨進程的概念。

對於應用程序之間是如此,對於應用程序與系統之間的交互也是如此,我們也需要達到一種簡單高效,同時讓應用程序看不到本地與遠程變化的效果。Android對於Java環境裏的系統級編程,也提供了良好的封裝,使Java環境裏編寫系統級服務也很簡單。

使用Binder之後,所有的跨進程的訪問,都可以通過一個Binder對象爲接口,屏蔽掉調用端與實現端的細節:

比如,我們這個應用環境裏,進程1訪問進程2的ClassOther時,我們並非直接訪問另一進程的對象,而在進程1的地址空間裏,會有一個objectRemote的遠程引用,這一遠程引用通過一個IBinder接口會引用到進程2的ClassRemote的實例。當在進程1裏訪問進程2的某個方法時,則直接會通過這一引用調用其ClassRemote裏實現的具體的方法。

這種命名方式跟我們的Android對不上號,這是因爲Android裏使用了一種叫Proxy的設計模式。Proxy設計模式,可以將設計與實現分離開。先定義接口類,調用端會通過一個Proxy接口來進行調用,而實現端則只通過接口類的定義來向外暴露其實現。

如圖所示,調用方會通過接口找到Proxy,而通過Proxy纔會具體地找到合適的Stub實現來進行通信。通過這樣的抽象,可以使代碼的互相合作時的耦合度被大大降低,Proxy實現部分可以根據調用的具體需要來向不同的實現發出調用請求,同時實現接口的部分也會因此而靈活,對同一接口可以有多個實現,也可以對接口之外的調用進行靈活地拓充。

對應到Android環境,我們就需要能夠將Binder的實現,進一步通過Proxy設計模式包裝起來,從而能夠提高實現上的靈活性。於是Android裏應用程序與系統的交互模型,就會變成這樣:

首先繼承自Binder的,不再是某一個對象,而是被拆分成Proxy與Stub兩個部分,Proxy部分被重命名爲xxxManager(與現實生活相符,我們總是不直接找某個人,而是找他的經驗來解決問題),而Stub部分則是通過繼承自Binder來得到一個Stub對象,這個Stub對象會作爲一個xxxService的屬性,從而對於某些功能xxx的存在週期,將通過Service的生命週期來進行管理。通過這樣的代碼重構的Proxy訪問模式,使我們的系統的靈活性得以大大提高,這也是爲什麼我們在系統裏可以見到大量的xxxManager.java,xxxService.java的原因,Manager供應用程序使用,屬於Proxy運行在應用程序進程空間,Service提供實現,一般運行在某個系統進程空間裏。

通過這種Proxy方式簡化之後,可能還是會有個代碼重複性的問題,比如我們的Manager怎麼寫、Service怎麼寫,總是會不停地重複寫這些交互的代碼,但對於通信過程而言,本質上從Manager的信息,怎麼到Service端,基本上是比較固化的。於是,Android裏提供了另一套機制,來簡化設計,減小需要重複的代碼量。

AIDL跟傳統的IDL有類似之處,但區別也很大,更加簡單,只用於Java環境,不能用於網絡環境裏。AIDL提供的功能,就是將這些重複性的通信代碼(Proxy與Stub的基本實現),固化到AIDL工具自動生成的代碼裏,這部分代碼用戶看不到,也不用去改寫它。

這時,在Android裏,寫一個系統級的Service,就不再有明顯的Proxy類、Service類,而變成調用端直接調用,實現端只提供Stub的具體實現即可。當然,我們系統環境裏的Proxy接口會複雜一些,需要考慮權限、共享、電源控制等多種需求,所以我們還是可以見到大量的Manager類的實現。

我們在Android系統編程裏研究得深入一點,也會發現Remote Service,也就是我們AIDL機制的妙用,很簡單地就可以提供一些方法或是屬性給另一個進程。Android系統,其實說白了,也就是大量這樣的基於AIDL的Remote Service的實現。

當我們遇到性能問題時,我們還可以在Binder之上,“黑”掉正常的Java代碼,全部使用C/C++來響應應用程序的請求,這就是所謂的Native Service。當然,我們就不能再使用AIDL了,AIDL是專用於Java環境裏跨進程調用所用的,必須自己手動來實現所有的Proxy接口與Stub接口。從這個意義上來說,Android也是Java操作系統,因爲使用C/C++反而是比較受限的編程環境。

於是,我們的跨進程通信的問題,幾乎可以得到完美解決了。可以再強調一下的是,在Android世界裏,如果是應用程序編程,可能只會與Activity打交道,因爲大部分情況下,我們只是把界面畫出來進行交互。而學習Android底層,需要做Android移植,甚至需要進行Android定製化的改進,對Binder、Remote Service、Native Service的深入理解與靈活使用則是關鍵點。不瞭解這些特點的Android系統工程,都將在性能、內存使用上遇到難以避免的麻煩,因爲系統本身都依賴於這種機制而存在。由於這樣的重要性,我們在後面會專門以這三個主題展開分析,說明在實踐運用時,我們可以怎麼靈活使用這三種功能部件。

Android應用程序開發以及背後的設計思想深度剖析(4)

緊接連載三,我們接下從性能的角度分別分析Android系統爲應用程序提供的支撐。

1.4 性能

Android使用Java作爲編程語言,這一直被認爲是一局雄心萬丈,但兇險異常的險棋。Java的好處是多,前面我們只是列舉了一小部分,但另一種普遍的現象是,Java在圖形編程上的應用環境並不是那麼多。除了出於Java編程的目的,我們是否使用過Java編寫的應用程序?我們的傳統手機一般都支持Java ME版本,有多少人用過?我們是否見過Java寫就的很流暢的應用程序?是否有過流行的Java操作系統?答應應該是幾乎爲零。通過這些疑問,我們就可以多少了解,Android這個Java移動操作系統難度了,還不用說,我們前面看到各種設計上的考量。

首先,得給Java正名,並非Java有多麼嚴重的性能問題,事實上,Java本是一種高效的運行環境。Android在設計上的兩個選擇,一個Linux內核,一個Java編程語言,都是神奇的萬能膠,從服務器、桌面系統、嵌入式平臺,它們都在整個計算機產業裏表現了強大的實力,在各個領域裏都霸主級的神器(當然,兩者在桌面領域表現都不是很好,都受到了Windows的強大壓力)。從Java可以充當服務器的編程語言,我們就可以知道Java本身的性能是夠強大的。標準的Java虛擬機還有個特點,性能幾乎會與內存與處理器能力成正比,內存越大CPU能力越強,當JIT的威力完全發揮出來時,Java語言的性能會比C/C++寫出來的代碼的性能還要好。

但是,這不是嵌入式環境的特色,嵌入式設備不僅是CPU與內存有限,而且還不能濫用,需要儘可能節省電源的使用。這種特殊的需求下,還需要使用Java虛擬機,就得到一個通用性極佳,但幾乎無用的JAVA ME環境,因爲JAVA ME的應用程序與本地代碼的性能差異實在太大了。在iPhone引發的“後PC時代”之前,大家沒得選擇,每臺手機上幾乎都支持,但使用率很低。而我們現在看到的Android系統,性能上是絕對不成問題,即便是面對iPhone這樣大量使用加速引擎的嵌入式設備,也沒有明確差異,這裏面竅門在哪裏呢?

我們前面也說明了,Java在嵌入式環境裏有性能問題,運行時過低的執行效率會引發使用體驗問題,同時還有版權問題,幾乎沒有免費的嵌入式Java解決方案。如果正向去解,困難重重,那我們是否可以嘗試逆向的解法呢?

在計算機工程領域,逆向思維從來都是重要武器之一,當我們在設計、算法上遇到問題時,正向解決過於痛苦時,可以逆向思考一下,往往會有奇效。比如我們做算法優化,拼了命使用各種手法提高性能,收效也不高,這時我們也可以考慮一下降低其他部分的性能。成功案例在Android裏就有,在Android環境裏,大量使用Thumb2這樣非優化指令,只優化性能需求最強的代碼,這時系統整體的性能反而比全局優化性能更好。我們做安全機制,試驗各種理論模型,最終都失敗於複雜帶來的開銷,而我們反向思考一下,借用進程模型來重新設計,這時我們就得到了“沙盒”這種模型,反而簡便快捷。只要思路開闊,在計算機科學裏條條大路通羅馬,應該是沒有解不掉的問題的。

回到Android的Java問題,如果我們正向地順着傳統Java系統的設計思路會走入痛苦之境,何不逆向朝着反Java的思路去嘗試呢?這時還帶來了邊際收益,Java語言的版權問題也繞開了。於是,天才的Android設計者們,得到了基於Dalvik虛擬機方案:

基於寄存器式訪問的Dalvik VM

Java語言天生是用於提升跨平臺能力的,於是在設計與優化時,考慮得更多是如何兼容更多的環境,於它可以運行在服務器級別的環境裏,也在運行在嵌入式環境。這種可伸縮的能力,便源自於它使用棧式的虛擬機實現。所謂的棧式虛擬機,就是在Java執行環境在邊解釋邊執行的時候,儘可能使用棧來保存操作數與結果,這樣可設計更精練的虛擬指令的翻譯器,性能很高,但麻煩就在於需要過多地訪問內存,因爲棧即內存。

比如,我們寫一個最簡單的Java方法,讀兩個操作數,先加,然後乘以2,最後返回:

 public int test01( int i1, int i2 ) {  
 int i3 = i1 + i2;  
 return i3 * 2;  
 } 

這樣的代碼,使用Java編譯器生成的.class文件裏,最後就會有這樣的僞代碼(虛擬機解析執行的代碼):

 0000: iload_1 // 01
 0001: iload_2 // 02
 0002: iadd
 0003: istore_3 // 03
 0004: iload_3 // 03
 0005: iconst_2 // #+02
 0006: imul
 0007: ireturn

出於棧式虛擬機的特點,這樣的僞指令追成的結果會操作到棧。比如iload_1, iload_2,iadd,istore_3,這四行,就是分別將參數1,2讀取到棧裏,再調用僞指令add將棧進行加操作,然後將結果寫入棧頂,也就是操作數3。這種操作模式,會使我們的虛擬機在僞指令解析器上實現起來簡單,因爲簡單而可以實現得高效,因爲僞指令必然很少(事實上,通用指令用8位的opcode就可以表達完)。可以再在這種設計基礎上,針對不同平臺進行具體的優化,由於是純軟件的算法,通用性非常好,無論是32位還是64位機器,都可以很靈活地針對處理器特點進行針對性的實現與優化。而且這種虛擬機實現,相當於所有操作都會在棧上有記錄,對每個僞指令(opcode)都相當於一次函數調用,這樣實現JIT就更加容易。

但問題是內存的訪問過多,在嵌入式設備上則會造成性能問題。嵌入式平臺上,出於成本與電池供電的因素,內存是很有限的,Android剛開始時使用的所謂頂級硬件配置,也纔不過192MB。雖然現在我們現在的手機動不動就上G,還有2G的怪獸機,但我們也不太可能使用太高的內存總線頻率,頻率高了則功耗也就會更高,而內存總線的功耗限制也使內存的訪問速度並不會太高,與PC環境還是有很大差異的。所以,直到今天,標準Java虛擬的ARM版本也沒有實現,只是使用JAVA ME框架裏的一種叫KVM的一種嵌入式版本上的特殊虛擬機。

基於Java虛擬機的實現,於是這時就可以使用逆向思維進行設計,棧式虛擬機的對立面就是寄存器式的。於是Android在系統設計便使用了Dan Bornstein開發的,基於寄存器式結構的虛擬機,命名源自於芬蘭的一個小鎮Dalvik,也就是Dalivk虛擬機。雖然Dalvik虛擬機在實現上有很多技巧的部分,有些甚至還是黑客式的實現,但其核心思想就是寄存器式的實現。

所謂的寄存器式,就是在虛擬機執行過程中,不再依賴於棧的訪問,而轉而儘可能直接使用寄存器進行操作。這跟傳統的編程意義上的基於寄存器式的系統構架還是有概念上的區別,即便是我們的標準的棧式的標準Java虛擬機,在RISC體系裏,我們也會在代碼執行被優化成大量使用寄存器。而這裏所指的寄存器,是指執行過程裏的一種算法上的思路,就是不依賴於棧式的內存訪問,而通過僞指令(opcode)裏的虛擬寄存器來進行翻譯,這種虛擬寄存器會在運行態被轉義成空閒的寄存器,進行直接數操作。如果這裏的解釋不夠清晰,大家可以把棧式看成是正常的函數式訪問,幾乎所有的語言都基於棧來實現函數調用,此時虛擬機裏每個僞指令(opcode),都類似於基於棧進行了函數調用。而基於寄存器式,則可以被看成是宏,宏在執行之前就會被編譯器翻譯成直接執行的一段代碼,這時將不再有過多的壓棧出棧操作,而是會儘可能使用立即數進行運算。

我們上面標準虛擬機裏編譯出來的代碼,經過dx工具轉出來的.dex文件,格式就會跟上面很不一樣:

代碼編譯僞指令

9000 0203 0000 add-int v0, v2, v3
da00 0002 0002 mul-int/lit8 v0, v0, #int 2 // #02
0f00 0004 return v0

對着代碼,我們可以看到(或者可以猜測),在Dalvik的僞指令體系裏,指令長度變成了16bit,同時,根本去除了棧操作,而通過使用00, 02, 03這樣的虛擬寄存器來進行立即數操作,操作完則直接將結果返回。大家如果對Dalvik的僞指令體系感興趣,可以參考Dalvik的指令說明:http://source.android.com/tech/dalvik/dalvik-bytecode.html
像Dalvik這樣虛擬機實現裏,當我們進行執行的時候,我們就可以通過將00,02,03這樣的虛擬寄存器,找一個空閒的真實的寄存器換上去,執行時會將立即數通過這些寄存器進行運算,而不再使用頻繁的棧進行存取操作。這時得到的代碼大小、執行性能都得到了提升。

如果寄存器式的虛擬機實現這麼好,爲什麼不大家都使用這種方式呢?也不是沒有過嘗試,寄存器試的虛擬機實現一直是學術研究上的一個熱點,只是在Dalvik虛擬機之前,沒有成功過。寄存器式,在實際應用中,未必會比棧式更高效,而且如果是通用的Java虛擬機,需要運行在各種不同平臺上,寄存器式實現還有着天生的缺陷。比如說性能,我們也看到在Dalvik的這種僞指令體系裏,使用16位的opcode用於實現更多支持,沒有棧訪問,則不得不靠增加opcode來彌補,再加需要進行虛擬寄存器的換算,這時解析器(Interpreter)在優化時就遠比簡單的8位解析器要複雜得多,複雜則優化起來更困難。從上面的函數與宏的對比裏,我們也可以看到寄存器實現上的毛病,代碼的重複量會變大,原來不停操作棧的8bit代碼會變成更長的加寄存器操作的代碼,理論上這種代碼會使代碼體系變大。之所以在前面我們看到.dex代碼反而更精減,只不過是Dalvik虛擬機進行了犧牲通用性代碼固化,這解決了問題,但會影響到僞代碼的可移植性。在棧式虛擬機裏,都使用8bit指令反覆操作棧,於是理論上16bit、32bit、64bit,都可以有針對性的優化,而寄存器式則不可能,像我們的Dalvik虛擬機,我們可以看到它的僞代碼會使用32位裏每個bit,這樣的方式不可能通用,16bit、32bit、64bit的處理器體系裏,都需要重新設計一整套新的指令體系,完全沒有通用性。最後,所有的操作都不再經過棧,則在運行態要得到正確的運算操作的歷史就很難,於是JIT則幾乎成了不可能完成的任務,於是Dalvik虛擬機剛開始則宣稱JIT是沒有必要,雖然從2.2開始加入了JIT,但這種JIT也只是統計意義上的,並不是完整意義上的JIT運算加速。所有這些因素,都導致在標準Java虛擬機裏,很難做出像Dalvik這樣的寄存器式的虛擬機。

幸運的是,我們上面說的這些限制條件,對Android來說,都不存在。嵌入式環境裏的CPU與內存都是有限的資源,我們不太可能通過全面的JIT提升性能,而嵌入式環境以寄存器訪問基礎的RISC構架爲主,從理論上來說,寄存器式的虛擬機將擁有更高的性能。如果是嵌入式平臺,則基本上都是32位的處理器,而出於功耗上的限制,這種狀況將持續很長一段時間,於是代碼通用性的需求不是那麼高。如果我們放棄全面支持Java執行環境的兼容性,進一步通過固化設計來提升性,這時我們就可以得到一個有商用價值的寄存器式的虛擬機,於是,我們就得到了Dalvik。

Dalvik虛擬機的性能上是不是比傳統的棧式實現有更高性能,一直是一個有爭議的話題,特別是後面當Dalvik也從2.2之後也不得不開始進行JIT嘗試之後。我們可以想像,基於前面提到的寄存器式的虛擬機的實現原理,Dalvik虛擬機通過JIT進行性能提升會遇到困難。在Dalvik引入JIT後,性能得到了好幾倍的提升,但Dalvik上的這種JIT,並非完整的JIT,如果是棧式的虛擬機實現,這方面的提升會要更強大。但Dalvik實現本身對於Android來講是意義非凡的,在Java授權上繞開了限制,更何況在Android誕生時,嵌入式上的硬件條件極度受限,是不太可能通過棧式虛擬機方式來實現出一個性能足夠的嵌入式產品的。而當Android系統通過Dalvik虛擬機成功殺出一條血路,讓大家都認可這套系統之後,圍繞Android來進行虛擬機提速也就變得更現實了,比如現在也有使用更好的虛擬機來改進Android的嘗試,比如標準棧式虛擬機,使用改進版的Java語言的變種,像Scalar、Groovy等。

我們再看看,在寄存器式的虛擬機之外,Android在性能設計上的其他一些特點。

以Android API爲基礎,不再以Java標準爲基礎

當我們對外提供的是一個系統,一種平臺之時,就必須要考慮到系統的可持續升級的能力,同時又需要保持這種升級之後的向後兼容性。使用Java語言作爲編程基礎,使我們的Android環境,得到了另一項好處,那就是可兼容性的提升。

在傳統的嵌入式Linux方案裏,受限於有限的CPU,有限的內存,大家還沒有能力去實施一套像Android這樣使用中間語言的操作系統。使用C語言還需要加各種各樣的加速技巧才能讓系統可以運行,基至有時還需要通過硬件來進行加速,再在這種平臺上運行虛擬機環境,則很不靠譜。這樣的開發,當然沒有升級性可言,連二次開發的能力都很有限,更不用說對話接口了,所謂的升級,僅僅是增加點功能,修改掉一些Bug,再加入再多Bug。而使用機器可以直接執行的代碼,就算是能夠提供升級和二次開發的能力,也會有嚴重問題。這樣的寫出來的代碼,在不同體系架構的機器(比如ARM、X86、MIPS、PowerPC)上,都需要重新編譯一次。更嚴重的是,我們的C或者C++,都是通過參數壓棧,再進行指令跳轉來進行函數調用的,如果升級造成了函數參數變動,則還必須修改所開發的源代碼,不然會直接崩潰掉。而比較幸運的是,所有的嵌入式Linux方案,在Android之前,都沒有流行開,比較成功的最多也不過自己陪自己玩,份額很小,大部分則都是紅顏薄命,出生時是demo,消亡時也是demo。不然,這樣的產品,將來維護起來也會是個很吐血的過程。

而Android所使用的Java,從一開始就是被定位於“一次編寫,到處運行”的,不用說它極強大的跨平臺能力,就是其升級性與兼容性,也都是Java語言的制勝法寶之一。Java編譯生成的結果,是.class的僞代碼,是需要由虛擬器來解析執行的,我們可以提供不同體系構架裏實現的Java虛擬機,甚至可以是不同產商設計生產的Java虛擬機,而這些不同虛擬機,都可以執行已經編譯過的.class文件,完全不需要重新編譯。Java是一種高級語言,具有極大的重用性,除非是極端的無法兼容接口變動,都可以通過重載來獲得更高可升級能力。最後,Java在歷史曾應用於多種用途的運行環境,於是定義針對不同場合的API標準,這些標準一般被稱爲JSR(Java Specification Request),特別是嵌入式平臺,針對帶不帶屏幕、屏幕大小,運算能力,都定義了詳細而複雜的標準,符合了這些標準的虛擬機應該會提供某種能力,從而保證符合同一標準的應用程序得以正常執行。我們的JAVA ME,就是這樣的產物,在比較長週期內,因爲沒有可選編程方案,JAVA ME在諸多領域裏都成爲了工業標準,但性能不佳,實用性較差。

JAVA ME之所以性能會不佳的一個重要原因,是它只是一種規範,作爲規範的東西,則需要考慮到不同平臺資源上的不同,不容易追求極致,而且在長期的開發與使用的歷史裏,一些歷史上的接口,也成爲了進一步提升的負擔。Android則不一樣,它是一個新生事物,它不需要遵守任何標準,即使它能夠提供JAVA ME兼容,它能得到資源回報也不會大,而且會帶來JAVA ME的授權費用。於是,Android在設計上就採取了另一次的反Java設計,不兼容任何Java標準,而只以Android的API作爲其兼容性的基礎,這樣就沒有了歷史包袱,可以輕裝上陣進行開發,也大大減小了維護的工作量。作爲一個Java寫的操作系統,但又不兼容任何Java標準,這貌似是比較諷刺的,但我們在整個行業內四顧一下,大家應該都會發現這樣一種特色,所有不支持JAVA ME標準的系統,都發展得很好,而支持JAVA ME標準,則多被時代所淘汰。這不能說JAVA ME有多大的缺陷,或是晦氣太重,只不過靠支持JAVA ME來提供有限開發能力的系統,的確也會受限於可開發能力,無法走得太遠罷了。

這樣會不會導致Java寫的代碼在Android環境裏運行不起來呢?理論上來說不會。如果是一些Java寫的通用算法,因爲只涉及語言本身,不存在問題。如果是代碼裏涉及一些基本的IO、網絡等通用操作,Android也使用了Apache組織的Harmony的Java IO庫實現,也不會有不兼容性。唯一不能兼容的是一些Java規範裏的特殊代碼,像圖形接口、窗口、Swing等方面的代碼。而我們在Android系統裏編程,最好也可以把算法與界面層代碼分離,這樣可以增加代碼複用性,也可以保證在UI編程上,保持跟Android系統的兼容性。

Android的版本有兩層作用,一是描述系統某一個階段性的軟硬件功能,另外就是用於界定API的規範。描述功能的作用,更多地用於宣傳,用於說該版本的Android是個什麼東西,也就是我們常見的食物版本號,像éclair(2.0,2.1),Froyo(2.2), Gingerbread(2.3),Icecream Sandswich(4.0),Jelly Bean(4.1),大家都可以通過這些美味的版本號,瞭解Android這個版本有什麼功能,有趣而易於宣傳。而對於這樣的版本號,實際上也意味着API接口上的升級,會增加或是改變一些接口。

所謂的API版本,是位於應用程序層與Framework層之間的一層接口層,如下所示:

應用程序只通過AndroidAPI來對下進行訪問,而我們每一個版本的Android系統,都會通過Framework來對上實現一套完整的API接口,提供給應用程序訪問。只要上下兩層這種調用與被調用的需求能夠在一定範圍內合拍,應用程序所需要的最低API版本低於Framework所提供的版本,這時應用程序就可以正常執行。從這個意義來說,API的版本,更大程度算是Android Framework實現上的版本,而Android系統,我們也可以看成就是Android的Framework層。

這種機制在Android發展過程中一直實施得很好,直到Android 2.3,都保持了向前發展,同時也保持向後兼容。Android 2.3也是Android歷史上的一個里程碑,一臺智能手機所應該實現的功能,Android2.3都基本上完成了。但這時又出現了平板(pad)的系統需求,從2.3又發展出一個跟手機平臺不兼容的3.0,然後這兩個版本再到4.0進行融合。在2.3到4..0,因爲運行機制都有大的變動,於是這樣的兼容性遇到了一定的挑戰,現在還是無法實現100%的從4.0到2.3的兼容。

只兼容自己API,是Android系統自信的一種體現,同時,也給它帶來另一個好處,那就是可以大量使用JNI加速。

大量使用JNI加速

JNI,全稱是Java本地化接口層(Java Native Interface),就是通過給Java虛擬機加動態鏈接庫插件的方式,將一些Java環境原本不支持功能加入到系統中。

我們前面說到過Dalvik虛擬機在JIT實現上有缺陷,這點在Android設計人員的演示說明裏,被狡猾地掩蓋了。他們說,在Android編程裏,JIT不是必須的,Android在2.2之前都不提供JIT支持,理由是應用程序不可能太複雜,同時Android本身是沒有必要使用JIT,因爲系統裏大部分功能是通過JNI來調用機器代碼(Native代碼)來實現的。這點也體現了Android設計人員,作爲技術狂熱者的可愛之處,類似這樣的錯誤還不少,比如Android剛開始的設計初衷是要改變大家編程的習慣,要通過Android應用程序在概念上的封裝,去除掉進程、線程這樣底層的概念;甚至他們還定義了一套工具,希望大家可以像玩積木一樣,在圖形界面裏拖拉一下,在完全沒有編程背景的情況下也可以編程;等等。這些錯誤當然也被Android強大的開源社區所改正了。但對於Android系統性能是由大量JNI來推進的,這點診斷倒沒有錯,Android發展也一直順着這個方向在走。

Android系統實現裏,大量使用JNI進行優化,這也是一個很大的反Java舉動,在Java的世界裏,爲了保持在各個環境的兼容性,除了Java虛擬機這個必須與底層操作系統打交道的執行實體,以及一些無法繞開底層限制的IO接口,Java環境的所有代碼,都儘可能使用Java語言來編寫。通過這種方式,可以有效減小平臺間差異所引發的不兼容。在Java虛擬機的開發文檔裏,有詳盡的JNI編程說明,同時也強烈建議,這樣的編程接口是需要避免使用的,使用了JNI,則會跟底層打上交道,這時就需要每個體系構架,每個不同操作系統都提供實現,並每種情況下都需要編譯一次,維護的代價會過高。

但Android則是另一個情況,它只是借用Java編程語言,應用程序使用的只是Android API接口,不存在平臺差異性問題。比如我們要把一個Android環境運行在不那麼流行的MIPS平臺之上,我們需要的JNI,是Android源代碼在MIPS構架上實現並編譯出來的結果,只要API兼容,則就不存在平臺差異性。對於系統構架層次來說,使用JNI造成的差異性,在Framework層裏,就已經被屏蔽掉了:

如上圖所示,Android應用程序,只知道有Framework層,通過API接口與Framework通信。而我們底層,在Library層裏,我們就可以使用大量的JNI,我們只需要在Framework向上的部分保持統一的接口就可以了。雖然我們的Library層在每個平臺上都需要重新編譯(有些部分可能還需要重新實現),但這是平臺產商或是硬件產商的工作,應用程序開發者只需要針對API版本寫代碼,而不需要關心這種差異性。於是,我們即得到高效的性能(與純用機器實現的軟件系統沒有多少性能差異),以使用到Java的一些高級特性。

單進程虛擬機

使用單進程虛擬機,是Android整個設計方案裏反Java的又一表現。我們前面提到了,如果在Android系統裏,要構建一種安全無憂的應用程序加載環境,這時,我們需要的是一種以進程爲單位的“沙盒(Sandbox)”模型。在實現這種模型時,我們可以有多種選擇,如果是遵循Java原則的話,我們的設計應該是這個樣子的:

我們運行起Java環境,然後再在這個環境裏構建應用程序的基於進程的“小牢房”。按照傳統的計算機理論,或是從資源有效的角度考慮,特別是如果需要使用棧式的標準Java虛擬機,這些都是沒話說的,只有這種構建方式才最優。

Java虛擬機,之所以被稱爲虛擬機,是因爲它真通過一個用戶態的虛擬機進程虛擬出了一個虛擬的計算機環境,是真正意義上的虛擬機。在這個環境裏,執行.class寫出來的僞代碼,這個世界裏的一切都是由對象構成的,支持進程,信號,Stream形式訪問的文件等一切本該是實際操作系統所支持的功能。這樣就抽象出來一個跟任何平臺無關的Java世界。如果在這個Java虛擬世界時打造“沙盒”模式,則只能使用同一個Java虛擬機環境(這不光是進程,因爲Java虛擬機內部還可以再創建出進程),這樣就可以通過統一的垃圾收集器進行有效的對象管理,同時,多進程則內存有可能需要在多個進程空間裏進行復制,在使用同一個Java虛擬機實例裏,纔有可能通過對象引用減小複製,不使用統一的Java虛擬機環境管理,則可複用性就會很低。

但這種方案,存在一些不容易解決的問題。這種單Java虛擬機環境的假設,是建立在標準Java虛擬機之上的,但如前面所說,這樣的選擇困難重重,於是Android是使用Dalvik虛擬機。這種單虛擬機實例設計,需要一個極其強大穩定的虛擬機實現,而我們的Dalvik虛擬機未必可以實現得如此功能複雜同時又能保證穩定性(簡單穩定容易,複雜穩定則難)。Android必須要使用大量的JNI開發,於是會進一步破壞虛擬機的穩定性,如果系統裏只有一個虛擬機實例,則這個實例將會非常脆弱。當在Java環境裏的進程,有惡意代碼或是實現不當,有可能破壞虛擬機環境,這時,我們只能靠重啓虛擬機來完成恢復,這時會影響到虛擬機裏運行的其他進程,失去了“沙盒”的意義。最後,虛擬機必須給預足夠的權限運行,才能保證核心進程可訪問硬件資源,則權限控制有可能被某個惡意應用程序破壞,從而失去對系統資源的保護。

Android既然使用是非標準的Dalvik虛擬機,我們就可以繼續在反Java的道路上嘗試得更遠,於是,我們得到的是Android裏的單進程虛擬機模型。在基於Dalvik虛擬機的方案裏,虛擬機的作用退回到了解析器的階段,並不再是一個完整的虛擬機,而只是進程中一個用於解析.dex僞代碼的解析執行工具:

在這種沙盒模式裏,每個進程都會執行起一個Dalvik虛擬機實例,應用程序在編程上,只能在這個受限的,以進程爲單位的虛擬機實例裏執行,任何出錯,也隻影響到這個應用程序的宿主進程本身,對系統,對其他進程都沒有嚴重影響。這種單進程的虛擬機,只有當這個應用程序被調用到時才予以創建,也不存在什麼需要重啓的問題,出了錯,殺掉出錯進程,再創建一個新的進程即可。基於uid/gid的權限控制,在虛擬機之外的實現,應用程序完全不可能通過Java代碼來破壞這種操作系統級別的權限控制,於是保護了系統。這時,我們的系統設計上反有了更高的靈活度,我們可以放心大膽地使用JNI開發,同時核心進程也有可能是直接通過C/C++寫出來的本地化代碼來實現,再通過JNI提供給Dalvik環境。而由於這時,由於我們降低了虛擬機在設計上的複雜程序,這時我們的執行性能必然會更好,更容易被優化。

當然,這種單進程虛擬機設計,在運行上也會帶來一些問題,比如以進程以單位進行GC,數據必然在每個進程裏都進行復制,而進程創建也是有開銷的,造成程序啓動緩慢,在跨進程的Intent調用時,嚴重影響用戶體驗。Java環境裏的GC是標準的,這方面的開銷倒是沒法繞開,所以Android應用程序編程優化裏的重要一招就是減小對象使用,繞開GC。但數據復製造成的冗餘,以及進程創建的開銷則可以進行精減,我們來看看Android如何解決這樣的問題。

儘可能共享內存

在幾乎所有的Unix進程管理模型裏,都使用延時分配來處理代碼的加載,從而達到減小內存使用的作用,Linux內核也不例外。所謂的進程,在Linux內核裏只是帶mm(虛存映射)的task_struct而已,而所謂的進程創建,就是通過fork()系統調用來創建一個進程,而在新創建的進程裏使用execve()系列的系統調用來執行新的代碼。這兩個步驟是分兩步進行,父進程調用fork(),子進程裏調用execve():

上圖的實線代表了函數調用,虛線代碼內存引用。在創建一個進程執行某些代碼時,一個進程會調用fork(),這個fork()會通過libc,通過系統調用,轉入內核實現的sys_fork()。然後在sys_fork()實現裏,這時就會創建新的task_struct,也就是新的進程空間,形成父子進程關係。但是,這時,兩個進程使用同一個進程空間。當被創建的子進程裏,自己主動地調用了execve()系列的函數之後,這時纔會去通過內核的sys_execve()去嘗試解析和加載所要執行的文件,比如a.out文件,驗證權限並加載成功之後,這時纔會建立起新的虛存映射(mm),但此時雖然子進程有了自己獨立的進程空間,並不會分配實際的物理內存。於是有了自己的進程空間,當下次執行到時,纔會通過一次缺頁中斷加載a.out的代碼段,數據段,而此時,libc.so因爲兩個進程都需要使用,於是會直接通過一次內存映射來完成。

通過Linux的進程創建,我們可以看到,進程之間雖然有獨立的空間,但進程之間會大量地通過頁面映射來實現內存頁的共享,從而減小內存的使用。雖然在代碼執行過程中都會形成它自己的進程空間,有各自獨立的內存類,但對於可執行文件、動態鏈接庫等這些靜態資源,則在進程之間會通過頁面映射進行共享進行共享。於是,可以得到的解決思路,就是如何加強頁面的共享。

加強共享的簡單一點的思路,就是人爲地將所有可能使用到的動態鏈接庫.so文件,dalvik虛擬機的執行文件,都通過強制讀一次,於是物理內存裏便有了存放這些文件內容的內存頁,其他部分則可以通過mmap()來借用這些被預加載過的內存頁。於是,當我們的用戶態進程被執行時,雖然還是同樣的執行流程,但因爲內存裏面有了所需要的虛擬機環境的物理頁,這時缺頁中斷則只是進行一次頁面映射,不需要讀文件,非常快就返回了,同時由於頁面映射只是對內存頁的引用,這種共享也減小實際物理頁的使用。我們將上面的fork()處理人爲地改進一下,就可以使用如下的模式:

這時,對於任一應用程序,在dalvik開始執行前,它所需要的物理頁就都已經存在了,對於非系統進程的應用程序而言,它所需要使用的Framework提供的功能、動態鏈接庫,都不會從文件系統裏再次讀取,而只需要通過page_fault觸發一次頁面映射,這時就可以大大提供加載時的性能。然後便是開始執行dalvik虛擬,解析.dex文件來執行應用程序的獨特實現,當然,每個classes.dex文件的內容則是需要各自獨立地進行加載。我們可以從.dex文件的解析入手,進一步加強內存使用。

Android環境裏,會使用dx工具,將.class文件翻譯成.dex文件,.dex文件與.class文件,不光是僞指令不同,它們的文件格式也完全不同,從而達到加強共享的目的。標準的Java一般使用.jar文件來包裝一個軟件包,在這個軟件裏會是以目錄結構組織的.class文件,比如org/lianlab/hello/Hello.class這樣的形式。這種格式需要在運行時進行解壓,需要進行目錄結構的檢索,還會因爲.class文件裏分散的定義,無法高效地加載。而在.dex文件裏,所有.class文件裏實現的內容,會合併到一個.dex文件裏,然後把每個.class文件裏的信息提取出來,放到同一個段位裏,以便通過內存映射的方式加速文件的操作與加載。

這時,我們的各個不同的.class文件裏內容被檢索併合併到同一個文件裏,這裏得到的.dex文件,有特定情況下會比壓縮過的.jar文件還要小,因爲此時可以合併不同.class文件裏的重複定義。這樣,在可以通過內存映射來加速的基礎上,也從側面降低了內存的使用,比如用於.class的文件系統開銷得到減小,用於加載單個.class文件的開銷也得以減小,於是得到了加速的目的。

這還不是全部,需要知道,我們的dalvik不光是一個可執行的ELF文件而已,還是Java語言的一個解析器,這時勢必需要一些額外的.class文件(當然,在Android環境裏,因爲使用了與Java虛擬機不兼容的Dalvik虛擬機,這樣的.class文件也會被翻譯成.dex文件)裏提供的內容,這些額外的文件主要就是Framework的實現部分,還有由Harmony提供的一些Java語言的基本類。還不止於此,作爲一個系統環境,一些特定的圖標,UI的一些控件資源文件,也都會在執行過程裏不斷被用到,最好我們也能實現這部分的預先加載。出於這樣的目的,我們又會面臨前面的兩難選擇,改內核的page_fault處理,還是自己設計。出於設計上的可移植性角度考慮,還是改設計吧。這時,就可以得到Android裏的第一個系統進程設計,Zygote。

我們這時對於Zygote的需求是,能夠實現動態鏈接庫、Dalvik執行進程的共享,同時它最好能實現一些Java環境裏的庫文件的預加載,以及一些資源文件的加載。出於這樣的目的,我們得到了Zygote實現的雛形:

這時,Zygote基本上可以滿足我們的需求,可以加載我們運行一個應用程序進程除了classes.dex之外的所有資源,而我們前面也看到.dex這種文件格式本身也被優化過,於是對於頁面共享上的優化基本上得以完成了。我們之後的操作完全可以依賴於zygote進程,以後的設計裏,我們就把所有的需要特權的服務都在zygote進程裏實現就好了。

有了zygote進程則我們解決掉了共享的問題,但如果把所有的功能部分都放在Zygote進程裏,則過猶不及,這樣的做法反而更不合適。Zygote則創建應用程序進程並共享應用程序程序所需要的頁,而並非所有的內存頁,我們的系統進程執行的絕大部分內容是應用程序所不需要的,所以沒必要共享。共享之後還會帶來潛在問題,影響應用程序的可用進程空間,另外惡意應用程序則可以取得我們系統進程的實現細節,反而使我們的辛辛苦苦構建的“沙盒”失效了。

Zygote,英文願意是“孵化器”的意思,既然是這種名字,我們就可以在設計上儘可能保持其簡單性,只做孵化這麼最簡單的工作,更符合我們目前的需求。但是還有一個實現上的小細節,我們是不是期望zygote通過fork()創建進程之後,每個應用程序自己去調用exec()來加載dalvik虛擬機呢?這樣實現也不合理,實現上很醜陋,還不安全,一旦惡意應用程序不停地調用到zygote創建進程,這時系統還是會由於創建進程造成的開銷而耗盡內存,這時系統也還是很脆弱的。這些應該是由系統進程來完成的,這個系統進程應該也需要兼職負責Intent的分發。當有Intent發送到某個應用程序,而這個應用程序並沒有被運行起來時,這時,這個系統進程應該發一個請求到Zygote創建虛擬機進程,然後再通過系統進程來驅動應用程序具體做怎麼樣的操作,這時,我們的Android的系統構架就基本上就緒了。在Android環境裏,系統進程就是我們的System Server,它是我們系統裏,通過init腳本創建的第一個Dalvik進程,也就是說Android系統,本就是構建在Dalvik虛擬機之上的。

在SystemServer裏,會實現ActivityManager,來實現對Activity、Service等應用程序執行實體的管理,分發Intent,並維護這些實體生命週期(比如Activity的棧式管理)。最終,在Android系統裏,最終會有3個進程,一個只負責進程創建以提供頁面共享,一個用戶應用程序進程,和我們實現一些系統級權限才能完成的特殊功能的SystemServer進程。在這3種進程的交互之下,我們的系統會堅固,我們不會盲目地創建進程,因爲應用程序完全不知道有進程這回事,它只會像調用函數那樣,調用一個個實現具體功能的Activity,我們在完成內存頁共享難題的同時,也完成Android系統設計的整體思路。

這時對於應用程序處理上,還剩下最後一個問題,如果加快應用程序的加載。

應用程序進程“永不退出”

雖然我們擁有了內存頁的預加載實現,但這還是無法保證Android應用程序執行上的高效性的。根據到現在爲此我們分析到的Android應用程序支持,我們在這方面必將面臨挑戰。像Activity之間進行跳轉,我們如果處理跳轉出的Activity所依附的那個進程呢?直接殺死掉,這時,當我們從被調用Activity返回時怎麼辦?

這也會是個比較複雜的問題。一是前一個進程的狀態如何處理,二是我們又如何對待上一個已經暫時退出執行的進程。

我們老式的應用程序是不存在這樣的問題的,因爲它不具備跨進程交互的能力,唯一的有可能進行跨進程交互的方式是在應用程序之間進行復制/粘貼操作。而對於進程內部的界面之間的切換,實際上只會發生在同一個While循環裏面,一旦退出某一個界面,則相應的代碼都不會被執行到,直到處理完成再返回原始界面:

而這種界面模型,在Android世界裏,只是一個UI線程所需要完成的工作,跟界面交互倒並不相關。我們的Android 在界面上進行交互,實際上是在Activity之間進行切換,而每個進程內部再維護一套上述的UI循環體:

在這樣的運行模式下,如果我們退出了某一個界面的執行,則沒有必要再維持其運行,我們可以通過特殊的設計使其退出執行。但這種調用是無論處理完,還是中途取消,我們還是會回到上一個界面,如果要達到一體化看上去像同一個應用程序的效果,這裏我們需要恢復上一個界面的狀態。比如我們例子裏,我們打了聯繫列表選擇了某個聯繫人,然後通過Gallery設置大頭貼,再返回到聯繫人列表時,一定要回到我們正在編譯聯繫人的界面裏。如果這時承載聯繫人列表的進程已經退出了話,我們將要使整個操作重做一次,很低效。

所以綜合考慮,最好的方式居然會是偷懶,對上個進程完全不處理,而需要提供一種暫停機制,可以讓不處理活躍交互狀態的進程進入暫停。當我們返回時則直接可以到上次調用前的那個界面,這時對用戶來說很友好,在多個進程間協作在用戶看來會是在同一個應用程序進行,這纔是Android設計的初衷。

因爲針對需要暫停的處理,所以我們的應用程序各個實體便有了生命週期,這種生命週期會隨着Android系統變得複雜而加入更多的生命週期的回調點。但對於偷懶處理,則會有後遺症,如果應用程序一直不退出,則對系統會是一個災難。系統會因爲應用程序不斷增加而耗盡資源,最後會崩潰掉。

不光Android會有這樣的問題的,Linux也會有。我們一直都說Linux內核強勁安全,但這也是相對的,如果我們系統裏有了一些流氓程序,也有可能通過耗盡資源的方式影響系統運行。大家可以寫一些簡單的例子做到這點,比如:

while(1)
{
  char * buf = malloc (30 * 1000);
  memset (buf, ‘a’, 30*1000);
  if (!fork() )
  fork();
}

這時會發現系統還是會受到影響,但Linux的健壯性表現在,雖然系統會暫時因爲資源不足而變得響應遲緩,但還是可以保證系統不會崩潰。爲了進程數過多而影響系統運行,Linux內核裏有一種OOM Killer(Out Of Memory Killer)機制,系統裏通過一種叫notifier的機制(顧名思義,跟我們的Listener設計模式類似的實現)監聽目前系統裏內存使用率,當內存使用達到比率時,就開始殺掉一些進程,回收內存,這裏系統就可以回到正常執行。當然,在真正發生Out Of Memory錯誤也會提前觸發這種殺死進程的操作。
一旦發生OOM事件,這時系統會通過一定規則殺死掉某種類型的進程來回收內存,所謂槍打出頭鳥,被殺的進程應該是能夠提供更多內存回收機會的,比如進程空間很大、內存共享性很小的。這種機制並不完全滿足Android需要,如果剛好這個“出頭鳥”就是產生調用的進程,或是系統進程,這時反而會影響到Android系統的正常運行。

這時,Android修改了Linux內核裏標準的OOM Killer,取而代之是一個叫LowMemKiller的驅動,觸發Out Of Memory事件的不再是Linux內核裏的Notifier,而由Android系統進程來驅動。像我們前面說明的,在Android裏負責管理進程生成與Activity調用棧的會是這個系統進程,這樣在遇到系統內存不夠(可以直接通過查詢空閒內存來得到)時,就觸發Low Memory Killer驅動來殺死進程來釋放內存。

這種設計,從我們感性認識裏也可以看到,用adb shell free登錄到設備上查看空閒內存,這時都會發現的內存的剩餘量很低。因爲在Android設備裏,系統裏空閒內存數量不低到一定的程度,是不會去回收內存的,Android在內存使用上,是“月光族”。Android通過這種方式,讓儘可能多的應用程序駐留在內存裏,從而達到一個加速執行的目的。在這種模型時,內存相當於一個我們TCP協議棧裏的一個窗口,儘可能多地進行緩衝,而落到窗口之外的則會被捨棄。

理論上來說,這是一種物盡其用,勤儉執家的做法,這樣使Android系統保持運行流暢,而且從側面也刺激了Android設備使用更大內存,因爲內存越多則內存池越大,可同時運行的任務越多,越流暢。唯一不足之處,一些試圖縮減Android內存的廠商就顯得很無辜,精減內存則有可能影響Android的使用體驗。

我們經常會見到系統間的對比,說Android是真實的多任務操作系統,而其他手機操作平臺只是僞多任務的。這是實話,但這不是被Android作爲優點來設計的,而只是整個系統設計迫使Android系統不得不使用這種設計,來維持系統的流暢度。至於多任務,這也是無心插柳柳成蔭的運氣吧。

Pre-runtime運算

在Android系統裏,無論我們今天可以得到的硬件平臺是多麼強大,我們還是有降低系統裏的運算量的需求。作爲一個開源的手機解決方案,我們不能假設系統具備多麼強勁的運算能力,出於成本的考慮,也會有產商生產一些更廉價的低端設備。而即便是在一些高端硬件平臺之上,我們也不能浪費手機上的運算能力,因爲我們受限於有限的電池供電能力。就算是將來這些限制都不存在,我們最好也還是減少不必要的損耗,將計算能力花到最需要使用它們的地方。於是,我們在前面談到的各種設計技巧之外,又增加了降低運算量的需求。

這些技巧,貌似更高深,但實際上在Android之前的嵌入式Linux開發過程裏,大家也被迫幹過很多次了。主要的思路時,所有跟運行環境無關運算操作,我們都在編譯時解決掉,與運行環境相關的部分,則儘可能使用固化設計,在安裝時或是系統啓動時做一次。

與運算環境無關的操作,在我們以前嵌入式開發裏,Codec會用到,比如一些碼錶,實際上每次算出來都是同樣或是類似的結構,於是我們可以直接在編譯時就把這張表算出來,在運行時則直接使用。在Android裏,因爲大量使用了XML文件,而XML在運行時解析很消耗內存,也會佔用大量內存空間,於是就把它在編譯時解析出來,在應用程序可能使用的內存段位裏找一個空閒位置放進去,然後再將這個內存偏移地址寫到R.java文件裏。在執行時,就是直接將二進制的解析好的xml樹形結構映射到內存R.java所指向的位置,這時應用程序的代碼在執行時就可以直接使用了。

在Android系統裏使用的另一項編譯態運算是prelink。我們Linux內核之睥系統環境,一般都會使用Gnu編譯器的動態鏈接功能,從而可以讓大量代碼通過動態鏈接庫的方式進行共享。在動態鏈接處理裏,一般會先把代碼編譯成位置無關代碼(Position Independent Code,PIC),然後在鏈接階段將共用代碼編譯成.so動態鏈接庫,而將可執行代碼鏈接到這樣的.so文件。而在動態鏈接處理裏,無論是.so庫文件還是可執行文件,在.text段位裏會有PLT(Procedure Linkage Table),在.data段位裏會有GOT(Global Offset Table)。這樣,在代碼執行時,這兩個文件都會被映射到同一進程空間,可執行程序執行到動態鏈接庫裏的代碼,會通過PLT,找到GOT裏定位到的動態鏈接庫裏代碼具體實現的位置,然後實現跳轉。

通過這樣的方式,我們就可以實現代碼的共享,如上圖中,我們的可執行文件a.out,是可以與其他可執行程序共享libxxx.so裏實現的func_from_dso()的。在動態鏈接的設計裏,PLT與GOT分開是因爲.text段位一般只會被映射到只讀字段,避免代碼被非法偷換,而.data段位映射後是可以被修改的,所以一般PLT表保持不動,而GOT會根據.so文件被映射到進程空間的偏移位置再進行轉換,這樣就實現了靈活的目的。同時,.so文件內部也是這樣的設計,也就是動態鏈接庫本身可以再次使用這樣的代碼共享技術鏈接到其他的動態鏈接庫,在運行時這些庫都必須被映射到同一進程空間裏。所以,實際上,我們的進程空間可能使用到大量的動態鏈接庫。

動態鏈接在運行時還進行一些運行態處理,像GOT表是需要根據進程上下文換算成正確的虛擬地址上的依稀,另外,還需要驗證這些動態鏈接代碼的合法性,並且可能需要處理鏈接時的一些符號衝突問題。出於加快動態連接庫的調用過程,PLT本身也會通過Hash表來進行索引以加快執行效率。但是動態鏈接庫文件有可能很大,裏面實現的函數很多很複雜,還有可能可執行程序使用了大量的動態鏈接庫,所有這些情況會導致使用了動態鏈接的應用程序,在啓動時都會很慢。在一些大型應用程序裏,這樣的開銷有可能需要花好幾秒才能完全。於是有了prelink的需求。Prelink就是用一個交叉編譯的完整環境,模擬一次完整地運行過程,把參與運行的可執行程序與動態鏈接所需要使用的地址空間都算出來一個合理的位置,然後再就這個值寫入到ELF文件裏的特殊段位裏。在執行時,就可以不再需要(即便需要,也只是小範圍的改正)進行動態鏈接處理,可以更快完成加載。這樣的技術一直是Linux環境裏一個熱門研究方向,像firefox這樣的大型應用程序經過prelink之後,可以減少幾乎一半的啓動時間,這樣的加速對於嵌入式環境來說,也就更加重要了。

但這種技術有種致命缺陷,需要一臺Linux機器,運行交叉編譯環境,才能使用prelink。而Android源代碼本就設計成至少在MacOS與Linux環境裏執行的,它使用的交叉編譯工具使用到Gnu編譯的部分只完成編譯,鏈接還是通過它自己實現的工具來完成的。有了需求,但受限於Linux環境,於是Android開發者又繼續創新。在Android世界裏使用的prelink,是固定段位的,在鏈接時會根據固定配置好地址信息來處理動態鏈接,比如libc.so,對於所有進程,libc.so都是固定的位置。在Android一直到2.3版本時,都會使用build/core/prelink-linux-arm.map這個文件來進行prelink操作,而這個文件也可以看到prelink處理是何其簡單:

 # core system libraries
 libdl.so 0xAFF00000 # [<64K]
 libc.so 0xAFD00000 # [~2M]
 libstdc++.so 0xAFC00000 # [<64K]
 libm.so 0xAFB00000 # [~1M]
 liblog.so 0xAFA00000 # [<64K]
 libcutils.so 0xAF900000 # [~1M]
 libthread_db.so 0xAF800000 # [<64K]
 libz.so 0xAF700000 # [~1M]
 libevent.so 0xAF600000 # [???]
 libssl.so 0xAF400000 # [~2M]
 libcrypto.so 0xAF000000 # [~4M]
 libsysutils.so 0xAEF00000 # [~1M] 

就像我們看到的,libdl.so,對於任何進程,都會是在0xAFD00001(libc.so結束)到0xAFF00000之間這個區域之間,而
在Android發展的初期,這種簡單的prelink機制,一直是有效的,但這不是一種很合理的解決方案。首先,這種方式不通用,也不夠節省資源,我們很難想像要在系統層加入firefox、openoffice這樣大型軟件(幾十、上百個.so文件),同時雖然絕大部分的.so是應用程序不會用到的,但都被一股腦地塞了進來。最好,這些鏈接方式也不安全,我們雖然可以通過“沙盒”模式來打造應用程序執行環境的安全性,但應用程序完全知道一些系統進程使用的.so文件的內容,則破解起來相對比較容易,進程空間分佈很固定,則還可以人爲地製造一些棧溢出方式來進行攻擊。

雖然作了這方面的努力,但當Android到4.0版時,爲了加強系統的安全性,開始使用新的動態鏈接技術,地址空間分佈隨機化(Address Space Layout Randomization,ASLR),將地址空間上的固定分配變成僞隨機分佈,這時就也取消了prelink。

Android系統設計上,對於性能,在各方面都進行了相當成功的嘗試,最後得到的效果也非常不錯。大家經常批評Android整個生態環境很惡劣,高中低檔的設備充斥市場,五花八門的分辨率,但拋開商業因素不談,Android作爲一套操作系統環境,可以兼容到這麼多種應用情境,本就是一種設計上很成功的表現。如果說這種實現很複雜,倒還顯得不那麼神奇,問題是Android在解決一些很難的工程問題的時候,用的技巧還是很簡單的,這就非常不容易了。我們寫過代碼的人都會知道,把代碼寫得極度讓人看不懂,邏輯複雜,其實並不需要太高智商,反而是編程能力不行所致。邏輯清晰,簡單明瞭,又能解決問題,才真正是大神級的代碼,業界成功的項目,linux、git、apache,都是這方面的典範。

Android所有這些提升性能的設計,都會導致另一個間接收益,就是所需使用的電量也相應大大降低。同樣的運算,如果節省了運算上的時間,變相地也減少了電量上的損失。但這不夠,我們的手機使用的電池非常有限,如果不使用一些特殊的省電技術,也是不行的。於是,我們可以再來透過應用程序,看看Android的功耗管理。

Android應用程序開發以及背後的設計思想深度剖析(5)

1.5 功耗控制

在嵌入式領域,功耗與運算量幾乎成正比。操作系統裏所需要的功能越來越複雜、安全性需求越來越高,則會需要更強大的處理能力支持。像在老式的實時操作系統裏,沒有進程概念,不需要虛擬內存支持,這時即便是寫一些簡單應用,所需要的運算量、內存都非常小,而一旦換用支持虛擬內存的系統,則所需要的硬件處理能力、電量都會成倍上漲,像一些功能性手機平臺,可以成爲一臺不錯的手機,但運行起一個Linux操作系統都很困難。而隨着操作系統能力增強,則所能支持的硬件又得以提升,可以使用更大的屏幕、使用更大量內存、支持更多的無線芯片,這些功能增強的同時,也進一步加劇了電量的消耗。雖然現在芯片技術不斷提高生產工藝降低製程(就是芯片內部燒寫邏輯時的門電路尺寸),幾乎都已經接近了物理上的極限(40納米、28納米、22納米),但是出於設計更復雜芯片爲目的的,隨着雙核、四核、以及越來越高的工作頻率,事實上,功耗問題不但沒有降低,反而進一步被加劇了。

面對這樣越來越大的功耗上的挑戰,Android在設計上,必須在考慮其他設計因素之前,更關注功耗控制問題。Android在設計上的一些特點,使系統所需要的功耗要高於傳統設計:Android是使用Java語言執行環境的,所有在虛擬機之上運行的代碼都需要更大的運算量,使用機器代碼中需要一條指令的地方,在虛擬機環境下執行則可能需要十幾條指令;與其他僞多任務不同,Android是真實多任務的,多任務則意味着在同一時刻會有更多任務在運行;Android是構建上Linux內核之上的系統,Linux內核在性能上表現奇佳,在功耗處理上則是短板,就拿PC環境來說,Linux的桌面環境在功耗控制上從來不如其他操作系統,MacOS或是Windows。

當然,有時沒有歷史包袱,也未必就是壞事,比如Linux內核在功耗管理上做得還不夠好,於是就不會在Linux內核環境裏死磕,Android可以通過新的設計來進行功耗控制上的提升。出於跟前面我們所說過的可減小對Linux內核依賴性、加強系統可移植性的設計需求,於是不可避免的,功耗控制將會儘可能多地被推到系統的上層。在我們前面對於安全性的分層中可以看到,Android相當於把整個操作系統都在用戶態重新設計了一次,SystemServer這個系統級進程相當於用戶態的一個Linux內核,於是將功耗控制更多地抽到用戶態來執行,也沒有什麼不合理的。

在Android的整體系統設計裏,功耗控制會先從應用程序着手,通過多任務並行時減小不必要的開銷開始;在整個系統構架裏,唯一知道當前系統對功耗需求的是SystemServer,於是可以通過相應的安全接口,將功耗的控制提取出來,可由SystemServer來進行後續的處理。Android系統所面臨的運行環境需求裏,電源是極度有限的資源,於是功耗控制應該是暴力型的,儘可能有能力關閉不需要使用的電源輸出。當然暴力關電,則可能引起某些外設芯片不正常工作,於是在芯片驅動裏需要做小範圍修改。與其他功能部分的設計不同,既然我們功耗控制是通過與驅動打交道來實現,可能無法避免地需要驅動,但要讓修改儘可能小,以提供可移植性。

在這種修改方案裏,最需要解決的當然首先是多任務處理。我們可以得到的就是我們的生命週期。所謂的生命週期,是不是僅僅只是提供更多一些編程上的回調接口而已呢?不僅如此,我們的所謂生命週期是一種休眠狀態點,更多地起到休眠操作時我們有機會插入代碼的作用。如果僅是提供編程功能,我們可以參考JAVA ME裏對於應用程序實現:

JAVA ME框架裏對待應用程序只有三個狀態點,運行、暫停、關閉,對應提供三種回調接口就可以驅動起這種編程模型。但我們的Android不是這樣處理的,Android在編程模型上,把帶顯示與不帶顯示的代碼邏輯分別抽象成Activity與Service,每種不同邏輯實現都有其獨特的生命週期,以更好地融入到系統的電源管理框架裏。

像我們的與顯示相關的處理,Activity,它擁有6種不同狀態:

它的不同生命週期階段,取決於這一Activity是否處於交互狀態,是否處理可見狀態。如果加入這兩個限制條件,於是Activity的生命週期則是爲這兩種狀態而設計的。onResume()與onResume()分別是進入交互與退出交互時的狀態點,在onResume()執行完之後,這時系統進入了交互狀態,也就是Activity的Running狀態,而此時如果由於Activity發生調用或是另一個Activity主動執行,彈出一個小對話框,使原來處於Running狀態的Activity被擋住,這時Activity就被視爲不需要交互了,這時Activity進入不可見互狀態,觸發onPause()回調。onStart()與onStop()則是對應於是否可見,在onStart()回調之後,應用程序這裏就可以被顯示出來,但不會真正進入交互期,當Activity變得完全不可見之後,則會觸發onStop()。而Android的多任務實現,還會造成進程會被殺死掉,於是也提供兩個onCreate()與onDestroy()兩種回調方法來提供進程被創建之後與進程被殺死之前的兩種不同操作。

這種設計的技巧在於,當Activity處於可交互狀況時,這是系統裏的全馬力執行的週期。而再向外走一個狀態期,只是處於可見但不可交互狀態時,我們就可以開始通過技巧降功耗了,比如此時界面不再刷新、可以關閉一些所有與用戶交互相關的硬件。當Activity再進一步退出可見狀態時,可以進一步退出所有硬件設備的使用,這時就可以全關電了。編寫應用程序時,當我們希望它有不一樣的表現時,我們可以去通過IoC去靈活地覆蓋並改進這些回調接口,而假如這種標準的模型滿足我們的需求,我們就什麼都不需要用,自動地被這種框架所管理起來。

當然,這種模型也不符合所有的需求,比如對於很多應用程序來說,在後臺不可見狀態下,仍然需要做一些特定的操作。於是Android的應用程序模型裏,又增加了一個Service。對於一些暴力派的開發者,比較喜歡使用後臺線程來實現這種需求,但這種實現在Android並不科學,因爲只通過Activity承載的後臺線程,有可能會被殺死掉,在有狀態更新需求時,後臺線程需要通過Activity重繪界面,實際上這樣也會破壞Android在功耗控制上的這種合理性設計。比較合適的做法,所有不帶界面、需要在後臺持續進行某些操作的實現,都需要使用Service來實現,而狀態顯示的改變應該是在onStart()裏完成的,狀態上的交互則需要放到onResume()方法裏,這樣的實現可以有效繞開進程被殺死的問題。並且在我們後面介紹AIDL的部分,還可以看到,這樣實現還可以加強後臺任務的可交互性,當我們進一步將Service通過AIDL轉換成Remote Service之後,則我們的實現會具備強大的可複用性,多個進程都可以訪問到。

Service也會有其生存週期,但Service的生存週期相對而言要簡單得多,因爲它的生存週期只存在“是否正在被使用”的區別。當然,同樣出於Android的多任務設計,“使用中”這個狀態之外,也會有進程是否存在的狀態。

於是,我們的Service也可被納入到這種代碼活躍狀態的受控環境,當是不需要與後臺的Service發生交互,這時,我們可能只是通過一個startService()發出Intent,這時Service在執行完相應的處理請求則直接退出。而如果是一個AIDL方式拋出的Remote Service,或是自己進程範圍內的Service,但使用bindService()進行了交互,這時,Service的運行狀態,只處於onBind()與OnUnbind()回調方法之間。

當我們的應用程序的各種不同執行邏輯,都是處於一個可控狀態下時,這時,我們的功耗控制就可以被集中到一個系統進程的SystemServer來完成。這時,我們面臨一種設計上的選擇,是默認提供一種鬆散的電源控制,讓應用程序儘可能多自由地控制電源使用,還是提供一種嚴格邏輯,默認情況下實施嚴格的電源輸出管理,只允許應用程序出於特殊的需求來調高它的需求?當然,前一種方式靈活,但出於電源的有限性,這時Android系統裏使用了第二次邏輯,儘可能多地嚴格電源輸出控制。

在默認情況下,Android會嘗試讓系統儘可能多地進入到休眠狀態之中。在從用戶開始進行了最後一次交互之後,系統則會觸發一個計時器,計時器會在一定的時間間隔後超時,但每次用戶的交互操作都會重置這一計時器。如果用戶一直沒有進行第二次交互,計時器超時則觸發一些功耗控制的操作。比如第一步,會先變暗直至關閉系統的屏幕,如果在後續的一定時間內用戶繼續沒有任何操作,這時系統則會進一步嘗試將整個系統變成休眠狀態。

休眠部分的操作,基本上是Linux內核的功耗控制邏輯了。休眠操作的最後,會將內存控制器設成自刷新模式,關掉CPU。到這種低功耗運行模式之下,這時系統功耗會降到最低,如果是不帶3G模組的芯片,待機電流應該處於1mA以下。但我們的系統是手機,一般2G、3G、或是4G是必須存在的,而且待機狀態時關掉這種不同網絡制式下的Modem,也失去了手機存在的意義,於是,一般功耗上會加上一個移動Modem,專業術語是基帶(Baseband)的功耗,這時一般要控制在10 – 30mA的待機電流,100mW左右的待機功耗。如果這時,用戶按些某些用於喚醒的按鍵、或是基帶芯片上過來了一些短信或是電話之類的信息,則系統會通過喚醒操作,回到休眠之前的狀態。

但是Linux內核的Suspend與Resume方案,是針對ACPI裏通用計算環境(我們的PC、筆記本、服務器)的功耗控制方案,並不完全與手機的使用需求相符合。而Linux內核所缺失的,主要是UI控制上功耗管理,手機平臺上耗電最大的元器件,是屏幕與背光,是無法通過Linux內核的suspend/resume兩級模型來實現高效的電源管理。於是,Android系統,在原始的suspend與resume接口之外,再增加了兩級early_suspend與late_resume,用於UI交互時的提前休眠。

我們的Android系統,在出現用戶操作超時的情況下,會先進入early_suspend狀態點,關閉一些UI交互相關的硬件設備,比如屏幕、背光、觸摸屏、Sensor、攝像頭等。然後,在進一步沒有相應喚醒操作時,會進入suspend關閉系統裏的其他類的硬件。最後系統進入到內存自刷新、CPU關電的狀態。如果在系統完全休眠的情況下,發生了某種喚醒事件,比如電話打進來、短信、或是用戶按了電源鍵,這時就會先進resume,將與UI交互不相關的硬件喚醒,再進入late_resume喚醒與UI交互相關的硬件。但如果設備在進入early_suspend狀態但還沒有開始suspend操作之前發生了喚醒事件,這時就直接會走到late_resume,喚醒UI交互的硬件驅動,從而用戶又可以看到屏幕上的顯示,並且可以進行交互操作。

經過了這樣的修改,在沒有用戶操作的情況下,系統會不斷進入休眠模式省電,而用戶並不會感受到這種變化,在頻繁操作時,實際上休眠與喚醒只是快進快出的UI相關硬件的休眠與喚醒。但完全暴力型的休眠也會存在問題,比如我們有些應用程序,QQ需要保持登錄,下載需要一直在後臺下載,這些都不符合Android的需求的,於是,我們還需要一種機制,讓某些特殊的應用程序,在萬不得已的情況下,我們還是可以得這些應用程序足夠的供電運行得下去。

於是Android在設計上,又提出了一套創新框架,wake_lock,在多加了early_suspend與late_resume之外,再加上可以提供功耗上的特殊控制。Wake_lock這套機制,跟我們C++裏使用的智能指針(Smart pointer),借用智能指針的思想來設計電源的使用和分配。我們也知道Smart Pointer都是引用,則它的引用計數會自動加1,取消引用則引用計數減1,使用了智能指針的對象,當它的引用計數爲0時,則該對象會被回收掉。同樣,我們的wake_lock也保持使用計數,只不過這種“智能指針”的所使用的資源不再是內存,而是電量。應用程序會通過特定的WakeLock去訪問硬件,然後硬件會根據引用計數是否爲0來決定是不是需要關閉這一硬件的供電。

Suspend與wake_lock這兩種新加入的機制,最後也是需要加放SystemServer這個進程裏,因爲這是屬於系統級的服務,需要特權才能保證“沙盒”機制。於是,我們得到了Android裏的電源管理框架:

當然,這裏唯一不太好的地方,就是Android系統設計必須對Linux內核原有的電源管理機制進行改動,需要加入wake_lock機制的處理,也需要在原始的內核驅動之上加入新的early_suspend與late_resume兩個新的電源管理級別與wake_lock相配套。這部分的代碼,則會造成Android系統所需要的驅動,與標準Linux內核的驅動並不完全匹配,同時這種簡單粗暴的方式,也會破壞掉內核原有的清晰簡要的風格。這方面也造成了Linux社區與Android社區之間曾一度吵得很兇,Linux內核拒絕Android提交的修改,而Android源代碼則不再使用標準的Linux內核源代碼,使用自己特殊的分支進行開發。

我們再來看Android系統對於功能接口的設計。

1.6 功能接口設計

我們實現一個系統,必須儘可能多地提供給應用程序儘可能多的開發接口,作爲一個開源系統更應該如此。雖然我們前面提到了,我們需要有權限控制機制來限制應用程序可訪問系統功能與硬件功能,但是這是權限控制的角度,如果應用程序得到了授權,應該有理由來使用這一功能,一個能夠獲得所有權限的應用程序,則理所當然應該享受系統裏所提供的一切功能。

對於一個標準的Java系統,無論是桌面環境裏使用的Java SE還是嵌入式環境裏使用的Java ME,都不存在任何問題,因爲這時Java本就只是系統的一層“皮”,每個Java寫成的應用程序,只是一層底層系統上的二次封裝,實際上都是借用底層操作系統來完成訪問請求的。對於傳統的應用程序,一個main()進入死循環處理UI,也不存在這個問題,通過鏈接到系統裏的動態鏈接庫或是直接訪問設備文件,也可以實現。但這樣的方式,到了Android系統裏,就會面臨一個功能接口的插分問題。因爲我們的Android,不再是一層操作系統之上的Java虛擬機封裝,而是抽象出來的在用戶態運轉的操作系統,同時還會有“沙盒”模式,應用程序並不見得擁有所有權限來訪問系統資源,則又不能影響它的正常運行。

於是,對於Android在功能接口設計上,會被劃分成兩個層次的,一種是以“受託管”環境下通過一個系統進程SystemServer來執行,另一種是被映射到應用程序的進程空間內來完成。而我們前面分析的使用Java編程語言,而Framework層功能只以API方式向上提供訪問接口,就變得非常有遠見。使用了Java語言,則我們更容易實現代碼結構上的重構,如果我們的功耗接口有變動,則可以通過訪問接口的重構來隱藏掉這樣的差異性;只以Framework的API版本爲標準來支持應用程序,則進一步提供封裝,在絕大部分情況下,雖然我們底層結構已經發生了巨大變動,應用程序卻完全不受影響,也不會知道有這樣的變化。

從這種設計思路,我們再去看Android的進程模型,我們就可以看到,通常意義上的Framework,實際上被拆分成兩部分:一部分被應用程序用Java實現的classes.dex所引用,這部分用來提供應用程序運行所必須的功能;另一部分,則是由我們的SystemServer進程來提供。

在應用程序只需要完成基本的功能,比如只是使用Activity來處理圖形交互時,通過Activity來構建方便用戶使用的一些功能時,這時會通過自己進程空間內映射的功能來完成。而如果要使用一些特殊功能,像打電話、發短信,則需要通過一種跨進程通訊,將請求提交到SystemServer來完成。

這種由於特殊設計而得到的運行模型很重要,也是Android系統有別於其他系統很重要的一個區別。這樣的框架設計,使Android與傳統Linux上所面臨的易用性問題在設計角度就更容易解決。

比如顯示處理。我們傳統的嵌入式環境裏,要不就是簡單的Framebuffer直接運行,要麼會針對通用性使用一個DirectFB的顯示處理方案,但這種方案通用性很低,安全性極差。爲了達到安全性,同時又能儘可能兼容傳統桌面環境下的應用程序,大都會傳承桌面環境裏的一個Xorg的顯示系統,比如Meego,以及Meego的前身Maemo,都是使用了Xorg用來處理圖形。但Xorg有個很嚴重的性能問題:

使用Xorg處理顯示的,所有的應用程序實際上只是一個客戶端,通過Unix Socket,使用一種與傳統兼容的X11的網絡協議。用戶交互,應用程序會在自己的交互循環裏,通過X11發起創建窗口的請求,之後的交互,則會通過輸入設備讀取輸入事件,再通過Xorg服務器,轉回客戶端,而應用程序界面上的重繪操作,則還是會通過X11協議,走回到Xorg Server之後,再進行最後的繪製與輸出。雖然現在我們使用的經過模塊化重新設計的XorgR7.7,已經儘可能通過硬件加速來完成這種操作,Xorg服務器還是有可能會成爲整個圖形交互的瓶頸,更重要的是複雜度太高,在這種構架裏修改一個bug都有點困難,更不要說改進。在嵌入式平臺上更是如此,性能本就不夠的系統環境,Xorg的缺陷暴露無移,比如使用Xorg的Meego更新過程遠比Android要困難,用戶交互體驗也比較差。

在Android裏,處理模型則跟傳統的Xorg構架很不一樣。從設計角度來講,繪製圖形界面與獲取輸入設備過來的輸入事件,本來不需要像Xorg那樣的中控服務器,尤其像Android運行環境這樣,並不存在多窗口問題(多窗口的系統需要有個服務器決定哪個窗口處於前臺,哪個窗口處於交互狀態中)。而從實現的角度,如果能夠提供一種設計,將圖形處理與最終輸出分開,則更容易實現優化處理。基於圖形界面的交互,實際上將由三個不同的功能實體來完成:應用程序、負責將圖層進行疊加渲染的SurfaceFlinger、以及負責輸入事件管理和選擇合適的地址進行發送的SystemServer。當然,我們的上層的應用程序不會看到內部的複雜邏輯,它只知道通過android.view這個包來訪問所有的圖形交互功能。

於是得到Android系統的圖形處理框架:

我們的SurfaceFlinger,是Android裏的一種Native Service的實現,所以有原理上來說,只要有一個承載它的執行體(進程、線程皆可),就可以在系統裏執行。在實現過程裏,SurfaceFlinger作爲一個線程在SystemServer這個進程空間裏完成也是可以的,只是出於穩定性的考慮,一般將它獨立成一個單獨的SurfaceFlinger的獨立進程。

這種設計,可以達到一個低耦合設計的優勢,這套圖形處理框架將變得更簡單,同時也不會將Xorg那樣需要大量的特殊內核接口與其適配,如果在別的操作系統內核之上進行移植,也不會有太大的依賴性。但這時會帶來嚴重的性能問題,因爲圖層的處理和輸出是需要大量內存的(如果是24位真彩色輸出,即使是800x480的分辯率,每秒60楨的輸出頻率,也需要3*800*480*60 = 69120000,69M Byte/s),這種開銷對於嵌入式方案而言,是難以承受的。在進程間傳遞數據時,會先需要在一個進程執行上下文環境裏通過copy_from_user()把數據從用戶態拷貝到內核態,然後在另一個進程執行的上下文環境裏通過copy_to_user()把數據拷貝從內核態拷貝到另一個用戶態環境,這樣才能保證互不干擾。

而回過頭來看Linux內核,搞過Linux內核態開發的都知道,在Linux系統的進程之間減小內存拷貝的開銷,最直接的手段就是通過mmap()來完成內存映射,讓保存數據的內存頁只會在內核態裏循環,這時就沒有內存拷拷貝的開銷了。使用了mmap()之後,內存頁是直接在內核態分配的內存,兩個進程都通過mmap()把這段區域映射到自己的用戶空間,然後可以一個進程直接操作內存,另一個進程就可以直接訪問到。在圖層處理上,最好這些在內核態申請的內存是連續內存,這時就可以直接通過LCD控制器的DMA直接輸出,Android於是提供了一種新的特殊驅動pmem,用來處理連續物理內存的分配與管理。同時,這種方式很裸,最好還在上層提供一次抽象,編程時則靈活度會更高,針對這種需求,就有了我們的Gralloc的HAL接口。加入了這兩種接口之後,Android在圖像處理上便自成體系,不再受限於傳統實現了。

我們的圖層,是由應用程序在創建是通過Gralloc來申請圖層存儲空間,然後被包裝成上層的Surface類,在Activity實現裏Surface則是按需要進行重繪(調用view的draw()方法),並在繪製完成後通過post()將繪製完成的消息發送給SurfaceComposer遠程對象。而在SurfaceFlinger這段,則是將已經繪製完成的Surface通過其對應的模式,進行圖層的合成並輸出到屏幕。對於上層實現,貌似是一種很鬆散的交互,而對於底層實現,實際則是一種很高效的流水線操作。

這裏,值得一提的是Surface本身也包含了圖層處理加速的另一種技巧,就是double buffer技術。一個Surface會有兩個圖層buffer,一楨在後臺被繪製,另一楨在前臺進行輸出。當後臺繪製完成後,會通過一次Page Flipping操作,原來的後臺楨被換到前臺進行輸出,而繪製操作則繼續在後臺完成。這樣用戶總會看到繪製完整的圖像,因爲圖層總是繪製完成後才能輸出。而有了double buffer,使我們圖形輸出的性能也得到提升,我們輸出繪製與輸出使用獨立的循環,通過流水線加快了圖層處理,尤其在Android裏,可能有多個繪製的邏輯部分,性能得以進一步加速。在Android 4.1裏面,這種圖形處理得以進一步優化,使用了triple buffer(三重緩衝),加深了圖層處理的流水線操作能力。

這種顯示處理上的靈活性,在Android系統裏也具備非常重要的意義,可以讓整個系統在功能設計上可以變得更加靈活。我們提供了一種“零拷貝”圖層處理技術之後,最終上層都可以通過一個特殊的可以跨進程的Surface對象來進行異步的繪製處理(如果我們不是直接操作控件,而是通過“打洞”方式來操作圖形界面上的某個區域,則屬於SurfaceView提供的,當然,這時也只是操作Surface的某一部分)。我們的Surface的繪製與post()異步進行的,於是多個執行體可以並行處理圖層,而用戶只會看到通過post()發送的圖層繪製完成的同步事件之後的完整圖層,圖層質量與流暢性反而可以更佳。比如,我們的VOIP應用程序,可以會涉及多個功能實體的交互,Camera、多媒體編解碼、應用程序、SurfaceFlinger。

應用程序、多媒體編解碼與Camera都只會通過一個Surface對象來在後臺楨上進行交互界面的繪製,像前攝像頭出來的回顯,從網絡解碼出來的遠端的視頻,然後應用程序的操作控件,都將重繪後臺圖層。而如果這一應用程序處於Activity的可交互狀態(見前面的生命週期的部分),就會通過找到同一Surface對象,將這一Surface對象的前臺楨(也就是繪製完成但還沒有輸出的圖層)輸出。輸出完則對這一Surface對象的前後兩楨圖層進行對調,於是這樣的流水線則可以很完美的運行下去。

Android並非是最高效的方案,而只是一種通過面向對象方式完全重新設計的嵌入式解決方案,高效是其設計的一部分要素。如果單從效率角度出發,無進程概念的實時操作系統最高效,調度開銷也小,沒有虛址切換時的開銷。作爲Android系統,通過目前我們看到的功能性接口的設計,至少得到了以良好的構架爲基礎同時又兼顧性能的一種設計。

當然,我們前面所總結的,對於Android系統的種種特性,最終得到的一種印象是每種設計都是萬能膠,同一種設計收穫了多種的好處。那是不是這種方式最好,大家都應該遵循這種設計上的思路與技巧?That depends,要看情況。像Android這樣要完整地實現一整套這種在嵌入式環境裏運行的,面向對象式的,而且是基於沙盒模式的系統,要麼會得到效率不高的解決方案,要麼會兼顧性能而得到大量黑客式的接口。Android最終也就是這麼一套黑客式的系統,這個系統一環套一環,作爲系統核心部分的設計,都彼此過分依賴,拆都拆不開,對它進行拆分、精減或是定製,其實都很困難。但Android,其系統的核心就是Framework,而所謂的Framework,從軟件工程學意義上來說,這樣的構架卻是可以接受的。所謂的Framework,對上提供統一接口,保持系統演進時的靈活性;對下則提供抽象,封裝掉底層實現的細節。Android的整個系統層構架,則很好的完成了這樣的抽象,出於這樣的角度,我們來看看Android的可移植性設計。

1.7 可移植性

單純從可移植性角度來說,Linux內核是目前世界上可移植性最強的操作系統內核,沒有之一。目前,只要處理器芯片能夠提供基本的運算能力(可以支撐多進程在調度上的開銷),只要能夠提供C語言的編譯器(準確地說是Gnu C編譯工具鏈),就可以運行Linux內核。Linux內核在設計上保持了傳統Unix的特點,大部分使用了C語言開發,極少部分機器相關的代碼使用匯編,這種結構使其可移植性很強。在Linux內核發展到2.6版本之後,這種強大的可移植性得到進一步提升,通過驅動模型與驅動框架的引入和不斷加強,使Linux內核裏絕大部分源代碼幾乎都沒有硬件平臺上的依賴性。於是,Linux內核幾乎能夠運行在所有的硬件平臺之上,常見有的X86、ARM,不那麼常見但可能也會在不知道不覺地使用到的有MIPS、PowerPC、Alpha,另外還有一些我們聽都沒有聽過的,像Blackfin,Cris、SuperH、Xtensa,Linux內核都支持,平臺支持可參考linux內核源代碼的arch目錄。甚至,出於Linux內核的可移植性,Linux一般也被作爲芯片驗證的工具,芯片從FPGA設計到最終出廠前,都會通過Linux內核來檢測這一芯片是否可以運行,是否存在芯片設計上的錯誤。

得益於Linux內核,構建於其上的操作系統,多多少少可繼承這樣的可移植性。但Android又完成應用程序運行環境的二次抽象,在用戶態幾乎又構造出一層新的操作系統,於是它的可移植性多多少少會受此影響,而且,像我們前面所分析出來的,Android的核心層構建本身,也因爲性能上的考慮,耦合性也有點強,於是在可移植性也會面臨挑戰。“窮山惡水出刁民”,正因爲挑戰大,於是Android反倒通過各種技巧來加強系統本身的可移植性,反而做得遠比其他系統要好得多。Android在可移植性上的特點有:

  • 按需要定製可移植性。與傳統嵌入式Linux操作系統不同,Android在設計上有明確的設計思想與目標,不會爲了使用更多開源軟件而提供更高兼容性的編譯環境,而是列出功能需求,按功能需求來定製所需要的開源軟件。有些開源軟件能夠提供更復雜的功能,但在Android環境裏,只會選擇其驗證過的必需功能,像藍牙,BlueZ本身可以提供更復雜的藍牙控制,但Android只選擇了BlueZ的基本功能,更多功能是由Android自己來實現,於是減小了依賴性,也降低了移植時的風險性。
  • 儘可能跨平臺。與以前的系統相比,Android在跨平臺上得益於Java語言的使用,使其跨平臺能力更強,在開發上幾乎可以使用任何Java環境可以運行的操作系統裏。而在源代碼級別,它也能夠在MacOSX與Linux環境裏進行編譯,這也是一個大的突破。
  • 硬件抽象層。Android在系統設計的最初,便規劃了硬件抽象層,通過對硬件訪問接口的抽象,使硬件的訪問接口相對穩定,而具體的實現則可在底層換用不同硬件訪問接口時靈活地加以實現,不要說應用程序,就是Framework都不會意識到這種變動。這是以前的嵌入式Linux操作系統所沒有的一種優點。硬件抽象層的使用,使Android並不一定需要侷限於Linux內核之上,如果將底層偷換成別的接口,也不會有太大的工作量。
  • 實現接口統一的規範化。Android在構架上,都是奉行一種統一化的思路,先定義好API,然後會有Framework層的實現,然後再到硬件抽象層上的變動。API可在同一版本上拓展,Framework也在逐步加強,而硬件抽象層本身可提供的能力也越來越強,但這一切都在有組織有紀律的環境下進行,變動在任何一次版本更新上來看,都是增量的小範圍變動,而不會像普通的Linux環境那樣時刻都在變,時刻都有不兼容的風險。從可移植性角度來說,這種規範化提供的好處,便是大幅降低了移植時的工作量。
  • 儘可能簡單。簡單明瞭是Android系統構成上的一大特色,這種特色在可移植性上也是如此。像編譯環境,Android在交叉編譯環境上,是通過固化編譯選項來達到簡編譯過程的上的,最終,Android源代碼的編譯工程,會是一個個由Android.mk來構造的可編譯環境,這當然會降低靈活性,但直接導致了這套框架在跨平臺上表現非常出色。再比如硬件抽象層,同樣的抽象在現代嵌入式操作系統上可能都有,但是大都會遠比Android的HAL層要複雜,簡單于是容易理解和開發,在跨平臺性方面也會表現更好。

我們傳統的嵌入式Linux環境,幾乎都會遵從一種約定俗成的傳統,就是專注於如何將開源軟件精減,然後儘可能將PC上的運行環境照搬到嵌入式。在這種思路引導下開發出來的系統,可移植性本身是沒什麼問題的,只是不是跟X86綁定的源代碼,鐵定是可以移植。但是,這樣構建出來的系統,一般都在結構上過於複雜,會有過多的依賴性,應用程序接口並不統一,升級也困難。所有這樣的系統,最後反倒是影響到了系統的可移植性。比如開源嵌入式Linux解決方案,maemo,就是一個很好的例子:

對於Maemo的整體框架而言,我們似乎也可以看到類似於Android的層次化結構,但注意看這種系統組成時,我們就不難發現,這樣的層次化結構是假的,在Maemo環境裏,實際上就是一個小型化的Linux桌面環境,有Xorg,有gtk,有一大堆的依賴庫,編程環境幾乎與傳統Linux沒任何區別。這種所謂的軟件上的構架,到了Maemo的後繼者Meego,也是如此,只不過把gtk的圖形界面換成了Qt的,然後再在Qt庫環境裏包裝出所謂的UX,換湯不換藥,這時,Meego還是擁有一顆PC的心。

一般這種系統的交叉編譯環境,還必須構建在一套比較複雜的編譯環境之上,通過在編譯環境裏模擬出一個Linux運行環境,然後才能編譯儘可能多的源代碼。這樣的交叉編譯環境有Open Embedded,ScratchBox等。雖然有不同的交叉編譯實現上的思路,但並沒有解決可移植性問題,它們必須在Linux操作系統裏運行,而且使用上的複雜程度,不是經驗豐富的Linux工作者還沒辦法靈活使用。即便是比較易用的ScratchBox,也會有如下令人眼花繚亂的結構。

針對這樣的現狀,Android在解決可移植性問題時的思路就要簡單得多。既然原來的嘗試不成功,PC被精減到嵌入式環境裏效果並不好,這時就可以換一種思路,一種“返璞歸真”的思路,直接從最底層來簡化設計,簡化交叉編譯。這樣做法的一個最重要前提條件,就是Android本身是完整重新設計與實現的,是一個自包含的系統,所有的編譯環境,都可以從源代碼裏把系統編譯出來。

在系統結構上,Android在設計上便拋棄了傳統的大肆搜刮開源代碼的做法,由自己的設計來定位需要使用的開源代碼,如果沒有合適的開源代碼,則會提供一個簡單實現來實現這一部分的功能。於是,得到我們經常見到的Android的四層結構:

從這樣簡化過的四層結構裏,最底層的Linux內核層,這些都與其他嵌入式Linux解決方案是共通的特性,都是一樣的。其他三層就與其他操作系統大相徑庭了:應用程序層是一種基於“沙盒”模式的,以功能共享爲最終目的的統一開發層,並非只是用於開發,同時還會通過API來規範這些應用程序的行爲;Framework層,這是Android真正的核心層,而從編程環境上來說,這一層算是Java層,任何底層功能或硬件接口的訪問,都會通過JNI訪問到更低層次來實現;給Framework提供支撐的就是Library層,也就是使用的一個自己實現的,或是第三方的庫環境,這一層以C/C++編寫的可以直接在機器上執行的ELF文件爲主。

有了這種更簡化的層次關係,使Android最後得到的源代碼相對來說更加固定,應用程序這層我們只會編譯Java,Framework層的編譯要麼是Java要麼是JNI,而Library層則會是C/C++的編譯。在比較固定的編譯目標基礎上,編譯環境所需要解決的問題則會比較少,於是更容易通過一些簡化過的編譯環境來實現。Android使用了最基本的編譯環境GnuMake,然後再在其上使用最基本的Gnu工具鏈(不帶library與動態鏈接支持)來編譯源代碼,最後再通過簡化過的鏈接器來完成動態鏈接。得到的結果是幾乎不需要編譯主機上的環境支持,從而可以在多種操作系統環境裏運行,Android的編譯工程文件還比較簡單,更容易編寫,Android.mk這種Android裏的編譯工程文件,遠比天書般的autoconf工具要簡單,比傳統的Makefile也更容易理解。

Android的編譯系統,由build目錄裏一系列.mk編譯腳本和其他腳本組成,以build/main.mk作爲主入口。main.mk文件會改到配置項,然後再通過配置項循環地去編譯出所需要的LOCAL_MODULE。而這些LOCAL_MODULE的編譯目標,是由Android.mk來定義的,而到底如何編譯目標對象,則是由簡單的include $(BUILD_*)這樣的編譯宏選項來提供,比如include $(BUILD_SHARED_LIBRARY)則會編譯生成動態鏈接庫.so文件。這樣的編譯系統拋棄autoconf的靈活性,換回了跨平臺、編寫簡單。

當然,這樣的編譯工具,也不是沒有代價的,使用簡易化的Android編譯環境則意味着Android放棄了一些現有絕大部分代碼的可移植性,Linux環境裏的一些常用庫移植到Android環境則需要大量移植工作,像萬能播放器核心ffmpeg,目前幾乎只有商用應用程序才肯花精力將它移植到Android系統裏。

當然,光有自己代碼結構,光有更簡易的編譯環境,並不解決所有問題,我們的Android系統,最後還必須通過訪問系統調用、讀寫驅動的設備文件來完成底層的操作。不然我們的Android系統就只會是一個光桿司令,什麼都不是,沒有任何功能。既然我們擁有了相關簡化的系統結構,我們在內部自己按自己的想法去訪問硬件也是可以,只是這樣在升級與維護起來的代碼會變得很大,比如我們通過”/dev/input/event0”來讀觸摸屏,”/dev/input/event1”來讀重力感應器,如果換個平臺,這些設備名字變了怎麼辦?或者有些私有化平臺,都完全不使用這樣的標準化設備文件命名時怎麼辦?難道針對每個平臺準備一份源代碼來做這些修改?於是就有了內部訪問接口統一化的問題,Android在對於底層的設備文件的訪問上,又完成了一層抽象,也就是我們的硬件抽象層。

硬件抽象層,準確地說,是介於Framework層與Linux內核層之間的一個層次,Framework通過硬件抽象層的統一接口來向下訪問,在Linux內核上硬件訪問接口上的差異性,則通過硬件抽象層來進行屏蔽。硬件抽象層,英文是Hardware Abstraction Layer,簡稱HAL,於是也就是我們常見到的AndroidHAL層。

提供硬件抽象層之後,這時Framework層與底層Linux內核之間的耦合性,就完全消除掉了。如果要將Android部署到其他操作系統、或是操作系統內核之上,只需要將HAL層的相應接口實現換成其他平臺上的訪問機制即可,而Framework只會使用HAL層的統一接口向下訪問,完全不知道底層變動信息。

在Android的世界裏,硬件抽象其實是包含多種含義的,可能會有多種的硬件訪問實現,比如RIL、BlueZ這樣的儘可能兼容已有解決方案的廣義上的HAL,Framework會通過Socket來訪問傳統方式實現的daemon,也有Gralloc、Camera這樣先定義Framework向下訪問的接口,然後由硬件向上提供接口實現的狹義上的HAL。我們的Android上的HAL實現,更多地專指狹義上的HAL,由源代碼裏的hardware目錄提供。

狹義上的HAL,在實現上也會分成兩種:一種基於函數直接訪問的方式來實現,這種方式比較簡單粗暴,所以在命名上,被稱爲libhardware_legacy實現,由目錄hardware/libhardware_legacy裏實現的一個個動態鏈接庫來實現;另一種則使用了面向對象的技巧,上層通過一個hardware_module_t來訪問,而具體的某一類HAL的實現,則會將這一hardware_module_t按需求進行拓展,這種方式被稱爲libhardware實現,由hardware/libhardware提供hardware_modult_t的訪問接口,硬件訪問的實現部分,也是動態鏈接庫文件,但會在運行時根據板卡的具體配置,進行動態加載。從長遠看,libhardware_legacy結構的HAL實現是一種中間方案,在Android發展過程裏有可能會被libhardware方式的實現所取代,比如Android 4.0裏,Audio、Camera完成了libhardware_legacy到libhardware的轉變。

從設計的角度來看,libhardware_legacy雖然解決了與Linux內核的耦合問題,但是直接函數接口的訪問,終究還是靈活性不夠。以libhardware_legacy實現HAL的動態鏈接庫,必須被直接鏈接到Framework的實現裏,通過JNI進行直接訪問,不具備動態性,不能支持同一份Android可執行環境支持不同硬件平臺。從設計角度來說,任何抽象都是由另一次間接調用來實現,於是,我們在硬件訪問接口裏再加入一層抽象,這就是libhardware.so。Framework並不直接訪問具體的動態鏈接庫實現,而是通過libhardware.so裏實現的通用接口來向下訪問,並且也只會調用到預定義好的一些訪問接口,而HAL實現的,是以Stub方式來提供到系統裏一些功能訪問的具體實現。如果不好理解的,可以認爲是系統提供頭文件,也就是我們右圖中的<<Hardware Access API>>,這些只是接口類定義。而實現上,並不是直接通過頭文件來實現,而是通過實現一個具有一定特性的hardware_module_t的數據結構,來向上提供具體的函數調用接口,與頭文件裏所需要的接口相對應。爲什麼叫它Stub呢?是因爲在libhardware這種模式裏,把接口定義與具體的實現抽離開來,雖然不一定會使用面嚮對象語言來實現(一般是通過C來實現的),但提供了Interface+Stub這樣面向對象式的實現,所以libhardware有時也被稱爲stub模式實現的HAL。

使用libhardware之後,HAL層實現上的可複用性與運行時的靈活性則被大大增強了。在libhardware框架下,Framework都不再直接調用HAL層,而是通過hw_get_module()方法來在/system/lib/hw和/system/vendor/lib/hw這兩個目錄裏循環尋找合適的.so實現,比如針對sensor,會有sensor.default.so,sensor.goldfish.so,sensor.xxx.so,會有不同種實現,以用於加載合適的實現。雖然這些只會在開機時執行一次,但通過簡單方式至少也實現了用同一份二進制代碼提供多種硬件平臺的支持。使用libhardware實現的HAL,當我們的實現的HAL有問題時,我們可以刪掉有問題的HAL,此時啓動時會使用一個xxx.default.so的純軟件實現的不進行任何硬件訪問的.so文件,讓我們還是可以繞開啓動時的HAL加載錯誤。

Android這種強化可移植性的設計,最終使Android的移植移植過程變得相對比較簡單。如果是做Android的手機或是平板的開發,也許我們需要做的只是提供板塊相關的配置文件,從而可以改變一些編譯時的配置參數。如果我們的硬件平臺跟Android源代碼時使用的標準平臺(比如Google的“親兒子”手機Nexus系列的產品,或是pandaboard這樣作爲參考設計的產品),對於移植過程而言,我們可能什麼都不需要做,直接可以編譯運行,然後再做產品化的微調;如果我們使用的硬件平臺跟某些產商提供的開源項目的硬件結構一樣,比如Qualcomm提供的codeaurora.org,TI的Omapedia,還有各大廠商都涌躍參與的linaro.org項目等等,這時需要完成的移植工作也會類似的很小;如果我們的提供的硬件平臺跟Android這些已有的開源資源很不一樣,這時,我們需要完成的移植工作也不會很大,只需要根據特定的硬件平臺實現HAL,這一過程所需要的工作量遠小於其他平臺的移植過程。

Android的移植過程,基本上分爲:

  • Bootloader與Linux內核的移植
  • Repo環境(Android源代碼基於repo管理,最後使用repo)
  • 交叉編譯器、Bionic C庫與Dalvik虛擬機的移植(如果不是ARM、X86和MIPS這三種基本構架)
  • 提供板卡支持所需要的配置
  • 實現所需要使用的HAL
  • Android產品化,完成界面或是功能上的定製

這些移植過程的步驟如下圖所示:

對於我們做Android移植與系統級開發而言,可能我們所需要花的代碼並不是那麼大。像Bootloader與Linux內核的移植,這一般都在進行Android系統移植時早就會就緒的,比如我們去選購某個產商的主芯片時(Application Processor,術語爲AP),這些Android之前的支持大都已經就緒。而作爲產業的霸主,我們除非極其特殊的情況,我們也不需要接觸交叉編譯器和Dalvik虛擬機的移植。所以一般情況下,我們的Android移植是從建立repo源代碼管理環境開始,然後再進行板卡相關的配置,然後實現HAL。而Android的產品化這個步驟,Framework的細微調整與編寫自己平臺上特殊的應用程序,嚴格意義上來說,Framework也不屬於Android移植工作範圍內的,我們一般把它定位於Android產品化或是Android定製化這個步驟裏。Android移植相對來說非常簡單,而真正完成Android產品化則會是一個比較耗時耗人力的過程。

所謂的板卡的配置文件,一般是放在一個專門的目錄裏,在2.3以前,是發在vendor目錄下,從2.3開始,vendor目錄只存放二進制代碼,配置文件移到了device目錄。在這一目錄裏,會以“產商名/設備名”的命名方式來規範配置的目錄結構。比如是產商名是ti,設備名是panda,則會以“device/ti/panda”來存放這些配置文件,再在這個目錄裏放置平臺相關的配置項。配置文件,則是會幾個關鍵文件構成:

} vendorsetup.sh,使用add_lunch_combo將配置項導入編譯環境

} AndroidProducts.mk,這是會被編譯系統掃描的文件,通過在這一文件裏再導入具體的編譯配置文件,比如ti_panda.mk

} ti_panda.mk,在這一文件裏定義具體的產品名,設備名這些關鍵變量,這些變量是在Android編譯過程裏起關鍵配置作用的變量。一般說來,這個文件不會很複雜,主要依賴導入一些其他的配置文件來完成所有的配置,比如語言配置等。而設備特殊的設置,則一般是使用同目錄下的device.mk文件來進行定製化的設置。

} device.mk,在這一文件會使用一些更加複雜一些配置,包含一些需要編譯的子工程,設置某些特殊的編譯參數,以及進行系統某些特性的定製化,比如需要自定義怎樣的顯示效果、配置文件等

} BoardConfig.mk,在這一文件則是板子相關的一些定製項,以宏的方式傳入到編譯過程裏,比如BOARD_SYSTEMIMAGE_PARTITION_SIZE來控制system分區的大小, TARGET_CPU_SMP來控制是否需要使用SMP(對稱多處理器)支持等。一般,對於同一個板卡環境,這些參數可以照抄,勿須修改。

所有的這些配置文件,並不是必須的,只不過是建議性的,在這一點上也常會透露出產商在開源文化的素質。畢竟是開源的方案,如果都使用約定俗成的解決方案,則大家都會不用看也知道怎麼改。但一些在開源做得不好的廠商,對這些的配置環境都喜歡自己搞一套東西出來,要顯得自己與衆不同,所以對於配置文件的寫法與移植過程,也需要具體情況具體對待。

當我們完成了這些配置上的工作後,可以先將這些配置上傳到repo的服務器管理起來,剩下的移植工作就是實現所需要的HAL了。在Android移植過程裏,很多HAL的實現,是可以大量複用的,可以找一個類似的配置複製過來,然後再進行細微調整,比如使用ALSA ASoC框架的音頻支持,基本功能都是通用的,只需要在Audio Path和HiJack功能上進行微調即可。

轉自:http://www.uml.org.cn/mobiledev/201211063.asp#1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章