<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                原文出處——>[Android系統的開機畫面顯示過程分析](http://blog.csdn.net/luoshengyang/article/details/7691321) 從今天開始,將嘗試對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)](http://blog.csdn.net/luoshengyang/article/details/6564592)一文。 幀緩沖區硬件設備在內核中有一個對應的驅動程序模塊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 < FB_MAX; 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<minor>,其中,<minor>表示一個從設備號。例如,第一個被注冊的幀緩沖區硬件設備在/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 < MAX_NR_CONSOLES; 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, }; ~~~ 接下來我們主要關注函數fbcon_init和fbcon_switch的實現,系統就是通過它來初始化和切換控制臺的。在初始化的過程中,會決定是否需要準備第一個開機畫面的內容,而在切換控制臺的過程中,會決定是否需要顯示第一個開機畫面的內容。 函數fbcon_init的實現如下所示: ~~~ static void fbcon_init(struct vc_data *vc, int init) { struct fb_info *info = registered_fb[con2fb_map[vc->vc_num]]; struct fbcon_ops *ops; struct vc_data **default_mode = vc->vc_display_fg; struct vc_data *svc = *default_mode; struct display *t, *p = &fb_display[vc->vc_num]; int logo = 1, new_rows, new_cols, rows, cols, charcnt = 256; int cap; if (info_idx == -1 || info == NULL) return; ...... if (vc != svc || logo_shown == FBCON_LOGO_DONTSHOW || (info->fix.type == FB_TYPE_TEXT)) logo = 0; ...... if (logo) fbcon_prepare_logo(vc, info, cols, rows, new_cols, new_rows); ...... } ~~~ 當前正在初始化的控制臺使用參數vc來描述,而它的成員變量vc_num用來描述當前正在初始化的控制臺的編號。通過這個編號之后,就可以在全局數組con2fb_map中找到對應的幀緩沖區硬件設備編號。有了幀緩沖區硬件設備編號之后,就可以在另外一個全局數組中registered_fb中找到一個fb_info結構體info,用來描述與當前正在初始化的控制臺所對應的幀緩沖區硬件設備。 參數vc的成員變量vc_display_fg用來描述系統當前可見的控制臺,它是一個類型為vc_data**的指針。從這里就可以看出,最終得到的vc_data結構體svc就是用來描述系統當前可見的控制臺的。 變量logo開始的時候被設置為1,表示需要顯示第一個開機畫面,但是在以下三種情況下,它的值會被設置為0,表示不需要顯示開機畫面: * A. 參數vc和變量svc指向的不是同一個vc_data結構體,即當前正在初始化的控制臺不是系統當前可見的控制臺。 * B. 全局變量logo_shown的值等于FBCON_LOGO_DONTSHOW,即系統不需要顯示第一個開機畫面。 * C. 與當前正在初始化的控制臺所對應的幀緩沖區硬件設備的顯示方式被設置為文本方式,即info->fix.type的值等于FB_TYPE_TEXT。 當最終得到的變量logo的值等于1的時候,接下來就會調用函數fbcon_prepare_logo來準備要顯示的第一個開機畫面的內容。 在函數fbcon_prepare_logo中,第一個開機畫面的內容是通過調用函數fb_prepare_logo來準備的,如下所示: ~~~ static void fbcon_prepare_logo(struct vc_data *vc, struct fb_info *info, int cols, int rows, int new_cols, int new_rows) { ...... int logo_height; ...... logo_height = fb_prepare_logo(info, ops->rotate); ...... if (logo_lines > vc->vc_bottom) { ...... } else if (logo_shown != FBCON_LOGO_DONTSHOW) { logo_shown = FBCON_LOGO_DRAW; ...... } } ~~~ 從函數fb_prepare_logo返回來之后,如果要顯示的第一個開機畫面所占用的控制臺行數小于等于參數vc所描述的控制臺的最大行數,并且全局變量logo_show的值不等于FBCON_LOGO_DONTSHOW,那么就說明前面所提到的第一個開機畫面可以顯示在控制臺中。這時候全局變量logo_show的值就會被設置為FBCON_LOGO_DRAW,表示第一個開機畫面處于等待渲染的狀態。 函數fb_prepare_logo實現在文件kernel/goldfish/drivers/video/fbmem.c中,如下所示: ~~~ int fb_prepare_logo(struct fb_info *info, int rotate) { int depth = fb_get_color_depth(&info->var, &info->fix); unsigned int yres; memset(&fb_logo, 0, sizeof(struct logo_data)); ...... if (info->fix.visual == FB_VISUAL_DIRECTCOLOR) { depth = info->var.blue.length; if (info->var.red.length < depth) depth = info->var.red.length; if (info->var.green.length < depth) depth = info->var.green.length; } if (info->fix.visual == FB_VISUAL_STATIC_PSEUDOCOLOR && depth > 4) { /* assume console colormap */ depth = 4; } /* Return if no suitable logo was found */ fb_logo.logo = fb_find_logo(depth); ...... return fb_prepare_extra_logos(info, fb_logo.logo->height, yres); } ~~~ 這個函數首先得到參數info所描述的幀緩沖區硬件設備的顏色深度depth,接著再調用函數fb_find_logo來獲得要顯示的第一個開機畫面的內容,并且保存在全局變量fb_logo的成員變量logo中。 函數fb_find_logo實現在文件kernel/goldfish/drivers/video/logo/logo.c文件中,如下所示: ~~~ extern const struct linux_logo logo_linux_mono; extern const struct linux_logo logo_linux_vga16; extern const struct linux_logo logo_linux_clut224; extern const struct linux_logo logo_blackfin_vga16; extern const struct linux_logo logo_blackfin_clut224; extern const struct linux_logo logo_dec_clut224; extern const struct linux_logo logo_mac_clut224; extern const struct linux_logo logo_parisc_clut224; extern const struct linux_logo logo_sgi_clut224; extern const struct linux_logo logo_sun_clut224; extern const struct linux_logo logo_superh_mono; extern const struct linux_logo logo_superh_vga16; extern const struct linux_logo logo_superh_clut224; extern const struct linux_logo logo_m32r_clut224; static int nologo; module_param(nologo, bool, 0); MODULE_PARM_DESC(nologo, "Disables startup logo"); /* logo's are marked __initdata. Use __init_refok to tell * modpost that it is intended that this function uses data * marked __initdata. */ const struct linux_logo * __init_refok fb_find_logo(int depth) { const struct linux_logo *logo = NULL; if (nologo) return NULL; if (depth >= 1) { #ifdef CONFIG_LOGO_LINUX_MONO /* Generic Linux logo */ logo = &logo_linux_mono; #endif #ifdef CONFIG_LOGO_SUPERH_MONO /* SuperH Linux logo */ logo = &logo_superh_mono; #endif } if (depth >= 4) { #ifdef CONFIG_LOGO_LINUX_VGA16 /* Generic Linux logo */ logo = &logo_linux_vga16; #endif #ifdef CONFIG_LOGO_BLACKFIN_VGA16 /* Blackfin processor logo */ logo = &logo_blackfin_vga16; #endif #ifdef CONFIG_LOGO_SUPERH_VGA16 /* SuperH Linux logo */ logo = &logo_superh_vga16; #endif } if (depth >= 8) { #ifdef CONFIG_LOGO_LINUX_CLUT224 /* Generic Linux logo */ logo = &logo_linux_clut224; #endif #ifdef CONFIG_LOGO_BLACKFIN_CLUT224 /* Blackfin Linux logo */ logo = &logo_blackfin_clut224; #endif #ifdef CONFIG_LOGO_DEC_CLUT224 /* DEC Linux logo on MIPS/MIPS64 or ALPHA */ logo = &logo_dec_clut224; #endif #ifdef CONFIG_LOGO_MAC_CLUT224 /* Macintosh Linux logo on m68k */ if (MACH_IS_MAC) logo = &logo_mac_clut224; #endif #ifdef CONFIG_LOGO_PARISC_CLUT224 /* PA-RISC Linux logo */ logo = &logo_parisc_clut224; #endif #ifdef CONFIG_LOGO_SGI_CLUT224 /* SGI Linux logo on MIPS/MIPS64 and VISWS */ logo = &logo_sgi_clut224; #endif #ifdef CONFIG_LOGO_SUN_CLUT224 /* Sun Linux logo */ logo = &logo_sun_clut224; #endif #ifdef CONFIG_LOGO_SUPERH_CLUT224 /* SuperH Linux logo */ logo = &logo_superh_clut224; #endif #ifdef CONFIG_LOGO_M32R_CLUT224 /* M32R Linux logo */ logo = &logo_m32r_clut224; #endif } return logo; } EXPORT_SYMBOL_GPL(fb_find_logo); ~~~ 文件開始聲明的一系列linux_logo結構體變量分別用來保存kernel/goldfish/drivers/video/logo目錄下的一系列ppm或者pbm文件的內容的。這些ppm或者pbm文件都是用來描述第一個開機畫面的。 全局變量nologo是一個類型為布爾變量的模塊參數,它的默認值等于0,表示要顯示第一個開機畫面。在這種情況下,函數fb_find_logo就會根據參數depth的值以及不同的編譯選項來選擇第一個開機畫面的內容,并且保存在變量logo中返回給調用者。 這一步執行完成之后,第一個開機畫面的內容就保存在模塊fbmem的全局變量fb_logo的成員變量logo中了。這時候控制臺的初始化過程也結束了,接下來系統就會執行切換控制臺的操作。前面提到,當系統執行切換控制臺的操作的時候,模塊fbcon中的函數fbcon_switch就會被調用。在調用的過程中,就會執行顯示第一個開機畫面的操作。 函數fbcon_switch實現在文件kernel/goldfish/drivers/video/console/fbcon.c中,顯示第一個開機畫面的過程如下所示: ~~~ static int fbcon_switch(struct vc_data *vc) { struct fb_info *info, *old_info = NULL; struct fbcon_ops *ops; struct display *p = &fb_display[vc->vc_num]; struct fb_var_screeninfo var; int i, prev_console, charcnt = 256; ...... if (logo_shown == FBCON_LOGO_DRAW) { logo_shown = fg_console; /* This is protected above by initmem_freed */ fb_show_logo(info, ops->rotate); ...... return 0; } return 1; } ~~~ 由于前面在準備第一個開機畫面的內容的時候,全局變量logo_show的值被設置為FBCON_LOGO_DRAW,因此,接下來就會調用函數fb_show_logo來顯示第一個開機畫面。在顯示之前,這個函數會將全局變量logo_shown的值設置為fg_console,后者表示系統當前可見的控制臺的編號。 函數fb_show_logo實現在文件kernel/goldfish/drivers/video/fbmem.c中,如下所示: ~~~ int fb_show_logo(struct fb_info *info, int rotate) { int y; y = fb_show_logo_line(info, rotate, fb_logo.logo, 0, num_online_cpus()); ...... return y; } ~~~ 這個函數調用另外一個函數fb_show_logo_line來進一步執行渲染第一個開機畫面的操作。 函數fb_show_logo_line也是實現在文件kernel/goldfish/drivers/video/fbmem.c中,如下所示: ~~~ static int fb_show_logo_line(struct fb_info *info, int rotate, const struct linux_logo *logo, int y, unsigned int n) { u32 *palette = NULL, *saved_pseudo_palette = NULL; unsigned char *logo_new = NULL, *logo_rotate = NULL; struct fb_image image; /* Return if the frame buffer is not mapped or suspended */ if (logo == NULL || info->state != FBINFO_STATE_RUNNING || info->flags & FBINFO_MODULE) return 0; image.depth = 8; image.data = logo->data; if (fb_logo.needs_cmapreset) fb_set_logocmap(info, logo); if (fb_logo.needs_truepalette || fb_logo.needs_directpalette) { palette = kmalloc(256 * 4, GFP_KERNEL); if (palette == NULL) return 0; if (fb_logo.needs_truepalette) fb_set_logo_truepalette(info, logo, palette); else fb_set_logo_directpalette(info, logo, palette); saved_pseudo_palette = info->pseudo_palette; info->pseudo_palette = palette; } if (fb_logo.depth <= 4) { logo_new = kmalloc(logo->width * logo->height, GFP_KERNEL); if (logo_new == NULL) { kfree(palette); if (saved_pseudo_palette) info->pseudo_palette = saved_pseudo_palette; return 0; } image.data = logo_new; fb_set_logo(info, logo, logo_new, fb_logo.depth); } image.dx = 0; image.dy = y; image.width = logo->width; image.height = logo->height; if (rotate) { logo_rotate = kmalloc(logo->width * logo->height, GFP_KERNEL); if (logo_rotate) fb_rotate_logo(info, logo_rotate, &image, rotate); } fb_do_show_logo(info, &image, rotate, n); kfree(palette); if (saved_pseudo_palette != NULL) info->pseudo_palette = saved_pseudo_palette; kfree(logo_new); kfree(logo_rotate); return logo->height; } ~~~ 參數logo指向了前面所準備的第一個開機畫面的內容。這個函數首先根據參數logo的內容來構造一個fb_image結構體image,用來描述最終要顯示的第一個開機畫面。最后就調用函數fb_do_show_logo來真正執行渲染第一個開機畫面的操作。 函數fb_do_show_logo也是實現在文件kernel/goldfish/drivers/video/fbmem.c中,如下所示: ~~~ static void fb_do_show_logo(struct fb_info *info, struct fb_image *image, int rotate, unsigned int num) { unsigned int x; if (rotate == FB_ROTATE_UR) { for (x = 0; x < num && image->dx + image->width <= info->var.xres; x++) { info->fbops->fb_imageblit(info, image); image->dx += image->width + 8; } } else if (rotate == FB_ROTATE_UD) { for (x = 0; x < num && image->dx >= 0; x++) { info->fbops->fb_imageblit(info, image); image->dx -= image->width + 8; } } else if (rotate == FB_ROTATE_CW) { for (x = 0; x < num && image->dy + image->height <= info->var.yres; x++) { info->fbops->fb_imageblit(info, image); image->dy += image->height + 8; } } else if (rotate == FB_ROTATE_CCW) { for (x = 0; x < num && image->dy >= 0; x++) { info->fbops->fb_imageblit(info, image); image->dy -= image->height + 8; } } } ~~~ 參數rotate用來描述屏幕的當前旋轉方向。屏幕旋轉方向不同,第一個開機畫面的渲染方式也有所不同。例如,當屏幕上下顛倒時(FB_ROTATE_UD),第一個開機畫面的左右順序就剛好調換過來,這時候就需要從右到左來渲染。其它三個方向FB_ROTATE_UR、FB_ROTATE_CW和FB_ROTATE_CCW分別表示沒有旋轉、順時針旋轉90度和逆時針旋轉90度。 參數info用來描述要渲染的幀緩沖區硬件設備,它的成員變量fbops指向了一系列回調函數,用來操作幀緩沖區硬件設備,其中,回調函數fb_imageblit就是用來在指定的幀緩沖區硬件設備渲染指定的圖像的。 至此,第一個開機畫面的顯示過程就分析完成了。 2. 第二個開機畫面的顯示過程 由于第二個開機畫面是在init進程啟動的過程中顯示的,因此,我們就從init進程的入口函數main開始分析第二個開機畫面的顯示過程。 init進程的入口函數main實現在文件system/core/init/init.c中,如下所示: ~~~ int main(int argc, char **argv) { int fd_count = 0; struct pollfd ufds[4]; ...... int property_set_fd_init = 0; int signal_fd_init = 0; int keychord_fd_init = 0; if (!strcmp(basename(argv[0]), "ueventd")) return ueventd_main(argc, argv); ...... queue_builtin_action(console_init_action, "console_init"); ...... for(;;) { int nr, i, timeout = -1; execute_one_command(); restart_processes(); if (!property_set_fd_init && get_property_set_fd() > 0) { ufds[fd_count].fd = get_property_set_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; property_set_fd_init = 1; } if (!signal_fd_init && get_signal_fd() > 0) { ufds[fd_count].fd = get_signal_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; signal_fd_init = 1; } if (!keychord_fd_init && get_keychord_fd() > 0) { ufds[fd_count].fd = get_keychord_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; keychord_fd_init = 1; } if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (!action_queue_empty() || cur_action) timeout = 0; ...... nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue; for (i = 0; i < fd_count; i++) { if (ufds[i].revents == POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); else if (ufds[i].fd == get_keychord_fd()) handle_keychord(); else if (ufds[i].fd == get_signal_fd()) handle_signal(); } } } return 0; } ~~~ 函數一開始就首先判斷參數argv[0]的值是否等于“ueventd”,即當前正在啟動的進程名稱是否等于“ueventd”。如果是的話,那么就以ueventd_main函數來作入口函數。這是怎么回事呢?當前正在啟動的進程不是init嗎?它的名稱怎么可能會等于“ueventd”?原來,在目標設備上,可執行文件/sbin/ueventd是可執行文件/init的一個符號鏈接文件,即應用程序ueventd和init運行的是同一個可執行文件。內核啟動完成之后,可執行文件/init首先會被執行,即init進程會首先被啟動。init進程在啟動的過程中,會對啟動腳本/init.rc進行解析。在啟動腳本/init.rc中,配置了一個ueventd進程,它對應的可執行文件為/sbin/ueventd,即ueventd進程加載的可執行文件也為/init。因此,通過判斷參數argv[0]的值,就可以知道當前正在啟動的是init進程還是ueventd進程。 ueventd進程是作什么用的呢?它是用來處理uevent事件的,即用來管理系統設備的。從前面的描述可以知道,它真正的入口函數為ueventd_main,實現在system/core/init/ueventd.c中。ueventd進程會通過一個socket接口來和內核通信,以便可以監控系統設備事件。例如,在前面在Ubuntu上為Android系統編寫Linux內核驅動程序一文中, 我們調用device_create函數來創建了一個名稱為“hello”的字符設備,這時候內核就會向前面提到的socket發送一個設備增加事件。ueventd進程通過這個socket獲得了這個設備增加事件之后,就會/dev目錄下創建一個名稱為“hello”的設備文件。這樣用戶空間的應用程序就可以通過設備文件/dev/hello來和驅動程序hello進行通信了。 接下來調用另外一個函數queue_builtin_action來向init進程中的一個待執行action隊列增加了一個名稱等于“console_init”的action。這個action對應的執行函數為console_init_action,它就是用來顯示第二個開機畫面的。 函數queue_builtin_action實現在文件system/core/init/init_parser.c文件中,如下所示: ~~~ static list_declare(action_list); static list_declare(action_queue); void queue_builtin_action(int (*func)(int nargs, char **args), char *name) { struct action *act; struct command *cmd; act = calloc(1, sizeof(*act)); act->name = name; list_init(&act->commands); cmd = calloc(1, sizeof(*cmd)); cmd->func = func; cmd->args[0] = name; list_add_tail(&act->commands, &cmd->clist); list_add_tail(&action_list, &act->alist); action_add_queue_tail(act); } void action_add_queue_tail(struct action *act) { list_add_tail(&action_queue, &act->qlist); } ~~~ action_list列表用來保存從啟動腳本/init.rc解析得到的一系列action,以及一系列內建的action。當這些action需要執行的時候,它們就會被添加到action_queue列表中去,以便init進程可以執行它們。 回到init進程的入口函數main中,最后init進程會進入到一個無限循環中去。在這個無限循環中,init進程會做以下五個事情: * A. 調用函數execute_one_command來檢查action_queue列表是否為空。如果不為空的話,那么init進程就會將保存在列表頭中的action移除,并且執行這個被移除的action。由于前面我們將一個名稱為“console_init”的action添加到了action_queue列表中,因此,在這個無限循環中,這個action就會被執行,即函數console_init_action會被調用。 * B. 調用函數restart_processes來檢查系統中是否有進程需要重啟。在啟動腳本/init.rc中,我們可以指定一個進程在退出之后會自動重新啟動。在這種情況下,函數restart_processes就會檢查是否存在需要重新啟動的進程,如果存在的話,那么就會將它重新啟動起來。 * C. 處理系統屬性變化事件。當我們調用函數property_set來改變一個系統屬性值時,系統就會通過一個socket(通過調用函數get_property_set_fd可以獲得它的文件描述符)來向init進程發送一個屬性值改變事件通知。init進程接收到這個屬性值改變事件之后,就會調用函數handle_property_set_fd來進行相應的處理。后面在分析第三個開機畫面的顯示過程時,我們就會看到,SurfaceFlinger服務就是通過修改“ctl.start”和“ctl.stop”屬性值來啟動和停止第三個開機畫面的。 * D. 處理一種稱為“chorded keyboard”的鍵盤輸入事件。這種類型為chorded keyboard”的鍵盤設備通過不同的銨鍵組合來描述不同的命令或者操作,它對應的設備文件為/dev/keychord。我們可以通過調用函數get_keychord_fd來獲得這個設備的文件描述符,以便可以監控它的輸入事件,并且調用函數handle_keychord來對這些輸入事件進行處理。 * E. 回收僵尸進程。我們知道,在Linux內核中,如果父進程不等待子進程結束就退出,那么當子進程結束的時候,就會變成一個僵尸進程,從而占用系統的資源。為了回收這些僵尸進程,init進程會安裝一個SIGCHLD信號接收器。當那些父進程已經退出了的子進程退出的時候,內核就會發出一個SIGCHLD信號給init進程。init進程可以通過一個socket(通過調用函數get_signal_fd可以獲得它的文件描述符)來將接收到的SIGCHLD信號讀取回來,并且調用函數handle_signal來對接收到的SIGCHLD信號進行處理,即回收那些已經變成了僵尸的子進程。 注意,由于后面三個事件都是可以通過文件描述符來描述的,因此,init進程的入口函數main使用poll機制來同時輪詢它們,以便可以提高效率。 接下來我們就重點分析函數console_init_action的實現,以便可以了解第二個開機畫面的顯示過程: ~~~ static int console_init_action(int nargs, char **args) { int fd; char tmp[PROP_VALUE_MAX]; if (console[0]) { snprintf(tmp, sizeof(tmp), "/dev/%s", console); console_name = strdup(tmp); } fd = open(console_name, O_RDWR); if (fd >= 0) have_console = 1; close(fd); if( load_565rle_image(INIT_IMAGE_FILE) ) { fd = open("/dev/tty0", O_WRONLY); if (fd >= 0) { const char *msg; msg = "\n" "\n" "\n" "\n" "\n" "\n" "\n" // console is 40 cols x 30 lines "\n" "\n" "\n" "\n" "\n" "\n" "\n" " A N D R O I D "; write(fd, msg, strlen(msg)); close(fd); } } return 0; } ~~~ 這個函數主要做了兩件事件: * A. 初始化控制臺。init進程在啟動的時候,會解析內核的啟動參數(保存在文件/proc/cmdline中)。如果發現內核的啟動參數中包含有了一個名稱為“androidboot.console”的屬性,那么就會將這個屬性的值保存在字符數組console中。這樣我們就可以通過設備文件`/dev/<console>`來訪問系統的控制臺。如果內核的啟動參數沒有包含名稱為“androidboot.console”的屬性,那么默認就通過設備文件/dev/console來訪問系統的控制臺。如果能夠成功地打開設備文件`/dev/<console>`或者`/dev/console`,那么就說明系統支持訪問控制臺,因此,全局變量have_console的就會被設置為1。 * B. 顯示第二個開機畫面。顯示第二個開機畫面是通過調用函數load_565rle_image來實現的。在調用函數load_565rle_image的時候,指定的開機畫面文件為INIT_IMAGE_FILE。INIT_IMAGE_FILE是一個宏,定義在system/core/init/init.h文件中,如下所示: ~~~ #define INIT_IMAGE_FILE "/initlogo.rle" ~~~ 即第二個開機畫面的內容是由文件/initlogo.rle來指定的。如果文件/initlogo.rle不存在,或者在顯示它的過程中出現異常,那么函數load_565rle_image的返回值就會等于-1,這時候函數console_init_action就以文本的方式來顯示第二個開機畫面,即向編號為0的控制臺(/dev/tty0)輸出“ANDROID”這7個字符。 函數load_565rle_image實現在文件system/core/init/logo.c中,如下所示: ~~~ /* 565RLE image format: [count(2 bytes), rle(2 bytes)] */ int load_565rle_image(char *fn) { struct FB fb; struct stat s; unsigned short *data, *bits, *ptr; unsigned count, max; int fd; if (vt_set_mode(1)) return -1; fd = open(fn, O_RDONLY); if (fd < 0) { ERROR("cannot open '%s'\n", fn); goto fail_restore_text; } if (fstat(fd, &s) < 0) { goto fail_close_file; } data = mmap(0, s.st_size, PROT_READ, MAP_SHARED, fd, 0); if (data == MAP_FAILED) goto fail_close_file; if (fb_open(&fb)) goto fail_unmap_data; max = fb_width(&fb) * fb_height(&fb); ptr = data; count = s.st_size; bits = fb.bits; while (count > 3) { unsigned n = ptr[0]; if (n > max) break; android_memset16(bits, ptr[1], n << 1); bits += n; max -= n; ptr += 2; count -= 4; } munmap(data, s.st_size); fb_update(&fb); fb_close(&fb); close(fd); unlink(fn); return 0; fail_unmap_data: munmap(data, s.st_size); fail_close_file: close(fd); fail_restore_text: vt_set_mode(0); return -1; } 函數首先將控制臺的顯示方式設置為圖形方式,這是通過調用函數vt_set_mode來實現的,如下所示: [cpp] view plain copy static int vt_set_mode(int graphics) { int fd, r; fd = open("/dev/tty0", O_RDWR | O_SYNC); if (fd < 0) return -1; r = ioctl(fd, KDSETMODE, (void*) (graphics ? KD_GRAPHICS : KD_TEXT)); close(fd); return r; } ~~~ 函數vt_set_mode首先打開控制臺設備文件/dev/tty0,接著再通過IO控制命令KDSETMODE來將控制臺的顯示方式設置為文本方式或者圖形方式,取決于參數graphics的值。從前面的調用過程可以知道,參數graphics的值等于1,因此,這里是將控制臺的顯示方式設備為圖形方式。 回到函數load_565rle_image中,從前面的調用過程可以知道,參數fn的值等于“/initlogo.rle”,即指向目標設備上的initlogo.rle文件。函數load_565rle_image首先調用函數open打開這個文件,并且將獲得的文件描述符保存在變量fd中,接著再調用函數fstat來獲得這個文件的大小。有了這些信息之后,函數load_565rle_image就可以調用函數mmap來把文件/initlogo.rle映射到init進程的地址空間來了,以便可以讀取它的內容。 將文件/initlogo.rle映射到init進程的地址空間之后,接下來再調用函數fb_open來打開設備文件/dev/graphics/fb0。前面在介紹第一個開機畫面的顯示過程中提到,設備文件/dev/graphics/fb0是用來訪問系統的幀緩沖區硬件設備的,因此,打開了設備文件/dev/graphics/fb0之后,我們就可以將文件/initlogo.rle的內容輸出到幀緩沖區硬件設備中去了。 函數fb_open的實現如下所示: ~~~ static int fb_open(struct FB *fb) { fb->fd = open("/dev/graphics/fb0", O_RDWR); if (fb->fd < 0) return -1; if (ioctl(fb->fd, FBIOGET_FSCREENINFO, &fb->fi) < 0) goto fail; if (ioctl(fb->fd, FBIOGET_VSCREENINFO, &fb->vi) < 0) goto fail; fb->bits = mmap(0, fb_size(fb), PROT_READ | PROT_WRITE, MAP_SHARED, fb->fd, 0); if (fb->bits == MAP_FAILED) goto fail; return 0; fail: close(fb->fd); return -1; } ~~~ 打開了設備文件/dev/graphics/fb0之后,接著再分別通過IO控制命令FBIOGET_FSCREENINFO和FBIOGET_VSCREENINFO來獲得幀緩沖硬件設備的固定信息和可變信息。固定信息使用一個fb_fix_screeninfo結構體來描述,它保存的是幀緩沖區硬件設備固有的特性,這些特性在幀緩沖區硬件設備被初始化了之后,就不會發生改變,例如屏幕大小以及物理地址等信息。可變信息使用一個fb_var_screeninfo結構體來描述,它保存的是幀緩沖區硬件設備可變的特性,這些特性在系統運行的期間是可以改變的,例如屏幕所使用的分辨率、顏色深度以及顏色格式等。 除了獲得幀緩沖區硬件設備的固定信息和可變信息之外,函數fb_open還會將設備文件/dev/graphics/fb0的內容映射到init進程的地址空間來,這樣init進程就可以通過映射得到的虛擬地址來訪問幀緩沖區硬件設備的內容了。 回到函數load_565rle_image中,接下來分別使用宏fb_width和fb_height來獲得屏幕所使用的的分辨率,即屏幕的寬度和高度。宏fb_width和fb_height的定義如下所示: ~~~ #define fb_width(fb) ((fb)->vi.xres) #define fb_height(fb) ((fb)->vi.yres) ~~~ 屏幕的所使用的分辨率使用結構體fb_var_screeninfo的成員變量xres和yres來描述,其中,成員變量xres用來描述屏幕的寬度,而成員變量成員變量yres用來描述屏幕的高度。得到了屏幕的分辨率之后,就可以知道最多可以向幀緩沖區硬件設備寫入的字節數的大小了,這個大小就等于屏幕的寬度乘以高度,保存在變量max中。 現在我們分別得到了文件initlogo.rle和幀緩沖區硬件設備在init進程中的虛擬訪問地址以及大小,這樣我們就可以將文件initlogo.rle的內容寫入到幀緩沖區硬件設備中去,以便可以將第二個開機畫面顯示出來,這是通過函數load_565rle_image中的while循環來實現的。 文件initlogo.rle保存的第二個開機畫面的圖像格式是565rle的。rle的全稱是run-length encoding,翻譯為游程編碼或者行程長度編碼,它可以使用4個字節來描述一個連續的具有相同顏色值的序列。在rle565格式,前面2個字節中用來描述序列的個數,而后面2個字節用來描述一個具體的顏色,其中,顏色的RGB值分別占5位、6位和5位。理解了565rle圖像格式之后,我們就可以理解函數load_565rle_image中的while循環的實現邏輯了。在每一次循環中,都會依次從文件initlogo.rle中讀出4個字節,其中,前兩個字節的內容保存在變量n中,而后面2個字節的內容用來寫入到幀緩沖區硬件設備中去。由于2個字節剛好就可以使用一個無符號短整數來描述,因此,函數load_565rle_image通過調用函數android_memset16來將從文件initlogo.rle中讀取出來的顏色值寫入到幀緩沖區硬件設備中去, 函數android_memset16的實現如下所示: ~~~ void android_memset16(void *_ptr, unsigned short val, unsigned count) { unsigned short *ptr = _ptr; count >>= 1; while(count--) *ptr++ = val; } ~~~ * 參數ptr指向被寫入的地址,在我們這個場景中,這個地址即為幀緩沖區硬件設備映射到init進程中的虛擬地址值。 * 參數val用來描述被寫入的值,在我們這個場景中,這個值即為從文件initlogo.rle中讀取出來的顏色值。 * 參數count用來描述被寫入的地址的長度,它是以字節為單位的。由于在將參數val的值寫入到參數ptr所描述的地址中去時,是以無符號短整數為單位的,即是以2個字節為單位的,因此,函數android_memset16在將參數val寫入到地址ptr中去之前,首先會將參數count的值除以2。相應的地,在函數load_565rle_image中,需要將具有相同顏色值的序列的個數乘以2之后,再調用函數android_memset16。 回到函數load_565rle_image中,將文件/initlogo.rle的內容寫入到幀緩沖區硬件設備去之后,第二個開機畫面就可以顯示出來了。接下來函數load_565rle_image就會調用函數munmap來注銷文件/initlogo.rle在init進程中的映射,并且調用函數close來關閉文件/initlogo.rle。關閉了文件/initlogo.rle之后,還會調用函數unlink來刪除目標設備上的/initlogo.rle文件。注意,這只是刪除了目標設備上的/initlogo.rle文件,而不是刪除ramdisk映像中的initlogo.rle文件,因此,每次關機啟動之后,系統都會重新將ramdisk映像中的initlogo.rle文件安裝到目標設備上的根目錄來,這樣就可以在每次開機的時候都能將它顯示出來。 除了需要注銷文件/initlogo.rle在init進程中的映射和關閉文件/initlogo.rle之外,還需要注銷文件/dev/graphics/fb0在init進程中的映射以及關閉文件/dev/graphics/fb0,這是通過調用fb_close函數來實現的,如下所示: ~~~ static void fb_close(struct FB *fb) { munmap(fb->bits, fb_size(fb)); close(fb->fd); } ~~~ 在調用fb_close函數之前,函數load_565rle_image還會調用另外一個函數fb_update來更新屏幕上的第二個開機畫面,它的實現如下所示: ~~~ static void fb_update(struct FB *fb) { fb->vi.yoffset = 1; ioctl(fb->fd, FBIOPUT_VSCREENINFO, &fb->vi); fb->vi.yoffset = 0; ioctl(fb->fd, FBIOPUT_VSCREENINFO, &fb->vi); } ~~~ 在結構體fb_var_screeninfo中,除了使用成員變量xres和yres來描述屏幕所使用的分辨率之外,還使用成員變量xres_virtual和yres_virtual來描述屏幕所使用的虛擬分辨率。成員變量xres和yres所描述屏幕的分辨率稱為可視分辨率。可視分辨率和虛擬分辨率有什么關系呢?可視分辨率是屏幕實際上使用的分辨率,即用戶所看到的分辨率,而虛擬分辨率是在系統內部使用的,它是不可見的,并且可以大于可視分辨率。例如,假設可視分辨率是800 x 600,那么虛擬分辨率可以設置為1600 x 600。由于屏幕最多只可以顯示800 x 600個像素,因此,在系統內部,就需要決定從1600 x 600中取出800 x 600個像素來顯示,這是通過結構體fb_var_screeninfo的成員變量xoffset和yoffset的值來描述的。成員變量xoffset和yoffset的默認值等于0,即默認從虛擬分辨率的左上角取出與可視分辨率大小相等的像素出來顯示,否則的話,就會根據成員變量xoffset和yoffset的值來從虛擬分辨率的中間位置取出與可視分辨率大小相等的像素出來顯示。 幀緩沖區的大小是由虛擬分辨率決定的,因此,我們就可以在幀緩沖中寫入比屏幕大小還要多的像素值,多出來的這個部分像素值就可以用作雙緩沖。我們仍然假設可視分辨率和虛擬分辨率分別是800 x 600和1600 x 600,那么我們就可以先將前一個圖像的內容寫入到幀緩沖區的前面800 x 600個像素中去,接著再將后一個圖像的內容寫入到幀緩沖區的后面800 x 600個像素中。通過分別將用來描述幀緩沖區硬件設備的fb_var_screeninfo結構體的成員變量yoffset的值設置為0和800,就可以平滑地顯示兩個圖像。 理解了幀緩沖區硬件設備的可視分辨性和虛擬分辨性之后,函數fb_update的實現邏輯就可以很好地理解了。 至此,第二個開機畫面的顯示過程就分析完成了。 3. 第三個開機畫面的顯示過程 第三個開機畫面是由應用程序bootanimation來負責顯示的。應用程序bootanimation在啟動腳本init.rc中被配置成了一個服務,如下所示: ~~~ service bootanim /system/bin/bootanimation user graphics group graphics disabled oneshot ~~~ 應用程序bootanimation的用戶和用戶組名稱分別被設置為graphics。注意, 用來啟動應用程序bootanimation的服務是disable的,即init進程在啟動的時候,不會主動將應用程序bootanimation啟動起來。當SurfaceFlinger服務啟動的時候,它會通過修改系統屬性ctl.start的值來通知init進程啟動應用程序bootanimation,以便可以顯示第三個開機畫面,而當System進程將系統中的關鍵服務都啟動起來之后,ActivityManagerService服務就會通知SurfaceFlinger服務來修改系統屬性ctl.stop的值,以便可以通知init進程停止執行應用程序bootanimation,即停止顯示第三個開機畫面。接下來我們就分別分析第三個開機畫面的顯示過程和停止過程。 從前面Android系統進程Zygote啟動過程的源代碼分析一文可以知道,Zygote進程在啟動的過程中,會將System進程啟動起來,而從前面Android應用程序安裝過程源代碼分析一文又可以知道,System進程在啟動的過程(Step 3)中,會調用SurfaceFlinger類的靜態成員函數instantiate來啟動SurfaceFlinger服務。Sytem進程在啟動SurfaceFlinger服務的過程中,首先會創建一個SurfaceFlinger實例,然后再將這個實例注冊到Service Manager中去。在注冊的過程,前面創建的SurfaceFlinger實例會被一個sp指針引用。從前面Android系統的智能指針(輕量級指針、強指針和弱指針)的實現原理分析一文可以知道,當一個對象第一次被智能指針引用的時候,這個對象的成員函數onFirstRef就會被調用。由于SurfaceFlinger重寫了父類RefBase的成員函數onFirstRef,因此,在注冊SurfaceFlinger服務的過程中,將會調用SurfaceFlinger類的成員函數onFirstRef。在調用的過程,就會創建一個線程來啟動第三個開機畫面。 SurfaceFlinger類實現在文件frameworks/base/services/surfaceflinger/SurfaceFlinger.cpp 中,它的成員函數onFirstRef的實現如下所示: ~~~ void SurfaceFlinger::onFirstRef() { run("SurfaceFlinger", PRIORITY_URGENT_DISPLAY); // Wait for the main thread to be done with its initialization mReadyToRunBarrier.wait(); } ~~~ SurfaceFlinger類繼承了Thread類,當它的成員函數run被調用的時候,系統就會創建一個新的線程。這個線程在第一次運行之前,會調用SurfaceFlinger類的成員函數readyToRun來通知SurfaceFlinger,它準備就緒了。當這個線程準備就緒之后,它就會循環執行SurfaceFlinger類的成員函數threadLoop,直到這個成員函數的返回值等于false為止。 注意,SurfaceFlinger類的成員函數onFirstRef是在System進程的主線程中調用的,它需要等待前面創建的線程準備就緒之后,再繼續往前執行,這個通過調用SurfaceFlinger類的成員變量mReadytoRunBarrier所描述的一個Barrier對象的成員函數wait來實現的。每一個Barrier對象內問都封裝了一個條件變量(Condition Variable),而條件變量是用來同步線程的。 接下來,我們繼續分析SurfaceFlinger類的成員函數readyToRun的實現,如下所示: ~~~ status_t SurfaceFlinger::readyToRun() { LOGI( "SurfaceFlinger's main thread ready to run. " "Initializing graphics H/W..."); ...... mReadyToRunBarrier.open(); /* * We're now ready to accept clients... */ // start boot animation property_set("ctl.start", "bootanim"); return NO_ERROR; } ~~~ 前面創建的線程用作SurfaceFlinger的主線程。這個線程在啟動的時候,會對設備主屏幕以及OpenGL庫進行初始化。初始化完成之后,接著就會調用SurfaceFlinger類的成員變量mReadyToRunBarrier所描述的一個Barrier對象的成員函數open來喚醒System進程的主線程,以便它可以繼續往前執行。最后,SurfaceFlinger類的成員函數readyToRun的成員函數會調用函數property_set來將系統屬性“ctl.start”的值設置為“bootanim”,表示要將應用程序bootanimation啟動起來,以便可以顯示第三個開機畫面。 前面在介紹第二個開機畫面的時候提到,當系統屬性發生改變時,init進程就會接收到一個系統屬性變化通知,這個通知最終是由在init進程中的函數handle_property_set_fd來處理的。 函數handle_property_set_fd實現在文件system/core/init/property_service.c中,如下所示: ~~~ void handle_property_set_fd() { prop_msg msg; int s; int r; int res; struct ucred cr; struct sockaddr_un addr; socklen_t addr_size = sizeof(addr); socklen_t cr_size = sizeof(cr); if ((s = accept(property_set_fd, (struct sockaddr *) &addr, &addr_size)) < 0) { return; } /* Check socket options here */ if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) { close(s); ERROR("Unable to recieve socket options\n"); return; } r = recv(s, &msg, sizeof(msg), 0); close(s); if(r != sizeof(prop_msg)) { ERROR("sys_prop: mis-match msg size recieved: %d expected: %d\n", r, sizeof(prop_msg)); return; } switch(msg.cmd) { case PROP_MSG_SETPROP: msg.name[PROP_NAME_MAX-1] = 0; msg.value[PROP_VALUE_MAX-1] = 0; if(memcmp(msg.name,"ctl.",4) == 0) { if (check_control_perms(msg.value, cr.uid, cr.gid)) { handle_control_message((char*) msg.name + 4, (char*) msg.value); } else { ERROR("sys_prop: Unable to %s service ctl [%s] uid: %d pid:%d\n", msg.name + 4, msg.value, cr.uid, cr.pid); } } else { if (check_perms(msg.name, cr.uid, cr.gid)) { property_set((char*) msg.name, (char*) msg.value); } else { ERROR("sys_prop: permission denied uid:%d name:%s\n", cr.uid, msg.name); } } break; default: break; } } ~~~ init進程是通過一個socket來接收系統屬性變化事件的。每一個系統屬性變化事件的內容都是通過一個prop_msg對象來描述的。在prop_msg對象對,成員變量name用來描述發生變化的系統屬性的名稱,而成員變量value用來描述發生變化的系統屬性的值。系統屬性分為兩種類型,一種是普通類型的系統屬性,另一種是控制類型的系統屬性(屬性名稱以“ctl.”開頭)。控制類型的系統屬性在發生變化時,會觸發init進程執行一個命令,而普通類型的系統屬性就不具有這個特性。注意,改變系統屬性是需要權限,因此,函數handle_property_set_fd在處理一個系統屬性變化事件之前,首先會檢查修改系統屬性的進程是否具有相應的權限,這是通過調用函數check_control_perms或者check_perms來實現的。 從前面的調用過程可以知道,當前發生變化的系統屬性的名稱為“ctl.start”,它的值被設置為“bootanim”。由于這是一個控制類型的系統屬性,因此,在通過了權限檢查之后,另外一個函數handle_control_message就會被調用,以便可以執行一個名稱為“bootanim”的命令。 函數handle_control_message實現在system/core/init/init.c中,如下所示: ~~~ void handle_control_message(const char *msg, const char *arg) { if (!strcmp(msg,"start")) { msg_start(arg); } else if (!strcmp(msg,"stop")) { msg_stop(arg); } else { ERROR("unknown control msg '%s'\n", msg); } } ~~~ 控制類型的系統屬性的名稱是以"ctl."開頭,并且是以“start”或者“stop”結尾的,其中,“start”表示要啟動某一個服務,而“stop”表示要停止某一個服務,它們是分別通過函數msg_start和msg_stop來實現的。由于當前發生變化的系統屬性是以“start”來結尾的,因此,接下來就會調用函數msg_start來啟動一個名稱為“bootanim”的服務。 函數msg_start實現在文件system/core/init/init.c中,如下所示: ~~~ static void msg_start(const char *name) { struct service *svc; char *tmp = NULL; char *args = NULL; if (!strchr(name, ':')) svc = service_find_by_name(name); else { tmp = strdup(name); args = strchr(tmp, ':'); *args = '\0'; args++; svc = service_find_by_name(tmp); } if (svc) { service_start(svc, args); } else { ERROR("no such service '%s'\n", name); } if (tmp) free(tmp); } ~~~ 參數name的值等于“bootanim”,它用來描述一個服務名稱。這個函數首先調用函數service_find_by_name來找到名稱等于“bootanim”的服務的信息,這些信息保存在一個service結構體svc中,接著再調用另外一個函數service_start來將對應的應用程序啟動起來。 從前面的內容可以知道,名稱等于“bootanim”的服務所對應的應用程序為/system/bin/bootanimation,這個應用程序實現在frameworks/base/cmds/bootanimation目錄中,其中,應用程序入口函數main是實現在frameworks/base/cmds/bootanimation/bootanimation_main.cpp中的,如下所示: ~~~ int main(int argc, char** argv) { #if defined(HAVE_PTHREADS) setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY); #endif char value[PROPERTY_VALUE_MAX]; property_get("debug.sf.nobootanimation", value, "0"); int noBootAnimation = atoi(value); LOGI_IF(noBootAnimation, "boot animation disabled"); if (!noBootAnimation) { sp<ProcessState> proc(ProcessState::self()); ProcessState::self()->startThreadPool(); // create the boot animation object sp<BootAnimation> boot = new BootAnimation(); IPCThreadState::self()->joinThreadPool(); } return 0; } ~~~ 這個函數首先檢查系統屬性“debug.sf.nobootnimaition”的值是否不等于0。如果不等于的話,那么接下來就會啟動一個Binder線程池,并且創建一個BootAnimation對象。這個BootAnimation對象就是用來顯示第三個開機畫面的。由于BootAnimation對象在顯示第三個開機畫面的過程中,需要與SurfaceFlinger服務通信,因此,應用程序bootanimation就需要啟動一個Binder線程池。 BootAnimation類間接地繼承了RefBase類,并且重寫了RefBase類的成員函數onFirstRef,因此,當一個BootAnimation對象第一次被智能指針引用的時,這個BootAnimation對象的成員函數onFirstRef就會被調用。 BootAnimation類的成員函數onFirstRef實現在文件frameworks/base/cmds/bootanimation/BootAnimation.cpp中,如下所示: ~~~ void BootAnimation::onFirstRef() { status_t err = mSession->linkToComposerDeath(this); LOGE_IF(err, "linkToComposerDeath failed (%s) ", strerror(-err)); if (err == NO_ERROR) { run("BootAnimation", PRIORITY_DISPLAY); } } ~~~ mSession是BootAnimation類的一個成員變量,它的類型為SurfaceComposerClient,是用來和SurfaceFlinger執行Binder進程間通信的,它是在BootAnimation類的構造函數中創建的,如下所示: ~~~ BootAnimation::BootAnimation() : Thread(false) { mSession = new SurfaceComposerClient(); } ~~~ SurfaceComposerClient類內部有一個實現了ISurfaceComposerClient接口的Binder代理對象mClient,這個Binder代理對象引用了SurfaceFlinger服務,SurfaceComposerClient類就是通過它來和SurfaceFlinger服務通信的。 回到BootAnimation類的成員函數onFirstRef中,由于BootAnimation類引用了SurfaceFlinger服務,因此,當SurfaceFlinger服務意外死亡時,BootAnimation類就需要得到通知,這是通過調用成員變量mSession的成員函數linkToComposerDeath來注冊SurfaceFlinger服務的死亡接收通知來實現的。 BootAnimation類繼承了Thread類,因此,當BootAnimation類的成員函數onFirstRef調用了父類Thread的成員函數run之后,系統就會創建一個線程,這個線程在第一次運行之前,會調用BootAnimation類的成員函數readyToRun來執行一些初始化工作,后面再調用BootAnimation類的成員函數htreadLoop來顯示第三個開機畫面。 BootAnimation類的成員函數readyToRun的實現如下所示: ~~~ status_t BootAnimation::readyToRun() { mAssets.addDefaultAssets(); DisplayInfo dinfo; status_t status = session()->getDisplayInfo(0, &dinfo); if (status) return -1; // create the native surface sp<SurfaceControl> control = session()->createSurface( getpid(), 0, dinfo.w, dinfo.h, PIXEL_FORMAT_RGB_565); session()->openTransaction(); control->setLayer(0x40000000); session()->closeTransaction(); sp<Surface> s = control->getSurface(); // initialize opengl and egl const EGLint attribs[] = { EGL_DEPTH_SIZE, 0, EGL_NONE }; EGLint w, h, dummy; EGLint numConfigs; EGLConfig config; EGLSurface surface; EGLContext context; EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(display, 0, 0); EGLUtils::selectConfigForNativeWindow(display, attribs, s.get(), &config); surface = eglCreateWindowSurface(display, config, s.get(), NULL); context = eglCreateContext(display, config, NULL, NULL); eglQuerySurface(display, surface, EGL_WIDTH, &w); eglQuerySurface(display, surface, EGL_HEIGHT, &h); if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) return NO_INIT; mDisplay = display; mContext = context; mSurface = surface; mWidth = w; mHeight = h; mFlingerSurfaceControl = control; mFlingerSurface = s; mAndroidAnimation = true; if ((access(USER_BOOTANIMATION_FILE, R_OK) == 0) && (mZip.open(USER_BOOTANIMATION_FILE) == NO_ERROR) || (access(SYSTEM_BOOTANIMATION_FILE, R_OK) == 0) && (mZip.open(SYSTEM_BOOTANIMATION_FILE) == NO_ERROR)) mAndroidAnimation = false; return NO_ERROR; } ~~~ BootAnimation類的成員函數session用來返回BootAnimation類的成員變量mSession所描述的一個SurfaceComposerClient對象。通過調用SurfaceComposerClient對象mSession的成員函數createSurface可以獲得一個SurfaceControl對象control。 SurfaceComposerClient類的成員函數createSurface首先調用內部的Binder代理對象mClient來請求SurfaceFlinger返回一個類型為SurfaceLayer的Binder代理對象,接著再使用這個Binder代理對象來創建一個SurfaceControl對象。創建出來的SurfaceControl對象的成員變量mSurface就指向了從SurfaceFlinger返回來的類型為SurfaceLayer的Binder代理對象。有了這個Binder代理對象之后,SurfaceControl對象就可以和SurfaceFlinger服務通信了。 調用SurfaceControl對象control的成員函數getSurface會返回一個Surface對象s。這個Surface對象s內部也有一個類型為SurfaceLayer的Binder代理對象mSurface,這個Binder代理對象與前面所創建的SurfaceControl對象control的內部的Binder代理對象mSurface引用的是同一個SurfaceLayer對象。這樣,Surface對象s也可以通過其內部的Binder代理對象mSurface來和SurfaceFlinger服務通信。 Surface類繼承了ANativeWindow類。ANativeWindow類是連接OpenGL和Android窗口系統的橋梁,即OpenGL需要通過ANativeWindow類來間接地操作Android窗口系統。這種橋梁關系是通過EGL庫來建立的,所有以egl為前綴的函數名均為EGL庫提供的接口。 為了能夠在OpenGL和Android窗口系統之間的建立一個橋梁,我們需要一個EGLDisplay對象display,一個EGLConfig對象config,一個EGLSurface對象surface,以及一個EGLContext對象context,其中,EGLDisplay對象display用來描述一個EGL顯示屏,EGLConfig對象config用來描述一個EGL幀緩沖區配置參數,EGLSurface對象surface用來描述一個EGL繪圖表面,EGLContext對象context用來描述一個EGL繪圖上下文(狀態),它們是分別通過調用egl庫函數eglGetDisplay、EGLUtils::selectConfigForNativeWindow、eglCreateWindowSurface和eglCreateContext來獲得的。注意,EGLConfig對象config、EGLSurface對象surface和EGLContext對象context都是用來描述EGLDisplay對象display的。有了這些對象之后,就可以調用函數eglMakeCurrent來設置當前EGL庫所使用的繪圖表面以及繪圖上下文。 還有另外一個地方需要注意的是,每一個EGLSurface對象surface有一個關聯的ANativeWindow對象。這個ANativeWindow對象是通過函數eglCreateWindowSurface的第三個參數來指定的。在我們這個場景中,這個ANativeWindow對象正好對應于前面所創建的 Surface對象s。每當OpenGL需要繪圖的時候,它就會找到前面所設置的繪圖表面,即EGLSurface對象surface。有了EGLSurface對象surface之后,就可以找到與它關聯的ANativeWindow對象,即Surface對象s。有了Surface對象s之后,就可以通過其內部的Binder代理對象mSurface來請求SurfaceFlinger服務返回幀緩沖區硬件設備的一個圖形訪問接口。這樣,OpenGL最終就可以將要繪制的圖形渲染到幀緩沖區硬件設備中去,即顯示在實際屏幕上。屏幕的大小,即寬度和高度,可以通過函數eglQuerySurface來獲得。 BootAnimation類的成員變量mAndroidAnimation是一個布爾變量。當它的值等于true的時候,那么就說明需要顯示的第三個開機畫面是Android系統默認的開機動畫,否則的話,第三個開機畫面就是由用戶自定義的開機動畫。 自定義的開機動畫是由文件USER_BOOTANIMATION_FILE或者文件SYSTEM_BOOTANIMATION_FILE來描述的。只要其中的一個文件存在,那么第三個開機畫面就會使用用戶自定義的開機動畫。USER_BOOTANIMATION_FILE和SYSTEM_BOOTANIMATION_FILE均是一個宏,它們的定義如下所示: ~~~ #define USER_BOOTANIMATION_FILE "/data/local/bootanimation.zip" #define SYSTEM_BOOTANIMATION_FILE "/system/media/bootanimation.zip" ~~~ 這一步執行完成之后,用來顯示第三個開機畫面的線程的初始化工作就執行完成了,接下來,就會執行這個線程的主體函數,即BootAnimation類的成員函數threadLoop。 BootAnimation類的成員函數threadLoop的實現如下所示: ~~~ bool BootAnimation::threadLoop() { bool r; if (mAndroidAnimation) { r = android(); } else { r = movie(); } eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroyContext(mDisplay, mContext); eglDestroySurface(mDisplay, mSurface); mFlingerSurface.clear(); mFlingerSurfaceControl.clear(); eglTerminate(mDisplay); IPCThreadState::self()->stopProcess(); return r; } ~~~ 如果BootAnimation類的成員變量mAndroidAnimation的值等于true,那么接下來就會調用BootAnimation類的成員函數android來顯示系統默認的開機動畫,否則的話,就會調用BootAnimation類的成員函數movie來顯示用戶自定義的開機動畫。顯示完成之后,就會銷毀前面所創建的EGLContext對象mContext、EGLSurface對象mSurface,以及EGLDisplay對象mDisplay等。 接下來,我們就分別分析BootAnimation類的成員函數android和movie的實現。 BootAnimation類的成員函數android的實現如下所示: ~~~ bool BootAnimation::android() { initTexture(&mAndroid[0], mAssets, "images/android-logo-mask.png"); initTexture(&mAndroid[1], mAssets, "images/android-logo-shine.png"); // clear screen glShadeModel(GL_FLAT); glDisable(GL_DITHER); glDisable(GL_SCISSOR_TEST); glClear(GL_COLOR_BUFFER_BIT); eglSwapBuffers(mDisplay, mSurface); glEnable(GL_TEXTURE_2D); glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); const GLint xc = (mWidth - mAndroid[0].w) / 2; const GLint yc = (mHeight - mAndroid[0].h) / 2; const Rect updateRect(xc, yc, xc + mAndroid[0].w, yc + mAndroid[0].h); // draw and update only what we need mFlingerSurface->setSwapRectangle(updateRect); glScissor(updateRect.left, mHeight - updateRect.bottom, updateRect.width(), updateRect.height()); // Blend state glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); const nsecs_t startTime = systemTime(); do { nsecs_t now = systemTime(); double time = now - startTime; float t = 4.0f * float(time / us2ns(16667)) / mAndroid[1].w; GLint offset = (1 - (t - floorf(t))) * mAndroid[1].w; GLint x = xc - offset; glDisable(GL_SCISSOR_TEST); glClear(GL_COLOR_BUFFER_BIT); glEnable(GL_SCISSOR_TEST); glDisable(GL_BLEND); glBindTexture(GL_TEXTURE_2D, mAndroid[1].name); glDrawTexiOES(x, yc, 0, mAndroid[1].w, mAndroid[1].h); glDrawTexiOES(x + mAndroid[1].w, yc, 0, mAndroid[1].w, mAndroid[1].h); glEnable(GL_BLEND); glBindTexture(GL_TEXTURE_2D, mAndroid[0].name); glDrawTexiOES(xc, yc, 0, mAndroid[0].w, mAndroid[0].h); EGLBoolean res = eglSwapBuffers(mDisplay, mSurface); if (res == EGL_FALSE) { break; } // 12fps: don't animate too fast to preserve CPU const nsecs_t sleepTime = 83333 - ns2us(systemTime() - now); if (sleepTime > 0) usleep(sleepTime); } while (!exitPending()); glDeleteTextures(1, &mAndroid[0].name); glDeleteTextures(1, &mAndroid[1].name); return false; } ~~~ Android系統默認的開機動畫是由兩張圖片android-logo-mask.png和android-logo-shine.png中。這兩張圖片保存在frameworks/base/core/res/assets/images目錄中,它們最終會被編譯在framework-res模塊(frameworks/base/core/res)中,即編譯在framework-res.apk文件中。編譯在framework-res模塊中的資源文件可以通過AssetManager類來訪問。 BootAnimation類的成員函數android首先調用另外一個成員函數initTexture來將根據圖片android-logo-mask.png和android-logo-shine.png的內容來分別創建兩個紋理對象,這兩個紋理對象就分別保存在BootAnimation類的成員變量mAndroid所描述的一個數組中。通過混合渲染這兩個紋理對象,我們就可以得到一個開機動畫,這是通過中間的while循環語句來實現的。 圖片android-logo-mask.png用作動畫前景,它是一個鏤空的“ANDROID”圖像。圖片android-logo-shine.png用作動畫背景,它的中間包含有一個高亮的呈45度角的條紋。在每一次循環中,圖片android-logo-shine.png被劃分成左右兩部分內容來顯示。左右兩個部分的圖像寬度隨著時間的推移而此消彼長,這樣就可以使得圖片android-logo-shine.png中間高亮的條紋好像在移動一樣。另一方面,在每一次循環中,圖片android-logo-shine.png都作為一個整體來渲染,而且它的位置是恒定不變的。由于它是一個鏤空的“ANDROID”圖像,因此,我們就可以通過它的鏤空來看到它背后的圖片android-logo-shine.png的條紋一閃一閃地劃過。 這個while循環語句會一直被執行,直到應用程序/system/bin/bootanimation被結束為止,后面我們再分析。 BootAnimation類的成員函數movie的實現比較長,我們分段來閱讀: ~~~ bool BootAnimation::movie() { ZipFileRO& zip(mZip); size_t numEntries = zip.getNumEntries(); ZipEntryRO desc = zip.findEntryByName("desc.txt"); FileMap* descMap = zip.createEntryFileMap(desc); LOGE_IF(!descMap, "descMap is null"); if (!descMap) { return false; } String8 desString((char const*)descMap->getDataPtr(), descMap->getDataLength()); char const* s = desString.string(); Animation animation; // Parse the description file for (;;) { const char* endl = strstr(s, "\n"); if (!endl) break; String8 line(s, endl - s); const char* l = line.string(); int fps, width, height, count, pause; char path[256]; if (sscanf(l, "%d %d %d", &width, &height, &fps) == 3) { //LOGD("> w=%d, h=%d, fps=%d", fps, width, height); animation.width = width; animation.height = height; animation.fps = fps; } if (sscanf(l, "p %d %d %s", &count, &pause, path) == 3) { //LOGD("> count=%d, pause=%d, path=%s", count, pause, path); Animation::Part part; part.count = count; part.pause = pause; part.path = path; animation.parts.add(part); } s = ++endl; } ~~~ 從前面BootAnimation類的成員函數readyToRun的實現可以知道,如果目標設備上存在壓縮文件/data/local/bootanimation.zip,那么BootAnimation類的成員變量mZip就會指向它,否則的話,就會指向目標設備上的壓縮文件/system/media/bootanimation.zip。無論BootAnimation類的成員變量mZip指向的是哪一個壓縮文件,這個壓縮文件都必須包含有一個名稱為“desc.txt”的文件,用來描述用戶自定義的開機動畫是如何顯示的。 文件desc.txt的內容格式如下面的例子所示: ~~~ 600 480 24 p 1 0 part1 p 0 10 part2 ~~~ 第一行的三個數字分別表示開機動畫在屏幕中的顯示寬度、高度以及幀速(fps)。剩余的每一行都用來描述一個動畫片斷,這些行必須要以字符“p”來開頭,后面緊跟著兩個數字以及一個文件目錄路徑名稱。第一個數字表示一個片斷的循環顯示次數,如果它的值等于0,那么就表示無限循環地顯示該動畫片斷。第二個數字表示每一個片斷在兩次循環顯示之間的時間間隔。這個時間間隔是以一個幀的時間為單位的。文件目錄下面保存的是一系列png文件,這些png文件會被依次顯示在屏幕中。 以上面這個desct.txt文件的內容為例,它描述了一個大小為600 x 480的開機動畫,動畫的顯示速度為24幀每秒。這個開機動畫包含有兩個片斷part1和part2。片斷part1只顯示一次,它對應的png圖片保存在目錄part1中。片斷part2無限循環地顯示,其中,每兩次循環顯示的時間間隔為10 x (1 / 24)秒,它對應的png圖片保存在目錄part2中。 上面的for循環語句分析完成desc.txt文件的內容后,就得到了開機動畫的顯示大小、速度以及片斷信息。這些信息都保存在Animation對象animation中,其中,每一個動畫片斷都使用一個Animation::Part對象來描述,并且保存在Animation對象animation的成員變量parts所描述的一個片斷列表中。 接下來,BootAnimation類的成員函數movie再斷續將每一個片斷所對應的png圖片讀取出來,如下所示: ~~~ // read all the data structures const size_t pcount = animation.parts.size(); for (size_t i=0 ; i<numEntries ; i++) { char name[256]; ZipEntryRO entry = zip.findEntryByIndex(i); if (zip.getEntryFileName(entry, name, 256) == 0) { const String8 entryName(name); const String8 path(entryName.getPathDir()); const String8 leaf(entryName.getPathLeaf()); if (leaf.size() > 0) { for (int j=0 ; j<pcount ; j++) { if (path == animation.parts[j].path) { int method; // supports only stored png files if (zip.getEntryInfo(entry, &method, 0, 0, 0, 0, 0)) { if (method == ZipFileRO::kCompressStored) { FileMap* map = zip.createEntryFileMap(entry); if (map) { Animation::Frame frame; frame.name = leaf; frame.map = map; Animation::Part& part(animation.parts.editItemAt(j)); part.frames.add(frame); } } } } } } } } ~~~ 每一個png圖片都表示一個動畫幀,使用一個Animation::Frame對象來描述,并且保存在對應的Animation::Part對象的成員變量frames所描述的一個幀列表中。 獲得了開機動畫的所有信息之后,接下來BootAnimation類的成員函數movie就準備開始顯示開機動畫了,如下所示: ~~~ // clear screen glShadeModel(GL_FLAT); glDisable(GL_DITHER); glDisable(GL_SCISSOR_TEST); glDisable(GL_BLEND); glClear(GL_COLOR_BUFFER_BIT); eglSwapBuffers(mDisplay, mSurface); glBindTexture(GL_TEXTURE_2D, 0); glEnable(GL_TEXTURE_2D); glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); const int xc = (mWidth - animation.width) / 2; const int yc = ((mHeight - animation.height) / 2); nsecs_t lastFrame = systemTime(); nsecs_t frameDuration = s2ns(1) / animation.fps; Region clearReg(Rect(mWidth, mHeight)); clearReg.subtractSelf(Rect(xc, yc, xc+animation.width, yc+animation.height)); ~~~ 前面的一系列gl函數首先用來清理屏幕,接下來的一系列gl函數用來設置OpenGL的紋理顯示方式。 變量xc和yc的值用來描述開機動畫的顯示位置,即需要在屏幕中間顯示開機動畫,另外一個變量frameDuration的值用來描述每一幀的顯示時間,它是以納秒為單位的。 Region對象clearReg用來描述屏幕中除了開機動畫之外的其它區域,它是用整個屏幕區域減去開機動畫所點據的區域來得到的。 準備好開機動畫的顯示參數之后,最后就可以執行顯示開機動畫的操作了,如下所示: ~~~ for (int i=0 ; i<pcount && !exitPending() ; i++) { const Animation::Part& part(animation.parts[i]); const size_t fcount = part.frames.size(); glBindTexture(GL_TEXTURE_2D, 0); for (int r=0 ; !part.count || r<part.count ; r++) { for (int j=0 ; j<fcount && !exitPending(); j++) { const Animation::Frame& frame(part.frames[j]); if (r > 0) { glBindTexture(GL_TEXTURE_2D, frame.tid); } else { if (part.count != 1) { glGenTextures(1, &frame.tid); glBindTexture(GL_TEXTURE_2D, frame.tid); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterx(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } initTexture( frame.map->getDataPtr(), frame.map->getDataLength()); } if (!clearReg.isEmpty()) { Region::const_iterator head(clearReg.begin()); Region::const_iterator tail(clearReg.end()); glEnable(GL_SCISSOR_TEST); while (head != tail) { const Rect& r(*head++); glScissor(r.left, mHeight - r.bottom, r.width(), r.height()); glClear(GL_COLOR_BUFFER_BIT); } glDisable(GL_SCISSOR_TEST); } glDrawTexiOES(xc, yc, 0, animation.width, animation.height); eglSwapBuffers(mDisplay, mSurface); nsecs_t now = systemTime(); nsecs_t delay = frameDuration - (now - lastFrame); lastFrame = now; long wait = ns2us(frameDuration); if (wait > 0) usleep(wait); } usleep(part.pause * ns2us(frameDuration)); } // free the textures for this part if (part.count != 1) { for (int j=0 ; j<fcount ; j++) { const Animation::Frame& frame(part.frames[j]); glDeleteTextures(1, &frame.tid); } } } return false; } ~~~ 第一層for循環用來顯示每一個動畫片斷,第二層的for循環用來循環顯示每一個動畫片斷,第三層的for循環用來顯示每一個動畫片斷所對應的png圖片。這些png圖片以紋理的方式來顯示在屏幕中。 注意,如果一個動畫片斷的循環顯示次數不等于1,那么就說明這個動畫片斷中的png圖片需要重復地顯示在屏幕中。由于每一個png圖片都需要轉換為一個紋理對象之后才能顯示在屏幕中,因此,為了避免重復地為同一個png圖片創建紋理對象,第三層的for循環在第一次顯示一個png圖片的時候,會調用函數glGenTextures來為這個png圖片創建一個紋理對象,并且將這個紋理對象的名稱保存在對應的Animation::Frame對象的成員變量tid中,這樣,下次再顯示相同的圖片時,就可以使用前面已經創建好了的紋理對象,即調用函數glBindTexture來指定當前要操作的紋理對象。 如果Region對象clearReg所包含的區域不為空,那么在調用函數glDrawTexiOES和eglSwapBuffers來顯示每一個png圖片之前,首先要將它所包含的區域裁剪掉,避免開機動畫可以顯示在指定的位置以及大小中。 每當顯示完成一個png圖片之后,都要將變量frameDuration的值從納秒轉換為毫秒。如果轉換后的值大小于,那么就需要調用函數usleep函數來讓線程睡眠一下,以保證每一個png圖片,即每一幀動畫都按照預先指定好的速度來顯示。注意,函數usleep指定的睡眠時間只能精確到毫秒,因此,如果預先指定的幀顯示時間小于1毫秒,那么BootAnimation類的成員函數movie是無法精確地控制地每一幀的顯示時間的。 還有另外一個地方需要注意的是,每當循環顯示完成一個片斷時,需要調用usleep函數來使得線程睡眠part.pause * ns2us(frameDuration)毫秒,以便可以按照預先設定的節奏來顯示開機動畫。 最后一個if語句判斷一個動畫片斷是否是循環顯示的,即循環次數不等于1。如果是的話,那么就說明前面為它所對應的每一個png圖片都創建過一個紋理對象。現在既然這個片斷的顯示過程已經結束了,因此,就需要釋放前面為它所創建的紋理對象。 至此,第三個開機畫面的顯示過程就分析完成了。 接下來,我們再繼續分析第三個開機畫面是如何停止顯示的。 從前面Android系統默認Home應用程序(Launcher)的啟動過程源代碼分析一文可以知道,當System進程將系統中的關鍵服務啟動起來之后,就會將應用程序啟動器(Launcher)啟動起來。從Android應用程序啟動過程源代碼分析一文又可以知道,Android應用程序的啟動過程實際上就是它的根Activity組件的啟動過程。對于應用程序Launcher來說,它的根Activity組件即為Launcher組件。 一個Activity組件在啟動起來之后,就會被記錄起來,等到它所運行在的主線程空閑的時候,這個主線程就會向ActivityManagerService發送一個Activity組件空閑的通知。由于應用程序Launcher是系統中第一個被啟動的應用程序,即它的根Activity組件是系統中第一個被啟動的Activity組件,因此,當ActivityManagerService接收到它的空閑通知的時候,就可以知道系統是剛剛啟動起來的。在這種情況下,ActivityManagerService就會停止顯示開機動畫,以便可以在屏幕中顯示應用程序Lancher的界面。 從前面Android應用程序消息處理機制(Looper、Handler)分析一文可以知道,如果一個線程想要在空閑的時候處理一些事務,那么就必須要向這個線程的消息隊列注冊一個空閑消息處理器。自定義的空閑消息處理器燈必須要從MessageQueue.IdleHandler類繼承下來,并且重寫成員函數queueIdle。當一個線程空閑的時候,即消息隊列中沒有新的消息需要處理的時候,那些注冊了的空閑消息處理器的成員函數queueIdle就會被調用。 應用程序的主線程是通過ActivityThread類來描述的,它實現在文件frameworks/base/core/java/android/app/ActivityThread.java中。每當有一個新的Activity組件啟動起來的時候,ActivityThread類都會向它所描述的應用程序主線程的消息隊列注冊一個類型為Idler的空閑消息處理器。這樣一個應用程序的主線程就可以在空閑的時候,向ActivityManagerService發送一個Activity組件空閑通知,相當于是通知ActivityManagerService,一個新的Activity組件已經準備就緒了。 Idler類定義在frameworks/base/core/java/android/app/ActivityThread.java中, 它的成員函數queueIdle的實現如下所示: ~~~ public final class ActivityThread { ...... private final class Idler implements MessageQueue.IdleHandler { public final boolean queueIdle() { ActivityClientRecord a = mNewActivities; if (a != null) { mNewActivities = null; IActivityManager am = ActivityManagerNative.getDefault(); ActivityClientRecord prev; do { ...... if (a.activity != null && !a.activity.mFinished) { try { am.activityIdle(a.token, a.createdConfig); a.createdConfig = null; } catch (RemoteException ex) { } } prev = a; a = a.nextIdle; prev.nextIdle = null; } while (a != null); } ensureJitEnabled(); return false; } } ...... } ~~~ ActivityThread類有一個類型為ActivityClientRecord的成員變量mNewActivities,用來描述所有在當前應用程序主線程中新啟動起來的Activity組件。這些新啟動起來的Activity組件通過ActivityClientRecord類的成員變量nextIdle連接在一起。一旦當前應用程序主線程向ActivityManagerService發送了這些新啟動的Activity組件的空閑通知之后,這些新啟動起來的Activity組件就不會再被保存在ActivityThread類的成員變量mNewActivities中了,即每一個新啟動的Activity組件只有一次機會向ActivityManagerService發送一個空閑通知。 向ActivityManagerService發送一個Activity組件空閑通知是通過調用ActivityManagerService代理對象的成員函數activityIdle來實現的,而ActivityManagerService代理對象可以通過調用ActivityManagerNative類的靜態成員函數getDefault來獲得。 ActivityManagerService代理對象的類型為ActivityManagerProxy,它的成員函數activityIdle實現在文件frameworks/base/core/java/android/app/ActivityManagerNative.java中,如下所示: ~~~ class ActivityManagerProxy implements IActivityManager { ...... public void activityIdle(IBinder token, Configuration config) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(token); if (config != null) { data.writeInt(1); config.writeToParcel(data, 0); } else { data.writeInt(0); } mRemote.transact(ACTIVITY_IDLE_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); reply.readException(); data.recycle(); reply.recycle(); } ...... } ~~~ ActivityManagerProxy類的成員函數activityIdle實際上是向ActivityManagerService發送一個類型為ACTIVITY_IDLE_TRANSACTION的Binder進程間通信請求,其中,參數token用來描述與這個進程間通信請求所關聯的一個Activity組件,在我們這個場景中,這個Activity組件即為應用程序Launcher的根Activity組件Launcher。 類型為ACTIVITY_IDLE_TRANSACTION的Binder進程間通信請求是由ActivityManagerService類的成員函數activityIdle來處理的,如下所示: ~~~ public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback { ...... public final void activityIdle(IBinder token, Configuration config) { final long origId = Binder.clearCallingIdentity(); mMainStack.activityIdleInternal(token, false, config); Binder.restoreCallingIdentity(origId); } ...... } ~~~ ActivityManagerService類有一個類型為ActivityStack的成員變量mMainStack,它用來描述系統的Activity組件堆棧,它的成員函數activityIdleInternal的實現如下所示: ~~~ public class ActivityStack { ...... final void activityIdleInternal(IBinder token, boolean fromTimeout, Configuration config) { ...... boolean enableScreen = false; synchronized (mService) { ...... // Get the activity record. int index = indexOfTokenLocked(token); if (index >= 0) { ActivityRecord r = (ActivityRecord)mHistory.get(index); ...... if (mMainStack) { if (!mService.mBooted && !fromTimeout) { mService.mBooted = true; enableScreen = true; } } } ...... } ...... if (enableScreen) { mService.enableScreenAfterBoot(); } } ...... } ~~~ 參數token用來描述剛剛啟動起來的Launcher組件,通過它來調用函數indexOfTokenLocked就可以得到Launcher組件在系統Activity組件堆棧中的位置index。得到了Launcher組件在系統Activity組件堆棧中的位置index之后,就可以在ActivityStack類的成員變量mHistory中得到一個ActivityRecord對象r。這個ActivityRecord對象r同樣是用來描述Launcher組件的。 ActivityStack類的成員變量mMainStack是一個布爾變量,當它的值等于true的時候,就說明當前正在處理的ActivityStack對象是用來描述系統的Activity組件堆棧的。 ActivityStack類的另外一個成員變量mService指向了系統中的ActivityManagerService服務。ActivityManagerService服務有一個類型為布爾值的成員變量mBooted,它的初始值為false,表示系統尚未啟動完成。 從前面的調用過程可以知道,參數fromTimeout的值等于false。在這種情況下,如果ActivityManagerService服務的成員變量mBooted也等于false,那么就說明應用程序已經啟動起來了,即說明系統已經啟動完成了。這時候ActivityManagerService服務的成員變量mBooted以及變量enableScreen的值就會被設置為true。 當變量enableScreen的值等于true的時候,ActivityStack類就會調用ActivityManagerService服務的成員函數enableScreenAfterBoot停止顯示開機動畫,以便可以將屏幕讓出來顯示應用程序Launcher的界面。 ActivityManagerService類的成員函數enableScreenAfterBoot的實現如下所示: ~~~ public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback { ...... void enableScreenAfterBoot() { EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_ENABLE_SCREEN, SystemClock.uptimeMillis()); mWindowManager.enableScreenAfterBoot(); } ...... } ~~~ ActivityManagerService類的成員變量mWindowManager指向了系統中的Window管理服務WindowManagerService,ActivityManagerService服務通過調用它的成員函數enableScreenAfterBoot來停止顯示開機動畫。 WindowManagerService類的成員函數enableScreenAfterBoot的實現如下所示: ~~~ public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor { ...... public void enableScreenAfterBoot() { synchronized(mWindowMap) { if (mSystemBooted) { return; } mSystemBooted = true; } performEnableScreen(); } ...... } ~~~ WindowManagerService類的成員變量mSystemBooted用來記錄系統是否已經啟動完成的。如果已經啟動完成的話,那么這個成員變量的值就會等于true,這時候WindowManagerService類的成員函數enableScreenAfterBoot什么也不做就返回了,否則的話,WindowManagerService類的成員函數enableScreenAfterBoot首先將這個成員變量的值設置為true,接著再調用另外一個成員函數performEnableScreen來執行停止顯示開機動畫的操作。 WindowManagerService類的成員函數performEnableScreen的實現如下所示: ~~~ public class WindowManagerService extends IWindowManager.Stub implements Watchdog.Monitor { ...... public void performEnableScreen() { synchronized(mWindowMap) { if (mDisplayEnabled) { return; } if (!mSystemBooted) { return; } ...... mDisplayEnabled = true; ...... try { IBinder surfaceFlinger = ServiceManager.getService("SurfaceFlinger"); if (surfaceFlinger != null) { //Slog.i(TAG, "******* TELLING SURFACE FLINGER WE ARE BOOTED!"); Parcel data = Parcel.obtain(); data.writeInterfaceToken("android.ui.ISurfaceComposer"); surfaceFlinger.transact(IBinder.FIRST_CALL_TRANSACTION, data, null, 0); data.recycle(); } } catch (RemoteException ex) { Slog.e(TAG, "Boot completed: SurfaceFlinger is dead!"); } } ...... } ...... } ~~~ WindowManagerService類的另外一個成員變量mDisplayEnabled用來描述WindowManagerService是否已經初始化過系統的屏幕了,只有當它的值等于false,并且系統已經完成啟動,即WindowManagerService類的成員變量mSystemBooted等于true的情況下,WindowManagerService類的成員函數performEnableScreen才通知SurfaceFlinger服務停止顯示開機動畫。 注意,WindowManagerService類的成員函數performEnableScreen是通過一個類型為IBinder.FIRST_CALL_TRANSACTION的進程間通信請求來通知SurfaceFlinger服務停止顯示開機動畫的。 在SurfaceFlinger服務,類型為IBinder.FIRST_CALL_TRANSACTION的進程間通信請求被定義為停止顯示開機動畫的請求,如下所示: ~~~ class BnSurfaceComposer : public BnInterface<ISurfaceComposer> { public: enum { // Note: BOOT_FINISHED must remain this value, it is called from // Java by ActivityManagerService. BOOT_FINISHED = IBinder::FIRST_CALL_TRANSACTION, ...... }; virtual status_t onTransact( uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0); }; ~~~ BnSurfaceComposer類定義在文件frameworks/base/include/surfaceflinger/ISurfaceComposer.h中,它是SurfaceFlinger服務所要繼承的Binder本地對象類,其中。當SurfaceFlinger服務接收到類型為IBinder::FIRST_CALL_TRANSACTION,即類型為BOOT_FINISHED的進程間通信請求時,它就會將該請求交給它的成員函數bootFinished來處理。 SurfaceFlinger服務的成員函數bootFinished實現在文件frameworks/base/services/surfaceflinger/SurfaceFlinger.cpp中,如下所示: ~~~ void SurfaceFlinger::bootFinished() { const nsecs_t now = systemTime(); const nsecs_t duration = now - mBootTime; LOGI("Boot is finished (%ld ms)", long(ns2ms(duration)) ); mBootFinished = true; property_set("ctl.stop", "bootanim"); } ~~~ 這個函數主要就是將系統屬性“ctl.stop”的值設置為“bootanim”。前面提到,每當有一個系統屬性發生變化時,init進程就會被喚醒,并且調用運行在它里面的函數handle_property_set_fd來處理這個系統屬性變化事件。在我們這個場景中,由于被改變的系統屬性的名稱是以"ctl."開頭的,即被改變的系統屬性是一個控制類型的屬性,因此,接下來函數handle_property_set_fd又會調用另外一個函數handle_control_message來處理該系統屬性變化事件。 函數handle_control_message實現在文件system/core/init/init.c中,如下所示: ~~~ void handle_control_message(const char *msg, const char *arg) { if (!strcmp(msg,"start")) { msg_start(arg); } else if (!strcmp(msg,"stop")) { msg_stop(arg); } else { ERROR("unknown control msg '%s'\n", msg); } } ~~~ 從前面的調用過程可以知道,參數msg和arg的值分別等于"stop"和“bootanim”,這表示要停止執行名稱為“bootanim”的服務,這是通過調用函數msg_stop來實現的。 函數msg_stop也是實現在文件system/core/init/init.c中,如下所示: ~~~ static void msg_stop(const char *name) { struct service *svc = service_find_by_name(name); if (svc) { service_stop(svc); } else { ERROR("no such service '%s'\n", name); } } ~~~ 這個函數首先調用函數service_find_by_name來找到名稱等于name,即“bootanim”的服務,然后再調用函數service_stop來停止這個服務。 前面提到,名稱為“bootanim”的服務對應的應用程序即為/system/bin/bootanimation。因此,停止名稱為“bootanim”的服務即為停止執行應用程序/system/bin/bootanimation,而當應用程序/system/bin/bootanimation停止執行的時候,開機動畫就會停止顯示了。 至此,Android系統的三個開機畫面的顯示過程就分析完成了。通過這個三個開機畫面的顯示過程分析,我們學習到: 1. 在內核層,系統屏幕是使用一個稱為幀緩沖區的硬件設備來描述的,而用戶空間的應用程序可以通過設備文件/dev/fb0或者/dev/graphics/fb0來操作這個硬件設備。實際上,幀緩沖區本身并不是一個真正的硬件,它只不過是對顯卡的一個抽象表示,不過,我們通過訪幀緩沖區就可以間接地操作顯卡內存以及顯卡中的其它寄存器。 2. OpenGL是通過EGL接口來渲染屏幕,而EGL接口是通過ANativeWindow類來間接地渲染屏幕的。我們可以將ANativeWindow類理解成一個Android系統的本地窗口類,即相當于是Windows系統中的窗口句柄概念,它最終是通過文件/dev/fb0或者/dev/graphics/fb0來渲染屏幕的。 3. init進程在啟動的過程中,會將另外一個ueventd進程也啟動起來。ueventd進程對應的可執行文件與init進程對應的可執行文件均為/init,不過ueventd進程主要負責處理內核發出的uevent事件,即負責管理系統中的設備文件。 4. 每當我們設置一個系統屬性的時候,init進程都會接收到一個系統屬性變化事件。當發生變化的系統屬性的名稱等于“ctl.start”或者“ctl.stop”,那么實際上是向init進程發出一個啟動或者停止服務的命令。 前面第1點和第2點的知識是與Android系統的UI實現相關的,而后面第3點和第4點是兩個額外獲得的知識點。 本文的目的并不是單純為了介紹Android系統的開機畫面,而是希望能夠以Android系統的開機畫面來作為切入點來分析Android系統的UI實現。在后面的文章中,我們就會根據本文所涉及到的知識點,來展開分析Android系統的UI實現,敬請關注。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看