好幾個月都沒有更新過博客了,從今天開始,老羅將嘗試對Android系統的UI實現作一個系統的分析,也算是落實之前所作出的承諾。提到Android系統的UI,我們最先接觸到的便是系統在啟動過程中所出現的畫面了。Android系統在啟動的過程中,最多可以出現三個畫面,每一個畫面都用來描述一個不同的啟動階段。本文將詳細分析這三個開機畫面的顯示過程,以便可以開啟我們對Android系統UI實現的分析之路。
第一個開機畫面是在內核啟動的過程中出現的,它是一個靜態的畫面。第二個開機畫面是在init進程啟動的過程中出現的,它也是一個靜態的畫面。第三個開機畫面是在系統服務啟動的過程中出現的,它是一個動態的畫面。無論是哪一個畫面,它們都是在一個稱為幀緩沖區(frame buffer,簡稱fb)的硬件設備上進行渲染的。接下來,我們就分別分析這三個畫面是如何在fb上顯示的。
1. 第一個開機畫面的顯示過程
Android系統的第一個開機畫面其實是Linux內核的啟動畫面。在默認情況下,這個畫面是不會出現的,除非我們在編譯內核的時候,啟用以下兩個編譯選項:
CONFIG_FRAMEBUFFER_CONSOLE
CONFIG_LOGO
第一個編譯選項表示內核支持幀緩沖區控制臺,它對應的配置菜單項為:Device Drivers ---> Graphics support ---> Console display driver support ---> Framebuffer Console support。第二個編譯選項表示內核在啟動的過程中,需要顯示LOGO,它對應的配置菜單項為:Device Drivers ---> Graphics support ---> Bootup logo。配置Android內核編譯選項可以參考在Ubuntu上下載、編譯和安裝Android最新內核源代碼(Linux Kernel)一文。
幀緩沖區硬件設備在內核中有一個對應的驅動程序模塊fbmem,它實現在文件kernel/goldfish/drivers/video/fbmem.c中,它的初始化函數如下所示:
/**
*??????fbmem_init?-?init?frame?buffer?subsystem
*
*??????Initialize?the?frame?buffer?subsystem.
*
*??????NOTE:?This?function?is?_only_?to?be?called?by?drivers/char/mem.c.
*
*/
static?int?__init
fbmem_init(void)
{
proc_create("fb",?0,?NULL,?&fb_proc_fops);
if?(register_chrdev(FB_MAJOR,"fb",&fb_fops))
printk("unable?to?get?major?%d?for?fb?devs\n",?FB_MAJOR);
fb_class?=?class_create(THIS_MODULE,?"graphics");
if?(IS_ERR(fb_class))?{
printk(KERN_WARNING?"Unable?to?create?fb?class;?errno?=?%ld\n",?PTR_ERR(fb_class));
fb_class?=?NULL;
}
return?0;
}
這個函數首先調用函數proc_create在/proc目錄下創建了一個fb文件,接著又調用函數register_chrdev來注冊了一個名稱為fb的字符設備,最后調用函數class_create在/sys/class目錄下創建了一個graphics目錄,用來描述內核的圖形系統。
模塊fbmem除了會執行上述初始化工作之外,還會導出一個函數register_framebuffer:
EXPORT_SYMBOL(register_framebuffer);
這個函數在內核的啟動過程會被調用,以便用來執行注冊幀緩沖區硬件設備的操作,它的實現如下所示:
/**
*??????register_framebuffer?-?registers?a?frame?buffer?device
*??????@fb_info:?frame?buffer?info?structure
*
*??????Registers?a?frame?buffer?device?@fb_info.
*
*??????Returns?negative?errno?on?error,?or?zero?for?success.
*
*/
int
register_framebuffer(struct?fb_info?*fb_info)
{
int?i;
struct?fb_event?event;
......
if?(num_registered_fb?==?FB_MAX)
return?-ENXIO;
......
num_registered_fb++;
for?(i?=?0?;?i?
if?(!registered_fb[i])
break;
fb_info->node?=?i;
mutex_init(&fb_info->lock);
fb_info->dev?=?device_create(fb_class,?fb_info->device,
MKDEV(FB_MAJOR,?i),?NULL,?"fb%d",?i);
if?(IS_ERR(fb_info->dev))?{
/*?Not?fatal?*/
printk(KERN_WARNING?"Unable?to?create?device?for?framebuffer?%d;?errno?=?%ld\n",?i,?PTR_ERR(fb_info->dev));
fb_info->dev?=?NULL;
}?else
fb_init_device(fb_info);
......
registered_fb[i]?=?fb_info;
event.info?=?fb_info;
fb_notifier_call_chain(FB_EVENT_FB_REGISTERED,?&event);
return?0;
}
由于系統中可能會存在多個幀緩沖區硬件設備,因此,fbmem模塊使用一個數組registered_fb保存所有已經注冊了的幀緩沖區硬件設備,其中,每一個幀緩沖區硬件都是使用一個結構體fb_info來描述的。
我們知道,在Linux內核中,每一個硬件設備都有一個主設備號和一個從設備號,它們用來唯一地標識一個硬件設備。對于幀緩沖區硬件設備來說,它們的主設備號定義為FB_MAJOR(29),而從設備號則與注冊的順序有關,它們的值依次等于0,1,2等。
每一個被注冊的幀緩沖區硬件設備在/dev/graphics目錄下都有一個對應的設備文件fb,其中,表示一個從設備號。例如,第一個被注冊的幀緩沖區硬件設備在/dev/graphics目錄下都有一個對應的設備文件fb0。用戶空間的應用程序通過這個設備文件就可以操作幀緩沖區硬件設備了,即將要顯示的畫面渲染到幀緩沖區硬件設備上去。
這個函數最后會通過調用函數fb_notifier_call_chain來通知幀緩沖區控制臺,有一個新的幀緩沖區設備被注冊到內核中來了。
幀緩沖區控制臺在內核中對應的驅動程序模塊為fbcon,它實現在文件kernel/goldfish/drivers/video/console/fbcon.c中,它的初始化函數如下所示:
static?struct?notifier_block?fbcon_event_notifier?=?{
.notifier_call??=?fbcon_event_notify,
};
......
static?int?__init?fb_console_init(void)
{
int?i;
acquire_console_sem();
fb_register_client(&fbcon_event_notifier);
fbcon_device?=?device_create(fb_class,?NULL,?MKDEV(0,?0),?NULL,
"fbcon");
if?(IS_ERR(fbcon_device))?{
printk(KERN_WARNING?"Unable?to?create?device?"
"for?fbcon;?errno?=?%ld\n",
PTR_ERR(fbcon_device));
fbcon_device?=?NULL;
}?else
fbcon_init_device();
for?(i?=?0;?i?
con2fb_map[i]?=?-1;
release_console_sem();
fbcon_start();
return?0;
}
這個函數除了會調用函數device_create來創建一個類別為graphics的設備fbcon之外,還會調用函數fb_register_client來監聽幀緩沖區硬件設備的注冊事件,這是由函數fbcon_event_notify來實現的,如下所示:
static?int?fbcon_event_notify(struct?notifier_block?*self,
unsigned?long?action,?void?*data)
{
struct?fb_event?*event?=?data;
struct?fb_info?*info?=?event->info;
......
int?ret?=?0;
......
switch(action)?{
......
case?FB_EVENT_FB_REGISTERED:
ret?=?fbcon_fb_registered(info);
break;
......
}
done:
return?ret;
}
幀緩沖區硬件設備的注冊事件最終是由函數fbcon_fb_registered來處理的,它的實現如下所示:
static?int?fbcon_fb_registered(struct?fb_info?*info)
{
int?ret?=?0,?i,?idx?=?info->node;
fbcon_select_primary(info);
if?(info_idx?==?-1)?{
for?(i?=?first_fb_vc;?i?<=?last_fb_vc;?i++)?{
if?(con2fb_map_boot[i]?==?idx)?{
info_idx?=?idx;
break;
}
}
if?(info_idx?!=?-1)
ret?=?fbcon_takeover(1);
}?else?{
for?(i?=?first_fb_vc;?i?<=?last_fb_vc;?i++)?{
if?(con2fb_map_boot[i]?==?idx)
set_con2fb_map(i,?idx,?0);
}
}
return?ret;
}
函數fbcon_select_primary用來檢查當前注冊的幀緩沖區硬件設備是否是一個主幀緩沖區硬件設備。如果是的話,那么就將它的信息記錄下來。這個函數只有當指定了CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY編譯選項時才有效,否則的話,它是一個空函數。
在Linux內核中,每一個控制臺和每一個幀緩沖區硬件設備都有一個從0開始的編號,它們的初始對應關系保存在全局數組con2fb_map_boot中。控制臺和幀緩沖區硬件設備的初始對應關系是可以通過設置內核啟動參數來初始化的。在模塊fbcon中,還有另外一個全局數組con2fb_map,也是用來映射控制臺和幀緩沖區硬件設備的對應關系,不過它映射的是控制臺和幀緩沖區硬件設備的實際對應關系。
全局變量first_fb_vc和last_fb_vc是全局數組con2fb_map_boot和con2fb_map的索引值,用來指定系統當前可用的控制臺編號范圍,它們也是可以通過設置內核啟動參數來初始化的。全局變量first_fb_vc的默認值等于0,而全局變量last_fb_vc的默認值等于MAX_NR_CONSOLES - 1。
全局變量info_idx表示系統當前所使用的幀緩沖區硬件的編號。如果它的值等于-1,那么就說明系統當前還沒有設置好當前所使用的幀緩沖區硬件設備。在這種情況下,函數fbcon_fb_registered就會在全局數組con2fb_map_boot中檢查是否存在一個控制臺編號與當前所注冊的幀緩沖區硬件設備的編號idx對應。如果存在的話,那么就會將當前所注冊的幀緩沖區硬件設備編號idx保存在全局變量info_idx中。接下來還會調用函數fbcon_takeover來初始化系統所使用的控制臺。在調用函數fbcon_takeover的時候,傳進去的參數為1,表示要顯示第一個開機畫面。
如果全局變量info_idx的值不等于-1,那么函數fbcon_fb_registered同樣會在全局數組con2fb_map_boot中檢查是否存在一個控制臺編號與當前所注冊的幀緩沖區硬件設備的編號idx對應。如果存在的話,那么就會調用函數set_con2fb_map來調整當前所注冊的幀緩沖區硬件設備與控制臺的映射關系,即調整數組con2fb_map_boot和con2fb_map的值。
為了簡單起見,我們假設系統只有一個幀緩沖區硬件設備,這樣當它被注冊的時候,全局變量info_idx的值就會等于-1。當函數fbcon_fb_registered在全局數組con2fb_map_boot中發現有一個控制臺的編號與這個幀緩沖區硬件設備的編號idx對應時,接下來就會調用函數fbcon_takeover來設置系統所使用的控制臺。
函數fbcon_takeover的實現如下所示:
static?int?fbcon_takeover(int?show_logo)
{
int?err,?i;
if?(!num_registered_fb)
return?-ENODEV;
if?(!show_logo)
logo_shown?=?FBCON_LOGO_DONTSHOW;
for?(i?=?first_fb_vc;?i?<=?last_fb_vc;?i++)
con2fb_map[i]?=?info_idx;
err?=?take_over_console(&fb_con,?first_fb_vc,?last_fb_vc,
fbcon_is_default);
if?(err)?{
for?(i?=?first_fb_vc;?i?<=?last_fb_vc;?i++)?{
con2fb_map[i]?=?-1;
}
info_idx?=?-1;
}
return?err;
}
全局變量logo_shown的初始值為FBCON_LOGO_CANSHOW,表示可以顯示第一個開機畫面。但是當參數show_logo的值等于0的時候,全局變量logo_shown的值會被重新設置為FBCON_LOGO_DONTSHOW,表示不可以顯示第一個開機畫面。
中間的for循環將當前可用的控制臺的編號都映射到當前正在注冊的幀緩沖區硬件設備的編號info_idx中去,表示當前可用的控制臺與緩沖區硬件設備的實際映射關系。
函數take_over_console用來初始化系統當前所使用的控制臺。如果它的返回值不等于0,那么就表示初始化失敗。在這種情況下,最后的for循環就會將全局數組con2fb_map的各個元素的值設置為-1,表示系統當前可用的控制臺還沒有映射到實際的幀緩沖區硬件設備中去。這時候全局變量info_idx的值也會被重新設置為-1。
調用函數take_over_console來初始化系統當前所使用的控制臺,實際上就是向系統注冊一系列回調函數,以便系統可以通過這些回調函數來操作當前所使用的控制臺。這些回調函數使用結構體consw來描述。這里所注冊的結構體consw是由全局變量fb_con來指定的,它的定義如下所示:
/*
*??The?console?`switch'?structure?for?the?frame?buffer?based?console
*/
static?const?struct?consw?fb_con?=?{
.owner??????????????????=?THIS_MODULE,
.con_startup????????????=?fbcon_startup,
.con_init???????????????=?fbcon_init,
.con_deinit?????????????=?fbcon_deinit,
.con_clear??????????????=?fbcon_clear,
.con_putc???????????????=?fbcon_putc,
.con_putcs??????????????=?fbcon_putcs,
.con_cursor?????????????=?fbcon_cursor,
.con_scroll?????????????=?fbcon_scroll,
.con_bmove??????????????=?fbcon_bmove,
.con_switch?????????????=?fbcon_switch,
.con_blank??????????????=?fbcon_blank,
.con_font_set???????????=?fbcon_set_font,
.con_font_get???????????=?fbcon_get_font,
.con_font_default???????=?fbcon_set_def_font,
.con_font_copy??????????=?fbcon_copy_font,
.con_set_palette????????=?fbcon_set_palette,
.con_scrolldelta????????=?fbcon_scrolldelta,
.con_set_origin?????????=?fbcon_set_origin,
.con_invert_region??????=?fbcon_invert_region,
.con_screen_pos?????????=?fbcon_screen_pos,
.con_getxy??????????????=?fbcon_getxy,
.con_resize?????????????=?fbcon_resize,
};