Notas do Flutter | Princípios Básicos do Flutter (2) Principais classes e processo de inicialização

Widget, Elemento, BuildContext e RenderObject

Ferramenta

WidgetO relacionamento de herança das classes-chave e suas subclasses é mostrado na figura:

insira a descrição da imagem aqui

Dentre elas, Widgetestá Widget Treea classe base de todos os nós. WidgetAs subclasses são divididas principalmente em 3 categorias:

  • A primeira categoria é RenderObjectWidgeta subcategoria de , especificamente, é dividida em SingleChildRenderObjectWidget(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 uma RenderObjectsubcategoria de , que pode realizar lógica como Layout,Paint

  • A segunda categoria é StatelessWidgete StatefulWidget, que são os mais usados ​​pelos desenvolvedores Widget.Eles não têm a capacidade de se desenhar (ou seja, não se correspondem Render Object), mas podem ser organizados e RenderObjectWidgetconfigurados Widget.

  • A categoria 3 é ProxyWidget, especificamente, subdividida em ParentDataWidgete InheritedWidget, que se caracterizam por fornecer dados adicionais para seus nós filhos.

Elemento

ElementA classe chave e seu relacionamento de herança de subclasse são mostrados na figura:

insira a descrição da imagem aqui
A partir do relacionamento de herança que pode ser visto claramente na Figura 5-2 Element, ele implementa BuildContexta interface. A Figura 5-2 corresponde à Figura 5-1, e cada uma Elementtem um correspondente Widget. ElementExistem duas subclasses diretas ComponentElemente RenderObjectElement, das quais ComponentElementas duas subclasses StatelessElemente StatefulElementcorrespondem a StatelessWidgete respectivamente StatefulWidget.

Sabemos que a árvore de IU final é, na verdade, composta de Elementnós individuais. O layout final e a renderização do componente são todos RenderObjectfeitos através do passe. O processo geral desde a criação até a renderização é: de acordo com Widgeta geração Element, crie o correspondente RenderObjecte associe-o ao Element.renderObjectatributo e, finalmente, RenderObjectconclua o arranjo do layout e o desenho através passar.

ElementÉ Widgetum objeto instanciado em um local específico na árvore de interface do usuário, a maioria dos quais Elementsão exclusivos renderObject, mas alguns Elementtêm vários nós filhos, como RenderObjectElementalgumas classes herdadas, por exemplo MultiChildRenderObjectElement. No final, todas Elementelas RenderObjectconstituem 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.

insira a descrição da imagem aqui
Agora vamos nos concentrar Elementno Elementciclo de vida do seguinte:

  1. Chamadas de estrutura para Widget.createElementcriar uma Elementinstância, denotadas comoelement

  2. Framework call element.mount(parentElement,newSlot), mountno método, primeiro chame o método elementcorrespondente 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 " ".WidgetcreateRenderObjectelementRenderObjectelement.attachRenderObjectelement.renderObjectElementelementactiveactive

  3. Quando os dados de configuração do pai Widgetmudam e a estrutura State.buildretornada Widgeté diferente da anterior, a Elementárvore correspondente precisa ser reconstruída. Para Elementreutilizar, Elementantes de reconstruir, ele tentará reutilizar a mesma posição na árvore antiga element. O nó chamará seu método correspondente elementantes 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 .WidgetcanUpdatetrueElementElementWidgetElement

    Widget.canUpdateO principal é julgar newWidgetse a soma oldWidgetde runtimeTypee keyé igual ao mesmo tempo, e retornar se forem iguais ao mesmo tempo true, caso contrário retornará false. De acordo com este princípio, quando precisamos forçar uma atualização , podemos evitar a multiplexação Widgetespecificando uma diferente .Key

  4. Quando um ancestral Elementdecide remover element(por exemplo, Widgeta estrutura da árvore foi alterada, resultando na remoção elementdo 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 " ".WidgetElementdeactivateChildelement.renderObjectelement.deactivateelementinactive

  5. O status " inactive" elementnão será exibido na tela novamente. Para evitar criar e remover repetidamente um objeto específico durante a execução de uma animação element, o inactiveestado " " será mantido até o final do último quadro da animação atual. Caso não tenha retornado ao estado " " elementapós o término da execução da animação , Framework activeSeu método será chamado unmountpara removê-lo completamente.Neste momento , elemento estado é que defunctele nunca mais será inserido na árvore.

  6. Se elementvocê deseja reinserir em Elementoutro local da árvore, como o ancestral elementou elementpossui um GlobalKey(para elementos de reutilização global), o Framework primeiro elemento removerá do local existente, chamará seu activatemétodo e renderObjectreinsira attacha renderização árvore .

insira a descrição da imagem aqui

Resumir:

  • Um objeto Element inicializará o estado quando for criado initiale mountse tornará um estado após ser adicionado à Árvore de Elementos por meio de um método active; quando o Widget correspondente ao nó falhar, ele entrará deactivateno inactiveestado por meio de um método. ElementSe outros nós tiverem keyreutilizado o nó durante o processo de construção do quadro atual, o activatemétodo será usado para fazer o nó entrar activeno estado novamente; se o nó ainda não estiver na árvore de elementos após o final do quadro atual, ele será unmountser desinstalado através do método e entrar defunctno estado, esperando que a lógica subseqüente seja destruída .

Depois de ler Elemento 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 Elementa á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 Elementdesse link que o Flutter se conecta Widgetcada vez RenderObjectmais. 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 StatelessWidgete passarão um objeto:StatefulWidgetbuildBuildContext

Widget build(BuildContext context) {
    
    }

Também sabemos que em muitos casos precisamos usar isso contextpara fazer algo, como:

Theme.of(context) // 获取主题
Navigator.push(context, route) // 入栈新路由
Localizations.of(context, type) // 获取Local
context.size // 获取上下文大小
context.findRenderObject() // 查找当前或最近的一个祖先RenderObject

Então, BuildContexto que é isso? Verifique sua definição e descubra que é uma classe de interface abstrata:

abstract class BuildContext {
    
    
    ...
}

contextQuem é a classe de implementação correspondente a este objeto? Seguimos o vine e descobrimos buildque a chamada ocorria no StatelessWidgetmétodo StatefulWidgetcorrespondente StatelessElement, StatefulElementpor buildexemplo 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 buildos parâmetros passados ​​são this, obviamente! Isto BuildContexté StatelessElementou StatefulElementem si. Mas StatelessElemente StatefulElementele mesmo não implementou BuildContexta interface. Continue a rastrear o código e descubra que eles herdam indiretamente da Elementclasse. Em seguida, verifique Elementa definição da classe e descubra Elementque a classe realmente implementa BuildContexta interface:

abstract class ComponentElement extends Element {
    
    ...}
abstract class Element extends DiagnosticableTree implements BuildContext {
    
    ...}

Até agora a verdade é clara, BuildContexté widgetcorrespondente Element, então podemos acessar diretamente o objeto no contextmétodo de StatelessWidgete . Dentro do código onde obtemos os dados do assunto está o método chamado .StatefulWidgetbuildElementTheme.of(context)ElementdependOnInheritedWidgetOfExactType()

Resumo: BuildContextÉ Elementa divindade, BuildContexta 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 é BuildContextque é uma referência a uma posição Widgetna Widgetárvore e contém informações sobre a posição na árvore Widget,Widget não sobre Widgetsi mesma.

No caso dos tópicos, como cada um Widgettem o seu BuildContext, isso significa que se você tiver vários tópicos espalhados na árvore, obter um Widgettópico para um pode retornar um Widgetresultado diferente do outro. No caso específico do tópico no programa de exemplo do aplicativo contador, ou em outros ofmétodos, você obterá o nó pai mais próximo desse tipo na árvore .

insira a descrição da imagem aqui

Avançado

Podemos ver que é o elo entre a conexão interna e Elemento 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.widgetRenderObjectwidgetwidgetElementStatelessWidgetStatefulWidgetbuildElementElement

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 widgeta árvore é apenas Elemento mapeamento da árvore, ela fornece apenas informações de configuração que descrevem a árvore da interface do usuário, Widgetou seja, o casaco, é claro que uma pessoa pode viver com vergonha sem usar roupas, mas usar roupas viverá uma vida mais decente, mesmo que Widgetnão dependamos disso, podemos Elementconstruir uma estrutura de interface do usuário completamente por meio dela.

Aqui está um exemplo:

ElementSimulamos 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 buildmétodo acima não recebe parâmetros, o que é diferente do método in StatelessWidgete StatefulWidgetin build(BuildContext). Os locais que precisam ser usados ​​no código podem ser substituídos BuildContextdiretamente 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.thisTheme.of(this)thisElement

  • Quando textocorre uma alteração, chamamos markNeedsBuild()o método Elementpara marcar o atual dirty, e o marcado dirtyserá Elementreconstruído no próximo quadro. Na verdade, State.setState()também é o método chamado internamente markNeedsBuild().

  • O método no código acima buildainda retorna um widget. Isso ocorre porque já existe widgetessa camada na estrutura do Flutter e a biblioteca de componentes já é widgetfornecida na forma de . Se todos os componentes na estrutura do Flutter forem fornecidos HomeViewna forma do exemplo Element, então você pode usar pure Elementpara construir a IU. HomeViewO tipo de valor de retorno do buildmétodo pode ser Element.

Se precisarmos executar o código acima no framework Flutter existente, ainda teremos que fornecer um "adaptador" que widgetserá HomeViewintegrado ao framework existente. O seguinte CustomHomeé equivalente ao "adaptador":

class CustomHome extends Widget {
    
    
  
  Element createElement() {
    
    
    return HomeView(this);
  }
}

Agora você pode CustomHomeadicioná-lo à widgetárvore, criamos em uma nova página de roteamento, o efeito final é o mostrado na figura a seguir:

insira a descrição da imagem aqui
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 Elementcorresponde a um RenderObject, e podemos Element.renderObjectpassar por isso. E também dissemos RenderObjectque as principais responsabilidades são o layout e o desenho, que RenderObjectformarão uma árvore de renderização Render Tree. A seguir, o foco será RenderObjecto 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 ( buildo processo é elementrealizado por ), incluindo: layout, desenho, composição de camadas e na tela .

insira a descrição da imagem aqui

RenderObjectA classe-chave e suas subclasses são mostradas na Figura 5-3, e cada uma de suas subclasses corresponde a um RenderObjectWidgettipo de Widgetnó.

  • RenderViewÉ especial RenderObject, é o nó raiz de toda a árvore de renderização .
  • Outra coisa especial RenderObjecté RenderAbstractViewportque é uma classe abstrata. RenderViewportimplementa sua interface e herda indiretamente de RenderBox.
  • RenderBoxe RenderSliversão os mais comuns no Flutter RenderObject, RenderBoxresponsáveis ​​pela disposição geral das linhas, colunas, etc., e RenderSliverresponsáveis ​​pela disposição de cada uma na lista Item.

RenderObjectEle tem um parente um parentDataatributos, parentapontando para seu próprio nó pai na árvore de renderização, parentDatamas 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 parentDatalayout Stack. RenderStackOs dados de deslocamento do elemento filho serão armazenados no elemento filho parentData(consulte Positioneda implementação para obter detalhes).

Pergunta: Agora que existem RenderObject, por que o framework Flutter fornece RenderBoxe RenderSliverduas subclasses?

  • Isso ocorre porque RenderObjecta 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 RenderBoxe uma RenderSliverclasse, todos herdados de RenderObject, 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:

insira a descrição da imagem aqui

Entre eles, o Dart Runtime primeiro criará e iniciará DartVMuma máquina virtual e, DartVMapós a inicialização, inicializará uma DartIsolatee, em seguida, iniciará e, ao final do processo de inicialização, executará o método de entrada DartIsolatedo aplicativo Dart . Ou seja, a função de main" " em nosso desenvolvimento diário :lib/main.dartmain()

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 WidgetsFlutterBindingponte que liga widgeto 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 WidgetsFlutterBindingo relacionamento de herança de , descobrimos que WidgetsFlutterBindingele herda BindingBasee se mistura em muitas Bindingclasses; portanto, quando iniciar, acionará os construtores dessas classes na ordem do mixin.

insira a descrição da imagem aqui

  • GestureBinding: Responsável pelo processamento de gestos, fornece window.onPointerDataPacketretornos 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, fornecendo window.onPlatformMessagecallbacks 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, fornecendo window.onBeginFramee window.onDrawFramecallbacks, 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, mantendo PipelineOwnerobjetos e fornecendo callbacks como window.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, guarda BuilderOwnerobjetos e fornece callbacks como window.onLocaleChanged, etc. onBuildScheduledÉ a ponte entre a camada do widget Flutter e o mecanismo.

Antes de entendermos por que eles estão misturados, Bindingvamos apresentá-lo primeiro Window, Windowque é a interface que o Flutter Framework conecta ao sistema operacional host. Vamos dar uma olhada em Windowparte 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 Windowa 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 WidgetsFlutterBindingas várias misturas Binding. Olhando para esses Bindingcódigos-fonte, podemos descobrir que eles Bindingestão basicamente ouvindo e processando Windowalguns eventos de objetos e, em seguida, empacotando, abstraindo e distribuindo esses eventos de acordo com o modelo do Framework. Pode-se ver WidgetsFlutterBindingque é a "cola" que une o Flutter Engine e o Framework superior. WidgetsFlutterBindingA 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 WidgetsBindingsingleton global e retornar WidgetsBindingo 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 runAppmétodo, WidgetsBindingapós obter o objeto singleton, o método será chamado imediatamente WidgetsBindinge 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 Treee Render Treecriar. Deve-se notar que attachRootWidgetela é iniciada iniciando Timer.run. Isso é para garantir que toda a lógica esteja sob o gerenciamento do loop de mensagem.

attachRootWidgetO método é o principal responsável por Widgetadicionar a raiz ao RenderViewtopo. Note que existem duas variáveis ​​no código, renderViewuma renderViewElement, renderViewque RenderObjecté a raiz da árvore de renderização, renderViewElementmas renderViewo Elementobjeto correspondente. Pode-se ver que este método completa principalmente todo o associação de raiz depois para o processo raizwidgetpara raiz eRenderObjectElement

attachToRenderTreeO método conduzirá Element Treea 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 elementa , 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 .widgetwidgetelementelementelementwidgetelementelementnullcreateElementBuildOwnerBuildOwnerwidgetwidgetElement Tree

Após a conclusão da construção das três árvores, a lógica attachRootWidgetin 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.
insira a descrição da imagem aqui

Na Figura 5-8, primeiro, se não houver externo (como setStatemétodo) e interno (como pulsação de animação, ouvinte para conclusão do carregamento da imagem), o Framework estará no idleestado padrão. Se houver uma nova solicitação de dados de quadro para renderização, o Framework entrará handleBeginFrameno 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á handleDrawFrameo método e o Framework atualizará o estado neste momento persistentCallbackspara 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 postFrameCallbackse 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.

scheduleFrameA lógica do método é a seguinte, ele irá iniciar uma requisição através da interface para renderizar quando platformDispatcher.scheduleFrameo 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 attachRootWidgetfor concluída , o último passo chamará o método WidgetsFlutterBindingde 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. scheduleWarmUpFrameO 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.runiniciadas no método. handleBeginFrameO método acionará a lógica relacionada à animação e handleDrawFrameacionará a atualização das 3 árvores e a lógica de renderização, como Render TreeLayout 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

insira a descrição da imagem aqui

pipeline de renderização

Na análise anterior , após a runAppexecuçã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 .ensureInitializedscheduleAttachRootWidgetscheduleWarmUpFrame

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.

  1. onBeginFrameO Flutter registra um e um onDrawFrameretorno de chamada na janela , onDrawFrameque eventualmente será chamado no retorno de chamada drawFrame.
  2. 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) .onBeginFrameonDrawFrame

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 idlee . 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 .frameidleframescheduleFrame()frameframeframeidleframe

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 frametermina. Em resumo, o Flutter divide todo o ciclo de vida em cinco estados e SchedulerPhaseos 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 framechega, o método WidgetsBindingde é 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 drawFrameo método da classe pai. Depois expandimos drawFrameo 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:

  1. Recrie a árvore de widgets.
  2. Atualize o layout.
  3. Atualize as informações da composição da camada.
  4. redesenhar.
  5. 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 Embedderobtém um Surfaceou Textureserve como o destino de saída final de seu próprio pipeline de renderização.

insira a descrição da imagem aqui

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 Vsyncsinal chegue e, em seguida, notificará o Framework para animar, construir, layout, paint e, finalmente, gerar layerSubmit to Engine . A Engine irá layercombinar 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:

insira a descrição da imagem aqui

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, StatelessWidgete os métodos Statede e buildserão executados nesta fase.

  • (4) Layout : Render TreeO cálculo do tamanho e posição de cada nó será concluído nesta etapa.

  • (5) Paint : O método de Render Treepercorrer cada nó e gerar Layer Treeserá executado nesta fase para gerar uma série de instruções de desenho.RenderObjectpaint

  • (6) Composição : Processamento Layer Treepara gerar um Sceneobjeto como entrada para rasterização.

  • (7) Rasterizar : processar instruções de desenho em dados brutos que podem ser exibidos na GPU .

Vamos pegar setStateo 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 setStatechamado:

  • elementPrimeiro, chame o método atual markNeedsBuildpara marcar o elementatual como ._dirtytrue
  • As chamadas subseqüentes adicionam scheduleBuildFora atual à lista de .elementBuildOwner_dirtyElements
  • Ao mesmo tempo será solicitado um novo frame, que será então sorteado frame: onBuildScheduled->ensureVisualUpdate->scheduleFrame().

O seguinte é setStateum fluxograma aproximado da execução:

insira a descrição da imagem aqui

A lógica disso updateChild()é a seguinte:

insira a descrição da imagem aqui

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, buildScopeo 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;
  }
}
  1. Reconstruir widgeta árvore: Se dirtyElementsa lista não estiver vazia, percorra a lista e chame cada elementmétodo rebuildpara reconstruir a nova widget(árvore), pois a nova widget(árvore) é construída com um novo estado, pode causar widgetinformações de layout (espaço ocupado e posição) muda, se mudar, seu método será chamado renderObject, markNeedsLayoutque irá procurar do nó atual para o pai até encontrar um relayoutBoundarynó e, em seguida, adicioná-lo a uma nodesNeedingLayoutlista global; se o nó raiz também for Se não encontrado relayoutBoundary, o nó raiz é adicionado à nodesNeedingLayoutlista.

  2. Layout de atualização: atravesse a matriz, refaça o layout (chame seu método) nodesNeedingLayoutpara 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 .renderObjectlayoutlayoutmarkNeedsPaint()markNeedsLayoutisRepaintBoundarytruenodesNeedingPaintRenderViewisRepaintBoundarytruebuildOwner.requestVisualUpdatescheduleFrame()frameframe

  3. Atualizar informações de composição: ignore por enquanto.

  4. Atualizar desenho: percorra nodesNeedingPainta lista, chame o método de cada nó paintpara redesenhar e o processo de desenho será gerado Layer. É preciso explicar que os resultados do desenho em vibração são armazenados em Layer, ou seja, enquanto Layernão forem liberados, os resultados do desenho serão armazenados em cache. Portanto, os resultados do desenho Layerpodem frameser armazenados em cache para evitar redesenhos desnecessários. Durante o processo de desenho do framework Flutter, quando um nó isRepaintBoundarycom for encontrado true, um novo será gerado Layer. Pode-se observar que não há correspondência biunívoca entre Layere renderObject, 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.

  5. Tela superior: Após a conclusão do desenho, o que obtemos é uma Layerárvore e, finalmente, precisamos Layerexibir 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 e renderView.compositeFrameessa missão é cumprida.

O acima é setStateo processo de atualização aproximado da chamada para a atualização da interface do usuário. O processo real será mais complicado. Por exemplo, buildnão é permitido chamar novamente durante o processo setStatee a estrutura precisa fazer algumas verificações. Outro exemplo é que frameenvolve o agendamento de animações, e quando estiver na tela, todas as animações serão Layeradicionadas ao objeto cena (Scene), e então a cena será renderizada.

problema de tempo de execução setState

setStateserá acionado build, mas executado buildna 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 :persistentCallbackssetStatetransientCallbacksmidFrameMicrotasksdirtyframeframepersistentCallbackssetStatedirtyidlepostFrameCallbacksframe

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 buildnovamente no palco, setStateainda haverá problemas, porque se chamarmos buildnovamente no palco setState, isso causará build... Isso levará a uma chamada circular, portanto, a vibração framework constata que em buildSe o stage for chamado, setStateum 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 builddiretamente 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 builddo componente atual dirty(correspondente ao meio element) é durante a execução truee buildserá definido somente após a execução false. Ao setStateexecutar, ele primeiro julgará o dirtyvalor atual e, se for true, retornará diretamente, portanto, nenhum erro será relatado.

Acima discutimos apenas buildque a chamada na fase setStatecausará 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 setStatepode 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 buildnã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 setStatecomo exemplo , você pode utilizar os seguintes métodos:

// 在build、布局、绘制阶段安全更新
void update(VoidCallback fn) {
    
    
  SchedulerBinding.instance.addPostFrameCallback((_) {
    
    
    setState(fn);
  });
}

Observe que updatea função só deve ser executada ao frameexecutar persistentCallbacks, outras etapas setStatepodem ser chamadas diretamente. Como idleo estado será um caso especial, se idlefor chamado no estado update, você precisa chamar manualmente para scheduleFrame()solicitar um novo frame, caso contrário , não será executado até que postFrameCallbackso próximo frame(solicitado por outros componentes ) chegue, para que possamos modificar isto:frameupdate

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 updatefunçã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

insira a descrição da imagem aqui
Deve-se observar que o processo de Construção e o processo de Layout podem ser executados alternadamente .


referência:

Acho que você gosta

Origin blog.csdn.net/lyabc123456/article/details/130931167
Recomendado
Clasificación