Optimización de instalación y descarga de streaming del paquete del programa Baidu Mini

Introducción: el texto presenta un método de optimización para el enlace de descarga del paquete del subprograma Baidu: descarga e instalación de transmisión. En primer lugar, se presentan los puntos de optimización del esquema original, y luego se analiza cómo el esquema de optimización hace un uso más completo de los recursos informáticos de la red IO, IO local y CPU, y finalmente presenta el principio de implementación a nivel de código.

El texto completo tiene 3608 palabras, el tiempo estimado de lectura es de 10 minutos

1. Antecedentes del problema

El proceso de instalación del applet implica varios pasos, como la descarga de red del paquete de instalación, el guardado de archivos, la verificación de firmas, la descompresión y el descifrado, etc. En la solución original, cada paso depende entre sí y se ejecuta en serie, y el proceso de instalación consume el suma de los pasos.

Sin embargo, los recursos que compiten en cada etapa del proceso de instalación son diferentes. Entre ellos, la etapa de descarga de los recursos de E/S de la red de la competencia lleva más tiempo, y los recursos informáticos de E/S y CPU locales están relativamente inactivos en esta etapa.

En teoría, puede romper las dependencias entre los distintos pasos del proceso de instalación y realizar los pasos de verificación de firma, descifrado y descompresión al mismo tiempo mientras lee el flujo de descarga de la red e intenta aprovechar al máximo el IO local y Computación de la CPU en el sistema durante la etapa de descarga de la red. recursos, lo que reduce el tiempo adicional consumido por cada paso después de la descarga de la red.

2. Soluciones

La secuencia del esquema original se muestra en la siguiente figura, y el análisis de la relación ocupada de la competencia en cada etapa es el siguiente:

  • Descargue el paquete de instalación: la E/S de red es la más ocupada, la informática de la CPU está menos ocupada y la E/S local está ocupada

  • Verifique el paquete de instalación: no se requiere E/S de red, el cálculo de CPU (cálculo de firma) es el más ocupado y la E/S local (lectura de archivos) está ocupada

  • Extraer archivos de paquetes: sin E/S de red, la informática de la CPU (descifrado, descompresión) es la más activa, la E/S local (lectura y escritura de archivos) es la más activa

imagen

Con respecto a la sobrecarga de rendimiento, se sabe que la E/S de la red consume mucho más tiempo que el cálculo de la CPU y la E/S local.

Combinando el análisis anterior, se puede obtener que mientras se lee el flujo de descarga, la descompresión del flujo y la verificación del archivo se pueden completar en paralelo, para lograr el objetivo de mejorar el rendimiento de la etapa de descarga e instalación del applet. esquema es el siguiente:

imagen

La solución de instalación de descarga de transmisión se muestra en la figura anterior. Para realizar la función de descarga e instalación de transmisión, debe implementar el acceso de transmisión de descarga (response.body) y manejar los dos problemas de la distribución de canalización de transmisión (PipeLine).

En términos de diseño de responsabilidad, MultiPipe es una herramienta básica para procesar flujos. Puede bombear un flujo de entrada entrante a las tuberías de ejecución en diferentes subprocesos al mismo tiempo para lograr más de un punto del flujo de entrada. Es una estructura similar a un múltiple de admisión. ,Como se muestra a continuación:

imagen

Durante el proceso de trabajo de MultiPipe, se construyen varias canalizaciones de procesamiento de consumidores (PipeLines), que un ejecutor ejecuta de forma asincrónica, y los datos del flujo de entrada se bombean continuamente a cada PipeLine hasta el final del flujo de entrada y se procesan todas las PipeLines. .completa

El siguiente es el caso de uso de MultiPipe. Por ejemplo, el canal de entrada es el resultado de retorno de okhttp3.ResponseBody#source, es decir, el flujo binario del cuerpo de respuesta de la solicitud de red. Dos consumidores completan las acciones de verificación de firma y descompresión y descifrado 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);

Resumen : es una tarea de alta calidad para el applet llamar la descarga del paquete de la escena.Al optimizar el método de procesamiento de la respuesta.cuerpo del paquete principal, al leer el flujo de red, copie la matriz de bytes leída cada vez , y luego pasar Pipe se transmite a cada consumidor (verificación de firma, descifrado y descompresión), de modo que el tiempo que lleva descargar paquetes en serie al local, la verificación de firma, el descifrado y la descompresión se reduce a los tres de lectura de flujo de red, firma verificación o descifrado y descompresión el tiempo máximo entre.

Beneficio : después de la optimización de la instalación y la descarga de transmisión, el tiempo de descarga de los paquetes en línea se reduce en un 21 %.

3. Análisis de implementación

MultiPipe es una clase de herramienta que puede dividir un canal de entrada en múltiples canales de salida. El código de ejemplo es el siguiente:

/**
 * 可以将一个输入通道,分为多个输出通道的管道
 */
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 Cree una lista de tuberías (PipeLineList) y conecte el canal de entrada (ReadableChannel)

El consumidor completa todo el trabajo a través de la conexión. En este método, la lista de canalizaciones y el pestillo se crean primero de acuerdo con la lista de consumidores en el método de construcción, y luego cada canalización se inicia a través del grupo de subprocesos, para que cada tarea comience a funcionar.

(1) Cree una tubería correspondiente (PipeLine) de acuerdo con la cantidad de consumidores y conecte el canal de entrada . La función de latch es garantizar que todas las tareas del consumidor se completen antes de que se complete la ejecución.La instrucción clave es 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) Cree una lista de canalización basada en la cantidad 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())); } devuelve la lista de tuberías;}

(3) Inicie cada tubería a través del grupo de subprocesos

El grupo de subprocesos se puede establecer en un grupo de subprocesos existente. Si desea evitar que se apague el grupo de subprocesos existente, debe volver a escribir el método onFinish y eliminar la instrucción 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 Transferir el contenido leído cada vez a cada tubería (PipeLine)

Al leer el canal de entrada, el búfer de bytes leído cada vez se transmite a cada consumidor.
Reciba el contenido que se puede leer cada vez a través de ByteBuffer, luego recorra la lista de consumidores y escriba el contenido en ByteBuffer en el canal receptor de la canalización.
Finalmente, se cierra el canal del sumidero de cada tubería de consumo.

El progreso actual se puede devolver al usuario a través del 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 Implementación de PipeLine

Cada tarea del consumidor corresponde a una canalización (PipeLine).En el proceso de transmisión del flujo de red, el búfer de bytes leído cada vez se escribe en el canal receptor de cada canalización, y luego el canal de origen de la canalización se proporciona a la tarea del consumidor. .

La implementación de la tubería se puede basar en java.nio.channels.Pipe, o se puede usar okio.Pipe con búfer.
Con un búfer, aumentará el tiempo de transmisión, pero puede evitar el problema de que la velocidad de lectura es lenta debido a un consumo demasiado lento en casos extremos: por ejemplo, la decodificación del consumidor tarda demasiado, lo que hace que TCP juzgue mal que la red está funcionando. no es bueno, y frecuentes retransmisiones de tiempo de espera.

/** * 管线,也作为工作任务将流导给消费者 */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();            }        }    }}

El getPipeMaxBufferBytesmétodo puede hacer referencia a la siguiente implementación, que devuelve una capacidad de búfer segura basada en la memoria disponible actual.

 /** 可用内存的比例 */
  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. Resumen y perspectiva

Resumen : este artículo describe un método de optimización en el enlace de descarga del paquete del subprograma Baidu, es decir, el esquema de instalación y descarga de transmisión. Tomando como pistas el objetivo de aprovechar al máximo los recursos de E/S de la red, E/S locales y CPU, el texto completo analiza los puntos de optimización del esquema original y compara el esquema de optimización con el esquema original. Finalmente, analiza el código de su implementación principal e introduce Una implementación que admite la división de un canal de entrada en múltiples canales de salida.

Mirando hacia el futuro : la solución de instalación de descarga de transmisión requiere descomprimir todo el paquete del subprograma. Mirando hacia el futuro, explore una solución de carga progresiva de recursos. Esta forma de compartir transmisiones se puede usar para descargar de forma asíncrona recursos de baja prioridad tanto como sea posible.

Lectura recomendada:

Una breve discusión sobre la realización de la plataforma intermedia de troncos que no es pesada ni se pierde

Diseño y práctica de la plataforma de autoridad de cuenta vertical Baidu ToB

Métodos de visualización de entrada en Visual Transformer

Comprensión profunda de WKWebView (renderizado) - Construcción del árbol DOM

Comprensión profunda de WKWebView (Introducción): depuración y análisis del código fuente de WebKit

{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

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