編輯:關於Android編程
Android的應用程序以後綴名為apk的文件發布。APK文件中包含應用程序的代碼和資源數據,清單文件以及簽名信息。APK文件格式擴展於JAR格式,而JAR文件格式又是擴展自ZIP格式。所以,可以使用ZIP解壓工具,打開APK文件。
一個典型的APK文件內容如下:├── AndroidManifest.xml (1) ├── classes.dex (2) ├── lib (3) │ ├── armeabi │ │ └── libhello-jni.so │ ├── armeabi-v7a │ │ └── libhello-jni.so │ ├── mips │ │ └── libhello-jni.so │ └── x86 │ └── libhello-jni.so ├── META-INF (4) │ ├── CERT.RSA │ ├── CERT.SF │ └── MANIFEST.MF └── resources.arsc (5)
<1>:AndroidManifest.xml文件中包含了應用程序的組件,包名,版本等信息。
<2>:classes.dex是應用的可執行代碼。
<3>:對於使用了ndk編程的應用來說,APK中還有lib文件夾,這個文件夾下面存放的是適配各個平台的共享庫。
<4>:包含應用的簽名和清單文件,這部分內容後面還會講述。
<5>:對於可編譯的資源,最後都編譯好後,打包進了resources.arsc文件,比如string和style。對於編譯好的APK文件,可以使用aapt工具查看資源表。
An Example aapt 用法aapt d resourcesxxx.apk可打印出資源表
更多aapt的用法,參考aapt -help。
與APK簽名相關的文件都位於META-INF目錄下。下面會對三個文件做一些簡單的分析,關於加密解密算法的實現,超出了本文的范圍,這裡不會涉及到。
後綴名為.MF的文件,裡面存放的是打包在APK中的文件的Message digest(消息摘要)。
MANIFEST.MF的內容Manifest-Version: 1.0 Created-By: 1.0 (Android) Name: AndroidManifest.xml SHA1-Digest: bbFIUGA27QAiLBforTb0B+GeAk8= Name: lib/armeabi-v7a/libhello-jni.so SHA1-Digest: imJ95BzGT7rI57k7xMnRNBKysgo= Name: lib/x86/libhello-jni.so SHA1-Digest: /QM7n8FQYZ7gTjHAfsNzcoPZsnY= Name: lib/mips/libhello-jni.so SHA1-Digest: A2WeX1JI3csDX8v/PapWe97Gx9k= Name: resources.arsc SHA1-Digest: rzak75eaqS4baaz+03KawAATuNk= Name: lib/armeabi/libhello-jni.so SHA1-Digest: 3SGVYUG2+pMhNqB7K5m/T4iA+K8= Name: classes.dex SHA1-Digest: 8XJt/CggZUAQU6ULBavrzodpwok=
對於APK包中的文件先使用hash函數獲取Message Digest,然後采用base64編碼得到的結果作為文件的最終Message Digest。MANIFEST.MF中的每一條目都是由Name和SHA1-Digest組成。Name對應的是文件名,SHA1-Digest對應的文件的Message Digest。
可以使用如下的命令生成Message Digest:
$ openssl sha1 -binary HelloJni/classes.dex | openssl base64
CERT.SF文件中存放的也是Message Digest。與上面MANIFEST.MF文件對應的CERT.SF文件內容如下:
CERT.SF 內容Signature-Version: 1.0 Created-By: 1.0 (Android) SHA1-Digest-Manifest: PDEMo/mMNPiPsuYop2qQpb9VjX0=1 Name: AndroidManifest.xml SHA1-Digest: sbIUZUy5AB2lb4RMZzyqlf+JEuw= Name: lib/armeabi-v7a/libhello-jni.so SHA1-Digest: cP7n4f23m5CWostVb5+C65095Oo= Name: lib/x86/libhello-jni.so SHA1-Digest: vFuCcAZFIQgcEkc6a6L395gkbdQ= Name: lib/mips/libhello-jni.so SHA1-Digest: 98f4yVRHgXH5cKKnufCTK7EbUVs= Name: resources.arsc SHA1-Digest: OynBh/Tw+yCvo6RPdC5subo2+HE= Name: classes.dex SHA1-Digest: NTB2dedbG357wxDmiiD7dHTFp6E= Name: lib/armeabi/libhello-jni.so SHA1-Digest: mjzjxE6Pt0Lrz4k5PeKTONgSh5A=
CERT.SF中的第一條內容來自對MANIFEST.MF文件Message Digest的結果;其他的條目分別是對MANIFEST.MF中對應的條目做Message Digest.下面的命令展示了這一過程:
An Example CERT.SF文件內容生成openssl sha1 -binary HelloJni/META-INF/MANIFEST.MF | openssl base64
其結果為:PDEMo/mMNPiPsuYop2qQpb9VjX0=
echo -en "Name: lib/armeabi-v7a/libhello-jni.so\r\nSHA1-Digest: imJ95BzGT7rI57k7xMnRNBKysgo=\r\n\r\n" | openssl sha1 -binary | openssl base64
其結果為:cP7n4f23m5CWostVb5+C65095Oo=
通過上面例子的結果,可以知道,CEART.SF中的內容來自於對MANIEST.MF文件以及文件中的條目做Message Digest的結果。
CERT.RSA文件中存放著CERT.SF的數字簽名,以及簽名證書。如果存在多個.SF和.RSA文件,那麼每一對的文件名都需要匹配。比如CERT1.SF對應著CERT1.RSA,CERT2.SF對應CERT2.RSA。
可以使用如下命令查看CERT.RAS文件中存放的證書.
openssl pkcs7 -inform DER -in HelloJni/META-INF/CERT.RSA -noout -print_certs -text
使用下面命令提取證書:
openssl pkcs7 -inform DER -print_certs -in HelloJni/META-INF/CERT.RSA -out cert.pem
使用如下命令打印證書內容:
openssl x509 -in cert.pem -noout -text
可以使用如下命令驗證SF文件的有效性:
openssl smime -verify -in HelloJni/META-INF/CERT.RSA -inform DER -content HelloJni/META-INF/CERT.SF -CAfile cert.pem
APK的安裝主要有三種方式:
對於在/system/app和/data/app目錄下的APK文件,在PackageManagerService的啟動過程中,會掃描安裝;
通過PackageInstaller的方式安裝;
安裝成功的APK,PackageManagerService會生成”記賬簿“來保存必要的信息。主要有以下兩個文件:
/data/system/packages.list
packages.list文件中保存著應用的包名,uid,所屬的用戶,以及“home”目錄.因為Android中每一個應用都類似linux系統中user的概念,它們都有自己的home目錄,用來保存應用特定的數據。
An Example packages.list文件com.android.defcontainer 10002 0 /data/data/com.android.defcontainer
com.android.providers.contacts 10021 0 /data/data/com.android.providers.contacts
com.android.quicksearchbox 10027 0 /data/data/com.android.quicksearchbox
com.android.demo.notepad3 10011 0 /data/data/com.android.demo.notepad3
com.android.contacts 10021 0 /data/data/com.android.contacts
com.android.systemui 10013 0 /data/data/com.android.systemui
com.android.packageinstaller 10012 0 /data/data/com.android.packageinstaller
com.android.calendar 10023 0 /data/data/com.android.calendar
packages.xml文件中保存著應用程序的詳細信息,包括包名,代碼路徑,jni庫路徑,應用標記,簽名,權限等。
對於APK的三種安裝方式,接下來的章節會詳細的講到。
PackageManagerService由system_server啟動,它全面負責應用包的安裝,卸載,權限檢查等工作。在每次開機的時候,PackageManagerService都會在其構造函數中,對指定的目錄的APK進行掃描。對於沒有安裝的APK文件會觸發安裝過程。 下面我們對PackageManagerService構造函數中出現的重點函數和重點流程展開介紹。
PackageManagerService的readPermissions方法負責讀取/etc/permission目錄下面的配置文件。這些配置文件中保存的信息有:系統支持的硬件,比如是否支持wifi,gps等;權限映射關系。
描述系統支持的硬件特性的文件,一般滿足這樣的命名規范:android.hardware.XXX.xml,XXX代表硬件模塊名。下面是 samsung manta 的nfc特性文件——android.hardware.nfc.xml的內容:
讀取出來的feature保存在HashMap中:
// These are the features this devices supports that // were read from the etc/permissions.xml file. final HashMapmAvailableFeatures = new HashMap ();
應用程序通過PackageManager類可以查詢指定的feature系統是否支持,以及獲得所有系統支持的feature.
frameworks/base/core/java/android/content/pm
public abstract FeatureInfo[] getSystemAvailableFeatures(); public abstract boolean hasSystemFeature(String name);
設備目錄/etc/permissions下面的特性文件來自於哪裡呢?它們實際上是在編譯的時候打包到system.image文件中。比如上面的samsung manta 的nfc特性文件就是在manta的device.mk文件中將frameworks/native/data/etc/android.hardware.nfc.xml文件copy到system/etc/permissions/android.hardware.nfc.xml。
/etc/permissions目錄下面還有一個非常重要的xml文件——platform.xml,這個文件中記錄了Android APP權限與gid,uid的對應關系。這個文件在源碼的位置:frameworks/base/data/etc。在這個目錄下面還有一個Android.mk文件,負責將platform.xml編譯到system鏡像中:
LOCAL_PATH := $(my-dir) ######################## include $(CLEAR_VARS) LOCAL_MODULE := platform.xml LOCAL_MODULE_CLASS := ETC # This will install the file in /system/etc/permissions # LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions LOCAL_SRC_FILES := $(LOCAL_MODULE) include $(BUILD_PREBUILT)
上面的例子也給我們提供了另一種參考:如何將配置文件編譯到system/ect/permissions中。
下面是platform.xml文件中的部分內容:
………… …………
platform.xml中主要有三塊內容:
將APP framework中的權限和底層的gid映射。當APP獲得某個權限之後,會獲得這個gid所具備的權限。
將APP framework的權限賦予某個系統級別的進程。這樣這個進程就可以獲得操作APP framework資源的。
readPermissions方法讀取/ect/permissions目錄下的xml文件,並為讀取的結果生成相應的數據結構。
在讀取完權限文件之後,PackageManagerService會在其構造函數中調用Settings的readLPw方法,讀取應用包的設置文件。
/data/system/packages.xml
/data/system/packages-backup.xml
/data/system/packages.list
對於packages.xml和packages.list在之前已經簡單的介紹過了,packages-backup.xml是packages.xml的備份文件。在每次寫packages.xml文件的時候,都會將舊的packages.xml文件先備份,這樣做是為了防止寫文件過程中文件以外損壞,還能從舊的文件中恢復。package-restrictions.xml保存著受限制的APP的狀態,比如某個APP處於disable狀態,或者某個APP具有更高的優先級等。這裡舉一個例子:
$adb shell pm disable com.android.providers.drm
運行上述命令之後,package-restrictions.xml文件就會存在一條受限制的記錄:
關於enable的含義可以參考:PackageManager.java中定義的常量:
public static final int COMPONENT_ENABLED_STATE_DEFAULT = 0; public static final int COMPONENT_ENABLED_STATE_ENABLED = 1; public static final int COMPONENT_ENABLED_STATE_DISABLED = 2; public static final int COMPONENT_ENABLED_STATE_DISABLED_USER = 3;
readLPw方法負責讀取packages.xml文件。它的邏輯是如果存在packages-backup.xml,就認為packages.xml已經損壞,將之刪除。然後從packages-backup.xml中讀取信息,用讀取的信息構造一個PackageSetting對象,然後以包名為key,PackageSetting為value,保存在HashMap中。
final HashMapmPackages = new HashMap ();
現在可以總結下readLPw的執行過程:
如果系統為第一次開機,則調用readDefaultPreferredAppsLPw方法,讀取/etc/preferred-apps目錄下的xml文件。這個目錄下面的xml文件提供了默認的優先APP的設置;
讀取packages.xml文件
在PackageManagerService的構造函數中,會調用此方法,對以下目錄下的APK文件進行掃描:
/system/framework
/system/app
掃描過程分為兩個步驟:
掃描APK安裝包的AndroidManifest.xml文件和提取證書信息,以此信息構建一個PackageParser.Package對象;
創建Package對應的PackageSetting對象,這個對象記錄了應用包的一些設置信息;
驗證簽名信息
安裝應用程序,包括創建應用程序數據目錄,動態庫的提取,對科執行代碼進行優化等
下面是一張流程圖,描述了系統第一次開機過程中對/data/app目錄掃描的過程。因為掃描是個很復雜的過程,所以在流程圖中省略了一些異常的case,主要集中在掃描安裝這條主線上,對於升級等流程的判斷,以及簽名信息不符合的判斷都進行了省略。
上圖中標記紅色的部分是掃描安裝過程中幾個關鍵的步驟。
mSetting.getPackageLPw方法會為Package創建對應的PackageSetting對象。這個對象中保存的信息最後會通過writeLPr寫入到/data/system/packages.xml文件中去。
createDataDirsLI方法會給installd發送消息,為應用程序創建對應的數據目錄。
copyNativeLibrariesForInternalApp會將應用程序包的動態庫復制到對應的動態庫目錄。
上面提到的installd,將在下一個小節講解。PackageManagerService掃描完APK之後,會在其內部建立復雜的數據結構。這裡我們只以簡單的一張類圖感受下兩個重要的類——PackageManagerService和Settings的關系。
簡單的介紹下上面的圖:
Package對象中的數據是從APK文件中掃描而得到,主要來源於AndroidManifest.xml文件
PackageSetting的父類是PackageSettingBase,這個類中的數據除了第一次開機以外,都是從packages.xml或者packages-backup.xml
上面講到,PackageManagerService在掃描過程中會給installd發送消息,請求創建應用程序的數據目錄和執行代碼優化。那這個installd是什麼東西呢?為什麼需要創建這麼個模塊?
installd的代碼路徑:
frameworks/base/cmds/installd
在應用程序的管理工作中,有時候需要對存儲設備做一些操作,比如創建目錄修改目錄權限等,這些操作有的是需要特權級的權限。PackageManagerService存活在system_server進程中,這個進程的用戶為system,它是沒有特權級的權限的,所以我猜想,出於安全的角度考慮,Android單獨將一部分需要特權的工作,轉交給installd進程去完成。
在installd的入口函數中,首先是做一些初始化的工作,然後放棄自己的root用戶身份,改變了自己的用戶類型和組用戶類型,但同時它保留了設置目錄權限,以及修改目錄屬主的權限:
static void drop_privileges() { if (prctl(PR_SET_KEEPCAPS, 1) < 0) { ALOGE("prctl(PR_SET_KEEPCAPS) failed: %s\n", strerror(errno)); exit(1); } if (setgid(AID_INSTALL) < 0) { ALOGE("setgid() can't drop privileges; exiting.\n"); exit(1); } if (setuid(AID_INSTALL) < 0) { ALOGE("setuid() can't drop privileges; exiting.\n"); exit(1); } struct __user_cap_header_struct capheader; struct __user_cap_data_struct capdata[2]; memset(&capheader, 0, sizeof(capheader)); memset(&capdata, 0, sizeof(capdata)); capheader.version = _LINUX_CAPABILITY_VERSION_3; capheader.pid = 0; capdata[CAP_TO_INDEX(CAP_DAC_OVERRIDE)].permitted |= CAP_TO_MASK(CAP_DAC_OVERRIDE); capdata[CAP_TO_INDEX(CAP_CHOWN)].permitted |= CAP_TO_MASK(CAP_CHOWN); capdata[CAP_TO_INDEX(CAP_SETUID)].permitted |= CAP_TO_MASK(CAP_SETUID); capdata[CAP_TO_INDEX(CAP_SETGID)].permitted |= CAP_TO_MASK(CAP_SETGID); capdata[0].effective = capdata[0].permitted; capdata[1].effective = capdata[1].permitted; capdata[0].inheritable = 0; capdata[1].inheritable = 0; if (capset(&capheader, &capdata[0]) < 0) { ALOGE("capset failed: %s\n", strerror(errno)); exit(1); } }
installd也算是個服務端了,它是通過socket來接收客戶端的消息,然後進行處理。在main函數中,通過for循環從socket中讀取消息,然後調用execute處理消息。
installd是通過init創建,在init.rc文件中有配置它的socket名:
service installd /system/bin/installd class main socket installd stream 600 system system
所以installd進程創建的socket名為:/dev/socket/installd, 600表示socket的用戶權限為可讀可寫,system表示用戶和用戶組。
installd可以處理的消息為:
struct cmdinfo { const char *name; unsigned numargs; int (*func)(char **arg, char reply[REPLY_MAX]); }; struct cmdinfo cmds[] = { { "ping", 0, do_ping }, { "install", 3, do_install }, { "dexopt", 3, do_dexopt }, { "movedex", 2, do_move_dex }, { "rmdex", 1, do_rm_dex }, { "remove", 2, do_remove }, { "rename", 2, do_rename }, { "fixuid", 3, do_fixuid }, { "freecache", 1, do_free_cache }, { "rmcache", 2, do_rm_cache }, { "getsize", 5, do_get_size }, { "rmuserdata", 2, do_rm_user_data }, { "movefiles", 0, do_movefiles }, { "linklib", 3, do_linklib }, { "mkuserdata", 3, do_mk_user_data }, { "rmuser", 1, do_rm_user }, { "cloneuserdata", 3, do_clone_user_data }, };
在PackageManagerService掃描安裝應用的過程中,給installd先後發送了如下消息:
install
這三個消息分別由do_install,do_dexopt函數來處理。
do_install函數為應用創建了以下目錄,在install過程中,同時為創建的目錄設置uid和gid。
數據目錄:/data/data/packageName/
lib目錄:/data/app-lib/packageName
lib符號鏈接:/data/data/packageName/lib → /data/app-lib/packageName
首先判斷apk所在目錄下面是否存在同名的odex文件,如果存在就直接返回;
以apk文件的路徑為名創建dex路徑。比如在debug版本中,/system/app/Settings.apk的dex文件將會保存在: /data/dalvik-cache/system@[email protected]@classes.dex文件中。dex文件命名規則是/data/dalvik-cache字符串拼接apk的路徑,再拼“/classs.dex"。然後將”/data/dalvik-cache/“字符串後面的‘/’替換為@字符。
PackageInstaller是Android提供的默認應用程序安裝與卸載管理應用程序。應用包中有兩個Activity:PackageInstallerActivity和UninstallerActivity分別用來管理應用程序的安裝和卸載。這兩個Activity可以通過發送Intent的方式啟動。下面的代碼片段指出了兩個Activity能處理的Intent:
通過給PackageInstallerActivity發送"android.intent.action.VIEW"或者是"android.intent.action.INSTALL_PACKAGE" Intent就能啟動對一個APK文件的安裝過程。例如,我們假設有一個應用程序位於/mnt/sdcard/Test.apk,為了啟動對這個應用程序的安裝,可以通過文件浏覽器點擊這個應用包文件,同時也可以通過adb shell登陸機器,然後輸入以下命令:
am start -a android.intent.action.VIEW -t application/vnd.android.package-archive -c android.intent.category.DEFAULT -d file:///mnt/sdcard/Test.apk
當輸入上述命令後,首先是以Action等參數構造一個Intent,然後am調用ActivityManagerService的startActivity方法。最後的結果是啟動PackageInstallerActivity,並將構造的Intent傳遞給啟動的Activity。下面簡單的描述下,PackageInstallerActivity安裝應用的過程:
首先PackageInstaller會檢查應用程序的完成性,嘗試著從應用包中提解析AndroidManifest.xml文件,構造ParckageParser.Package對象。如果成功,則繼續安裝;否則安裝退出
然後對發起安裝請求的應用屬性進行判斷:如果是system應用,那麼就進入初始化安裝步驟;如果是非system應用,那麼存在一個驗證是否允許安裝“unknown source”的步驟。
如果上面的步驟通過,就進入了初始化安裝過程。初始化過程主要是檢測應用是否安裝,以及提取相關的權限信息,用於界面顯示。這一過程主要是UI界面相關,略過不表
PackageManager的installPackageWithVerificationAndEncryption最終是一個binder調用,是在PackageManagerService中負責實現。在此方法中,獲取一個Message對象,其id為INIT_COPY,其obj為InstallParams。其中InstallParams對象根據installPackageWithVerificationAndEncryption的方法參數構造。INIT_COPY消息是在PackageHandler中進行處理。對於此消息的處理流程為:
首先通過connectToService方法綁定IMediaContainerService服務;在綁定成功之後,發送MCS_BOUND消息;IMeidaContainerService的實現是在frameworks/base/packages/DefaultContainerService/src/com/android/def/DefaultContainerService.java中。它負責原始apk文件的解密工作。
在PackageHandler消息中再次對MCS_BOUND消息進行處理。這次就會調用InstallParams的startCopy方法,開啟復制過程。startcopy方法又會調用handleStartCopy進行復制工作。主要做了如下工作:確定APK的安裝位置
檢查磁盤空間是否足夠
以InstallParams構造一份InstallArgs對象。InstallArgs有兩個子類:AsecInstallArgs和FileInstallArgs,對於安裝於外部sd卡或者是有Forward Lock屬性的應用,將會構造AsecInstallArgs,否則構造的是FileInstallArgs對象。
檢查APK是否需要驗證(一般是不需要)
所以,handleStartCopy最重要的工作就是由InstallParams的copyApk來完成。假設在這個InstallParams引用是一個FileInstallArgs對象,那麼其copy流程如下:
創建臨時文件。對於最終安裝在/data/app目錄下面的應用,其臨時文件的共享名為:/data/app/vmdl-XXX.tmp。這個XXX是一個隨機數
調用IMediaContainerService的服務接口,從原始APK文件中提取出解密過的APK數據,寫入到臨時文件中
在InstallParams的startCopy處理完handleStartCopy之後,會調用handleRetureCode方法,此方法中將會進行觸發真正的安裝流程。
void handleReturnCode() { if (mArgs != null) { processPendingInstall(mArgs, mRet); if (mTempPackage != null) { if (!mTempPackage.delete()) { Slog.w(TAG, "Couldn't delete temporary file: " + mTempPackage.getAbsolutePath()); } } } }
在handleReturnCode中又調用了processPendingInstall方法,這個方法主要做了以下幾件事情:
調用FileInstallArgs對象的doPreInstall方法:如果之前的流程有錯誤,此方法將會執行清理動作
調用PackageManagerService的installPackageLI方法,開始安裝過程
installPackgeLI執行步驟如下:
從臨時文件中提取出Package對象:final PackageParser.Package pkg = pp.parsePackage(tmpPackageFile, null, mMetrics, parseFlags)
提取證書信息,並驗證
scanPackageLI方法負責安裝apk
APK的安裝,在eng版本的開發中,還可以通過pm命令。pm實際上是一個腳本文件,腳本代碼位於frameworks/base/cmds/pm/pm。
base=/system export CLASSPATH=$base/framework/pm.jar exec app_process $base/bin com.android.commands.pm.Pm "$@"
從中可以看出,利用app_process程序,加載pm.jar包來執行具體的動作。
pm.jar的源碼和pm腳本位於同一目錄下面。 pm命令安裝應用,實際上走的流程和PackageInstaller差不多,都是調用PackageManagerService的installPackageWithVerificationAndEncryption方法。
但是他們也有不同的地方,pm命令可以指定加密解密參數,而PackageInstaller無法指定。
先給大家展示下效果圖,喜歡的朋友可以下載源碼哦。完成這個效果的是使用了 IOS_Dialog_Library下載地址:http://xiazai.jb51.net/201
圖像狀態資源只能定義有限的幾種狀態。如果需要更多的狀態,就要使用圖像級別資源。在該資源文件中可以定義任意多個圖像級別。每個圖像級別是一個整數區間,可以通過ImageVie
前言 下拉刷新組件在開發中使用率是非常高的,基本上聯網的APP都會采用這種方式。對於開發效率而言,使用獲得大家認可的開源庫必然是效率最高的,但是不重復發明輪子的
本文主要記錄了Launcher3拖動時的流程和代碼記錄,在桌面圖標拖動時會引起圖標的重排,拖動時受影響的圖標在文中由item或cell來表示。 圖標點擊效果和搖動效