1. 程式人生 > >vivi原始碼分析3

vivi原始碼分析3

繼續分析vivi原始碼。 step 5:     MTD裝置初始化。     關於什麼是MTD,為什麼要使用MTD,MTD技術的架構是什麼,等等,可以參考《Linux MTD原始碼分析》(作者:Jim Zeus,2002-04-29)。這份文件的參考價值比較大,猜想作者在當時可能研究了很長時間,畢竟2002年的時候資料還比較缺乏。當然,因為完全分析透徹,方方面面都點透,這份文件還是沒有做到。     vivi採用Linux kernel的架構,所以把Linux kernel的MTD子系統借用過來了,做了一些裁減。可以簡單地看成:Flash硬體驅動層和MTD裝置層。這樣,最終以抽象的統一的介面向vivi提供。     還是以nand flash啟動這個情景為主線,對MTD初始化流程進行分析。下面先從入口開始。

ret = mtd_dev_init();

    利用source insight跟蹤,看一下此函式的介面定義部分:

/*
 * VIVI Interfaces
 */

#ifdef CONFIG_MTD
int write_to_flash(loff_t ofs, size_t len, const u_char *buf, int flag);
int mtd_dev_init(void);
#else
#define write_to_flash(a, b, c, d) (int)(1)
#define mtd_dev_init(

) (int)(1)
#endif

    可見,vivi在配置的時候是必須配置MTD功能部分的。如果不配置MTD,那麼CONFIG_MTD就不存在定義。由此導致寫flash的動作實際上是沒有的。也就是說,無法完成寫flash的動作。當然,在這裡可以做測試,就是使用MTD子系統的vivi把分割槽等都設定好。然後重新編譯一下vivi,把mtd功能去除,做簡單的修改(把bon_cmd部分從【lib/command.c】中去掉,否則編譯不通過),生成大小為35152位元組。給開發板重新上電,利用老的vivi燒寫nand flash的vivi分割槽,完成後做一下reset,於是沒有MTD功能的vivi就跑起來了。但是,這樣的bootloader僅僅適合於最終的產品階段,不適合開發,沒什麼太大的價值。有興趣倒是可以據此研究一下配置部分,整個引導時間相應的縮短。我的最小配置檔案如下:
檔案: config.rar
大小: 0KB
下載: 下載
    下面【drivers/mtd/mtdcore.c】,看看mtd_dev_init函式,核心部分就是呼叫mtd_init函式(【drivers/mtd/maps/s3c2410_flash.c】)。

int

mtd_init(void)
{
    int ret;

#ifdef CONFIG_MTD_CFI
    ret = cfi_init();
#endif
#ifdef CONFIG_MTD_SMC
    ret = smc_init();
#endif
#ifdef CONFIG_S3C2410_AMD_BOOT
    ret = amd_init();
#endif

    可見,vivi現在支援三種類型的儲存介面,一種是CFI,也就是Intel發起的一個flash的介面標準,主要就是intel的nor flash系列;一種是smc,智慧卡系列介面,nand flash就是通過這個介面實現讀寫的;一種是AMD的flash系列。選擇什麼啟動方式,就要選擇相應的配置項。     核心部分根據配置應該呼叫smc_init函式。-->【drivers/mtd/maps/s3c2410_flash.c】。這裡最為核心的就是兩個資料結構,一個是mtd_info,位於【include/mtd/mtd.h】,如下:     mtd_info是表示MTD裝置的結構,每個分割槽也被表示為一個mtd_info,如果有兩個MTD裝置,每個裝置有三個分割槽,那麼在系統中就一共有6個mtd_info結構。關於mtd_info,在《Linux MTD原始碼分析》中講解非常透徹,不過需要注意的是,在vivi的實現中沒有使用mtd_table,另外priv指向的是nand_info,這些都是與Linux下不同的地方,主要是為了簡化。另一個是nand_info,這個結構則包含了nand flash的所有資訊。     所謂的初始化,其實就是填充處理上述兩個資料結構的過程。填充完畢之後,後續的工作都會基於此展開。下面開始看smc_init的程式碼。

mymtd = mmalloc(sizeof(struct mtd_info) + sizeof(struct nand_chip));
this = (struct nand_chip *)(&mymtd[1]);

    在這裡,第一句參考前面heap的實現程式碼,重點看第二句程式碼。這句程式碼是有一定的技巧性,但是也存在著很大的風險。其中,mymtd是指向struct mtd_info的指標,那麼mymtd[1]實際上是等效於*(mymtd + 1)的數學計算模式,注意mymtd並非陣列,這裡僅僅利用了編譯器翻譯的特點。對於指標而言,加1實際上增加的指標對應型別的值,在這裡地址實際上增加了sizeof(struct mtd_info),因為前面分配了兩塊連續的地址空間,所以&(*(mymtd + 1))實際上就是mtd_info資料結構結束的下一個地址,然後實現強制轉換,於是this就成為了nand_chip的入口指標了。但是,這裡必須要把握好,因為這個地方是不會進行記憶體的檢查的,也就是說,如果你使用了mymtd[2],那麼仍然按照上述公式解析,雖然可以運算,可是就是明顯的指標洩漏了,可能會出現意料不到的結果。寫了一個測試程式,對這點進行了探討,要小心記憶體問題。
檔案: array_test.tar.gz
大小: 0KB
下載: 下載
    瞭解清楚了,mymtd指向mtd_info的入口,this指向nand_chip的入口。

    memset((char *)mymtd, 0, sizeof(struct mtd_info));
    memset((char *)this, 0, sizeof(struct nand_chip));

mymtd->priv = this;

    上述程式碼首先初始化這兩個結構體,即均為0.然後利用priv把二者聯絡起來,也就是mymtd通過其成員priv指向this,那麼mymtd中的抽閒操作函式,比如read、write等,真正的是通過this來實現的。很明顯,this的實現部分屬於flash硬體驅動層,而mymtd部分則屬於MTD裝置層,二者的聯絡就是通過成員priv實現的。     接下來首先是初始化nand flash裝置,這跟前面的基礎實驗一致。

    /* set NAND Flash controller */
    nfconf = NFCONF;
    /* NAND Flash controller enable */
    nfconf |= NFCONF_FCTRL_EN;

    /* Set flash memory timing */
    nfconf &= ~NFCONF_TWRPH1;     /* 0x0 */
    nfconf |= NFCONF_TWRPH0_3;    /* 0x3 */
    nfconf &= ~NFCONF_TACLS;      /* 0x0 */

    NFCONF = nfconf;

    然後填充nand flash的資料結構的一個例項this,分成了兩個部分,nand flash基本操作函式成員的初始化、其餘資訊的填寫。

    /* Set address of NAND IO lines */
    this->hwcontrol = smc_hwcontrol;
    this->write_cmd = write_cmd;
    this->write_addr = write_addr;
    this->read_data = read_data;
    this->write_data = write_data;
    this->wait_for_ready = wait_for_ready;

    /* Chip Enable -> RESET -> Wait for Ready -> Chip Disable */
    this->hwcontrol(NAND_CTL_SETNCE);
    this->write_cmd(NAND_CMD_RESET);
    this->wait_for_ready();
    this->hwcontrol(NAND_CTL_CLRNCE);

    smc_insert(this);

    上面這些都不難理解,感覺在結構體設計上還是比較出色的,把成員和相應的操作封裝起來,面向物件的一種方法。下面看smc_insert,無非還是按照結構體填寫相應的資訊,細節部分就不深入探討了。


inline int
smc_insert(struct nand_chip *this) {
    /* Scan to find existance of the device */
    if (smc_scan(mymtd)) {
        return -ENXIO;
    }
    /* Allocate memory for internal data buffer */
    this->data_buf = mmalloc(sizeof(u_char) *
             (mymtd->oobblock + mymtd->oobsize));

    if (!this->data_buf) {
        printk("Unable to allocate NAND data buffer for S3C2410./n");
        this->data_buf = NULL;
        return -ENOMEM;
    }

    return 0;
}

    第一部分掃描填充mymtd資料結構。後面主要用於nand flash的oob緩衝處理。具體部分可以參考《s3c2410完全開發》。     這裡重點是學習一種結構體的構造技巧。     首先構造一級資料結構,表示抽象實體。例如:

struct nand_flash_dev {
    char * name;
    int manufacture_id;
    int model_id;
    int chipshift;
    char page256;
    char pageadrlen;
    unsigned long erasesize;
};

    然後構造例項集合,表現形式就是一個大的陣列。

static struct nand_flash_dev nand_flash_ids[] = {
    {"Toshiba TC5816BDC", NAND_MFR_TOSHIBA, 0x64, 21, 1, 2, 0x1000},    
// 2Mb 5V

    ... ....

    {"Samsung K9D1G08V0M", NAND_MFR_SAMSUNG, 0x79, 27, 0, 3, 0x4000},    // 128Mb

    {NULL,}
};

    這樣修改擴充套件等等後續的操作就簡便多了。抽象的能力及其訓練在讀程式碼的時候是可以很好的學習的,在vivi中,多處都採用了這種設計原則,應該掌握並利用。 step 6:     此部分的功能是把vivi可能用到的所有私有引數都放在預先規劃的記憶體區域,大小為48K,基地址為0x 33df0000。在記憶體的分配示意圖方面,《s3c2410完全開發》已經比較詳盡,就不放在這裡了。到此為止,vivi作為bootloader的三大核心任務:initialise various devices, and eventually call the Linux kernel,passing information to the kernel.,現在只是完成第一方面的工作,裝置初始化基本完成,實際上step 6是為啟動Linux核心和傳遞引數做準備的,把vivi的私有資訊,核心啟動引數,mtd分割槽資訊等都放到特定的記憶體區域,等待後面兩個重要工作使用(在step 8完成,後面的step 7也是為step 8服務的)。這48K區域分為三個組成部分:MTD引數、vivi parameter、Linux啟動命令。每塊的具體內容框架一致,以vivi param tlb這個情景為主線進行分析: 入口:

init_priv_data();

進入【lib/priv_data/rw.c】--init_priv_data()

int
init_priv_data(void)
{
    int ret_def;
#ifdef CONFIG_PARSE_PRIV_DATA
    int ret_saved;
#endif
    ret_def = get_default_priv_data();
#ifdef CONFIG_PARSE_PRIV_DATA
    ret_saved = load_saved_priv_data();
    if (ret_def && ret_saved) {
        printk("Could not found vivi parameters./n");
        return -1;
    } else if (ret_saved && !ret_def) {
        printk("Could not found stored vivi parameters.");
        printk(" Use default vivi parameters./n");
    } else {
        printk("Found saved vivi parameters./n");
    }
#else
    if (ret_def) {
        printk("Could not found vivi parameters/n");
        return -1;
    } else {
        printk("Found default vivi parameters/n");
    }
#endif
    return 0;

    下面分為兩步:首先讀取預設設定到特定的記憶體區域,然後讀取nand flash的param區域的資訊,如果讀取成功,就覆蓋掉前面的預設設定。首先看第一步,get_default_priv_data--get_default_param_tlb-->

int get_default_param_tlb(void)
{
    char *src = (char *)&default_vivi_parameters;
    char *dst = (char *)(VIVI_PRIV_RAM_BASE + PARAMETER_TLB_OFFSET);
    int num = default_nb_params;

    if (src == NULL) return -1;

    /*printk("number of vivi parameters = %d/n", num); */
    *(nb_params) = num;

    //引數表的長度不可以超過預設記憶體的大小

    if ((sizeof(vivi_parameter_t)*num) > PARAMETER_TLB_SIZE) {
        printk("Error: too large partition table/n");
        return -1;
    }

    //首先複製magic number
    memcpy(dst, vivi_param_magic, 8);

    //預留下8個位元組作為擴充套件
    dst += 16;

    //複製真正的parameter
    memcpy(dst, src, (sizeof(vivi_parameter_t)*num));
    return 0;
}

    記憶體的入口地址為VIVI_PRIV_RAM_BASE+PARAMETER_TLB_OFFSET,開始的8個位元組放magic number,這裡vivi定義為“VIVIPARA”,後面空下8個位元組,留作擴充套件,從第17個位元組開始放置真正的param。這裡用到了多處技巧,第一處就是上面剛剛介紹過的資料結構構造技巧,這裡的vivi_parameter_t就是一級資料結構:

typedef struct parameter {
    char name[MAX_PARAM_NAME];
    param_value_t value;
    void (*update_func)(param_value_t value);
} vivi_parameter_t;    

    利用其構造了預設的成員表:

vivi_parameter_t default_vivi_parameters[] = {
    { "mach_type",            MACH_TYPE,    NULL },
    { "media_type",            MT_S3C2410,    NULL },
    { "boot_mem_base",        0x30000000,    NULL },
    { "baudrate",            UART_BAUD_RATE,    NULL },
    { "xmodem_one_nak",        0,        NULL },
    { "xmodem_initial_timeout",    300000,        NULL },
    { "xmodem_timeout",        1000000,    NULL },
    { "ymodem_initial_timeout",    1500000,    NULL },
    { "boot_delay",            0x1000000,    NULL }
};

    我們這時就可以很清楚的看到param show列出的配置引數了。     另外一個技巧就是利用巨集計算陣列長度。

int default_nb_params = ARRAY_SIZE(default_vivi_parameters);

    其中ARRAY_SIZE為:

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

    這是從Linux kernel中拿來的,也是值得學習和利用的地方。     在load階段內無非就是找到param分割槽,然後根據配置,找到相應的flash硬體驅動(這就是MTD層的作用所在,不過可以看出nand chip的databuf確實沒有起到作用,現在也未看出這部分究竟用在何處)。然後就是讀操作。當然,讀取出來的資訊先放到臨時緩衝區,判斷頭部的magic number,如果符合則說明是正確的分割槽資訊,然後把資訊從臨時緩衝區複製到對應的預設配置區,這樣就完成了真正的配置。     其實這個地方可以改進。首先看看param分割槽是否有合適的分割槽資訊,如果有,直接讀取到vivi parameter區域,不需要再讀取預設的配置資訊;如果沒有合適的分割槽資訊,然後讀取預設的配置資訊。這樣在使用者修正了分割槽資訊時,不必再讀取預設的配置資訊,這也算是一處優化。 step 7:     呼叫add_command()函式,增加vivi作為終端時命令的相應處理函式。其實,這種機制還是比較簡單的,就是利用了連結串列。     整個命令處理機制及其初始化的實現是在【lib/command.c】中完成的,包括新增命令、查詢命令、執行命令、解析命令列等等。具體的命令函式則在相應的模組裡面,這樣形成了一個2層的軟體架構:頂部管理層+底部執行層。維護的核心就是一個數據結構user_command:

typedef struct user_command {
    const char *name;
    void (*cmdfunc)(int argc, const char **);
    struct user_command *next_cmd;
    const char *helpstr;
} user_command_t;

    第一個成員是指向name字串的指標,第二個成員就是命令的處理函式,第三個成員是指向下一個命令,第四個成員是幫助資訊。如果你想新增一個命令,那麼首先需要構造一個數據結構user_command的例項,比如:

user_command_t help_cmd = {
    "help",
    command_help,
    NULL,
    "help [{cmds}] /t/t/t-- Help about help?"
};

    然後實現命令的真正處理函式command_help。

void command_help(int argc, const char **argv)
{
    user_command_t *curr;

    /* help <command>. invoke <command> with 'help' as an argument */
    if (argc == 2) {
        if (strncmp(argv[1], "help", strlen(argv[1])) == 0) {
            printk("Are you kidding?/n");
            return;
        }
        argv[0] = argv[1];
        argv[1] = "help";
        execcmd(argc, argv);
        return;
    }

    printk("Usage:/n");
    curr = head_cmd;
    while(curr != NULL) {
        printk(" %s/n", curr->helpstr);
        curr = curr->next_cmd;
    }
}

    構造好之後,需要把它加入連結串列,也就是在init_builtin_cmds中增加add_command(&help_cmd);,其中add_command的實現如下:

void add_command(user_command_t *cmd)
{
    if (head_cmd == NULL) {
        head_cmd = tail_cmd = cmd;
    } else {
        tail_cmd->next_cmd = cmd;
        tail_cmd = cmd;
    }
    /*printk("Registered '%s' command/n", cmd->name);*/
}

    這樣,自己如果增加新的程式,就按照如上的步驟新增即可。     其餘具體命令的實現暫時不做解釋。 step 8:     根據情況,要麼進入vivi的命令列互動介面,要麼直接啟動核心。關於此部分的流程分析,有了前面的基礎和經驗,是不難理解的。很容易通過vivi的列印資訊得知進行到了第幾步,《s3c2410完全開發》在過程上講解的也很清楚。所以不打算具體分析了。現在翻閱網上資料,有一個問題實際上模模糊糊,如下:     vivi作為bootloader的一個重要的功能就是向Linux kernel傳遞啟動引數,這個情景究竟是如何完成的呢?雖然網上討論很多,但是因為vivi具有一點特殊性,所以使得理解上有一定的困難。現在已經比較清晰了,算是回答網友的一個問題,也算是總結,就bootloader如何於kernel傳遞引數,作為一個情景進行詳盡的分析。事先需要說明的是,我們假定vivi為A,Linux kernel為B,A要傳給B東西,這就是一個通訊的過程。要想通訊,至少我們得有一個約定,那就是協議。現在存在的協議有兩種,一種是基於struct param_struct,不過這種因為其侷限性即將作廢;一種是基於tags技術。基本的情景框架就是A必須按照協議設定好引數,B呢,就需要來讀取解析這些引數。它們之間必須配合好,如果配合不好,那麼,kernel是無法引導成功的。現在嵌入式系統的移植,很多時候kernel引導不起來,部分原因就直接來自於引數傳遞問題。但是設計到這個問題,不能不分析Linux kernel的引導過程。現在還不想細緻到程式碼層,只是根據部分程式碼把Linux kernel啟動至獲取引導引數的過程從整體上了解清楚,必要的時候輔助相應的程式碼。這部分內容的詳細分析,專門在下篇總結中完成。 學習總結:     學習一種技術,採用歷史的觀點是很好的方法。我們現在學習的技術並非最新的理論研究,所以有大量前人的工作經驗可以借鑑。站在巨人的肩上,不做無謂的工作,是好的學習方法。我現在的學習觀點就是事先要分析閱讀前人的相關經驗,包括經典書籍、網上資料、網友的經驗等等,然後呢,需要對這些知識理解消化,深入,深入再深入,形成自己的認識,轉化成自己的經驗。正像網友所說,這些都是現成的技術,只要靜下心來肯學,就一定能夠學好。     另外,一定要多思考,多動手,多給自己提出問題。沒有問題說明你根本就沒有深入,有問題才能在解決的過程中提升自己!學習首先從整體上把握流程,然後呢,需要具體的細節。只看整體,不看細節,容易“眼高手低”;只看細節,不看整體,容易“只見樹木,不見森林”,提高不到一定的層次。     這些都是學習過程中的經驗總結。歡迎交流!

相關推薦

vivi原始碼分析3

繼續分析vivi原始碼。 step 5:     MTD裝置初始化。     關於什麼是MTD,為什麼要使用MTD,MTD技術的架構是什麼,等等,可以參考《Linux MTD原始碼分析》(作者:Jim Zeus,2002-04-29)。這份文件的參考價值比較大,猜想作者

lucene原始碼分析(3)facet例項

簡單的facet例項 public class SimpleFacetsExample { private final Directory indexDir = new RAMDirectory(); private final Directory taxoDir = new RAMD

Shiro原始碼分析(3) - 認證器(Authenticator)

本文在於分析Shiro原始碼,對於新學習的朋友可以參考 [開濤部落格](http://jinnianshilongnian.iteye.com/blog/2018398)進行學習。 Authenticator就是認證器,在Shiro中負責認證使用者提交的資訊,

CocosCreator物理引擎Demo原始碼分析(3)-stick-arrow

stick-arrow示例展示瞭如何動態發射剛體飛往目標點。 技術點 1、觸控式螢幕幕發射剛體,計算起點和目標點的夾角,設定剛體的線性速度。 2、在Update中不斷施加一個作用力到剛體尾部,使它能一直往目標點飛去。 3、在碰撞上後,動態計算並設定WeldJoin

記一次 JVM 原始碼分析(3.記憶體管理與GC)

簡介 miniJVM 的記憶體管理的實現較為簡單 記憶體分配使用了開源的 ltalloc 庫 GC就是經典的 Mark-Sweep GC 物件分配 物件分配要關注的就兩個過程 New 一個 Java 物件的過程 記憶體塊在堆上分配的過程 物件在 JVM

NodeJS原始碼分析(3)

Node Stream模組 Stream在平時業務開發時很少用到, 但是很多模組都是基於stream實現的,引用官方文件的解釋: 流(stream)在 Node.js 中是處理流資料的抽象介面(abstract interface)。 stream 模組提

OKHttp原始碼分析3

1 概述 上篇文章,我們詳細分析了OKHttp中Request的建立和傳送過程。其中sendRequest(), readResponse(), followUpRequest()三個關鍵方法在底層HttpEngine中實現。革命尚未成功,我們接下來在這篇文章

coreutils4.5.1 dirname.c原始碼分析3

老調重彈,每次先按程式碼量排序,從行數少的程式開始讀,總能有所收穫。比如,在dirname.c中,我發現幾條: 第一、函式和括號可以用空格隔開,很奇怪。如 void usage (int status) 在usage與(中有一個空格,我寫了一個測試程式,也驗證了猜想。 第二、對字元取地址,真怪異!

coreutils4.5.1 basename.c原始碼分析3

coreutils4.5.1 basename.c原始碼分析2 前幾天又重新讀了basename.c對其中去掉字尾的那段,終於理解了。現總結如下; static void remove_suffix (char *name, const char *suffix) {   char *

Erlang:RabbitMQ原始碼分析 3. supervisor和supervisor2深入分析

supervisor也是Erlang/OTP裡一個常用的behavior,用於構建supervisor tree實現程序監控,故障恢復。 而RabbitMQ實現了一個supervisor2,我們從原始碼角度分析二者的實現和區別。 先介紹一些supervisor的基本概念,

LAV Filter 原始碼分析 3: LAV Video (1)

LAV Video 是使用很廣泛的DirectShow Filter。它封裝了FFMPEG中的libavcodec,支援十分廣泛的視訊格式的解碼。在這裡對其原始碼進行詳細的分析。LAV Video 工程程式碼的結構如下圖所示直接看LAV Video最主要的類CLAVVideo

vivi原始碼分析1

通過vivi研究bootloader有一段時間了,基本是在與之相關的基礎方面做工作,還沒有真正深入研究vivi。以後的學習重心就要放到研究vivi原始碼上面了。我想,真正細緻地弄清楚vivi實現的細節,對C語言水平的提高,對ARM體系結構的認識,對S3C2410的熟悉,對嵌入

AngularJS 原始碼分析3

本文接著上一篇講 上一篇地址 回顧 上次說到了rootScope裡的$watch方法中的解析監控表示式,即而引出了對parse的分析,今天我們接著這裡繼續挖程式碼. $watch續 先上一塊$watch程式碼 $watch: function(watchExp, listener, objectEquali

Spring原始碼分析3 — spring XML配置檔案的解析流程

1 介紹 建立並初始化spring容器中,關鍵一步就是讀取並解析spring XML配置檔案。這個過程比較複雜,本文將詳細分析整個流程。先看涉及到的關鍵類。 XmlWebApplicationContext:web應用的預設Spring容器 XmlBean

Redis網路庫原始碼分析(3)之ae.c

一、aeCreateEventLoop & aeCreateFileEvent 上一篇文章中,我們已經將伺服器啟動,只是其中有些細節我們跳過了,比如aeCreateEventLoop函式到底做了什麼? 接下來我們要分析ae.c檔案,它是整個Redis

Django原始碼分析3:處理請求wsgi分析與檢視View

django原始碼分析 本文環境python3.5.2,django1.10.x系列 根據前上一篇runserver的博文,已經分析了本地除錯伺服器的大致流程,現在我們來分析一下當runserver執行起來後,django框架是如何處理一個請求的,djan

malloc原始碼分析---3

malloc原始碼分析—_int_malloc 上一章分析了_int_malloc的前面一小部分,本章繼續往下看, _int_malloc — fastbin static void * _int_malloc(mstate av, size_t by

Monkey原始碼分析3—Monkey原始碼的整體設計結構

Monkey原始碼地址,點選檢視 Monkey自動化測試的本質就是隨機生成一個事件,然後向Android設備註入一個事件。通俗的來說就是,monkey隨機生成一個點選螢幕事件,然後選取Android螢幕的一個座標,對此座標進行點選操作。來實現自動化測試的。當然產生的事件不

Android 5.0 Camera系統原始碼分析(3):Camera預覽流程控制流

1. 前言 本文分析的是Android系統原始碼,從frameworks層到hal層,記錄了Camera進入預覽模式的重點程式碼,主要為控制流程的程式碼,有關影象buffer的傳遞暫不涉及,硬體平臺基於mt6735。由於某些函式比較複雜,在貼出程式碼時會適當對

JFinal個人學習筆記之原始碼分析3

上篇分析完了initActionMapping()的原始碼。JFinal原始碼裡初始化init方法還有: boolean init(JFinalConfig jfinalConfig, Serv