通過前兩節HAL架構分析和JNI概述,我們對Android提供的Stub HAL有了比較詳細的瞭解了,下面我們來看下led的執行個體,寫驅動點亮led燈,就如同寫程式,學語言列印HelloWorld一樣,如果說列印HelloWorld是一門新語言使用的第一聲吆喝,那麼點亮led燈就是我們學習HAL的一座燈塔,指揮我們在後面的複雜的HAL代碼裡準確找到方向。
LedHAL執行個體架構
描述了我們Led執行個體的架構層次:
l LedDemo.java:是我們寫的Android應用程式
l LedService.java:是根據Led HAL封裝的Java架構層的API,主要用於嚮應用層提供架構層API,它屬於Android的架構層
l libled_runtime.so:由於Java代碼不能訪問HAL層,該庫是LedService.java對應的本地代碼部分
l led.default.so:針對led硬體的HAL代碼
LedDemo通過LedService提供的架構層API訪問Led裝置,LedService對於LedDemo應用程式而言是Led裝置的服務提供者,LedService運行在Dalvik中沒有辦法直接存取Led硬體裝置,它只能將具體的Led操作交給本地代碼來實現,通過JNI來調用Led硬體操作的封裝庫libled_runtime.so,由HAL Stub架構可知,在libled_runtime.so中首先尋找註冊為led的硬體裝置module,找到之後儲存其操作介面指標在本地庫中等待架構層LedService調用。led.default.so是HAL層代碼,它是上層操作的具體實施者,它並不是一個動態庫(也就是說它並沒有被任何進程載入並連結),它只是在本地代碼尋找硬體裝置module時通過ldopen”殺雞取卵”找module,返回該硬體module對應的device操作結構體中封裝的函數指標。
其調用時序如下:
Led HAL執行個體程式碼分析
我們來看下led執行個體的目錄結構:
主要檔案如下:
com.hello.LedService.cpp:它在frameworks/services/jni目錄下,是的Led本地服務代碼
led.c:HAL代碼
led.h:HAL代碼標頭檔
LedDemo.java:應用程式代碼
LedService.java:Led架構層服務代碼
在Android的源碼目錄下,架構層服務代碼應該放在frameworks/services/java/包名/目錄下,由Android的編譯系統統一編譯產生system/framework/services.jar檔案,由於我們的測試代碼屬於廠商定製代碼,盡量不要放到frameworks的源碼樹裡,我將其和LedDemo應用程式放在一起了,雖然這種方式從Android架構層次上不標準。
另外,本地服務代碼的檔案名稱要和對應的架構層Java代碼的名字匹配(包名+類檔案名稱,包目錄用“_“代替)。有源碼目錄裡都有對應的一個Android.mk檔案,它是Android編譯系統的指導檔案,用來編譯目標module。
1) Android.mk檔案分析
先來看下led源碼中①號Android.mk:
include $(call all-subdir-makefiles)
代碼很簡單,表示包含目前的目錄下所有的Android.mk檔案
先來看下led_app目錄下的③號Android.mk:
# 調用宏my-dir,這個宏返回當前Android.mk檔案所在的路徑LOCAL_PATH:= $(call my-dir) # 包含CLEAR_VARS變數指向的mk檔案build/core/clear_vars.mk,它主要用來清除編譯時間依賴的編譯變數include $(CLEAR_VARS) # 指定當前目標的TAG標籤,關於其作用見前面Android編譯系統章節LOCAL_MODULE_TAGS := user# 當前mk檔案的編譯目標模組LOCAL_PACKAGE_NAME := LedDemo# 編譯目標時依賴的源碼,它調用了一個宏all-java-files-under,該宏在build/core/definitions.mk中定義# 表示在目前的目錄下尋找所有的java檔案,將尋找到的java檔案返回LOCAL_SRC_FILES := $(callall-java-files-under, src)# 在編譯Android應用程式時都要指定API level,也就是當前程式的編譯平台版本# 這裡表示使用當前源碼的版本LOCAL_SDK_VERSION := current# 最重要的就是這句代碼,它包含了一個檔案build/core/package.mk,根據前面設定的編譯變數,編譯產生Android包檔案,即:apk檔案include $(BUILD_PACKAGE)
上述代碼中都加了注釋,基本上每一個編譯目標都有類似上述的編譯變數的聲明:
LOCAL_MODULE_TAGS
LOCAL_PACKAGE_NAME
LOCAL_SRC_FILES
由於所有的Android.mk最終被編譯系統包含,所以在編譯每個目標模組時,都要通過LOCAL_PATH:= $(call my-dir)指定當前目標的目錄,然後調用include $(CLEAR_VARS)先清除編譯系統依賴的重要的編譯變數,再產生新的編譯變數。
讓我們來看看LedDemo目標對應的源碼吧。
2) LedDemo程式碼分析
學習過Android應用的同學對其目錄結構很熟悉,LedDemo的源碼在src目錄下。
@ led_app/src/com/farsight/LedDemo.java:
package com.hello; import com.hello.LedService; import com.hello.R; importandroid.app.Activity; importandroid.os.Bundle; importandroid.util.Log; importandroid.view.View; import android.view.View.OnClickListener; importandroid.widget.Button; public classLedDemo extends Activity { privateLedService led_svc; private Buttonbtn; private booleaniflag = false; private Stringtitle; /** Calledwhen the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Log.i("Java App", "OnCreate"); led_svc =new LedService(); btn =(Button) this.findViewById(R.id.Button01); this.btn.setOnClickListener(new OnClickListener() { public void onClick(View v) { Log.i("Java App", "btnOnClicked"); if (iflag) { title = led_svc.set_off(); btn.setText("Turn On"); setTitle(title); iflag = false; } else { title = led_svc.set_on(); btn.setText("Turn Off"); setTitle(title); iflag = true; } } }); } }
代碼很簡單,Activity上有一個按鈕,當Activity初始化時建立LedService對象,按鈕按下時通過LedService對象調用其方法set_on()和set_off()。
3) LedService程式碼分析
我們來看下LedService的代碼:
@led_app/src/com/farsight/LedService.java:
package com.hello;import android.util.Log;public class LedService { /* * loadnative service. */ static { // 靜態初始化語言塊,僅在類被載入時被執行一次,通常用來載入庫 Log.i ("Java Service" , "Load Native Serivce LIB" ); System.loadLibrary ( "led_runtime" ); } // 構造方法 public LedService() { int icount ; Log.i ("Java Service" , "do init Native Call" ); _init (); icount =_get_count (); Log.d ("Java Service" , "led count = " + icount ); Log.d ("Java Service" , "Init OK " ); } /* * LED nativemethods. */ public Stringset_on() { Log.i ("com.hello.LedService" , "LED On" ); _set_on(); return"led on" ; } public String set_off() { Log.i ("com.hello.LedService" , "LED Off" ); _set_off(); return"led off" ; } /* * declare all the native interface. */ private static native boolean _init(); private static native int _set_on(); private static native int _set_off(); private static native int _get_count(); }
通過分析上面代碼可知LedService的工作:
l 載入本地服務的庫代碼
l 在構造方法裡調用_init本地代碼,對Led進行初始化,並調用get_count得到Led燈的個數
l 為LedDemo應用程式提供兩個API:set_on和set_off,這兩個API方法實際上也是交給了本地服務代碼來操作的
由於Java代碼無法直接操作底層硬體,通過JNI方法將具體的操作交給本地底層代碼實現,自己只是一個API Provider,即:服務提供者。
讓我們來到底層本地代碼,先看下底層代碼的Android.mk檔案:
@ frameworks/Android.mk:
LOCAL_PATH:= $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE_TAGS := engLOCAL_MODULE:= libled_runtime # 編譯目標模組LOCAL_SRC_FILES:= \ services/jni/com_farsight_LedService.cpp LOCAL_SHARED_LIBRARIES := \ # 編譯時間依賴的動態庫 libandroid_runtime \ libnativehelper \ libcutils \ libutils \ libhardware LOCAL_C_INCLUDES += \ #編譯時間用到的標頭檔目錄 $(JNI_H_INCLUDE)LOCAL_PRELINK_MODULE := false # 本目標為非預連結模組include $(BUILD_SHARED_LIBRARY) # 編譯產生共用動態庫
結合前面分析的Android.mk不難看懂這個mk檔案。之前的mk檔案是編譯成Android apk檔案,這兒編譯成so共用庫,所以LOCAL_MODULE和include $(BUILD_SHARED_LIBRARY)與前面mk檔案不同,關於Android.mk檔案裡的變數作用,請查看Android編譯系統章節。
總而言之,本地代碼編譯產生的目標是libled_runtime.so檔案。
4) Led本地服務程式碼分析
我們來看下本地服務的源碼:
@ frameworks/services/jni/com_hello_LedService.cpp:
#define LOG_TAG "LedService"#include "utils/Log.h"#include <stdlib.h>#include <string.h>#include <unistd.h>#include <assert.h>#include <jni.h>#include "../../../hardware/led.h"static led_control_device_t *sLedDevice = 0;static led_module_t* sLedModule=0; static jint get_count(void){ LOGI("%sE", __func__); if(sLedDevice) returnsLedDevice->get_led_count(sLedDevice); else LOGI("sLedDevice is null"); return 0;}static jint led_setOn(JNIEnv* env, jobject thiz) { LOGI("%sE", __func__); if(sLedDevice) { sLedDevice->set_on(sLedDevice); }else{ LOGI("sLedDevice is null"); } return 0; } static jint led_setOff(JNIEnv* env, jobject thiz) { LOGI("%s E", __func__); if(sLedDevice) { sLedDevice->set_off(sLedDevice); }else{ LOGI("sLedDevice is null"); } return 0; }static inline int led_control_open(const structhw_module_t* module, structled_control_device_t** device) { LOGI("%s E ", __func__); returnmodule->methods->open(module, LED_HARDWARE_MODULE_ID, (struct hw_device_t**)device);}static jint led_init(JNIEnv *env, jclass clazz){ led_module_tconst * module; LOGI("%s E ", __func__); if(hw_get_module(LED_HARDWARE_MODULE_ID, (const hw_module_t**)&module) == 0){ LOGI("get Module OK"); sLedModule = (led_module_t *) module; if(led_control_open(&module->common, &sLedDevice) != 0) { LOGI("led_init error"); return-1; } } LOGI("led_init success"); return 0;} /* * * Array ofmethods. * Each entryhas three fields: the name of the method, the method * signature,and a pointer to the native implementation. */static const JNINativeMethod gMethods[] = { {"_init", "()Z",(void*)led_init}, {"_set_on", "()I",(void*)led_setOn }, {"_set_off", "()I",(void*)led_setOff }, {"_get_count", "()I",(void*)get_count },}; static int registerMethods(JNIEnv* env) { static constchar* const kClassName = "com/hello/LedService"; jclass clazz; /* look upthe class */ clazz =env->FindClass(kClassName); if (clazz ==NULL) { LOGE("Can't find class %s\n", kClassName); return-1; } /* registerall the methods */ if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK) { LOGE("Failed registering methods for %s\n", kClassName); return -1; } /* fill outthe rest of the ID cache */ return 0; } /* * This iscalled by the VM when the shared library is first loaded. */ jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env= NULL; jint result= -1; LOGI("JNI_OnLoad"); if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { LOGE("ERROR: GetEnv failed\n"); gotofail; } assert(env!= NULL); if(registerMethods(env) != 0) { LOGE("ERROR: PlatformLibrary nativeregistration failed\n"); gotofail; } /* success-- return valid version number */ result =JNI_VERSION_1_4; fail: return result; }
這兒的代碼不太容易讀,因為裡面是JNI的類型和JNI特性的代碼,看代碼先找入口。LedService.java架構代碼一載入就調用靜態初始化語句塊裡的System.loadLibrary ( "led_runtime" ),載入libled_runtime.so,該庫剛好是前面Android.mk檔案的目標檔案,也就是說LedService載入的庫就是由上面的本地代碼產生的。當一個動態庫被Dalvik載入時,首先在Dalvik會回調該庫代碼裡的JNI_OnLoad函數。也就是說JNI_OnLoad就是本地服務代碼的入口函數。
JNI_OnLoad的代碼一般來說是死的,使用的時候直接拷貝過來即可,vm->GetEnv會返回JNIEnv指標,而這個指標其實就是Java虛擬機器的環境變數,我們可以通過該指標去調用JNI提供的方法,如FindClass等,調用registerMethods方法,在方法裡通過JNIEnv的FindClass尋找LedService類的引用,然後在該類中註冊本地方法與Java方法的映射關係,上層Java代碼可以通過這個映射關係調用到本地代碼的實現。RegisterNatives方法接收三個參數:
l 第一個參數jclass:要註冊哪個類裡的本地方法映射關係
l 第二個參數JNINativeMethod*:這是一個本地方法與Java方法映射數組,JNINativeMethod是個結構體,每個元素是一個Java方法到本地方法的映射。
typedef struct { constchar* name; constchar* signature; void*fnPtr;} JNINativeMethod;
name:表示Java方法名
signature:表示方法的簽名
fnPtr:Java方法對應的本地方法指標
l 第三個參數size:映射關係個數
由代碼可知,Java方法與本地方法的映射關係如下:
Java方法 |
本地方法 |
void _init() |
jint led_init(JNIEnv *env, jclass clazz) |
int _set_on() |
jint led_setOn(JNIEnv* env, jobject thiz) |
int _set_off() |
jint led_setOff(JNIEnv* env, jobject thiz) |
int _get_count() |
jint get_count(void) |
通過上表可知,本地方法參數中預設會有兩個參數:JNIEnv* env, jobject thiz,分別表示JNI環境和調用當前方法的對象引用,當然你也可以不設定這兩個參數,在這種情況下你就不能訪問Java環境中的成員。本地方法與Java方法的簽名必須一致,傳回值不一致不會造成錯誤。
現在我們再來回顧下我們的調用調用流程:
l LedDemo建立了LedService對象
l LedService類載入時載入了對應的本地服務庫,在本地服務庫裡Dalvik自動調用JNI_OnLoad函數,註冊Java方法和本地方法映射關係。
根據Java語言特點,當LedDemo對象建立時會調用其構造方法LedService()。
// 構造方法 public LedService() { int icount ; Log.i ("Java Service" , "do init Native Call" ); _init (); icount =_get_count (); Log.d ("Java Service" , "led count = " + icount ); Log.d ("Java Service" , "Init OK " ); }
在LedService構造方法裡直接調用了本地方法_init和_get_count(通過native保留字聲明),也就是說調用了本地服務代碼裡的jint led_init(JNIEnv *env, jclass clazz)和jintget_count(void)。
在led_init方法裡的內容就是我們前面分析HAL架構代碼的使用規則了。
l 通過hw_get_module方法查到到註冊為LED_HARDWARE_MODULE_ID,即:”led”的module模組。
l 通過與led_module關聯的open函數指標開啟led裝置,返回其device_t結構體,儲存在本地代碼中,有的朋友可能會問,不是本地方法不能持續儲存一個引用嗎?由於device_t結構是在open裝置時通過malloc分配的,只要當前進程不死,該指標一直可用,在這兒本地代碼並沒有儲存Dalvik裡的引用,儲存的是mallco的分配空間地址,但是在關閉裝置時記得要將該地址空間free了,否則就記憶體流失了。
l 拿到了led裝置的device_t結構之後,當LedDemo上的按鈕按下時調用LedService對象的set_on和set_off方法,這兩個LedService方法直接調用了本地服務代碼的對應映射方法,本地方法直接調用使用device_t指向的函數來間接調用驅動作業碼。
好吧,讓我們再來看一個詳細的時序圖:
不用多解釋了。
最後一個檔案,HAL對應的Android.mk檔案:
@ hardware/Android.mk:
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_C_INCLUDES += \ include/LOCAL_PRELINK_MODULE := falseLOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hwLOCAL_SHARED_LIBRARIES := liblogLOCAL_SRC_FILES := led.cLOCAL_MODULE := led.defaultinclude $(BUILD_SHARED_LIBRARY)
註:LOCAL_PRELINK_MODULE:= false要加上,否則編譯出錯
指定目標名為:led.default
目標輸入目錄LOCAL_MODULE_PATH為:/system/lib/hw/,不指定會預設輸出到/system/lib目錄下。
根據前面HAL架構分析可知,HAL Stub庫預設載入地址為:/vendor/lib/hw/或/system/lib/hw/,在這兩個目錄尋找:硬體id名.default.so,所以我們這兒指定了HAL Stub的編譯目標名為led.default,編譯成動態庫,輸出目錄為:$(TARGET_OUT_SHARED_LIBRARIES)/hw,TARGET_OUT_SHARED_LIBRARIES指/system/lib/目錄。
5) 深入理解
我們從進程空間的概念來分析下我們上面寫的代碼。
我們前面的範例程式碼中,將LedDemo.java和LedService.java都放在了一個APK檔案裡,這也就意味著這個應用程式編譯完之後,它會運行在一個Dalvik虛擬機器執行個體中,即:一個進程裡,在LedService.java中載入了libled_runtime.so庫,通過JNI調用了本地代碼,根據動態庫的運行原理,我們知道,libled_runtime.so在第一次引用時會被載入到記憶體中並映射到引用庫的進程空間中,我們可以簡單理解為引用庫的程式和被引用的庫在一個進程中,而在libled_runtime.so庫中,又通過dlopen開啟了庫檔案led.default.so(該庫並沒有被庫載入器載入,而是被當成一個檔案開啟的),同樣我們可以理解為led.default.so和libled_runtime.so在同一個進程中。
由此可見,上面樣本的Led HAL代碼全部都在一個進程中實現,在該樣本中的LedService功能比較多餘,基本上不能算是一個服務。如果LedDemo運行在兩個進程中,就意味著兩個進程裡的LedService不能複用,通常我們所謂的Service服務一般向用戶端提供服務並且同時可以為多個用戶端服務(如),所以我們的樣本Led HAL代碼不是完美的HAL模型,我們後面章節會再實現一個比較完美的HAL架構。