Widget, Elemento, BuildContext e RenderObject
Ferramenta
Widget
O relacionamento de herança das classes-chave e suas subclasses é mostrado na figura:
Dentre elas, Widget
está Widget Tree
a classe base de todos os nós. Widget
As subclasses são divididas principalmente em 3 categorias:
-
A primeira categoria é
RenderObjectWidget
a subcategoria de , especificamente, é dividida emSingleChildRenderObjectWidget
(contêiner de nó filho único),LeafRenderObjectWidget
(nó folha),MultiChildRenderObjectWidget
(contêiner de nó multifilho) e sua característica comum é que todos eles correspondem a umaRenderObject
subcategoria de , que pode realizar lógica comoLayout
,Paint
-
A segunda categoria é
StatelessWidget
eStatefulWidget
, que são os mais usados pelos desenvolvedoresWidget
.Eles não têm a capacidade de se desenhar (ou seja, não se correspondemRender Object
), mas podem ser organizados eRenderObjectWidget
configuradosWidget
. -
A categoria 3 é
ProxyWidget
, especificamente, subdividida emParentDataWidget
eInheritedWidget
, que se caracterizam por fornecer dados adicionais para seus nós filhos.
Elemento
Element
A classe chave e seu relacionamento de herança de subclasse são mostrados na figura:
A partir do relacionamento de herança que pode ser visto claramente na Figura 5-2 Element
, ele implementa BuildContext
a interface. A Figura 5-2 corresponde à Figura 5-1, e cada uma Element
tem um correspondente Widget
. Element
Existem duas subclasses diretas ComponentElement
e RenderObjectElement
, das quais ComponentElement
as duas subclasses StatelessElement
e StatefulElement
correspondem a StatelessWidget
e respectivamente StatefulWidget
.
Sabemos que a árvore de IU final é, na verdade, composta de Element
nós individuais. O layout final e a renderização do componente são todos RenderObject
feitos através do passe. O processo geral desde a criação até a renderização é: de acordo com Widget
a geração Element
, crie o correspondente RenderObject
e associe-o ao Element.renderObject
atributo e, finalmente, RenderObject
conclua o arranjo do layout e o desenho através passar.
Element
É Widget
um objeto instanciado em um local específico na árvore de interface do usuário, a maioria dos quais Element
são exclusivos renderObject
, mas alguns Element
têm vários nós filhos, como RenderObjectElement
algumas classes herdadas, por exemplo MultiChildRenderObjectElement
. No final, todas Element
elas RenderObject
constituem uma árvore, que chamamos de “ Render Tree ” ou “ render tree ”.
Resumindo, podemos pensar que o sistema de IU do Flutter contém três árvores: Widget tree , Element tree e rendering tree . Suas dependências são: a árvore Element é gerada com base na árvore Widget, e a árvore de renderização depende da árvore Element , conforme mostrado na figura.
Agora vamos nos concentrar Element
no Element
ciclo de vida do seguinte:
-
Chamadas de estrutura para
Widget.createElement
criar umaElement
instância, denotadas comoelement
-
Framework call
element.mount(parentElement,newSlot)
,mount
no método, primeiro chame o métodoelement
correspondente para criar o objeto associado a ele e, em seguida, chame o método para adicioná-lo à posição especificada pelo slot na árvore de renderização (essa etapa não é necessária, geralmente precisa ser recriado quando a estrutura da árvore for alterada Adicionar a). Após ser inserido na árvore de renderização, fica no estado " ", podendo ser exibido na tela (podendo ser ocultado) após estar no estado " ".Widget
createRenderObject
element
RenderObject
element.attachRenderObject
element.renderObject
Element
element
active
active
-
Quando os dados de configuração do pai
Widget
mudam e a estruturaState.build
retornadaWidget
é diferente da anterior, aElement
árvore correspondente precisa ser reconstruída. ParaElement
reutilizar,Element
antes de reconstruir, ele tentará reutilizar a mesma posição na árvore antigaelement
. O nó chamará seu método correspondenteelement
antes de atualizar. Se retornar , o antigo será reutilizado e o antigo será atualizado com os novos dados de configuração. , caso contrário, um novo será criado .Widget
canUpdate
true
Element
Element
Widget
Element
Widget.canUpdate
O principal é julgarnewWidget
se a somaoldWidget
deruntimeType
ekey
é igual ao mesmo tempo, e retornar se forem iguais ao mesmo tempotrue
, caso contrário retornaráfalse
. De acordo com este princípio, quando precisamos forçar uma atualização , podemos evitar a multiplexaçãoWidget
especificando uma diferente .Key
-
Quando um ancestral
Element
decide removerelement
(por exemplo,Widget
a estrutura da árvore foi alterada, resultando na remoçãoelement
do correspondente ), o ancestral chamará o método para removê-lo e também será removido da árvore de renderização após a remoção o método e o status mudará para " ".Widget
Element
deactivateChild
element.renderObject
element.deactivate
element
inactive
-
O status "
inactive
"element
não será exibido na tela novamente. Para evitar criar e remover repetidamente um objeto específico durante a execução de uma animaçãoelement
, oinactive
estado " " será mantido até o final do último quadro da animação atual. Caso não tenha retornado ao estado " "element
após o término da execução da animação , Frameworkactive
Seu método será chamadounmount
para removê-lo completamente.Neste momento ,element
o estado é quedefunct
ele nunca mais será inserido na árvore. -
Se
element
você deseja reinserir emElement
outro local da árvore, como o ancestralelement
ouelement
possui umGlobalKey
(para elementos de reutilização global), o Framework primeiroelement
o removerá do local existente, chamará seuactivate
método erenderObject
reinsiraattach
a renderização árvore .
Resumir:
- Um objeto Element inicializará o estado quando for criado
initial
emount
se tornará um estado após ser adicionado à Árvore de Elementos por meio de um métodoactive
; quando o Widget correspondente ao nó falhar, ele entrarádeactivate
noinactive
estado por meio de um método.Element
Se outros nós tiveremkey
reutilizado o nó durante o processo de construção do quadro atual, oactivate
método será usado para fazer o nó entraractive
no estado novamente; se o nó ainda não estiver na árvore de elementos após o final do quadro atual, ele seráunmount
ser desinstalado através do método e entrardefunct
no estado, esperando que a lógica subseqüente seja destruída .
Depois de ler Element
o ciclo de vida, algumas pessoas podem ter dúvidas, o desenvolvedor irá operar diretamente a árvore Element?
Na verdade, para os desenvolvedores, na maioria dos casos, eles só precisam prestar atenção Widget
à árvore. O framework Flutter mapeou as operações na árvore Widget para Element
a árvore, o que pode reduzir muito a complexidade e melhorar a eficiência do desenvolvimento.
Mas a compreensão Element
é crucial para entender toda a estrutura de interface do usuário do Flutter. É por meio Element
desse link que o Flutter se conecta Widget
cada vez RenderObject
mais. Compreender a camada Element não apenas ajudará os desenvolvedores a ter uma compreensão clara da estrutura de interface do usuário do Flutter, mas também melhorará sua própria abstração. recursos e capacidades de projeto. Além disso, às vezes, temos que usar diretamente o objeto Element para concluir algumas operações, como obter dados do tema.
BuildContext
Já sabemos que os métodos de StatelessWidget
e passarão um objeto:StatefulWidget
build
BuildContext
Widget build(BuildContext context) {
}
Também sabemos que em muitos casos precisamos usar isso context
para fazer algo, como:
Theme.of(context) // 获取主题
Navigator.push(context, route) // 入栈新路由
Localizations.of(context, type) // 获取Local
context.size // 获取上下文大小
context.findRenderObject() // 查找当前或最近的一个祖先RenderObject
Então, BuildContext
o que é isso? Verifique sua definição e descubra que é uma classe de interface abstrata:
abstract class BuildContext {
...
}
context
Quem é a classe de implementação correspondente a este objeto? Seguimos o vine e descobrimos build
que a chamada ocorria no StatelessWidget
método StatefulWidget
correspondente StatelessElement
, StatefulElement
por build
exemplo em StatelessElement
:
class StatelessElement extends ComponentElement {
...
Widget build() => widget.build(this);
...
}
Também em StatefulElement
:
class StatefulElement extends ComponentElement {
...
Widget build() => state.build(this);
...
}
Verifica-se que build
os parâmetros passados são this
, obviamente! Isto BuildContext
é StatelessElement
ou StatefulElement
em si. Mas StatelessElement
e StatefulElement
ele mesmo não implementou BuildContext
a interface. Continue a rastrear o código e descubra que eles herdam indiretamente da Element
classe. Em seguida, verifique Element
a definição da classe e descubra Element
que a classe realmente implementa BuildContext
a interface:
abstract class ComponentElement extends Element {
...}
abstract class Element extends DiagnosticableTree implements BuildContext {
...}
Até agora a verdade é clara, BuildContext
é widget
correspondente Element
, então podemos acessar diretamente o objeto no context
método de StatelessWidget
e . Dentro do código onde obtemos os dados do assunto está o método chamado .StatefulWidget
build
Element
Theme.of(context)
Element
dependOnInheritedWidgetOfExactType()
Resumo: BuildContext
É Element
a divindade, BuildContext
a chamada do método é a operação Element
, Widget
é o casaco e Element
é o corpo nu sob o casaco.
Outro significado de ConstruirContext
Outra implicação é BuildContext
que é uma referência a uma posição Widget
na Widget
árvore e contém informações sobre a posição na árvore Widget
,Widget
não sobre Widget
si mesma.
No caso dos tópicos, como cada um Widget
tem o seu BuildContext
, isso significa que se você tiver vários tópicos espalhados na árvore, obter um Widget
tópico para um pode retornar um Widget
resultado diferente do outro. No caso específico do tópico no programa de exemplo do aplicativo contador, ou em outros of
métodos, você obterá o nó pai mais próximo desse tipo na árvore .
Avançado
Podemos ver que é o elo entre a conexão interna e Element
o framework Flutter UI . Na maioria das vezes, os desenvolvedores só precisam prestar atenção na camada, mas às vezes a camada não consegue blindar completamente os detalhes, então o Framework passa o objeto para os Desenvolvedores, desta forma, os desenvolvedores podem manipular objetos diretamente quando necessário.widget
RenderObject
widget
widget
Element
StatelessWidget
StatefulWidget
build
Element
Element
Então agora ficam duas perguntas:
1. Se não houver camada de widget, uma estrutura de interface do usuário utilizável pode ser criada apenas com a camada de elemento? Se sim, como deve ser?
2. O framework Flutter UI pode não ser responsivo?
Para a pergunta 1, a resposta é claro que sim, porque dissemos antes que widget
a árvore é apenas Element
o mapeamento da árvore, ela fornece apenas informações de configuração que descrevem a árvore da interface do usuário, Widget
ou seja, o casaco, é claro que uma pessoa pode viver com vergonha sem usar roupas, mas usar roupas viverá uma vida mais decente, mesmo que Widget
não dependamos disso, podemos Element
construir uma estrutura de interface do usuário completamente por meio dela.
Aqui está um exemplo:
Element
Simulamos uma função puramente StatefulWidget
. Suponha que haja uma página com um botão. O texto do botão é um número de 9 dígitos. Clique no botão uma vez para organizar aleatoriamente os 9 números. O código é o seguinte:
class HomeView extends ComponentElement{
HomeView(Widget widget) : super(widget);
String text = "123456789";
Widget build() {
Color primary = Theme.of(this).primaryColor; //1
return GestureDetector(
child: Center(
child: TextButton(
child: Text(text, style: TextStyle(color: primary),),
onPressed: () {
var t = text.split("")..shuffle();
text = t.join();
markNeedsBuild(); //点击后将该Element标记为dirty,Element将会rebuild
},
),
),
);
}
}
-
O
build
método acima não recebe parâmetros, o que é diferente do método inStatelessWidget
eStatefulWidget
inbuild(BuildContext)
. Os locais que precisam ser usados no código podem ser substituídosBuildContext
diretamente por .Por exemplo, os parâmetros no comentário de código 1 podem ser passados diretamente , porque o próprio objeto atual é uma instância.this
Theme.of(this)
this
Element
-
Quando
text
ocorre uma alteração, chamamosmarkNeedsBuild()
o métodoElement
para marcar o atualdirty
, e o marcadodirty
seráElement
reconstruído no próximo quadro. Na verdade,State.setState()
também é o método chamado internamentemarkNeedsBuild()
. -
O método no código acima
build
ainda retorna umwidget
. Isso ocorre porque já existewidget
essa camada na estrutura do Flutter e a biblioteca de componentes já éwidget
fornecida na forma de . Se todos os componentes na estrutura do Flutter forem fornecidosHomeView
na forma do exemploElement
, então você pode usar pureElement
para construir a IU.HomeView
O tipo de valor de retorno dobuild
método pode serElement
.
Se precisarmos executar o código acima no framework Flutter existente, ainda teremos que fornecer um "adaptador" que widget
será HomeView
integrado ao framework existente. O seguinte CustomHome
é equivalente ao "adaptador":
class CustomHome extends Widget {
Element createElement() {
return HomeView(this);
}
}
Agora você pode CustomHome
adicioná-lo à widget
árvore, criamos em uma nova página de roteamento, o efeito final é o mostrado na figura a seguir:
Clique no botão e o texto do botão será classificado aleatoriamente.
Para a pergunta 2, a resposta é sim. A API fornecida pelo mecanismo Flutter é original e independente. Isso é semelhante à API fornecida pelo sistema operacional. O design da estrutura superior da interface do usuário depende inteiramente do designer. A interface do usuário framework pode ser projetado no estilo Android ou no estilo iOS, mas essas coisas o Google não fará mais. Então, em teoria, podemos fazer isso, mas não é necessário. Isso ocorre porque a ideia de capacidade de resposta em si é ótima. A razão pela qual essa questão é levantada é porque uma coisa é fazer ou não fazer, mas é outra saber se isso pode ser feito, pode refletir o grau de nossa compreensão do conhecimento.
RenderObject
Dissemos que cada um Element
corresponde a um RenderObject
, e podemos Element.renderObject
passar por isso. E também dissemos RenderObject
que as principais responsabilidades são o layout e o desenho, que RenderObject
formarão uma árvore de renderização Render Tree. A seguir, o foco será RenderObject
o papel.
RenderObject
É um objeto na árvore de renderização, cuja principal função é implementar a resposta do eventobuild
e o processo de execução, exceto no pipeline de renderização ( build
o processo é element
realizado por ), incluindo: layout, desenho, composição de camadas e na tela .
RenderObject
A classe-chave e suas subclasses são mostradas na Figura 5-3, e cada uma de suas subclasses corresponde a um RenderObjectWidget
tipo de Widget
nó.
RenderView
É especialRenderObject
, é o nó raiz de toda a árvore de renderização .- Outra coisa especial
RenderObject
éRenderAbstractViewport
que é uma classe abstrata.RenderViewport
implementa sua interface e herda indiretamente deRenderBox
. RenderBox
eRenderSliver
são os mais comuns no FlutterRenderObject
,RenderBox
responsáveis pela disposição geral das linhas, colunas, etc., eRenderSliver
responsáveis pela disposição de cada uma na listaItem
.
RenderObject
Ele tem um parent
e um parentData
atributos, parent
apontando para seu próprio nó pai na árvore de renderização, parentData
mas uma variável reservada. Durante o processo de layout do componente pai, ele determinará as informações de layout de todos os seus componentes filhos (como informações de posição, que é, o deslocamento relativo ao componente pai ), e essas informações de layout precisam ser salvas na fase de layout, porque as informações de layout precisam ser usadas na fase de desenho subsequente (para determinar a posição de desenho do componente) e o A principal função do atributo é salvar as informações de layout, como no parentData
layout Stack
. RenderStack
Os dados de deslocamento do elemento filho serão armazenados no elemento filho parentData
(consulte Positioned
a implementação para obter detalhes).
Pergunta: Agora que existem RenderObject
, por que o framework Flutter fornece RenderBox
e RenderSliver
duas subclasses?
-
Isso ocorre porque
RenderObject
a própria classe implementa um conjunto de layout básico e protocolos de desenho, mas não define o modelo do nó filho (por exemplo, quantos nós filho um nó pode ter?), nem define o sistema de coordenadas (por exemplo , o posicionamento do nó filho está na flauta, coordenadas Carl ou coordenadas polares?) e protocolo de layout específico (seja por largura e altura ou por restrição e tamanho? layout do nó filho, etc.). -
Para tanto, o framework Flutter fornece uma
RenderBox
e umaRenderSliver
classe, todos herdados deRenderObject
, o sistema de coordenadas de layout adota o sistema de coordenadas cartesianas e a tela(top, left)
é a origem. Com base nessas duas classes, o Flutter implementa o layout do modelo de caixaRenderBox
baseado em e o modelo de carregamento sob demanda baseado em .Sliver
Inicie o processo (processo de construção do nó raiz)
O Flutter Engine é baseado no ambiente operacional Dart, ou seja, Dart Runtime. O processo principal para iniciar o Dart Runtime é o seguinte:
Entre eles, o Dart Runtime primeiro criará e iniciará DartVM
uma máquina virtual e, DartVM
após a inicialização, inicializará uma DartIsolate
e, em seguida, iniciará e, ao final do processo de inicialização, executará o método de entrada DartIsolate
do aplicativo Dart . Ou seja, a função de main
" " em nosso desenvolvimento diário :lib/main.dart
main()
void main() => runApp(MyApp());
Você pode ver main()
que a função chama apenas um runApp()
método, vamos ver runApp()
o que é feito no método:
void runApp(Widget app) {
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame();
}
O parâmetro aqui app
é um widget
, que é o Widget passado para o framework Flutter por nossos desenvolvedores. É o primeiro componente a ser exibido após o início do aplicativo Flutter, e é a WidgetsFlutterBinding
ponte que liga widget
o framework e o motor Flutter. É definido do seguinte modo:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding._instance == null) {
WidgetsFlutterBinding();
}
return WidgetsBinding.instance;
}
}
Primeiro, observe WidgetsFlutterBinding
o relacionamento de herança de , descobrimos que WidgetsFlutterBinding
ele herda BindingBase
e se mistura em muitas Binding
classes; portanto, quando iniciar, acionará os construtores dessas classes na ordem do mixin.
GestureBinding
: Responsável pelo processamento de gestos, fornecewindow.onPointerDataPacket
retornos de chamada, liga o subsistema de gestos do Framework e é a entrada de ligação para o modelo de evento do Framework e eventos subjacentes.ServicesBinding
: Responsável por fornecer recursos relacionados à plataforma, fornecendowindow.onPlatformMessage
callbacks para vincular os canais de mensagem da plataforma (canais de mensagem), lidando principalmente com comunicações nativas e Flutter.SchedulerBinding
: Responsável pelo gerenciamento de vários callbacks no processo de renderização, fornecendowindow.onBeginFrame
ewindow.onDrawFrame
callbacks, monitorando eventos de atualização e vinculando o subsistema de agendamento de desenho do Framework.PaintingBinding
: Responsável pela lógica relacionada ao desenho, vinculando a biblioteca de desenhos, usada principalmente para processar o cache de imagens.SemanticsBinding
: Responsável por fornecer acessibilidade, a ponte entre a camada semântica e o mecanismo Flutter, principalmente o suporte subjacente para funções auxiliares.RendererBinding
: Responsável pela renderização final da Render Tree, mantendoPipelineOwner
objetos e fornecendo callbacks comowindow.onMetricsChanged
, etc.window.onTextScaleFactorChanged
É uma ponte entre a árvore de renderização e o motor Flutter.WidgetsBinding
: Responsável pelo gerenciamento das três árvores do Flutter, guardaBuilderOwner
objetos e fornece callbacks comowindow.onLocaleChanged
, etc.onBuildScheduled
É a ponte entre a camada do widget Flutter e o mecanismo.
Antes de entendermos por que eles estão misturados, Binding
vamos apresentá-lo primeiro Window
, Window
que é a interface que o Flutter Framework conecta ao sistema operacional host. Vamos dar uma olhada em Window
parte da definição da classe:
class Window {
// 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
// DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5
double get devicePixelRatio => _devicePixelRatio;
// Flutter UI绘制区域的大小
Size get physicalSize => _physicalSize;
// 当前系统默认的语言Locale
Locale get locale;
// 当前系统字体缩放比例。
double get textScaleFactor => _textScaleFactor;
// 当绘制区域大小改变回调
VoidCallback get onMetricsChanged => _onMetricsChanged;
// Locale发生变化回调
VoidCallback get onLocaleChanged => _onLocaleChanged;
// 系统字体缩放变化回调
VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
// 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
FrameCallback get onBeginFrame => _onBeginFrame;
// 绘制回调
VoidCallback get onDrawFrame => _onDrawFrame;
// 点击或指针事件回调
PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
// 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
// 此方法会直接调用Flutter engine的Window_scheduleFrame方法
void scheduleFrame() native 'Window_scheduleFrame';
// 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
void render(Scene scene) native 'Window_render';
// 发送平台消息
void sendPlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) ;
// 平台通道消息处理回调
PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
... //其他属性及回调
}
Você pode ver que Window
a classe contém algumas informações sobre o dispositivo e o sistema atuais, bem como alguns retornos de chamada do Flutter Engine.
Agora vamos voltar e olhar para WidgetsFlutterBinding
as várias misturas Binding
. Olhando para esses Binding
códigos-fonte, podemos descobrir que eles Binding
estão basicamente ouvindo e processando Window
alguns eventos de objetos e, em seguida, empacotando, abstraindo e distribuindo esses eventos de acordo com o modelo do Framework. Pode-se ver WidgetsFlutterBinding
que é a "cola" que une o Flutter Engine e o Framework superior. WidgetsFlutterBinding
A essência de é one WidgetsBinding
, e não possui lógica especial em si, portanto binding
, ganha recursos adicionais misturando essas classes.
O método WidgetsFlutterBinding.ensureInitialized()
é o principal responsável por inicializar um WidgetsBinding
singleton global e retornar WidgetsBinding
o objeto singleton, exceto que ele não faz mais nada. Isso também mostra que é apenas um adesivo sobre os ombros de todos.
Voltando ao runApp
método, WidgetsBinding
após obter o objeto singleton, o método será chamado imediatamente WidgetsBinding
e o método scheduleAttachRootWidget
é chamado nele attachRootWidget
, o código é o seguinte:
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget); }); // 注意,不是立即执行
}
void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = rootElement == null;
_readyToProduceFrames = true; // 开始生成 Element Tree
_rootElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView, // Render Tree的根节点
debugShortDescription: '[root]',
child: rootWidget, // 开发者通过runApp传入Widget Tree的根节点
).attachToRenderTree(buildOwner!, rootElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance.ensureVisualUpdate(); // 请求渲染
}
}
A lógica acima é o ponto de entrada para dirigir Element Tree
e Render Tree
criar. Deve-se notar que attachRootWidget
ela é iniciada iniciando Timer.run
. Isso é para garantir que toda a lógica esteja sob o gerenciamento do loop de mensagem.
attachRootWidget
O método é o principal responsável por Widget
adicionar a raiz ao RenderView
topo. Note que existem duas variáveis no código, renderView
uma renderViewElement
, renderView
que RenderObject
é a raiz da árvore de renderização, renderViewElement
mas renderView
o Element
objeto correspondente. Pode-se ver que este método completa principalmente todo o associação de raiz depois para o processo raizwidget
para raiz eRenderObject
Element
attachToRenderTree
O método conduzirá Element Tree
a compilação e retornará seu nó raiz. A implementação do código-fonte é a seguinte:
RenderObjectToWidgetElement<T> attachToRenderTree(
BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
if (element == null) {
// 首帧构建,element参数为空
owner.lockState(() {
element = createElement(); // 创建Widget对应的Element
element!.assignOwner(owner); // 绑定BuildOwner
});
owner.buildScope(element!, () {
// 开始子节点的解析与挂载
element!.mount(null, null);
});
} else {
// 如热重载等场景
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}
Este método é responsável por criar a raiz element
, ou seja RenderObjectToWidgetElement
, e será associado element
a , ou seja, criar uma árvore correspondente à árvore. Caso já tenha sido criado, defina o associado na raiz para um novo, assim fica visível que só será criado uma vez e será reutilizado posteriormente. Como o parâmetro do primeiro quadro é , a criação é concluída primeiro por meio do método e, em seguida, vinculada à instância de , então o que é isso? Na verdade, é a classe de gerenciamento do framework que rastreia o que precisa ser reconstruído. Este objeto conduzirá atualizações mais tarde .widget
widget
element
element
element
widget
element
element
null
createElement
BuildOwner
BuildOwner
widget
widget
Element Tree
Após a conclusão da construção das três árvores, a lógica attachRootWidget
in será acionada ensureVisualUpdate
:
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle: // 闲置阶段,没有需要渲染的帧
// 计算注册到本次帧渲染的一次性高优先级回调,通常是与动画相关的计算
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks: // 处理Dart中的微任务
// 计算待渲染帧的数据,包括Build、Layout、Paint等流程,这部分内容后面将详细介绍
case SchedulerPhase.midFrameMicrotasks:
// 帧渲染的逻辑结束,处理注册到本次帧渲染的一次性低优先级回调
case SchedulerPhase.persistentCallbacks:
return;
}
}
A lógica acima julgará se a renderização de um quadro precisa ser iniciada de acordo com o estágio atual. A transição de estado de cada estágio é mostrada na Figura 5-8.
Na Figura 5-8, primeiro, se não houver externo (como setState
método) e interno (como pulsação de animação, ouvinte para conclusão do carregamento da imagem), o Framework estará no idle
estado padrão. Se houver uma nova solicitação de dados de quadro para renderização, o Framework entrará handleBeginFrame
no estado no método conduzido pelo Engine transientCallbacks
, principalmente para lidar com callbacks únicos de alta prioridade, como cálculos de animação. Depois que a lógica acima for concluída, o Framework atualizará seu status para midFrameMicrotasks
, e o processamento de microtarefa específico será conduzido pelo Engine. Em segundo lugar, o Engine chamará handleDrawFrame
o método e o Framework atualizará o estado neste momento persistentCallbacks
para indicar que processará a lógica que deve ser executada para cada quadro, principalmente relacionada ao pipeline de renderização. Depois de concluir a lógica relacionada ao pipeline de renderização no Framework, o Framework atualizará seu próprio estado postFrameCallbacks
e manipulará retornos de chamada únicos de baixa prioridade (geralmente registrados por desenvolvedores ou lógica de nível superior). Por fim, o Framework redefine o estado para idle
. idle
É o estado final do Framework, e um loop de estado só será iniciado quando a renderização de um quadro for necessária.
scheduleFrame
A lógica do método é a seguinte, ele irá iniciar uma requisição através da interface para renderizar quando platformDispatcher.scheduleFrame
o próximo sinal chegar.Vsync
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) return;
ensureFrameCallbacksRegistered();
platformDispatcher.scheduleFrame();
_hasScheduledFrame = true;
}
Voltando runApp
à implementação, após a construção da árvore de componentes (build), quando a chamada attachRootWidget
for concluída , o último passo chamará o método WidgetsFlutterBinding
de instância scheduleWarmUpFrame()
, que está implementado em SchedulerBinding
, e será desenhado imediatamente após ser chamado. Antes do desenho terminar , esse método bloqueará a distribuição de eventos, ou seja, o Flutter não responderá a vários eventos até que o desenho seja concluído, o que pode garantir que nenhum novo redesenho seja acionado durante o processo de desenho. scheduleWarmUpFrame
O código do método é o seguinte:
// flutter/packages/flutter/lib/src/scheduler/binding.dart
void scheduleWarmUpFrame() {
if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle) return; // 已发送帧渲染请求
_warmUpFrame = true;
Timeline.startSync('Warm-up frame');
final bool hadScheduledFrame = _hasScheduledFrame;
Timer.run(() {
// 第1步,动画等相关逻辑
handleBeginFrame(null);
});
Timer.run(() {
// 第2步,立即渲染一帧(通常是首帧)
handleDrawFrame();
resetEpoch();
_warmUpFrame = false; // 首帧渲染完成
if (hadScheduledFrame) scheduleFrame();
});
lockEvents(() async {
// 第3步,首帧渲染前不消费手势
await endOfFrame;
Timeline.finishSync();
});
}
A lógica acima é dividida principalmente em 3 etapas, mas vale ressaltar que a terceira etapa é executada primeiro, pois as duas primeiras etapas são Timer.run
iniciadas no método. handleBeginFrame
O método acionará a lógica relacionada à animação e handleDrawFrame
acionará a atualização das 3 árvores e a lógica de renderização, como Render Tree
Layout e Paint . Normalmente, essas duas lógicas são conduzidas pelo mecanismo ouvindo o sinal Vsync . O motivo da execução direta aqui é garantir que o primeiro quadro seja renderizado o mais rápido possível, porque não importa quando o sinal Vsync chegar, o primeiro quadro deve ser renderizado.
Resumir
pipeline de renderização
Na análise anterior , após a runApp
execução do processo de inicialização acionado pelo método , dois métodos serão acionados, o primeiro é responsável pela geração da Render Tree, e o segundo é responsável pelo acionamento do primeiro frame de renderização .ensureInitialized
scheduleAttachRootWidget
scheduleWarmUpFrame
1. Moldura
Um processo de desenho, chamamos de frame (quadro). O que dissemos antes que o Flutter pode atingir 60fps (Frame Per-Second) significa que ele pode disparar até 60 redesenhos por segundo. Quanto maior o valor do FPS, mais suave será a interface. O que precisa ser explicado aqui é que o conceito de frame no Flutter não é equivalente ao frame de atualização de tela (frame), pois o frame do framework Flutter UI não é acionado toda vez que a tela é atualizada, pois se a UI não mudar por um período de tempo, então a cada É desnecessário passar pelo processo de renderização toda vez que a tela é atualizada. Portanto, o Flutter usará um quadro de solicitação ativo após o primeiro quadro ser renderizado para perceber que o processo de renderização será executado novamente somente quando a interface do usuário pode mudar.
onBeginFrame
O Flutter registra um e umonDrawFrame
retorno de chamada na janela ,onDrawFrame
que eventualmente será chamado no retorno de chamadadrawFrame
.- Após chamarmos o método, o motor Flutter irá chamar e
window.scheduleFrame()
no momento certo (pode ser considerado como antes da próxima atualização da tela, dependendo da implementação do motor Flutter) .onBeginFrame
onDrawFrame
Pode-se ver que apenas as chamadas ativas scheduleFrame()
serão executadas drawFrame
. Portanto, quando mencionamos em Flutter frame
, salvo indicação em contrário, drawFrame()
corresponde à chamada de , não à taxa de atualização da tela.
2. Flutuação do processo de agendamento SchedulerPhase
O processo de execução do aplicativo Flutter é simplesmente dividido em dois estados idle
e . O estado significa que não há processamento. Se o estado do aplicativo mudar e a interface do usuário precisar ser atualizada, você precisará solicitar um novo . Quando ele chegar, ele entra no estado. Todo o ciclo de vida do aplicativo Flutter é Alterna entre .frame
idle
frame
scheduleFrame()
frame
frame
frame
idle
frame
fluxo de processamento de quadro
Quando chega um novo frame
, o processo específico é executar as quatro filas de tarefas em sequência: transientCallbacks、midFrameMicrotasks、persistentCallbacks、postFrameCallbacks
, quando as quatro filas de tarefas são executadas, a corrente frame
termina. Em resumo, o Flutter divide todo o ciclo de vida em cinco estados e SchedulerPhase
os expressa por meio de classes de enumeração:
enum SchedulerPhase {
/// 空闲状态,并没有 frame 在处理。这种状态代表页面未发生变化,并不需要重新渲染。
/// 如果页面发生变化,需要调用`scheduleFrame()`来请求 frame。
/// 注意,空闲状态只是指没有 frame 在处理,通常微任务、定时器回调或者用户事件回调都
/// 可能被执行,比如监听了tap事件,用户点击后我们 onTap 回调就是在idle阶段被执行的。
idle,
/// 执行”临时“回调任务,”临时“回调任务只能被执行一次,执行后会被移出”临时“任务队列。
/// 典型的代表就是动画回调会在该阶段执行。
transientCallbacks,
/// 在执行临时任务时可能会产生一些新的微任务,比如在执行第一个临时任务时创建了一个
/// Future,且这个 Future 在所有临时任务执行完毕前就已经 resolve 了,这中情况
/// Future 的回调将在[midFrameMicrotasks]阶段执行
midFrameMicrotasks,
/// 执行一些持久的任务(每一个frame都要执行的任务),比如渲染管线(构建、布局、绘制)
/// 就是在该任务队列中执行的.
persistentCallbacks,
/// 在当前 frame 在结束之前将会执行 postFrameCallbacks,通常进行一些清理工作和
/// 请求新的 frame。
postFrameCallbacks,
}
3. Pipeline de renderização
Quando um new frame
chega, o método WidgetsBinding
de é chamado drawFrame()
, vamos dar uma olhada em sua implementação:
void drawFrame() {
...//省略无关代码
try {
buildOwner.buildScope(renderViewElement); // 先执行构建
super.drawFrame(); //然后调用父类的 drawFrame 方法
}
}
Na verdade, o código-chave é de apenas duas linhas: primeiro reconstrua ( build
) e depois chame drawFrame
o método da classe pai. Depois expandimos drawFrame
o método da classe pai:
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
//下面是 展开 super.drawFrame() 方法
pipelineOwner.flushLayout(); // 2.更新布局
pipelineOwner.flushCompositingBits(); //3.更新“层合成”信息
pipelineOwner.flushPaint(); // 4.重绘
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
...
}
}
Você pode ver que há principalmente 5 coisas feitas:
- Recrie a árvore de widgets.
- Atualize o layout.
- Atualize as informações da composição da camada.
- redesenhar.
- Tela superior: exibe o produto desenhado na tela.
Chamamos as 5 etapas acima rendering pipeline
, a tradução chinesa é "pipeline de renderização" ou "pipeline de renderização".
Qualquer estrutura de interface do usuário, seja Web ou Android, terá seu próprio pipeline de renderização. O pipeline de renderização é o núcleo da estrutura de interface do usuário, responsável por processar a entrada do usuário, gerar a descrição da interface do usuário, rasterizar as instruções de desenho e os dados finais na tela. Flutter não é exceção. Devido ao método de auto-renderização, o pipeline de renderização do Flutter é independente da plataforma . Tomando o Android como exemplo, o Flutter apenas Embedder
obtém um Surface
ou Texture
serve como o destino de saída final de seu próprio pipeline de renderização.
O pipeline de renderização do Flutter precisa ser conduzido pelo sinal Vsync do sistema. Quando a interface do usuário precisar ser atualizada, o Framework notificará o Engine , e o Engine aguardará até que o próximo Vsync
sinal chegue e, em seguida, notificará o Framework para animar, construir, layout, paint e, finalmente, gerar layer
Submit to Engine . A Engine irá layer
combinar para gerar as texturas, e por fim enviar os dados para a GPU através da interface Open GL , e a GPU irá exibi-los no monitor após o processamento, conforme a figura abaixo:
Especificamente, o pipeline de renderização do Flutter é dividido nas sete etapas a seguir.
-
(1) Entrada do usuário (Entrada do usuário) : responda ao comportamento do gesto gerado pelo usuário por meio do mouse, teclado, tela sensível ao toque e outros dispositivos.
-
(2) Animação (Animação) : Atualize os dados do quadro atual com base no cronômetro (Timer).
-
(3) Construir (Build) : as fases de criação, atualização e destruição das três árvores,
StatelessWidget
e os métodosState
de ebuild
serão executados nesta fase. -
(4) Layout :
Render Tree
O cálculo do tamanho e posição de cada nó será concluído nesta etapa. -
(5) Paint : O método de
Render Tree
percorrer cada nó e gerarLayer Tree
será executado nesta fase para gerar uma série de instruções de desenho.RenderObject
paint
-
(6) Composição : Processamento
Layer Tree
para gerar umScene
objeto como entrada para rasterização. -
(7) Rasterizar : processar instruções de desenho em dados brutos que podem ser exibidos na GPU .
Vamos pegar setState
o processo de execução de atualização de , como exemplo, para ter uma compreensão geral de todo o processo de atualização.
fluxo de execução setState
Quando setState
chamado:
element
Primeiro, chame o método atualmarkNeedsBuild
para marcar oelement
atual como ._dirty
true
- As chamadas subseqüentes adicionam
scheduleBuildFor
a atual à lista de .element
BuildOwner
_dirtyElements
- Ao mesmo tempo será solicitado um novo
frame
, que será então sorteadoframe
:onBuildScheduled->ensureVisualUpdate->scheduleFrame()
.
O seguinte é setState
um fluxograma aproximado da execução:
A lógica disso updateChild()
é a seguinte:
Dentre eles onBuildScheduled
, o método é inicializado na fase de inicialização, e eventualmente será chamado ensureVisualUpdate
, que acionará o monitoramento do sinal Vsync . Quando o novo sinal Vsync chegar, buildScope
o método será acionado, que irá reconstruir a subárvore e executar o processo de pipeline de renderização ao mesmo tempo:
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); //重新构建widget树
pipelineOwner.flushLayout(); // 更新布局
pipelineOwner.flushCompositingBits(); //更新合成信息
pipelineOwner.flushPaint(); // 更新绘制
if (sendFramesToEngine) {
renderView.compositeFrame(); // 上屏,会将绘制出的bit数据发送给GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
-
Reconstruir
widget
a árvore: SedirtyElements
a lista não estiver vazia, percorra a lista e chame cadaelement
métodorebuild
para reconstruir a novawidget
(árvore), pois a novawidget
(árvore) é construída com um novo estado, pode causarwidget
informações de layout (espaço ocupado e posição) muda, se mudar, seu método será chamadorenderObject
,markNeedsLayout
que irá procurar do nó atual para o pai até encontrar umrelayoutBoundary
nó e, em seguida, adicioná-lo a umanodesNeedingLayout
lista global; se o nó raiz também for Se não encontradorelayoutBoundary
, o nó raiz é adicionado ànodesNeedingLayout
lista. -
Layout de atualização: atravesse a matriz, refaça o layout (chame seu método)
nodesNeedingLayout
para cada um e determine o novo tamanho e deslocamento. Ele será chamado no método , que é semelhante à função do método. Ele também pesquisará do nó atual para o pai até encontrar um nó pai cuja propriedade seja , e então o adicionará a uma lista global; porque o nó raiz ( ) é , Portanto, um deve ser encontrado. Após o término do processo de busca , o método será chamado , e o método será chamado eventualmente, neste método, ele primeiro determinará se um novo foi solicitado e, se não, solicitará um novo .renderObject
layout
layout
markNeedsPaint()
markNeedsLayout
isRepaintBoundary
true
nodesNeedingPaint
RenderView
isRepaintBoundary
true
buildOwner.requestVisualUpdate
scheduleFrame()
frame
frame
-
Atualizar informações de composição: ignore por enquanto.
-
Atualizar desenho: percorra
nodesNeedingPaint
a lista, chame o método de cada nópaint
para redesenhar e o processo de desenho será geradoLayer
. É preciso explicar que os resultados do desenho em vibração são armazenados emLayer
, ou seja, enquantoLayer
não forem liberados, os resultados do desenho serão armazenados em cache. Portanto, os resultados do desenhoLayer
podemframe
ser armazenados em cache para evitar redesenhos desnecessários. Durante o processo de desenho do framework Flutter, quando um nóisRepaintBoundary
com for encontradotrue
, um novo será geradoLayer
. Pode-se observar que não há correspondência biunívoca entreLayer
erenderObject
, e os nós pai-filho podem ser compartilhados, o que verificaremos em um experimento posterior. Claro, se for um componente personalizado, podemos adicionar manualmente qualquer número de camadas no renderObject. Isso geralmente é usado para armazenar em cache cenas de elementos de desenho que precisam ser desenhados apenas uma vez e não serão alterados posteriormente. Também usaremos um exemplo para explicar esta demonstração posterior. -
Tela superior: Após a conclusão do desenho, o que obtemos é uma
Layer
árvore e, finalmente, precisamosLayer
exibir as informações do desenho na árvore na tela. Sabemos que o Flutter é um mecanismo de renderização auto-implementado, portanto, precisamos enviar as informações do desenho ao mecanismo do Flutter erenderView.compositeFrame
essa missão é cumprida.
O acima é setState
o processo de atualização aproximado da chamada para a atualização da interface do usuário. O processo real será mais complicado. Por exemplo, build
não é permitido chamar novamente durante o processo setState
e a estrutura precisa fazer algumas verificações. Outro exemplo é que frame
envolve o agendamento de animações, e quando estiver na tela, todas as animações serão Layer
adicionadas ao objeto cena (Scene), e então a cena será renderizada.
problema de tempo de execução setState
setState
será acionado build
, mas executado build
na fase de execução , desde que não seja executado nesta fase , é absolutamente seguro, mas esse tipo de granularidade é muito grosseiro, por exemplo, nas fases e , se o estado do aplicativo mudar , a melhor maneira é apenas marcar o componente como , em vez de solicitar um novo , porque o atual ainda não foi executado , portanto, a interface do usuário será atualizada no pipeline de renderização do quadro atual após sua execução posterior. Portanto, após a finalização da marcação, será julgado primeiro o status do agendamento, e um novo será solicitado somente se estiver ou em fase de execução :persistentCallbacks
setState
transientCallbacks
midFrameMicrotasks
dirty
frame
frame
persistentCallbacks
setState
dirty
idle
postFrameCallbacks
frame
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame(); // 请求新的frame
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks: // 注意这一行
return;
}
}
O código acima está bom na maioria dos casos, mas se chamarmos build
novamente no palco, setState
ainda haverá problemas, porque se chamarmos build
novamente no palco setState
, isso causará build
... Isso levará a uma chamada circular, portanto, a vibração framework constata que em build
Se o stage for chamado, setState
um erro será reportado, como:
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, c) {
// build 阶段不能调用 setState, 会报错
setState(() {
++index;
});
return Text('xx');
},
);
}
Após a execução, um erro será relatado e o console imprimirá:
==== Exception caught by widgets library ====
The following assertion was thrown building LayoutBuilder:
setState() or markNeedsBuild() called during build.
Deve-se notar que se chamarmos build
diretamente em setState
, o código é o seguinte:
Widget build(BuildContext context) {
setState(() {
++index;
});
return Text('$index');
}
Nenhum erro será relatado após a execução. O motivo é que o estado build
do componente atual dirty
(correspondente ao meio element
) é durante a execução true
e build
será definido somente após a execução false
. Ao setState
executar, ele primeiro julgará o dirty
valor atual e, se for true
, retornará diretamente, portanto, nenhum erro será relatado.
Acima discutimos apenas build
que a chamada na fase setState
causará erros. Na verdade, ela não pode ser chamada de forma síncrona em todas as fases de construção, layout e desenho setState
. Isso porque a chamada nessas fases setState
pode solicitar novas frame
, o que pode levar a chamadas cíclicas. Se você deseja atualizar o estado do aplicativo durante esses estágios, não pode chamá-lo diretamente setState
.
atualização de segurança
Agora sabemos que build
não pode ser chamado na fase setState
. Na verdade, não é possível solicitar diretamente o relayout ou redesenho de forma síncrona durante a fase de layout e a fase de desenho do componente. O motivo é o mesmo. Qual é o correto update nestas fases? Tomando setState
como exemplo , você pode utilizar os seguintes métodos:
// 在build、布局、绘制阶段安全更新
void update(VoidCallback fn) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(fn);
});
}
Observe que update
a função só deve ser executada ao frame
executar persistentCallbacks
, outras etapas setState
podem ser chamadas diretamente. Como idle
o estado será um caso especial, se idle
for chamado no estado update
, você precisa chamar manualmente para scheduleFrame()
solicitar um novo frame
, caso contrário , não será executado até que postFrameCallbacks
o próximo frame
(solicitado por outros componentes ) chegue, para que possamos modificar isto:frame
update
void update(VoidCallback fn) {
final schedulerPhase = SchedulerBinding.instance.schedulerPhase;
if (schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(fn);
});
} else {
setState(fn);
}
}
Até agora, encapsulamos uma update
função que pode atualizar o estado com segurança.
Agora vamos relembrar que na seção "Custom Component: CustomCheckbox", para realizar a animação solicitamos o redesenho através do seguinte código após a finalização do desenho:
SchedulerBinding.instance.addPostFrameCallback((_) {
...
markNeedsPaint();
});
Não o chamamos diretamente markNeedsPaint()
e o motivo é o mencionado acima.
Resumir
Deve-se observar que o processo de Construção e o processo de Layout podem ser executados alternadamente .
referência: