Otimização de página de otimização de desempenho do Android

Prefácio:

1. Existem muitos artigos na Internet sobre como otimizar o desempenho da interface, mas geralmente falam sobre como resolver o problema da flutuação da memória. Embora os dois estejam relacionados, eles não podem ser completamente equacionados.A flutuação da memória afeta o desenho da interface até certo ponto, mas é apenas uma das razões e não pode ser confundida. E há muitas introduções sobre desenho excessivo, etc., e todas elas parecem iguais.

2. O objetivo de escrever este artigo é ajudar iniciantes, intermediários, incluindo alguns desenvolvedores Android avançados, a localizar/resolver rapidamente os problemas de travamento de interface encontrados em cenários de aplicativos práticos ou orientar uma direção razoável para resolver o problema. Soluções específicas ainda requerem soluções específicas.

3. Claro, depois de entender completamente o princípio deste artigo, também será útil para entrevistas Android.

1. Como medir se a interface está travada

Existem basicamente três gerações de soluções para medir se a página está travada, embora existam alguns frameworks de monitoramento como o perfdog no meio, eles basicamente pertencem aos princípios dessas três gerações.

1.1 A solução de primeira geração: Looper registra método de retorno de chamada (framework representativo: BlockCanary)

1.1.1 Introdução do princípio:

O princípio central é usar o retorno de chamada no Looper para julgar o tempo de execução da tarefa em cada Mensagem. Se registrarmos que o objeto de retorno de chamada é o Looper da thread principal, podemos saber o tempo de execução de cada tarefa na thread principal. E se uma tarefa demorar muito para ser executada, causará um problema travado.

Portanto, em sentido estrito, essa solução deve ser detectar se o thread principal está travado, não se a interface é desenhada sem problemas.

O princípio específico não será descrito em detalhes aqui. Se você quiser conhecer o princípio, pode consultar o Capítulo 7 do meu outro artigo:

Mecanismo de manipulação de aprendizado de código-fonte do Android e seus seis pontos principais - Compartilhamento + Gravação - CSDN Blog

1.1.2 Como usar:

Existem métodos mais detalhados no BlockCanary, os links são os seguintes:

GitHub - markzhai/AndroidPerformanceMonitor: Uma biblioteca transparente de detecção de bloco de interface do usuário para Android. (conhecido como BlockCanary)

Aqui eu forneço uma versão simples do método use, o efeito é basicamente o mesmo, o código é o seguinte:

public class ANRMonitor extends BaseMonitor {

    final static String TAG = "anr";

    public static void init(Context context) {
        if (true) {
            return;
        }
        ANRMonitor anrMonitor = new ANRMonitor();
        anrMonitor.start(context);
        Log.i(TAG, "ANRMonitor init");
    }

    private void start(Context context) {
        Looper mainLooper = Looper.getMainLooper();
        mainLooper.setMessageLogging(printer);
        HandlerThread handlerThread = new HandlerThread(ANRMonitor.class.getSimpleName());
        handlerThread.start();
        //时间较长,则记录堆栈
        threadHandler = new Handler(handlerThread.getLooper());
    }

    private long lastFrameTime = 0L;
    private Handler threadHandler;
    private long mSampleInterval = 40;

    private Printer printer = new Printer() {
        @Override
        public void println(String it) {
            long currentTimeMillis = System.currentTimeMillis();
            //其实这里应该是一一对应判断的,但是由于是运行主线程中,所以Dispatching之后一定是Finished,依次执行
            if (it.contains("Dispatching")) {
                lastFrameTime = currentTimeMillis;
                //开始进行记录
                return;
            }
            if (it.contains("Finished")) {
                long useTime = currentTimeMillis - lastFrameTime;
                //记录时间
                if (useTime > 20) {
                    //todo 这里超过20毫秒卡顿了
                    Log.i(TAG, "ANR:" + it + ", useTime:" + useTime);
                }
                threadHandler.removeCallbacks(mRunnable);
            }
        }
    };


    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            threadHandler.postDelayed(mRunnable, mSampleInterval);
        }
    };
}

1.1.3 Conclusão:

Desta forma, podemos obter o tempo de execução de cada tarefa de thread principal. De acordo com a atualização padrão de 16ms, cada tarefa de thread principal não deve exceder 16ms, caso contrário, significa que a interface está travada.

1.2 Solução de segunda geração: Coreógrafo registra renderização de retorno de chamada (estrutura representativa: Tencent GT)

O defeito da solução de primeira geração é que o encadeamento principal está preso e esse atraso do encadeamento principal não causa necessariamente um atraso percebido pelo usuário. Por exemplo, se o usuário parar em uma determinada página e não operar, mesmo que o thread principal esteja bloqueado neste momento, o usuário não o sentirá. O desenho de interface que queremos está travado e deve ser direcionado para todo o processo de desenho.

1.2.1 Introdução do princípio:

Todo o processo de desenho da vista é primeiramente notificado do filho para o pai camada por camada, e a camada superior é ViewRootImpl. Após ser notificado ao ViewRootImpl, ele irá criar uma mensagem de desenho de interface e então registrá-la no Choreographer. O coreógrafo usará o mecanismo nativo para garantir que o callback seja uma vez a cada 16ms e, após o callback, o processo de desenho da interface será executado.

O ponto central está no retorno de chamada. O retorno de chamada notifica o método doFrame para desenhar a interface. Existem quatro retornos de chamada no doFrame, dos quais CALLBACK_TRAVERSAL é a notificação real para executar todo o processo de desenho.

try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
 
            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
 
            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
 
            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);//这里是核心
 
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

Assim podemos fazer um pequeno truque para os outros três, por exemplo, podemos registrar o callback de CALLBACK_ANIMATION. Dessa forma, se o retorno de chamada CALLBACK_ANIMATION for uma notificação de retorno de chamada com tempo de 16ms, pode-se provar que CALLBACK_ANIMATION também é uma notificação de retorno de chamada recebida a uma taxa de atualização de 16ms.

Outro artigo meu tem uma introdução mais detalhada. Se você estiver interessado, pode lê-lo:

Aprendizado de código-fonte do Android - Visualize o processo de desenho - Compartilhamento + Gravação - CSDN Blog

1.2.2 Como usar: Podemos implementar uma classe de FPSFrameCallBacl, e então enviar

public class FPSFrameCallback implements Choreographer.FrameCallback {
   private static final String TAG = "FPS_TEST";
    private long mLastFrameTimeNanos = 0;
    private long mFrameIntervalNanos;
    public FPSFrameCallback(long lastFrameTimeNanos) {
        mLastFrameTimeNanos = lastFrameTimeNanos;
        mFrameIntervalNanos = (long)(1000000000 / 60.0);
    }
    @Override
    public void doFrame(long frameTimeNanos) {
        //初始化时间
        if (mLastFrameTimeNanos == 0) {
            mLastFrameTimeNanos = frameTimeNanos;
        }
        final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if(skippedFrames>30){
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
        }
        mLastFrameTimeNanos=frameTimeNanos;
        //注册下一帧回调
        Choreographer.getInstance().postFrameCallback(this);
    }
}

Código de Registo:

 Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));

1.2.3 Conclusão:

A solução de segunda geração atendeu bem às nossas necessidades de avaliar apenas se o desenho da interface é suave. Mas também existem alguns problemas, como os dados não serem intuitivos, o uso de callbacks de animação pode afetar o processo de desenho da animação e assim por diante.

1.3 A solução de terceira geração: retorno de chamada de renderização de registro de janela

As duas primeiras gerações foram pesquisadas pelos próprios desenvolvedores, e a terceira geração é um produto oficial do Google, por isso também é a solução mais confiável com os dados mais intuitivos e completos.

1.3.1 Introdução ao princípio:

Nossa renderização final será convertida em uma tarefa de renderização Renderer por ViewRootImpl, e registrará callbacks com a camada nativa para obter o número de quadros perdidos.

Os detalhes específicos serão explicados em um capítulo separado posteriormente.

1.3.2 Como usar:

getWindow().addOnFrameMetricsAvailableListener(new Window.OnFrameMetricsAvailableListener() {
            @Override
            public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {

            }
        });

1.3.3 Conclusão:

A solução de terceira geração nos ajudou perfeitamente a determinar se está travada, mas não pode nos ajudar a encontrar diretamente a causa do problema, portanto, precisamos usar vários meios para investigar.

2. Os meios de verificar a interface travada

2.1 Razões para Caton:

Caton é dividido principalmente em três categorias na minha opinião,

O primeiro tipo é o problema da CPU, por exemplo, a CPU está sobrecarregada devido à quantidade excessiva de cálculo, de modo que o cálculo normal não pode ser realizado.

O segundo tipo é o problema de GC, quando a máquina virtual tem GC frequente, a tarefa da thread principal será suspensa.

A terceira categoria é o bloqueio do thread principal. Por exemplo, nosso encadeamento principal executa operações demoradas ou adiciona bloqueios inadequados, ou ainda possui um layout complexo ou muitos níveis de aninhamento.

2.2 Opção 1:

Para o problema 1, podemos usar ferramentas como perfdog para ver a taxa de carga da CPU.

2.3 Opção 2:

Em resposta à segunda pergunta, há muitos artigos desse tipo na Internet, e o mainstream é desse tipo, então não vou expandi-lo aqui.

Os leitores podem Baidu

2.4 Opção três:

Para o problema 3, geralmente é a causa que realmente faz com que o desenho da interface congele. Então é nisso que vamos focar. O esquema que eu uso aqui é o esquema de retorno de chamada Looper introduzido em 1.1 acima.

Agora que podemos passar o callback, sabemos o tempo de execução da tarefa de cada thread principal. Em seguida, iniciamos um novo encadeamento durante esse período e despejamos continuamente o status da pilha do encadeamento principal para saber onde o encadeamento principal está bloqueado.

Como exemplo, estou lendo um arquivo no thread principal e demora 100ms. Então eu capturo a pilha da thread principal a cada 20 milissegundos.A pilha de código da pilha da thread principal lendo o arquivo será capturada pelo menos 5 vezes, então podemos saber que existe um problema com esse código.

Como outro exemplo, um RecyclerView carrega centenas de itemViews em uma interface. Quando você entra na interface pela primeira vez, há uma alta probabilidade de que ela fique travada. Neste momento, descobriremos que a maioria das pilhas de código imprime o método onCreateViewHolder(). Quanto mais frequente, maior a probabilidade de ocorrência. Saberemos que é causado pela criação frequente de ViewHolder.

Abaixo está o código da ferramenta que escrevi para simplesmente solucionar o problema travado:

public class ANRMonitor extends BaseMonitor {

    final static String TAG = "anr";

    public static void init(Context context) {
        //开关
        if (true){
            return;
        }
        ANRMonitor anrMonitor = new ANRMonitor();
        anrMonitor.start(context);
        Log.i(TAG, "ANRMonitor init");
    }

    private void start(Context context) {
        Looper mainLooper = Looper.getMainLooper();
        mainLooper.setMessageLogging(printer);
        HandlerThread handlerThread = new HandlerThread(ANRMonitor.class.getSimpleName());
        handlerThread.start();
        //时间较长,则记录堆栈
        threadHandler = new Handler(handlerThread.getLooper());
        mCurrentThread = Thread.currentThread();
    }

    private long lastFrameTime = 0L;
    private Handler threadHandler;
    private long mSampleInterval = 40;
    private Thread mCurrentThread;//主线程
    private final Map<String, String> mStackMap = new HashMap<>();

    private Printer printer = new Printer() {
        @Override
        public void println(String it) {
            long currentTimeMillis = System.currentTimeMillis();
            //其实这里应该是一一对应判断的,但是由于是运行主线程中,所以Dispatching之后一定是Finished,依次执行
            if (it.contains("Dispatching")) {
                lastFrameTime = currentTimeMillis;
                //开始进行记录
                threadHandler.postDelayed(mRunnable, mSampleInterval);
                synchronized (mStackMap) {
                    mStackMap.clear();
                }
                return;
            }
            if (it.contains("Finished")) {
                long useTime = currentTimeMillis - lastFrameTime;
                //记录时间
                if (useTime > 20) {
                    //todo 要判断哪里耗时操作导致的
                    Log.i(TAG, "ANR:" + it + ", useTime:" + useTime);
                    //大于100毫秒,则打印出来卡顿日志
                    if (useTime > 100) {
                        synchronized (mStackMap) {
                            Log.i(TAG, "mStackMap.size:" + mStackMap.size());
                            for (String key : mStackMap.keySet()) {
                                Log.i(TAG, "key:" + key + ",state:" + mStackMap.get(key));
                            }
                            mStackMap.clear();
                        }
                    }
                }
                threadHandler.removeCallbacks(mRunnable);
            }
        }
    };


    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            doSample();
            threadHandler
                    .postDelayed(mRunnable, mSampleInterval);
        }
    };

    protected void doSample() {
        StringBuilder stringBuilder = new StringBuilder();

        for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
            stringBuilder
                    .append(stackTraceElement.toString())
                    .append("\n");
        }
        synchronized (mStackMap) {
            mStackMap.put(mStackMap.size() + "", stringBuilder.toString());
        }
    }

}

3. Resolva o caso real de interface travada

3.1 Caso 1. Resolva computação de CPU de alta frequência por meio de otimização de algoritmo

Requisitos e cenários de problemas:

Os dados da página são recebidos continuamente via Bluetooth, combinados com a interpretação consultada no banco de dados local, e finalmente emendados no Modelo final para renderização. Verificou-se que a taxa de atualização não atingiu a velocidade esperada e não seguiu o exemplo.

Processo de investigação:

Através das ferramentas em 2.4, verifica-se que a thread principal não está bloqueada. Mas a operação de deslizamento parece um pouco travada, o que não é suave. Através do perdog, encontrou alto uso da CPU. Portanto, inicialmente suspeitei que isso fosse causado por operações complexas, então verifiquei o código e descobri que havia loops for de duas camadas e uma grande quantidade de dados coletados.

solução:

1. Esquema 1, algoritmo de otimização

A seguir está a estrutura de código antiga, insira 1.000 valores, corresponda ao valor apropriado de 10W de dados e, em seguida, pegue os primeiros 100.

private List<String> show() {
        List<Model> list = new ArrayList<>();//长度10W
        List<String> input = new ArrayList<>();//长度1000
        
        List<String> showList = new ArrayList<>();
        for (String key : input) {
            for (Model model : list) {
                if (model.name.equals(key)) {
                    showList.add(model.value);
                }
            }
        }
        return showList.subList(0, 100);
    }

Em seguida, primeiro convertemos a lista mais longa em map e, em seguida, pulamos para fora do loop depois de obter 100. A eficiência computacional é muito melhorada.

O código otimizado é o seguinte:

//优化后代码
    private List<String> show2() {
        List<Model> list = new ArrayList<>();//长度10W
        List<String> input = new ArrayList<>();//长度1000

        Map<String, Model> cache = new HashMap<>();
        for (Model model : list) {
            cache.put(model.name, model);
        }
        List<String> showList = new ArrayList<>();
        for (int i = 0; i < Math.min(input.size(), 100); i++) {
            Model model = cache.get(input.get(i));
            showList.add(model.value);
        }
        return showList.subList(0, 100);
    }

2. Opção 2, mude para JNI para realizar ou tentar a operação de bits

Se o algoritmo de código em si não tiver espaço para otimização e houver muitas operações de negócios e o valor de saída final não for muito, você pode considerar mudar para JNI para implementação ou converter para operações de bits para melhorar a eficiência, e não darei exemplos aqui.

3.2 Caso 2. Problema de bloqueio de thread principal

Requisitos e cenários de problemas:

Entrando em uma página, descobri que toda vez que entrava, havia uma clara sensação de gagueira.

Processo de investigação:

Através das ferramentas em 2.4, foi constatado que havia um grande número de pilhas de código no log, então suspeitava-se que o problema estava aqui. O posicionamento final é causado pela operação de IO da rosca principal.

solução:

1. Esquema 1, carregamento assíncrono

IO é uma operação demorada, use um thread para ler e notifique a linha principal para atualizar a interface do usuário após a conclusão da leitura:

 //3.2案例 优化代码
        new Thread(() -> {
            String s = "";
            try {
                InputStream is = getAssets().open("content.txt");
                List<String> strings = IOHelper.readListStrByCode(is, "utf-8");
                s = strings.get(0);
            } catch (IOException e) {
                Log.i("lxltest", e.getMessage());
                e.printStackTrace();
            }
            String show = s;
            handler.post(() -> title.setText(show));
        }).start();

PS: Para manter o código conciso e intuitivo, não há necessidade de usar o pool de threads, assim como os cenários subsequentes.

 link de código de amostra completo

android_all_demo/PerformanceCaseActivity.java no master · aa5279aa/android_all_demo · GitHub

3.3 Caso 3. Resolva o problema de atualização de alta frequência do RecyclerView

Requisitos e cenários de problemas:

O requisito é muito simples, semelhante a olhar o preço das ações, solicitar um serviço a cada 100 milissegundos e, em seguida, buscar os dados retornados e exibi-los ao usuário.

O código simples é o seguinte:

    RecyclerView recyclerView;
    ModelAdapter adapter;
    boolean flag = true;
    Handler handler = new Handler();

    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler);
        recyclerView = findViewById(R.id.recycler_view);
        new Thread(() -> {
            while (flag) {
                List<Map<String, String>> data = getResponse();
                notifyData(data);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void notifyData(List<Map<String, String>> data) {
        handler.post(() -> {
            adapter.data = data;
            adapter.notifyDataSetChanged();
        });
    }

No entanto, descobrimos na operação real que deslizar para cima e para baixo neste cenário está travado.

Processo de investigação:

Primeiro, usamos as ferramentas fornecidas no artigo 2.4 para verificar e descobrimos que havia muitas pilhas, conforme mostrado na figura a seguir:

Assim sabemos a causa do problema, que é o congelamento causado pela frequente criação do ViewHolder. Então, por que criar ViewHolder com frequência? Com essa pergunta em mente, nos aprofundamos no código-fonte do RcyclerView e finalmente soubemos o motivo. Toda vez que o método notifyDataSetChanged() é chamado, uma operação de reciclagem é acionada. Como o número padrão de caches no RecyclerBin é 5 e exibimos 15 dados em uma página, 10 desses 15 ItemViews são liberados.

solução:

1. Opção 1: Altere o tamanho do cache

Faça o seguinte, aumente o número de caches e o problema será resolvido.

recyclerView.getRecycledViewPool().setMaxRecycledViews(0,15);

2. Opção 2: Algoritmo de Otimização

Claro, também podemos resolver o problema através do nível de dados, os dados retornados pelo serviço, combinados com o cálculo dos dados existentes, calcular quais dados foram alterados e atualizar apenas os dados que foram alterados.

link de código de amostra completo

android_all_demo/PerformanceCaseActivity.java no master · aa5279aa/android_all_demo · GitHub

3.4 Caso 4. Página complicada entra em Caton pela primeira vez

Requisitos e cenários de problemas:

Uma página com muitos elementos e uma interface complexa, quando clicamos para entrar pela primeira vez, descobrimos que levaria de 1 a 2 segundos para entrar depois de clicar, o que dava às pessoas a sensação de obviamente não seguir.

Processo de investigação:

Ou através da ferramenta ANRMonitor, analisamos o log para ver o que o causou.

No final, descobrimos que no log, a pilha mais demorada foi impressa no método setContentView. Então significa que é demorado criar o layout.

solução:

1. Opção 1, pré-carregamento

Em geral, páginas complexas não serão páginas iniciais. Assim, podemos usar o pré-carregamento para converter xml em View com antecedência antes de entrar na página complexa. Quando uma página complexa está onCreate, julga-se se há uma View correspondente no cache, caso exista, será utilizada no cache e, caso não exista, será criada.

Adicionar cache:

 private var cacheMap = HashMap<String, View>()

    fun addCachePageView(pageClassName: String, layoutId: Int) {
        if (cacheMap[pageClassName] != null) {
            return
        }
        val context = DemoApplication.getInstance()
        val inflate = View.inflate(context, layoutId, null)
        inflate.measure(1, 1)
        cacheMap[pageClassName] = inflate
    }

Usar cache:

View cachePageView = PageViewCache.Companion.getInstance().getCachePageView(PrepareMiddleActivity.class.getName());
        if (cachePageView != null) {
            setContentView(cachePageView);
        } else {
            setContentView(R.layout.prepare_middle_page);
        }

Link de código de amostra completo https://github.com/aa5279aa/android_all_demo/blob/master/DemoClient/app/src/main/java/com/xt/client/activitys/PrepareActivity.kt

3.5 Caso 5. Resolva a atualização de formas de onda de alta frequência

Requisitos e cenários de problemas:

O diagrama de efeito é mostrado na figura abaixo, e a frequência de atualização de cada diagrama de forma de onda deve atingir mais de 10 vezes por segundo.

Neste momento, descobrimos que, embora notifiquemos a atualização de acordo com o método de 10 vezes por segundo, na verdade, ela só pode ser atualizada de 2 a 3 vezes por segundo.

Processo de investigação:

Usando as ferramentas fornecidas pelo 2.4, descobri que o motivo do atraso é que existem dois grandes consumos de tempo:

1. Cálculo de coordenadas de dados de cada gráfico,

2. medida, layout e outros processos.

solução:

1. Esquema 1, desacoplamento de dados e renderização

Podemos iniciar uma thread para realizar o cálculo de coordenadas e colocar os dados calculados no cache.

No encadeamento principal, os dados calculados são obtidos do cache em intervalos regulares de 100 milissegundos e renderizados diretamente. Desta forma, os dados são desacoplados da renderização.

2. Transfira o surfaceView para alcançar

Como é uma exibição personalizada, ela também pode ser implementada no SurfaceView para aproveitar ao máximo o desempenho da GPU.

link de código de amostra completo

Este pedaço de código não foi desclassificado, portanto, ainda não é de código aberto. Alguns códigos de exemplo são fornecidos para referência:

public class DataSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
...省略代码
 /**
     * 绘制方波图和波形图
     */
    protected void drawArc(Canvas canvas) {
        //长度不对等或者没有初始化,则不绘制
         if (mShowPointList.length != mSettings.length || !isInit) {
            return;
        }
//        canvas.save();
        float startX = mMargin;
        float startY = mMargin - mOffset;
        RectF rectF;
        for (int index = 0; index < mShowPointList.length; index++) {
            List<Integer> integers = mShowPointList[index];
            ShowPointSetting setting = mSettings[index];
            int count = integers.size();
            if (setting.showType == ShowConstant.ShowTypeWave) {
                count--;
            }
            float itemWidth = itemCoordWidth / count;//每一个的宽度
            //绘制背景  mBgPaint
            rectF = new RectF(startX, startY, startX + itemViewWidth - mMargin * 2, startY + itemViewHeight - mMargin * 2);
            if (mIndex == index) {
                canvas.drawRoundRect(rectF, mItemRound, mItemRound, mBgClickPaint);
            } else {
                canvas.drawRoundRect(rectF, mItemRound, mItemRound, mBgPaint);
            }

            float itemX = startX + mItemPadding;
            float itemY = startY + mItemPadding;
            float nextY = 0;
            float[] pts = new float[integers.size() * 8 - 4];
            for (int innerIndex = 0; innerIndex < count; innerIndex++) {
                Integer value = integers.get(innerIndex);
                if (value != null) {
                    value = value > setting.showMaxValue ? setting.showMaxValue : value;
                    itemY = startY + mItemPadding + mItemTopHeight + itemCoordHeight - itemCoordHeight * value / setting.showMaxValue;
                    if (setting.showType == ShowConstant.ShowTypeSquare) {
                        pts[innerIndex * 8 + 0] = itemX;
                        pts[innerIndex * 8 + 1] = itemY;
                        pts[innerIndex * 8 + 2] = itemX + itemWidth;
                        pts[innerIndex * 8 + 3] = itemY;
                    }
                }
                itemX = itemX + itemWidth;
                //方形图逻辑
                if (setting.showType == ShowConstant.ShowTypeSquare) {
                    if (innerIndex != count - 1) {
                        Integer nextValue = integers.get(innerIndex + 1);
                        if (value != null && nextValue != null) {
                            nextValue = nextValue > setting.showMaxValue ? setting.showMaxValue : nextValue;
                            nextY = startY + mItemPadding + mItemTopHeight + itemCoordHeight - itemCoordHeight * nextValue / setting.showMaxValue;
                            pts[innerIndex * 8 + 4] = itemX;
                            pts[innerIndex * 8 + 5] = itemY;
                            pts[innerIndex * 8 + 6] = itemX;
                            pts[innerIndex * 8 + 7] = nextY;
                        }
                    } else {
                        //绘制坐标
                        canvas.drawText(String.valueOf(innerIndex + 2), itemX - 5, startY + mItemPadding + mItemTopHeight + itemCoordHeight + 20, mFontPaint);
                    }
                } else if ((setting.showType == ShowConstant.ShowTypeWave)) {
                    if (value != null && integers.get(innerIndex + 1) != null) {
                        nextY = startY + mItemPadding + mItemTopHeight + itemCoordHeight - itemCoordHeight * integers.get(innerIndex + 1) / setting.showMaxValue;
                        pts[innerIndex * 8 + 4] = itemX - itemWidth;
                        pts[innerIndex * 8 + 5] = itemY;
                        pts[innerIndex * 8 + 6] = itemX;
                        pts[innerIndex * 8 + 7] = nextY;
                    }
                    if (innerIndex == count - 1) {
                        //绘制坐标
                        canvas.drawText(String.valueOf(innerIndex + 2), itemX - 5, startY + mItemPadding + mItemTopHeight + itemCoordHeight + 20, mFontPaint);
                    }
                }
                //绘制坐标
                canvas.drawText(String.valueOf(innerIndex + 1), itemX - itemWidth - 5, startY + mItemPadding + mItemTopHeight + itemCoordHeight + 20, mFontPaint);

//                mWaveShadowPaint.set
                //渐变色
//                canvas.drawRect(itemX - itemWidth, itemY, itemX, startY + mItemPadding + mItemTopHeight + itemCoordHeight, mWaveShadowPaint);

                //绘制虚线
                canvas.drawLine(itemX, startY + mItemPadding + mItemTopHeight, itemX, startY + mItemPadding + mItemTopHeight + itemCoordHeight, mEffectPaint);
            }
            //绘制最大值
            if (!StringUtil.emptyOrNull(setting.showMaxValueShow)) {
                canvas.drawText(setting.showMaxValueShow, startX + mItemPadding + 5, startY + mItemPadding + mItemTopHeight + 30, mFontPaint);//ok
            }

            //todo 绘制描述
            canvas.drawText(setting.showDesc, startX + mItemPadding, startY + mItemPadding + 30, mDescPaint);

            //todo 描述当前值
            String currentStr = String.valueOf(mCurrents[index]);
            float v = mCurrentPaint.measureText(currentStr);
            canvas.drawText(currentStr, startX + mItemPadding + itemCoordWidth - v, startY + mItemPadding + 30, mCurrentPaint);

            //绘制方形图的线
            canvas.drawLines(pts, mWavePaint);

            if (index % mAttr.widthCount == (mAttr.widthCount - 1)) {
                startX = mMargin;
                startY += itemViewHeight;
            } else {
                startX += itemViewWidth;
            }
        }
//        canvas.restore();
    }
}

3.6 Caso 6. Otimização de página para carregar centenas de dados em uma tela

Requisitos e cenários de problemas:

Em alguns cenários, precisamos carregar uma grande quantidade de dados em uma página para que os usuários vejam. Conforme mostrado na figura abaixo, uma grande quantidade de dados é exibida em uma tela. Acabou sendo implementado usando o RecyclerView. Descobrimos que toda vez que você entra nesta página, leva de 2 a 3 segundos para entrar. Como mostrado abaixo:

Processo de investigação:

Também é verificado com a ferramenta de detecção.Descobrimos que a maioria das pilhas demoradas são exibidas pelo método onCreateViewHolder. Com centenas de itemViews por metro quadrado, centenas de ItemViews precisam ser criados, o que é naturalmente uma operação demorada. É claro que não é apenas demorado criar um itemView, centenas de itemViews precisam executar métodos como medida, layout e desenho, e o consumo de tempo também é bastante grande.

solução:

Opção 1, Visualização Personalizada

Criar centenas de Views e renderizar centenas delas deve ser demorado. Podemos apenas criar um, renderizar um e exibir todos os dados. Claro, a solução é customizar a View.

Na visualização personalizada, podemos calcular a posição de cada dado de item separadamente, que é desenhado diretamente com a tela. Desta forma, apenas um layout precisa ser criado, e cada medida/layout/desenho é executado apenas uma vez. Na prática, a eficiência foi muito melhorada.

Link para o código de exemplo completo:

O código específico do View personalizado não será postado. Você pode consultar um dos meus projetos de código aberto, que terá implementações de funções mais detalhadas.

GitHub - September26/ExcelView: Um projeto android, que é implementado imitando as funções do excel no WPS, e posterior expansão funcional.

3.7 Caso 7. Otimização de páginas complexas de tela longa

Requisitos e cenários de problemas:

Uma página com conteúdo complexo, em primeiro lugar, a camada externa é o RecyclerView, que contém vários módulos. Em cada módulo, o RecyclerView contém vários controles finais.

Em primeiro lugar, o layout em cascata é usado, portanto, as alterações de controle finais afetarão o layout do módulo geral.

No final, descobrimos que, quando os dados eram alterados e atualizados com frequência, a página não era suave e havia uma clara sensação de atraso. 

Processo de investigação:

Da mesma forma, quando usamos a ferramenta de detecção para detectar, descobrimos que a maioria das pilhas foi impressa no processo de notificação. Assim, podemos simplesmente inferir que o atraso é causado por chamar muitos notifyChangeds.

solução:

Então, como reduzir o número de notifyChanged? Tecnicamente, parece não haver nada para otimizar, a menos que todos sejam implementados com visualizações personalizadas, mas o custo de desenvolvimento é muito alto.

Opção 1, notificar apenas quando os dados forem alterados

Podemos comparar os dados antigos e novos para conhecer os dados que foram alterados e apenas notificar e atualizar esses dados.

Opção 2, atualização nível a nível

O RecyclerView de dois níveis, se possível, deve ser atualizado naturalmente para obter a menor granularidade. Podemos dividir as alterações de dados em dois tipos. Uma é fazer com que a altura do módulo mude, e a outra é afetar apenas sua própria linha.

Para o primeiro, posso chamar a notificação de atualização usando o adaptador correspondente ao RecyclerView externo.

mRecyclerViewAdapt.data[positionMajor].items.clear()
mRecyclerViewAdapt.data[positionMajor].items.addAll(arrayList)
mRecyclerViewAdapt.notifyItemChanged(positionMajor)

Em segundo lugar, precisamos apenas obter a atualização de notificação do adaptador correspondente ao RecyclerView interno.

val recyclerView: RecyclerView = mRecyclerViewAdapt.getViewByPosition(positionMajor, R.id.recycler_view) as RecyclerView
val adapter = recyclerView.getTag(R.id.tag_adapter)  //as GasWidgetDashboard.MultipleItemQuickAdapter
when (adapter) {
        is GasWidgetDashboard.MultipleItemQuickAdapter -> adapter.notifyItemChanged(positionMinor)
        is GasWidgetForm.QuickAdapter                  -> adapter.notifyItemChanged(positionMinor)
}

4. Resumo

4.1. Encontre a causa primeiro e depois resolva-a

Existem várias otimizações de desempenho de interface, mas o mais importante é encontrar a causa do problema. Em seguida, discuta como resolver o problema de desempenho de acordo com o motivo. Copiar cegamente vários modos de otimização não funcionará. Por exemplo, é obviamente uma operação de E/S do thread principal que causa o congelamento, mas otimizando cegamente a memória, é naturalmente impossível resolver o problema.

4.2. Pontos de conhecimento necessários para otimização do desempenho da interface

Se você deseja resolver o problema de desempenho da interface perfeitamente, ainda precisa ter uma certa reserva de conhecimento. Com essa reserva de conhecimento, você pode nos ajudar a solucionar o problema rapidamente e encontrar uma solução razoável.

4.2.1 Compreender o mecanismo do Handler

Isso pode nos ajudar a solucionar o problema do encadeamento principal travado.

4.2.2 Todo o processo de desenho do View também precisa ser claro

Desde a notificação de alteração de dados, até a medição de cada View filho, etc.

4.3.3 Os princípios de implementação de contêiner no nível do ViewGroup comumente usados ​​devem ser dominados

Este tipo de contêiner, como RecyclerView, RelativeLayout, ConstraintLayout e assim por diante.

4.3.4 Finalmente, algumas habilidades de raciocínio lógico e abstrato também são necessárias.

As Views personalizadas exemplificadas acima de tudo requerem uma certa capacidade de abstração e capacidade de raciocínio lógico para saber como implementá-las.

O acima são minhas sugestões pessoais, se você estiver interessado, você pode se preparar de acordo.

5. Observações

5.1 Declaração

Como a solução otimizada neste artigo é baseada em projetos existentes da empresa, para evitar o vazamento de conteúdo sensível, o código e as legendas relevantes são demonstrados por demonstração, o que leva a algumas interfaces feias.

5.2 Este artigo envolve o endereço do projeto

https://github.com/sollyu/3a5fcd5eeeb90696

https://github.com/aa5279aa/android_all_demo

https://github.com/aa5279aa/CommonLibs

5.3 Links para referências citadas neste artigo:

Mecanismo de manipulação de aprendizado de código-fonte do Android e seus seis pontos principais - Compartilhamento + Gravação - CSDN Blog

5.4 Agradecimentos

grato

@sollyu (sollyu) · GitHub

Apoio para a criação deste artigo.

Autor GitHub: @  https://github.com/aa5279aa/

Acho que você gosta

Origin blog.csdn.net/AA5279AA/article/details/123252533
Recomendado
Clasificación