Otimização de download e instalação de streaming de pacotes do Baidu Mini Program

Introdução: O texto apresenta um método de otimização para o link de download do pacote de miniaplicativos Baidu - download e instalação de streaming. Primeiro, os pontos de otimização do esquema original são apresentados e, em seguida, é discutido como o esquema de otimização faz uso mais completo dos recursos de E/S de rede, E/S locais e de computação da CPU e, finalmente, introduz o princípio de implementação no nível de código.

O texto completo tem 3608 palavras, o tempo estimado de leitura é de 10 minutos

1. Antecedentes do problema

O processo de instalação do applet envolve várias etapas, como download de rede do pacote de instalação, salvamento de arquivos, verificação de assinatura, descompactação e descriptografia, etc. Na solução original, cada etapa depende uma da outra e é executada serialmente, e o processo de instalação consome o soma dos passos.

No entanto, os recursos disputados em cada etapa do processo de instalação são diferentes, entre eles, a etapa de download dos recursos de E/S da rede concorrentes é a mais demorada, e os recursos de E/S locais e de computação da CPU estão relativamente ociosos nesta fase.

Em teoria, ele pode quebrar as dependências entre as várias etapas do processo de instalação e realizar as etapas de verificação de assinatura, descriptografia e descompactação ao mesmo tempo enquanto lê o fluxo de download da rede e tenta fazer uso total do IO local e Computação da CPU no sistema durante o estágio de download da rede, reduzindo assim o tempo extra consumido por cada etapa após o download da rede.

2. Soluções

A sequência do esquema original é mostrada na figura abaixo, e a análise do relacionamento ocupado da concorrência em cada etapa é a seguinte:

  • Baixe o pacote de instalação: A E/S de rede é a mais ocupada, a computação da CPU é menos ocupada e a E/S local está ocupada

  • Verifique o pacote de instalação: Nenhuma E/S de rede é necessária, o cálculo da CPU (cálculo de assinatura) é o mais ocupado e a E/S local (leitura de arquivos) está ocupada

  • Extrair arquivos de pacote: Sem E/S de rede, a computação da CPU (descriptografia, descompactação) é a mais ocupada, a E/S local (leitura e gravação de arquivos) é a mais ocupada

foto

Em relação à sobrecarga de desempenho, sabe-se que a E/S de rede consome muito mais tempo do que a E/S local e a computação da CPU.

Combinando a análise acima, pode-se obter que durante a leitura do fluxo de download, a descompressão do fluxo e a verificação do arquivo podem ser concluídas em paralelo, de modo a atingir o objetivo de melhorar o desempenho da etapa de download e instalação do applet. esquema é o seguinte:

foto

A solução de instalação de download de streaming é mostrada na figura acima.Para realizar a função de download e instalação de streaming, você precisa implementar o acesso ao stream de download (response.body) e lidar com os dois problemas da distribuição do pipeline de stream (PipeLine).

Em termos de design de responsabilidade, o MultiPipe é uma ferramenta básica para processamento de fluxos. Ele pode bombear um fluxo de entrada de entrada para os pipelines de execução em diferentes threads ao mesmo tempo para atingir mais de um ponto do fluxo de entrada. É uma estrutura semelhante a um coletor de admissão. ,Como mostrado abaixo:

foto

Durante o processo de trabalho do MultiPipe, vários pipelines de processamento de consumidor (PipeLines) são construídos, que são executados de forma assíncrona por um executor, e os dados no fluxo de entrada são bombeados continuamente para cada PipeLine até o final do fluxo de entrada e todos os PipeLines são processados .completo.

Veja a seguir o caso de uso do MultiPipe. Por exemplo, o canal de entrada é o resultado de retorno de okhttp3.ResponseBody#source, ou seja, o fluxo binário do corpo de resposta da solicitação de rede. Dois consumidores concluem as ações de verificação de assinatura e descompressão e decriptação, respectivamente.

ReadableByteChannel srcChannel = ... // 例如 okhttp3.ResponseBody#source 方法的返回结果ExecutorService threadPool = Executors.newFixedThreadPool(2); // 可选参数MultiPipe multiplePipe = new MultiPipe(new Consumer<ReadableByteChannel>() {    @Override    public void accept(ReadableByteChannel source) {        // 对整个流做md5,进行签名校验,CPU忙    }}, new Consumer<ReadableByteChannel>() {    @Override    public void accept(ReadableByteChannel source) {        // 对整个流进行解密、解压、写入磁盘,CPU和IO忙    }}) {    @Override    protected ExecutorService onCreateExecutor(int consumerSize) {        return threadPool;    }};// 每次能从网络流中读取的最大字节数,对应 okio.Segment#SIZEmultiplePipe.setTmpBufferCapacity(MultiPipe.TMP_BUFFER_CAPACITY); // 开始传输multiplePipe.connect(srcChannel);

Resumo : É uma tarefa de alta qualidade para o applet chamar o download do pacote da cena. Ao otimizar o método de processamento do response.body do pacote principal, ao ler o fluxo de rede, copie o array de bytes lido a cada vez , e, em seguida, passar Pipe é transmitido para cada consumidor (verificação de assinatura, descriptografia e descompactação), de modo que o tempo de download de pacotes seriais para o local, verificação de assinatura, descriptografia e descompactação seja reduzido para três de leitura de fluxo de rede, assinatura verificação ou descriptografia e descompactação no tempo máximo entre.

Benefício : Após a otimização de download e instalação de streaming, o tempo de download de pacotes online é reduzido em 21%.

3. Análise de Implementação

MultiPipe é uma classe de ferramenta que pode dividir um canal de entrada em vários canais de saída. O código de exemplo é o seguinte:

/**
 * 可以将一个输入通道,分为多个输出通道的管道
 */
public class MultiPipe {
    /** 临时缓存的大小 {@see okio.Segment#SIZE} */
    public static final int TMP_BUFFER_CAPACITY = 8 * 1024;
    /** 消费者列表 */
    private final List<Consumer<ReadableByteChannel>> mConsumerList;
    /** 临时缓存的大小 {@see okio.Segment#SIZE} */
    private int mTmpBufferCapacity = TMP_BUFFER_CAPACITY;
    
    /**
     * 构造方法
     *
     * @param consumers 消费者列表
     */
    @SafeVarargs
    public MultiPipe(Consumer<ReadableByteChannel>... consumers) {
        mConsumerList = Arrays.asList(consumers);
    }
    
    // 设置表示每次最多传输多少字节,例如 8 * 1024
    // public final void setTmpBufferCapacity(int maxBytes)
    
    // connect 方法及其依赖的方法:
    //   transfer、createPipeLineList、launchPipeLineList 方法
    
    // 可供使用方重写的方法:
    //   setHasPipeBuffer、onStart、onCreateExecutor、onException、
    //   onTransferComplete、onUpdateProgress、onFinish
}

3.1 Crie uma lista de pipeline (PipeLineList) e conecte o canal de entrada (ReadableChannel)

O consumidor finaliza todo o trabalho por meio da conexão. Nesse método, a lista de pipeline e o latch são criados primeiro de acordo com a lista de consumidores no método de construção e, em seguida, cada pipeline é iniciado por meio do pool de threads, para que cada tarefa comece a funcionar.

(1) Crie um pipeline correspondente (PipeLine) de acordo com o número de consumidores e conecte o canal de entrada . A função de latch é garantir que todas as tarefas do consumidor sejam concluídas antes que a execução seja concluída.A instrução de chave é latch.await();

/** * 连接输入流 * * @param source 输入流 */public final void connect(ReadableByteChannel source) {    onStart(source); // 回调 - 开始        // 创建管线列表    List<PipeLine> pipeLineList = createPipeLineList();    // 根据消费者数量,创建latch    CountDownLatch latch = new CountDownLatch(pipeLineList.size());     // 让连接各个管线的任务开始工作    ExecutorService executorService = launchPipeLineList(pipeLineList, latch);     try {        transfer(source, pipeLineList); // 开始传输        onTransferComplete(latch); // 回调 - 传输完成(等待关闭,默认等待latch)    } catch (IOException e) {        onException(e); // 回调 - 异常处理    } finally {        onFinish(source, executorService); // 回调 - 结束    }}
// 当开始连接时,回调给使用方// protected void onStart(ReadableByteChannel source)
/** * 可以由使用方重写,传输完成,处理Latch,可以选择一直等待,也可以设置为超时机制 * * @param latch CountDownLatch */protected void onTransferComplete(CountDownLatch latch) {    try {        latch.await();    } catch (InterruptedException ignored) {    }}
/** * 可以选择是否关闭线程池 * * @param source          输入 * @param executorService 线程池 */protected void onFinish(ReadableByteChannel source,                         ExecutorService executorService) {    closeChannel(source);    executorService.shutdown();}

(2) Crie uma lista de pipeline com base no número de consumidores

/** * 创建管道列表 * * @return 管道列表 */private List<PipeLine> createPipeLineList() { final List<PipeLine> pipeLineList = new ArrayList<>(mConsumerList.size()); for (Consumer<ReadableByteChannel> consumidor: mConsumerList) { pipeLineList.add(new PipeLine(consumer, hasPipeBuffer())); } return pipeLineList;}

(3) Inicie cada pipeline por meio do pool de threads

O conjunto de encadeamentos pode ser definido como um conjunto de encadeamentos existente. Se você deseja evitar que o pool de encadeamentos existente seja encerrado, você precisa reescrever o método onFinish e remover a instrução executorService.shutdown();.

/** * 调起管线列表,返回执行者实例 * * @param pipeLineList 管线列表 * @param latch        用于确保所有任务一起完成 * @return 执行者实例 */private ExecutorService launchPipeLineList(List<PipeLine> pipeLineList,                                            CountDownLatch latch) {    ExecutorService executorService = onCreateExecutor(pipeLineList.size());    for (PipeLine pipeLine : pipeLineList) {        pipeLine.setLaunch(latch);        executorService.submit(pipeLine);    }    return executorService;}/** * 由使用方决定如何创建线程池,例如使用已有的线程池 * * @param consumerSize 消费者个数 * @return 可用的线程池实例 */protected ExecutorService onCreateExecutor(int consumerSize) {    return Executors.newFixedThreadPool(consumerSize);}

3.2 Transfira o conteúdo lido a cada vez para cada pipeline (PipeLine)

Ao ler o canal de entrada, o buffer de bytes lido a cada vez é transmitido para cada consumidor.
Receba o conteúdo que pode ser lido a cada vez por meio do ByteBuffer, percorra a lista de consumidores e grave o conteúdo no ByteBuffer no canal coletor do pipeline.
Finalmente, o canal coletor de cada pipeline consumidor é fechado.

O progresso atual pode ser chamado de volta para o usuário por meio do método onUpdateProgress.

/** * 传输 * * @param source       输入流/输入通道 * @param pipeLineList 管线列表 */private void transfer(ReadableByteChannel source,                       List<PipeLine> pipeLineList) throws IOException {    long writeBytes = 0; // 累计写出的字节数    onUpdateProgress(writeBytes); // 通知使用方当前的传输进度    try {        final ByteBuffer buf = ByteBuffer.allocate(mTmpBufferCapacity);        long reads;        while ((reads = source.read(buf)) != -1) {            buf.flip(); // 开始读取buf中的内容            for (PipeLine pipeLine : pipeLineList) {                if (pipeLine.mSink.isOpen() && pipeLine.mSource.isOpen()) {                    buf.rewind(); // 重读Buffer中的所有数据                    pipeLine.mSink.write(buf); // 向管线中传输内容                }            }            buf.clear();            writeBytes += reads;            onUpdateProgress(writeBytes); // 通知使用方当前的传输进度        }    } finally {        for (PipeLine pipeLine : pipeLineList) {            closeChannel(pipeLine.mSink); // 需要关闭,否则会陷入阻塞        }    }}

3.3 Implementação do PipeLine

Cada tarefa do consumidor corresponde a um pipeline (PipeLine). No processo de transmissão do fluxo de rede, o buffer de bytes lido a cada vez é gravado no canal coletor de cada pipeline e, em seguida, o canal de origem do pipeline é fornecido à tarefa do consumidor .

A implementação do pipeline pode ser baseada em java.nio.channels.Pipe, ou pode ser usado okio.Pipe com buffer.
Com um buffer, aumentará o tempo de transmissão, mas pode evitar o problema de que a velocidade de leitura é lenta devido ao consumo muito lento em casos extremos: por exemplo, a decodificação do consumidor demora muito, fazendo com que o TCP julgue mal que a rede está não é bom, e retransmissões de tempo limite freqüentes.

/** * 管线,也作为工作任务将流导给消费者 */private static class PipeLine implements Runnable {    /** 管线消费者 */    final transient Consumer<ReadableByteChannel> mConsumer;    /** pipe.source */    final transient ReadableByteChannel mSource;    /** pipe.sink */    final transient WritableByteChannel mSink;    /** 用来做线程同步 */    transient CountDownLatch mLatch;        /**     * 构造方法     *     * @param hasBuffer 是否带缓冲区     * @param consumer  消费者     */    public PipeLine(Consumer<ReadableByteChannel> consumer,                     boolean hasBuffer) {        mConsumer = consumer;        if (hasBuffer) { // 带缓冲区的Pipe            okio.Pipe okioPipe = new okio.Pipe(getPipeMaxBufferBytes());            mSink = okio.Okio.buffer(okioPipe.sink());            mSource = okio.Okio.buffer(okioPipe.source());        } else { // 无缓冲区的Pipe            try {                java.nio.channels.Pipe pipe = java.nio.channels.Pipe.open();                mSource = pipe.source();                mSink = pipe.sink();            } catch (IOException e) {                throw new IllegalStateException(e);            }        }    }        @Override    public void run() {        try {            mConsumer.accept(mSource);        } finally {            closeChannel(mSink);            closeChannel(mSource);            if (mLatch != null) {                mLatch.countDown();            }        }    }}

O getPipeMaxBufferBytesmétodo pode se referir à implementação a seguir, que retorna uma capacidade de buffer segura com base na memória disponível atual.

 /** 可用内存的比例 */
  private static final float FACTOR = 0.75F;
    
  /**
   * 返回缓冲区的最大容量
   *
   * @return 基于实际内存考虑,避免OOM 的可用内存字节数
   */
  private static long getPipeMaxBufferBytes() {
      Runtime r = Runtime.getRuntime();
      long available = r.maxMemory() - r.totalMemory() + r.freeMemory();
      return (long) (available * FACTOR);
  }

4. Resumo e Perspectivas

Resumo : Este artigo descreve um método de otimização no link de download do pacote do miniaplicativo Baidu, ou seja, o esquema de download e instalação de streaming. Tomando o objetivo de aproveitar ao máximo os recursos de E/S de rede, E/S locais e de CPU como pistas, o texto completo analisa os pontos de otimização do esquema original e compara o esquema de otimização com o esquema original. implementação principal e apresenta Uma implementação que suporta a divisão de um canal de entrada em vários canais de saída.

Olhando para o futuro : A solução de instalação de download de streaming requer a descompactação de todo o pacote de miniaplicativos. Olhando para o futuro, explore uma solução de carregamento progressivo de recursos. Essa forma de compartilhamento de fluxos pode ser usada para baixar recursos de baixa prioridade de forma assíncrona o máximo possível.

Leitura recomendada:

Uma breve discussão sobre a realização da plataforma log middle que não é pesada nem perdida

Projeto e prática da plataforma de autoridade de conta vertical Baidu ToB

Métodos de visualização de entrada no Visual Transformer

Compreensão aprofundada do WKWebView (renderização) - construção de árvore DOM

Compreensão aprofundada do WKWebView (Introdução) - depuração e análise do código-fonte do WebKit

{{o.name}}
{{m.name}}

Acho que você gosta

Origin my.oschina.net/u/4939618/blog/5516422
Recomendado
Clasificación