LVGL雙向連結串列學習筆記

一蓑煙雨任平生&wf發表於2023-10-08

LVGL雙向連結串列學習筆記

1、LVGL連結串列資料型別分析

對於LVGL雙向連結串列的使用,我們需要關注lv_ll.h和lv_ll.c兩個檔案,其中lv_ll.h裡面包含了連結串列結構型別定義,以及相關API的宣告,首先介紹連結串列的結構類,如下圖所示:
image
一開始看到這個型別宣告我是懵的,怎麼連結串列的一個結點的型別是uint8_t,那是不是LVGL這個雙向連結串列只能用於uint8_t型別的資料?可是轉念一想LVGL內部的定時器、任務都是基於這個雙向連結串列實現的,肯定有我沒有理解到的地方,uint8_t是啥,不就是一個位元組嗎?在計算機記憶體中的基本單位,也是硬體所能訪問的最小單位,這裡我們可以聯想到任何資料型別都可以位元組進行訪問,那麼怎麼訪問呢?答案:指標。可以看出連結串列型別中頭尾指標都是lv_ll_node_t *也就是uint8_t *,這樣就可以透過head和tail對任意型別的結點進行訪問。

2、LVGL連結串列實現原理

上一節已經對LVGL雙向連結串列的資料型別進行分析,接下來開始分析其實現原理。

2.1、雙向連結串列初始化

雙向連結串列初始化_lv_ll_init()函式,其定義如下:
image
該函式主要用於初始化一個雙向連結串列,並透過傳入引數lv_ll_ *ll_p返回已經初始化的雙向連結串列控制程式碼,這裡要重點關注第二個引數node_size,顧名思義該參數列示的是結點所佔位元組的大小,但要特別說明一下這個node_size表示的只是結點的資料域大小,並沒有包含next、prv指標域,這一點在後面分析結點插入時會詳細說明。在函式內部對node_size進行了8位元組或4位元組的記憶體對齊,具體是8位元組對齊還是4位元組對齊跟具體的系統位數相關了(比如WIN32就是4位元組,WIN64就是8位元組)。

2.2、插入結點

透過分析LVGL插入一個結點我們才能真正理解其雙向連結串列的實現原理以及巧妙之處,這裡以尾插法的實現進行分析,即_lv_ll_ins_tail()函式,其定義如下圖所示:
image
① 建立一個新的結點
使用lv_mem_alloc()進行動態申請,注意這裡申請的記憶體大小是ll_p->n
_size + LL_NODE_META_SIZE,其中ll_p->n_size就是之前在初始化時傳入的node_size,那麼我們來看看LL_NODE_META_SIZE是多大,轉到定義可以看到:
image
哈哈,果然是兩個指標的大小,如果熟悉雙向連結串列馬上就可以推測出這兩個指標對應的就是next、prev指標,那麼我們可以得到下面的結點記憶體模型:
image
還有個問題就是為什麼可以確定prev在前,next在後呢?答案可以在下面這兩個宏定義中找到:
image
其中LL_PREV_P_OFFSET表示prev指標相對域結點首地址的偏移,同理LL_NEXT_P_OFFSET表示的是next指標相對於結點首地址的偏移。透過這個偏移地址應該可以很清楚的看出prev在next前面的位置吧。
然後我們可以得到如下的雙向連結串列模型:
image
② 設定新結點的next
使用node_set_next()函式設定結點的next,因為尾插法,所以新結點的next為空,這裡重點分析node_set_next()函式的實現,其定義如下所示:
image
可以看出該函式內部都是指標的操作,對於指標操作來說,使用記憶體變化來理解是最好不過了,該函式的記憶體變化過程如下:
image
最終記憶體0x00000024的值為NULL,這也符合我們的預期:尾插法新結點的next為NULL。這裡也值得思考一下:為什麼設定next指標為什麼需要如此複雜?因為LVGL雙向連結串列的結點資料域是由外部決定的,我們只能透過地址這個資訊來訪問,同樣的結點型別為uint8_t,我們也是隻能透過地址資訊來進行訪問,不透過結構體成員的方式來訪問資料域、prev指標以及next指標。同時函式內部中出現了兩個二級指標,他們的作用就是用來訪問地址,如果我們直接對傳入act、next這兩個一級指標進行操作,只能改這兩個指標變數儲存的地址值,並不能對傳入地址進行訪問,所以需要藉助二級指標來對傳入地址進行訪問。
③ 設定新結點的prev
使用node_set_prev()函式設定結點的next,同樣這裡重點分析node_set_prev()函式的實現,其定義如下所示:
image
可以看出node_set_prev()和node_set_next()內部實現幾乎是一模一樣的只是act8獲取的prev指標的偏移,就這個差異。其記憶體變化如下:
image
最終記憶體0x00000040(n_new的prev指標)的值為0x00000008(n_prev)。
④ 設定連結串列尾結點的next
⑤ 更新鏈尾為新結點
實際上LVGL實現原理的核心就是對node_set_next()和node_set_prev兩個函式的理解,掌握了這兩個函式的實現剩下的就是對雙向連結串列的理解了,相信學習過資料結構理解雙向連結串列應該是小菜一碟了吧。所以剩下的頭插法、刪除結點這些就不再贅述。

3、LVGL連結串列應用例項

點選檢視程式碼
#define STD_NAME_LEN_MAX  15

//學生資訊型別
typedef struct StudentInfo StudentInfo_t;
struct StudentInfo
{
    char name[STD_NAME_LEN_MAX];
    int age;
    int sex;
};

//學生資訊表
StudentInfo_t std_table[] = {
    {"ZhangSan",  23,  1},
    {"LiSi",  25,  0},
    {"WangWu",  26,  1},
};

void lv_ll_test(void)
{
    StudentInfo_t* std;
    lv_ll_t std_ll;

    _lv_ll_init(&std_ll, sizeof(StudentInfo_t)); // 初始化std_ll連結串列

    /* 遍歷學生資訊表,將學生資訊新增到std_ll中 */
    for (int i = 0; i < (sizeof(std_table) / sizeof(std_table[0])); i++) 
    {
        std = (StudentInfo_t*)_lv_ll_ins_tail(&std_ll);
        lv_snprintf(std->name, sizeof(std->name), std_table[i].name);
        std->age = std_table[i].age;
        std->sex = std_table[i].sex;
    }

    /* 遍歷std_ll,驗證學生資訊是否正確新增到std_ll中 */
    std = (StudentInfo_t*)_lv_ll_get_head(&std_ll);
    while (std)
    {
        printf("name:%s  age:%d  sex:%d \n", std->name, std->age, std->sex);
        std = (StudentInfo_t*)_lv_ll_get_next(&std_ll, std);
    }

}

相關文章