No LVGL, é inevitável o uso de uma lista encadeada: os objetos do grupo precisam ser armazenados com uma lista encadeada, para que o foco do objeto possa ser trocado; outro exemplo é o cronômetro dentro do LVGL, e vários cronômetros também são armazenados com uma lista encadeada. Este artigo analisará o código-fonte da lista encadeada em LVGL.
Diretório de artigos
1 Estrutura de lista encadeada
Para uma lista vinculada, deve haver um ponteiro inicial e um ponteiro final. Em LVGL, a estrutura de dados da lista vinculada é a seguinte:
/** Dummy type to make handling easier*/
typedef uint8_t lv_ll_node_t;
/** Description of a linked list*/
typedef struct {
uint32_t n_size;
lv_ll_node_t * head;
lv_ll_node_t * tail;
} lv_ll_t;
Pode-se ver que os ponteiros de cabeça e cauda realmente usam um uint8_t *
ponteiro para armazenar o endereço de determinados dados.
2 Insira a análise do código-fonte do elemento
Vamos pegar como exemplo a inserção de elementos no final da lista encadeada para analisar o código-fonte:
2.1 Função de inicialização
void _lv_ll_init(lv_ll_t * ll_p, uint32_t node_size)
{
ll_p->head = NULL;
ll_p->tail = NULL;
/*Round the size up to 4*/
node_size = (node_size + 3) & (~0x3);
ll_p->n_size = node_size;
}
A função de inicialização é inicializar um único node
tamanho na lista encadeada, e aqui o comprimento é alinhado a quatro bytes.
2.2 Inserindo elementos
_lv_ll_ins_head
Usado para inserir um nó na frente da lista vinculada e _lv_ll_ins_tail
usado para inserir um nó no final da lista vinculada. A implementação deles é basicamente a mesma, aqui está _lv_ll_ins_tail
um exemplo para análise.
#define LL_NODE_META_SIZE (sizeof(lv_ll_node_t *) + sizeof(lv_ll_node_t *))
void * _lv_ll_ins_tail(lv_ll_t * ll_p)
{
lv_ll_node_t * n_new;
n_new = lv_mem_alloc(ll_p->n_size + LL_NODE_META_SIZE);
if(n_new != NULL) {
node_set_next(ll_p, n_new, NULL); /*No next after the new tail*/
node_set_prev(ll_p, n_new, ll_p->tail); /*The prev. before new is the old tail*/
if(ll_p->tail != NULL) { /*If there is old tail then the new comes after it*/
node_set_next(ll_p, ll_p->tail, n_new);
}
ll_p->tail = n_new; /*Set the new tail in the dsc.*/
if(ll_p->head == NULL) { /*If there is no head (1. node) set the head too*/
ll_p->head = n_new;
}
}
return n_new;
}
A primeira é alocar uma memória com tamanho ll_p->n_size + LL_NODE_META_SIZE
de size, que é o tamanho de cada nó que acabamos de definir, e depois adicionar dois ponteiros para salvar o elemento anterior e o próximo elemento.
Em seguida, pegue node_set_prev
a função como exemplo para ver o que o código faz:
#define LL_PREV_P_OFFSET(ll_p) (ll_p->n_size)
static void node_set_prev(lv_ll_t * ll_p, lv_ll_node_t * act, lv_ll_node_t * prev)
{
if(act == NULL) return; /*Can't set the prev node of `NULL`*/
uint8_t * act8 = (uint8_t *)act;
act8 += LL_PREV_P_OFFSET(ll_p);
lv_ll_node_t ** act_node_p = (lv_ll_node_t **) act8;
lv_ll_node_t ** prev_node_p = (lv_ll_node_t **) &prev;
*act_node_p = *prev_node_p;
}
Em primeiro lugar act8 += LL_PREV_P_OFFSET(ll_p)
, é realmente a localização do act
ponteiro prev
e, em seguida, atribua o valor apontado pelo ponteiro ao ponteiro no parâmetro prev
. Para node_set_next
, a operação concluída é semelhante, que é alterar o valor do ponteiro act
em next
.
Portanto, para as duas linhas de código a seguir, o nó recém-criado prev
aponta para o último elemento da lista vinculada atual e apontará next
para NULL, para que um elemento seja inserido no final da lista vinculada.
node_set_next(ll_p, n_new, NULL);
node_set_prev(ll_p, n_new, ll_p->tail);
Continue analisando o código:
if(ll_p->tail != NULL) { /*If there is old tail then the new comes after it*/
node_set_next(ll_p, ll_p->tail, n_new);
}
ll_p->tail = n_new; /*Set the new tail in the dsc.*/
if(ll_p->head == NULL) { /*If there is no head (1. node) set the head too*/
ll_p->head = n_new;
}
Isso significa que, para ll_p
a estrutura, sabemos que apenas o tamanho de um único elemento é salvo na frente, assim como os ponteiros de cabeça e cauda. Portanto, primeiro julgue se o ponteiro da cauda da lista vinculada atual tem um valor e, em caso afirmativo, aponte-o next
para o nó recém-criado. Em seguida, atribua o ponteiro de cauda na lista encadeada como o endereço do novo nó. Se o cabeçalho da lista vinculada também estiver vazio, isso significa que a lista vinculada acabou de ser criada e o nó não é apenas o nó principal, mas também o nó final.
2.3 Uso de inserir elementos
Com a análise acima do código-fonte para inserir elementos no final da lista encadeada, vamos dar uma olhada em como a função é realmente usada _lv_ll_ins_tail
.
lv_obj_t ** next = _lv_ll_ins_tail(&ll);
LV_ASSERT_MALLOC(next);
*next = next_node;
No código-fonte anterior, sabemos que a memória do novo elemento inserido está _lv_ll_ins_tail
alocada em , então inserimos primeiro e depois julgamos que, se a alocação de memória for bem-sucedida, podemos atribuir o valor do ponteiro inserido ao final como nosso nó next_node
.
3 resumo
Na verdade, a implementação da lista encadeada em LVGL é semelhante à nossa estrutura de dados de lista encadeada esperada. A única diferença é que o tamanho de cada nó pode ser personalizado e, em seguida, os dados são salvos diretamente no nó em vez do ponteiro.Esta é também uma maneira de pensar. É claro que a operação da lista encadeada não se limita a inserir elementos no final, lv_ll.c
também existem funções no arquivo para obter o comprimento da lista encadeada e excluir nós. O objetivo deste artigo é entender a estrutura de dados da lista vinculada em LVGL e, a seguir, usar o exemplo de inserção de elementos no final para aprofundar o entendimento da lista vinculada implementada em LVGL.