O framework de desenvolvimento da camada de aplicativo Android deve saber

Autor: Takayuki por favor responda

prefácio

Acredito que os alunos que fazem desenvolvimento de negócios na camada de aplicativos, como eu, "odeiam" o Framework. De fato, se estivermos desenvolvendo a camada de aplicativos durante a semana, basicamente raramente tocamos no conhecimento do Framework. Mas devido à pressão da vida, as entrevistas são sempre inevitáveis, portanto, como trabalhador migrante qualificado, devemos ter algum conhecimento básico do Framework.

Existem muitos artigos na Internet que falam sobre Framework de uma forma superficial ou profunda.Existem muitos códigos-fonte que você já deve ter visto mais de uma ou duas vezes, mas vai esquecer depois de um tempo. Neste artigo, listarei o conhecimento básico do Framework que acho que precisa ser dominado do ponto de vista do desenvolvimento da camada de aplicativo. Para tornar mais fácil para todos memorizar e consolidar, explicarei mais sobre o processo, em vez de uma interpretação muito aprofundada do código-fonte. O artigo será relativamente longo. Você pode usá-lo como um livreto de conhecimento e levar leitura direcionada de acordo com o diretório Navegar (baseado no Android 8.0/9.0).

Não há muito a dizer, vamos nos apressar!

1. Processo de inicialização do sistema

Processo do zigoto

O processo Zygote é um processo muito importante, que é responsável pela criação da máquina virtual (DVM ou ART) no sistema Android, a criação do processo de aplicação e a criação do processo SystemServer.

Depois que o sistema for ligado, o processo init será iniciado e o processo init irá desmembrar o processo Zygote. Quando o processo do zigoto começa, ele faz principalmente as seguintes coisas:

  1. Crie uma máquina virtual. Quando o Zygote cria o processo de aplicativo e o processo do SystemServer à sua maneira, eles podem obter uma cópia da máquina virtual.
  2. Como um servidor Socket para monitorar o processo de criação de solicitação AMS
  3. Inicie o processo SystemServer

Processo SystemServer

O processo SystemServer é o processo de nível de sistema mais importante em nosso Framework de aprendizado. Muitos serviços que conhecemos (AMS, WMS, PMS, etc.) pertencem a um serviço fornecido por ele e é o processo que se comunica com nosso APP processo com mais frequência.

Quando o processo do SystemServer é iniciado, ele faz principalmente o seguinte:

  1. Inicie o pool de encadeamentos do Binder para se preparar para a comunicação entre processos
  2. Inicie vários serviços do sistema, como AMS, WMS, PMS

processo de inicialização

O processo Launcher é, na verdade, nosso processo de desktop. Os alunos que estão familiarizados com ele sabem que uma página de desktop é essencialmente um RecylerView do tipo Grid, então como ele é criado?

Esta é a última etapa da inicialização do sistema.Depois que o processo SystemServer iniciar o processo AMS, o processo AMS iniciará o processo Launcher. O processo Launcher se comunica com o processo PMS, obtém todas as informações do pacote de instalação na máquina e processa os dados (ícone do APP, nome do APP, etc.) na área de trabalho.

resumo:

O processo de inicialização do sistema pode ser resumido pela figura a seguir:

2. Processo de inicialização do processo de inscrição

Anteriormente, falamos sobre como o processo do SystemServer e o processo da área de trabalho são iniciados. Depois de iniciados, podemos iniciar nosso processo de aplicativo a partir da área de trabalho.

Depois de clicar no ícone do APP na área de trabalho, o processo do Launcher solicitará que o AMS crie a página de inicialização do APP. Após o AMS reconhecer que o processo APP não existe, ele usará o método Socket para solicitar ao processo Zygote a criação de um processo APP. Depois que o Zygote criar o processo APP por bifurcação, ele refletirá o método chamado android.app.ActivityThreade main()inicializará o ActivityThread.

ActivityThread pode ser entendida como uma classe que gerencia as tarefas da thread principal do APP. Ao main()inicializar no método, vamos inicializar o loop de mensagem da thread principal, de forma a receber todas as mensagens que a thread principal precisa processar, e garantir a renderização da UI na thread principal (o mecanismo da mensagem será descrito em detalhes mais tarde).


public static void main(String[] args) {
    ...
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);//注释1

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler(); //注释2
    }

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();
}

private void attach(boolean system, long startSeq) {
    ...
    if (!system) {
        ...
        final IActivityManager mgr = ActivityManager.getService();
        try {
            mgr.attachApplication(mAppThread, startSeq); // 注释3
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
        ...
    } else {
        ...
        try {
            mInstrumentation = new Instrumentation();
            mInstrumentation.basicInit(this);
            ContextImpl context = ContextImpl.createAppContext(
                    this, getSystemContext().mPackageInfo);
            mInitialApplication = context.mPackageInfo.makeApplication(true, null);
            mInitialApplication.onCreate();
        } catch (Exception e) {
            throw new RuntimeException(
                    "Unable to instantiate Application():" + e.toString(), e);
        }
    }
    ...
}

Você pode ver que o thread principal Handler será criado na Nota 2. O nome da classe Handler aqui Hpertence à classe interna de ActivityThread. Ele será mencionado várias vezes mais tarde.

Ao chamar o método na Nota 1 attach, o valor entrado system参数é false, então ele irá para o código da Nota 3, e aqui ele retorna para o AMS, chamando o attachApplicationmétodo AMS. attachApplicationO método sempre será chamado para ApplicationThreado bindApplicationmétodo e, finalmente, para ActivityThread.handleBindApplicationo método:

private void handleBindApplication(AppBindData data) {
    ...
    try {
        final ClassLoader cl = instrContext.getClassLoader();
        // 注释1
        mInstrumentation = (Instrumentation)
            cl.loadClass(data.instrumentationName.getClassName()).newInstance();
    } catch (Exception e) {
        throw new RuntimeException(
            "Unable to instantiate instrumentation "
            + data.instrumentationName + ": " + e.toString(), e);
    }

    final ComponentName component = new ComponentName(ii.packageName, ii.name);
    mInstrumentation.init(this, instrContext, appContext, component,
            data.instrumentationWatcher, data.instrumentationUiAutomationConnection);
    ...
    
    try {
        ...
        try {
            // 注释2
            app = data.info.makeApplication(data.restrictedBackupMode, null);
            ...
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    // 注释3
                    installContentProviders(app, data.providers);
                    ...
                }
            }
            // 注释4
            mInstrumentation.onCreate(data.instrumentationArgs);
        } catch (Exception e) {
            throw new RuntimeException(
                "Exception thrown in onCreate() of "
                + data.instrumentationName + ": " + e.toString(), e);
        }
        try {
            // 注释5
            mInstrumentation.callApplicationOnCreate(app);
        } catch (Exception e) {
            if (!mInstrumentation.onException(app, e)) {
                throw new RuntimeException(
                  "Unable to create application " + app.getClass().getName()
                  + ": " + e.toString(), e);
            }
        }
    }
    ...
}

A nota 1 será inicializada Instrumentation, e a classe Instrumentation é muito importante, que será apresentada posteriormente. Aqui, ele é usado principalmente para criar aplicativos na Nota 2, Nota 4 e Nota 5 e chamar o ciclo de vida onCreate do Aplicativo. Ao criar um aplicativo, attachBaseContexto método do aplicativo será chamado primeiro. Além disso, podemos ver na Nota 3 que conforme o processo de candidatura for iniciado, os ContentProviders também serão iniciados .

resumo:

O processo de inicialização do processo de aplicação pode ser resumido pela figura a seguir:

Três, processo de inicialização da atividade

Na seção anterior, iniciamos o processo de APP com sucesso, mas nossa página ainda não foi iniciada. Nesta seção, falaremos sobre como a atividade é iniciada.

Lembremos que na seção anterior, o processo Launcher primeiro solicitou ao AMS para criar a página de inicialização do APP, então a área de trabalho do processo Launcher é na verdade um Activtiy, que inicia nossa página de inicialização e chama o método Activity#startActivity(). Depois de ir camada por camada, podemos descobrir que o método é realmente Instrumentationchamado execStartActivity():

#Activity

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
        @Nullable Bundle options) {
    if (mParent == null) { //注释1
        options = transferSpringboardActivityOptions(options);
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options);  //注释2
        ……
    } else {
        if (options != null) {
            mParent.startActivityFromChild(this, intent, requestCode, options);
        } else {
            // Note we want to go through this method for compatibility with
            // existing applications that may have overridden it.
            mParent.startActivityFromChild(this, intent, requestCode);
        }
    }
}

Como você pode ver na Nota 1, mParent representa a página anterior. Quando a página inicial é aberta, mParent é nulo, e o método execStartActivity() de Instrumentação na Nota 2 é chamado. A instrumentação não apenas executa startActivity, mas também é responsável por todas as chamadas do ciclo de vida da Activity:

[Falha na transferência da imagem do link externo, o site de origem pode ter um mecanismo de link anti-roubo, é recomendável salvar a imagem e carregá-la diretamente (img-9Oz7klLG-1689227673647)(https://upload-images.jianshu.io/ upload_images/23087443-fa3785fb293e8509.png ?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

Mas não é um executor real, é apenas embrulhado, por que embrulhar assim? A compreensão pessoal é porque a Instrumentação precisa monitorar esses comportamentos, e suas variáveis ​​de membro mActivityMonitorssão para essa finalidade.

Vamos entrar e ver o execStartActivity()método de instrumentação:

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
    ……
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityManager.getService()
            .startActivity(whoThread, who.getBasePackageName(), intent,
                    intent.resolveTypeIfNeeded(who.getContentResolver()),
                    token, target != null ? target.mEmbeddedID : null,
                    requestCode, 0, null, options); //注释1
        checkStartActivityResult(result, intent);
    } catch (RemoteException e) {
        throw new RuntimeException("Failure from system", e);
    }
    return null;
}

A nota 1 realmente obtém ActivityManager.getService()o startActivity para chamar. Então, ActivityManager.getService()onde está isso sagrado? ir mais para baixo

private static final Singleton<IActivityManager> IActivityManagerSingleton =
        new Singleton<IActivityManager>() {
            @Override
            protected IActivityManager create() {
                final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);//注释1
                final IActivityManager am = IActivityManager.Stub.asInterface(b);
                return am;
            }
        };

Na Nota 1, podemos ver ActivityManager.getService()o que finalmente conseguimos IActivityManager. Este código usa AIDL (alunos que não conhecem podem aprender sozinhos) e obtêm o objeto proxy do AMS no processo do APP IActivityManager. Então a startActivity que finalmente é chamada é a startActivity do AMS.

O AMS também realizará um link relativamente longo no processo de startActivity, principalmente para verificar permissões, processo ActivityRecord, pilha de tarefas Activity, etc. Não entrarei em detalhes aqui e, eventualmente, chamarei o método app.thread.scheduleLaunchActivity. app.threadNa verdade IApplicationThread, como o IActivityManager anterior, também é um objeto proxy e o proxy é o processo do aplicativo ApplicationThread. ApplicationThreadPertence à classe interna de ActivityThread, podemos ver seu scheduleLaunchActivitymétodo:

@Override

public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,

    ActivityInfo info, Configuration curConfig, Configuration overrideConfig,

    CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,

    int procState, Bundle state, PersistableBundle persistentState,

    List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,

    boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

    updateProcessState(procState, false);

    ActivityClientRecord r = new ActivityClientRecord(); // 注释1

    r.token = token;

    r.ident = ident;

    ...

    updatePendingConfiguration(curConfig);

    sendMessage(H.LAUNCH_ACTIVITY, r); // 注释2

}

A Nota 1 encapsula os parâmetros para iniciar a Atividade em ActivityClientRecord. Nota 2 para enviar uma mensagem para H. O motivo para enviá-lo Hé porque o próprio ApplicationThread é um objeto Binder e está scheduleLaunchActivityno encadeamento Binder quando é executado, portanto, precisamos Halternar para o encadeamento principal.

HQuando uma mensagem é recebida LAUNCH_ACTIVITY, o método é chamado handleLaunchActivity:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {

    ...

    Activity a = performLaunchActivity(r, customIntent);//注释1

    if (a != null) {

        r.createdConfig = new Configuration(mConfiguration);

        reportSizeConfigurations(r);

        Bundle oldState = r.state;

        handleResumeActivity(r.token, false, r.isForward,//注释2

        !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

    if (!r.activity.mFinished && r.startsNotResumed) {
        ...

        performPauseActivityIfNeeded(r, reason);//注释3

    } else {

       ...

    }

}

Olhando para o código da Nota 2 e Nota 3, você pode imaginar que eles devem estar relacionados ao ciclo de vida da Activity, e aqui também explica porque o ciclo de vida onPause da página anterior é chamado após o onResume da próxima página. O método da nota 1 performLaunchActivityé muito importante, vamos dar uma olhada:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

...

    try {
        // 注释1

        java.lang.ClassLoader cl = appContext.getClassLoader();

        activity = mInstrumentation.newActivity(

        cl, component.getClassName(), r.intent);

        ...

    } catch (Exception e) {

        ...

    }

    try {

        Application app = r.packageInfo.makeApplication(false, mInstrumentation); // 注释2

        ...
        // 注释3

        activity.attach(appContext, this, getInstrumentation(), r.token,

        r.ident, app, r.intent, r.activityInfo, title, r.parent,

        r.embeddedID, r.lastNonConfigurationInstances, config,

        r.referrer, r.voiceInteractor, window, r.configCallback);

        ...

        int theme = r.activityInfo.getThemeResource();

        if (theme != 0) {

            activity.setTheme(theme); // 注释4

        }

        activity.mCalled = false;
        
        // 注释5
        if (r.isPersistable()) {

            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);

        } else {

            mInstrumentation.callActivityOnCreate(activity, r.state);

        }

        ...
        // 注释6

        if (!r.activity.mFinished) {

            activity.performStart();

            r.stopped = false;

        }

        ...
        // 注释7

        if (r.state != null || r.persistentState != null) {

            mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,

        r.persistentState);

        }

        } else if (r.state != null) {

            mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);

        }

    }

    ...

}

Uma instância de Activity é criada por meio de reflexão na Nota 1. Para criar a Aplicação é chamada a Nota 2. Caso a Aplicação tenha sido criada r.packageInfo.makeApplicationna etapa aqui , retornará diretamente.ActivityThread#handleBindApplication()

O método da Activity é chamado na Nota 3 attach, e a Activity associada será criada internamente PhoneWindow. A Nota 4 define o tema da Activity, a Nota 5 chama o ciclo de vida onCreate() da Activity, a Nota 6 chama o ciclo de vida onStart() e a Nota 7 chama o ciclo de vida OnRestoreInstanceState() quando a Activity é retomada. Combinado com o ciclo de vida onResume() mencionado acima, o processo de inicialização de nossa Activity foi finalizado. Claro, aqui está o link para iniciar a atividade. A lógica da atividade não iniciada não é ruim. Se você estiver interessado, pode dar uma olhada no código-fonte você mesmo.

resumo

A relação de interação de cada processo durante o processo de inicialização da página inicial:

O processo de inicialização da página de inicialização pode ser resumido pelo seguinte diagrama:

4. Contexto

Quantos Contextos existem no APP? A resposta é o número de Atividades + o número de Serviços + 1, 1 refere-se ao Aplicativo.

Conforme mostrado na figura acima, ContextImplambos ContextWrapperherdam de Context e a classe de implementação específica de Context existe ContextImplcomo ContextWrapperuma variável de membro de Context mBase. Activity, Service e Application herdam e ContextWrapperpertencem ao Context, mas todos dependem ContextImplda implementação de funções relacionadas ao Context.

O tempo específico de criação do ContextImpl é chamar o método quando a Atividade, o Serviço e o Aplicativo são criados.O attachBaseContext()código específico não será postado aqui, e os alunos interessados ​​podem conferir por conta própria.

Application#getApplicationContext()O que o método obtém?

# ContextImpl

@Override
public Context getApplicationContext() {
    return (mPackageInfo != null) ?
            mPackageInfo.getApplication() : mMainThread.getApplication();
}

Por fim, o método é chamado ContextImpl#getApplicationContext()e você pode ver que o objeto é finalmente retornado ao Context Application. O Context retornado fragment#getContext()é Activityum objeto.

Cinco, o princípio de funcionamento do View

View e Window são duas existências complementares.Esta seção introduz View primeiro, e os pontos de conhecimento relacionados a Window podem ser ignorados e serão apresentados em detalhes na próxima seção. Você pode primeiro entender Window como uma tela e View como uma pintura.As pinturas são sempre renderizadas na tela, o que significa que View deve ser exibida com a ajuda de Window.

Veja o processo de desenho

Cada View terá um ViewRootImplobjeto, o ponto inicial do desenho da View é ViewRootImpl的perfromTraversalso método, e esses três processos perfromTraversalsserão chamados internamente .measure、layout、draw

measurePara medir o tamanho da View, chamar-se-á novamente a medida onMeasure.No onMeasure, medir-se-á primeiro a sub-View e por fim obter-se-á o tamanho de toda a View. Após o processo de medição, já podemos chamar View#getMeasuredWidth()e View#getMeasuredHeight()obter a largura e altura medidas da View.

layoutO processo determina a posição e a largura e altura reais da View no contêiner pai, ou seja, a posição das quatro coordenadas de vértice da View. Layout é o oposto de medida.Layout primeiro obtém a posição da View pai e então onLayoutrecursivamente obtém a posição da View filha. Após o processo de layout, View#getWidth()e View#getHeight()tenha um valor, que é a largura e a altura reais da View.

drawÉ a última etapa do desenho, chamando onDrawo método para desenhar a View na tela. dispatchDrawObviamente, a View filha também chamará métodos recursivamente (através de métodos) camada por camada onDrawpara descer.

Medir

O sentimento pessoal mais importante no processo de desenho é measureo método, então deixe-me falar sobre isso. Quando olharmos para os métodos relacionados à medida, com certeza veremos MeasureSpecesse parâmetro.

MeasureSpecÉ um valor int de 32 bits, representado pelos 2 bits superiores SpecModee os 30 bits inferiores SpecSize. O primeiro representa o modo e o último representa o tamanho.Um valor contém dois significados. Podemos MeasureSpec#makeMeasureSpec(size, mode)sintetizá-lo através de métodos MeasureSpec, ou podemos obtê -lo através MeasureSpec#getMode(spec)de e . Somente depois que o MeasureSpec da sub-View é gerado, podemos medir o tamanho da sub-View chamando o método de medida da sub-View.MeasureSpec#getSize(spec)MeasureSpecModeSize

Então, como o MeasureSpec é criado?

Existem três tipos de SpecMode, que são UNSPECIFIED、EXACTLY、AT_MOSTtrês tipos. Não se preocupe com o UNSPECIFED, ele é usado internamente pelo sistema. Então, quando é EXATAMENTE e quando é AT_MOST? Alguns alunos podem saber que seu valor LayoutParamsestá relacionado a . Mas deve-se notar que não é completamente determinado por seu próprio LayoutParams.LayoutParams precisa trabalhar com o container pai para determinar o SpecMode da própria View . Quando View é DecorView, MeasureSpec pode ser determinado de acordo com o tamanho da janela e seus próprios LayoutParams:

# ViewRootImpl

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
        final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    ...
    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); // 注释1
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); // 注释2
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
}

Nota 1 e Nota 2 obtêm o tamanho da janela e seus próprios LayoutParams. Quando a View de nível superior MeasureSpecfor confirmada, onMeasureo método medirá a View da próxima camada e obterá o MeasureSpec da sub-View . Podemos dar uma olhada no measureChildWithMarginsmétodo ViewGroup, que é chamado em muitos ViewGroup onMeasures:

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // 注释1

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);  // 注释2
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec); // 注释3
}

Podemos ver que a sub-View em si é obtida primeiro na Nota 1 LayoutParams. Então, na Nota 2, a View filha é gerada com base na View pai MeasureSpece . Por fim, o comprimento da subvisão é medido chamando a Nota 3 . Então, qual é a utilidade de passar o MeasureSpec da View pai? O código a seguir é um pouco longo, então escrevo a conclusão diretamente:paddingmarginMeasureSpecmeasure

当父View的MeasureMode是EXACTLY时,子View的LayoutParams如果是MATCH_PARENT或者写死的值,子View的MeasureMode是EXACTLY;子View的LayoutParams如果是WRAP_CONTENT,子View的MeasureMode是AT_MOST。

当父View的MeasureMode是AT_MOST时,子View的LayoutParams只有是写死的值时,子View的MeasureMode才会是EXACTLY,不然这种情况都是AT_MOST。

Existe um método que é mais fácil de entender, AT_MOSTgeralmente o LayoutParams correspondente é WRAP_CONTENT. Podemos pensar nisso, se a View pai for WRAP_CONTENT, mesmo que a View filha seja MATCH_CONTENT, a View filha não é equivalente a um tamanho incerto? Portanto, o MeasureMode neste caso da View filha ainda é AT_MOST. O MeasureMode da View será EXATAMENTE somente se o tamanho for determinado . Se houver alunos que estão confusos aqui, você pode pensar sobre isso novamente.

Quanto ao preenchimento e à margem, provavelmente pensamos nisso e sabemos que eles devem ser usados ​​ao medir o comprimento. Por exemplo, ao calcular o comprimento de uma sub-View, é necessário remover seu preenchimento e margem para ser preciso.

Além disso, observamos o método onMeasure de View e descobrimos que ele fornece uma implementação padrão:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

Ao setMeasuredDimension(width, height)definir a largura e a altura do View. Mas getDefaultSize() é preciso aqui? Podemos dar uma olhada em:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize; // 注释1
        break;
    }
    return result;
}

Nota 1, podemos ver que quando SpecMode é AT_MOST, specSize é usado diretamente por padrão. Portanto, há um problema aqui , porque este specSize representa o tamanho da View pai, o que fará com que o efeito de LayoutParams para WRAP_CONTENT seja MATCH_PARENT . Muitos modos de exibição personalizados oficiais reescrevem o método onMeasure para calcular o tamanho por si mesmos. Também precisamos prestar atenção especial a isso ao escrever exibições personalizadas.

ViewGroup é uma classe abstrata e não implementa o método onMeasure de View. Isso ocorre porque as regras de layout de cada ViewGroup são diferentes e os métodos de medição naturais também serão diferentes. Cada subclasse precisa implementar onMeasure por si só.

processo de layout

layoutO processo só precisa saber que o layout é principalmente um processo usado pelo contêiner pai para determinar a posição da Visualização fácil de nêutrons . Quando o contêiner pai determina a posição, todos os seus elementos filhos serão percorridos no contêiner pai onLayoutpara chamar seus layoutmétodos. O método de layout é, na verdade , determinar a posição das quatro coordenadas de vértice .

Podemos ver que semelhante onLayoutao onMeasuremétodo, nem View nem ViewGroup o implementam, pois cada View tem um layout diferente e precisa ser implementado por si só. Mas como o comprimento e a largura precisam ser usados ​​no processo de determinação das coordenadas, a ordem do layout vem depois da medida.

Observação View#getWidth()e View#getHeight()método:

public final int getWidth() {
    return mRight - mLeft;
}

public final int getWidth() {
    return mRight - mLeft;
}

Pode-se ver que na verdade são a subtração entre as coordenadas, então esses dois métodos só conseguem obter o valor real após o método onLayout. Ou seja, a largura e a altura reais da View. Em geral, measureWidth e width serão iguais, a menos que reescrevamos deliberadamente o método de layout (o método de layout é diferente do método de medida, pode ser reescrito):

public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r+11, b-11)
}

Desta forma, a largura e altura reais serão inconsistentes com a largura e altura medidas.

Visualização personalizada

Existem quatro tipos de visualizações personalizadas:

  • Herdar visualização
    • Esse tipo geralmente é usado para desenhar alguns gráficos irregulares e seu onDrawmétodo precisa ser reescrito. Deve-se notar que a propriedade Padding da View não funciona por padrão durante o processo de desenho.Se você quiser que a propriedade Padding funcione, você precisa onDrawdesenhar o Padding depois de obtê-lo você mesmo. Além disso, onMeasurevocê precisa considerar a situação de wrap_content e padding, conforme mencionado acima.
  • Herdar ViewGroup
    • Esse tipo é relativamente raro e geralmente é usado para implementar layouts personalizados. Isso significa regras de layout personalizadas, ou seja, custom onMeasure, onLayout. Em onMeasure, você também precisa lidar com wrap_content e padding. Além disso, em onMeasuree onLayout, também é necessário considerar o cenário em que o preenchimento e a margem do subelemento trabalham juntos.
  • Herdar uma View específica, como TextView
    • Esse tipo geralmente é usado para estender a função de uma View existente, que é relativamente fácil de implementar e não requer reescrita onMeasureou onLayoutmétodos.
  • Herdar um ViewGroup específico, como FrameLayout
    • Este tipo também é muito comum, geralmente é usado para combinar várias Views, mas é muito mais simples que o segundo método e não requer reescrita onMeasureou onLayoutmétodos.

Na exibição personalizada, você precisa prestar atenção extra. Se houver threads ou animações adicionais na exibição, você precisará reciclar ou pausar no momento certo (como o ciclo de vida onDetachedFromWindow), caso contrário, isso causará facilmente vazamentos de memória .

Six, Window e WindowManager

Classes relacionadas à janela

Window é uma classe abstrata e sua implementação concreta é PhoneWindow. é criado no método PhoneWindow:Activity#attach

#Activity

final void attach(Context context...) {
    ...
    mWindow = new PhoneWindow(this, window, activityConfigCallback); // 注释1
    ...
    mWindow.setWindowManager(
        (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
        mToken, mComponent.flattenToString(),
        (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); // 注释2

}

A Nota 1 é inicializada PhoneWindowe a Nota 2 é definida para Janela WindowManager. WindowManager, como o nome sugere é usado para gerenciar o Window, ele herda ViewManagera interface:

public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

A partir do código acima, podemos ver que gerenciar Window é, na verdade, gerenciar View. context.getSystemService(Context.WINDOW_SERVICE)O que o método finalmente obtém é WindowManagera classe de implementação WindowManagerImpl. Podemos observar WindowManagerImplesses três métodos, como addView:

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

addViewPode-se ver que ele é realmente mGlobalimplementado pelo Go, WindowManagerImplapenas em ponte. Isto mGlobalé WindowManagerGlobal, é um singleton, globalmente único:

#WindowManagerImpl

private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

Tipo de janela

O tipo Window é especificamente propriedade de Window Type. TypeO atributo é na verdade um ponteiro int, que é dividido em três tipos:

  • janela do aplicativo
    • A janela de atividade comum pertence à janela do aplicativo e seu intervalo de valores int é de 1 a 99.
  • janela infantil
    • Uma janela filho significa que ela deve ser anexada a outras janelas para existir.Por exemplo, PopupWindow pertence a uma janela filho e seu intervalo de valores int é 1000-1999.
  • janela do sistema
    • Toast, barra de volume e janelas de método de entrada pertencem à janela do sistema e o intervalo de valores int é 2000-2999. Quando queremos criar uma janela do sistema, precisamos solicitar a permissão do sistema android.permission.SYSTEM_ALERT_WINDOW.

De um modo geral, quanto maior o valor do atributo type, maior é a ordem Z e mais próxima a janela fica do usuário.

operação da janela

Adicionar Visualização

Vamos continuar o código acima addViewe dar uma olhada no processo interno:

# WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params); // 注释1
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {
    // Only use the default token if we don't have a parent window.
    if (mDefaultToken != null && mParentWindow == null) {
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        // Only use the default token if we don't already have a token.
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (wparams.token == null) {
            wparams.token = mDefaultToken;
        }
    }
}

# WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ...
    root = new ViewRootImpl(view.getContext(), display); // 注释2

    view.setLayoutParams(wparams);

    // 注释3
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    // do this last because it fires off messages to start doing things
    try {
        // 注释4
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        if (index >= 0) {
            removeViewLocked(index, true);
        }
        throw e;
    }
}

A Nota 1 chama applyDefaultTokeno método e tokeno coloca em LayoutParams. Podemos encontrar este token em vários métodos do Window, o que exatamente é este token e qual sua função? Na verdade, este token é um IBinderobjeto, que é criado quando o AMS cria uma Atividade. Ele é usado para identificar uma Atividade de forma única. Após a criação, o WMS também obterá uma cópia e a armazenará. Quando o AMS chamar scheduleLauncherActivityo método e retornar ao processo APP, o token será passado para o processo APP. Este token será utilizado quando operarmos no Window no processo APP. Como a operação final do Window é no WMS, vamos passar esse token quando chamarmos o método WMS, e o WMS vai comparar esse token com o token armazenado inicialmente para saber a qual Activity essa Window pertence. ,

Volte novamente, vamos ver que WindowManagerGlobal#addViewum objeto será novo toda vez no método do comentário 2 ViewRootImpl. ViewRootImplEstamos familiarizados com isso, como mencionado na seção anterior, é responsável pelo desenho de View. Aqui não é apenas responsável pelo desenho do View, mas também responsável pela comunicação com o WMS final. Especificamente, você pode ver que o método é chamado na Nota 4 ViewRootImpl#setViewe o método é chamado internamente IWindowSession#addToDisplay.

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ...
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
        getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
        mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
    ...
}

IWindowSessionNa verdade, ele é o agente do WMS Session对象no processo APP, então nossa lógica vai para o WMS. O WMS concluirá as operações addView restantes, incluindo a atribuição de superfícies às janelas adicionadas, determinando a ordem de exibição das janelas e, finalmente, entregando as superfícies para processamento SurfaceFlinger. , composto na tela para exibição. Além disso, addToDisplayo primeiro parâmetro do método, mWindow, é ViewRootImpluma classe interna W, que é a classe de implementação do Binder do processo do aplicativo, por meio da qual o WMS pode chamar métodos do processo do aplicativo.

Além disso, podemos ver que WindowManagerGlobaltrês listas são mantidas na Nota 3, uma é View, uma é ViewRootImpl, e uma é Paramsparâmetros de layout, que serão usados ​​ao atualizar Window/remover Window.

resumo

O processo de adição de uma View pode ser resumido pela figura a seguir:

atualizar janelas

O processo de atualização de Window é semelhante ao processo de adição e o relacionamento de classe é exatamente o mesmo. A principal diferença é WindowManagerGlobalque você precisa obter ViewRootImpl da lista e atualizar os parâmetros de layout:

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    ...
    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        mParams.add(index, wparams);
        root.setLayoutParams(wparams, false);
    }
}

ViewRootImpl chamará scheduleTraversalso método para redesenhar a página e, finalmente, performTraversalschamará IWindowSession#relayouto método no método para atualizar a Janela e acionará novamente os três principais processos de desenho da Visualização.

Renderização de atividades

Na terceira seção, falamos sobre o processo de inicialização da Activity, mas ainda não terminamos, porque a Activity não é realmente renderizada. Como a Atividade é renderizada? Claro que também depende do Window.

ActivityThreadO método que o AMS chamará quando a interface estiver pronta para interagir com o usuário handleResumeActivity:

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
    ...
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason); // 注释1
    ...
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager(); 
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
        l.softInputMode |= forwardBit;
        ...
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                wm.addView(decor, l); // 注释2
            } else {
            ...
        }
        ...

Chamar o método no comentário 1 performResumeActivityacionará internamente onResumeo ciclo de vida da Activity. A Nota 2 usa WindowManager#addViewo método para desenhar o decorView para a Window, para que toda a Activity seja renderizada. É por isso que a Activity só será exibida no ciclo de vida onResume.

Diálogo、PopupWindow、Toast

No final, ainda sinto que é necessário entender o que são as três coisas Dialog, PopupWindow e Toast. Não há dúvida de que todos eles são renderizados por meio de Window, mas Dialog pertence à janela do aplicativo, PopupWindow pertence à janela filha e Toast pertence à janela do nível do sistema.

Quando o Dialog for criado, ele criará um PhoneWindow como Activity:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean      createContextThemeWrapper) {
    ···
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

    final Window w = new PhoneWindow(mContext); // 注释1
    mWindow = w;
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setOnWindowSwipeDismissedCallback(() -> {
        if (mCancelable) {
            cancel();
        }
    });
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);

    mListenersHandler = new ListenersHandler(this);
}

Vemos que o Dialog na Nota 1 cria sua própria Janela quando é inicializado, portanto não existe anexado a outra Janela e não pertence a uma janela filha, mas a uma janela de aplicativo.

No Dialog#show()momento, o WindowManager será usado para adicionar o DecorView à janela, e quando ele for removido, o DecorView também será removido da janela. Existe um recurso especial que o contexto de um diálogo normal deve ser uma Activity, caso contrário, um erro será relatado durante a exibição, porque o token da Activity precisa ser verificado ao adicionar uma janela, a menos que definamos a janela do diálogo como uma janela do sistema, é desnecessário.

E por que PopupWindow é uma janela filha? Podemos verificar o código-fonte do PopupWindow:

# PopupWindow

private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
public PopupWindow(View contentView, int width, int height, boolean focusable) {
    if (contentView != null) {
        mContext = contentView.getContext();
        // 注释1
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 
    }

    setContentView(contentView);
    setWidth(width);
    setHeight(height);
    setFocusable(focusable);
}

public void showAtLocation(IBinder token, int gravity, int x, int y) {
    ...
    final WindowManager.LayoutParams p = createPopupLayoutParams(token); // 注释2
    preparePopup(p);

    ...
    invokePopup(p);
}
   
protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
    final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
    p.gravity = computeGravity();
    p.flags = computeFlags(p.flags);
    p.type = mWindowLayoutType; // 注释3
    p.token = token; // 注释4
    p.softInputMode = mSoftInputMode;
    p.windowAnimations = computeAnimationResource();
    ...
}
    
private void invokePopup(WindowManager.LayoutParams p) {
    ...
    final PopupDecorView decorView = mDecorView;
    decorView.setFitsSystemWindows(mLayoutInsetDecor);

    setLayoutDirectionFromAnchor();

    mWindowManager.addView(decorView, p); // 注释5
    ...
}

No código-fonte, podemos ver que não haverá nenhuma operação de criação de uma nova janela em PopupWindow, pois ela depende da janela de outra pessoa. Obtenha o WindowNManager na Nota 1 e chame o método na Nota 2 para atribuir createPopupLayoutParamsvalores aos parâmetros do Window. Preste atenção especial aos dois campos da Nota 3 e Nota 4. typeO campo representa o tipo de janela, podemos ver que o valor de mWindowLayoutType é WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, o que significa que é uma janela filha. E a nota 4 tokennão é mais o token da Activity, mas o token da View passada durante o show.

WindowParams.LayoutParams.typeToast também é semelhante, podemos ver que o valor em seu código-fonte WindowManager.LayoutParams.TYPE_TOASTcorresponde à janela do sistema. O mecanismo interno específico não será analisado aqui, e os alunos interessados ​​podem conferir por si mesmos.

Sete, mecanismo de mensagem do manipulador

A principal função do mecanismo de mensagem do manipulador é que, no contexto de multiencadeamento, por meio do mecanismo de mensagem, você pode alternar do subencadeamento para o encadeamento principal para atualizar a interface do usuário e garantir a segurança do encadeamento.

Primeiro apresente os membros principais e a relação entre eles .

  • Mensagem
    • Message, que pode ser usado para armazenar dados, e um objeto de mensagem pode ser obtido do buffer pool por meio de Message.obtain().
  • MessageQueue
    • Uma fila para armazenar mensagens.
  • Looper
    • O método de loop continuará monitorando o MessageQueue e, quando houver uma mensagem no MessageQueue, ele retirará a mensagem para distribuição.
    • Uma thread terá apenas um Looper, e um Looper corresponde a uma MessageQueue.Quando o Loope for criado, a MessageQueue correspondente será criada. Um Handler só pode ser vinculado a um Looper, mas um Looper pode ser vinculado a vários Handlers e o Looper está em unidades de threads .
    • O isolamento de encadeamento do looper é implementado por meio de Threadlocal . Existe uma variável estática sThreadLocal em Looper.Ao chamar Looper.myLooper() em cada thread, ela é retirada dessa variável para obter o Looper na thread atual.
    • Looper é dividido em sub-thread Looper e MainLooper. main方MainLooper estará Looper.prepareMainLooper()pronto através do método no método ActivityThread . A diferença entre eles é que um pode desistir e o outro não. O chamado quit é chamar looper.quito método, que na verdade é sair da MessageQueue e remover as mensagens dela. Sair é dividido em se é seguro parar. Em safequit, as mensagens existentes na fila de mensagens serão enviadas antes de sair.
  • Manipulador
    • Usado para enviar Message e receber Message, como o alvo em Message .
  • Mensagem.Callback
    • Quando Message.callback existir, o Handler não receberá a Message, mas retornará a chamada diretamente para o callback . Quando o Handler publica um Runnable, na verdade ele envia uma mensagem e usa o Runnable como retorno de chamada da mensagem.

E todo o processo de comunicação da mensagem ? Podemos conversar sobre isso juntos.

  1. Primeiro, no estágio de preparação da comunicação, Looper.prepare()um Looper é criado para o thread atual e um MessageQueue é criado ao mesmo tempo. novo um manipulador.
  2. handler.postOu handler.sendMessageenvie uma mensagem e coloque-a na fila para MessageQueue.
  3. Looper distribui as filas em MessageQueue para Handler de acordo com a prioridade.
  4. O Handler obtém a mensagem e processa a mensagem de acordo com os dados nela contidos.

Ao escrever negócios, muitas vezes temos post/senduma mensagem atrasada. Como essa mensagem atrasada é percebida? Cada Mensagem possui um whencampo correspondente a quando ela precisa ser despachada. Ao entrar no MessageQueue, ele será whenorganizado de acordo com a sequência. Quando o loop busca uma mensagem, ele irá julgar se o tempo de distribuição da mensagem chegou, se não chegou, ele não irá distribuí-la primeiro, e depois distribuí-la quando o tempo acabar.

Portanto, no mecanismo de mensagens, o envio de uma mensagem não garante que ela seja processada imediatamente, pois há um arranjo de prioridades dentro do mecanismo. Então, como podemos aumentar a prioridade da mensagem quando precisamos dela? Podemos chamar postAtFrontOfQueuee sendMessageAtFrontOfQueuecolocar a mensagem no início da fila. Na verdade, definimos o quando dessa mensagem como 0. Como alternativa, use mensagens assíncronas com barreiras síncronas.

Mensagens assíncronas e barreiras síncronas

Mensagens assíncronas e barreiras síncronas raramente são usadas no desenvolvimento e geralmente são usadas pelo próprio sistema. A interface da barreira síncrona também é oculta e podemos chamá-la apenas reflexivamente. Mas como as entrevistas são muito comuns, vou mencioná-las aqui já agora.

Para criar uma mensagem assíncrona,setAsynchronous podemos chamar um método para definir o Message como uma mensagem assíncrona ao criar um Message, ou passar parâmetros para selecionar se é assíncrono ao criar um Handler. Se for assíncrono, todas as mensagens enviadas pelo Handler são mensagens assíncronas .

Então, o que é uma barreira de sincronização? A essência da barreira de sincronização é na verdade uma Message especial, o especial é que o Target desta Message não é um Handler, mas null , de forma a diferenciá-la das outras Messages. Podemos MessageQueue的postSyncBarrierenviar uma barreira de sincronização chamando o método reflexivamente e removeSyncBarriero método a remove.

Podemos combinar barreiras síncronas com mensagens assíncronas para melhorar a prioridade das mensagens assíncronas . O princípio específico é que quando o MessageQueue retira a Mensagem do Loop, ele julgará se a Mensagem é uma barreira de sincronização. Se for uma barreira síncrona, você precisa encontrar a primeira mensagem assíncrona na fila para processamento prioritário em vez de processar as mensagens síncronas na frente:

#MessageQueue

Message next() {
    ...
    synchronized (this) {
        // Try to retrieve the next message.  Return if found.
        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
        Message msg = mMessages;
        if (msg != null && msg.target == null) {
            // Stalled by a barrier.  Find the next asynchronous message in the queue.
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous()); // 注释1
        }
    ...
}

Quando a nota 1 for msg.targetnula, significa que a mensagem é uma barreira síncrona, e a primeira mensagem assíncrona da fila será buscada.

Como mencionado acima, a operação de atualização da vista é mencionada.Na etapa final do desenho, ViewRootImpl atualizará requestLayout()o layout e requestLayout()chamará o método internamente scheduleTraversals()para percorrer novamente os três principais processos de desenho.

# ViewRootImpl

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 注释1
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //注释2
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

Nesse método, não vimos o acionamento dos três principais processos de desenho, mas enviamos uma barreira de sincronização na Nota 1 para bloquear a fila de mensagens do thread principal. Ouça também o sinal VSYNC na nota 2. Quando o sinal VSYNC vier, o Choreographer enviará uma mensagem assíncrona, que será executada com a ajuda da barreira de sincronização e acionará o callback do ouvinte. No retorno de chamada de monitoramento, ViewRootImpl removerá a barreira de sincronização e chamará e performTraversals()executará os três principais processos de desenho para atualizar a IU.

Resumir

Este artigo apresenta um total de sete pontos de conhecimento do Framework, a saber: processo de inicialização do sistema, processo de inicialização do processo de aplicativo, processo de inicialização da atividade, contexto, princípio de funcionamento do View, janela e WindowManager, mecanismo de mensagem do manipulador .

notas de estudo do Android

Android Performance Optimization: https://qr18.cn/FVlo89
Android Vehicle: https://qr18.cn/F05ZCM
Android Reverse Security Study Notes: https://qr18.cn/CQ5TcL
Android Framework Principles: https://qr18.cn/AQpN4J
Android Audio and Video: https://qr18.cn/Ei3VPD
Jetpack (incluindo Compose): https://qr18.cn/A0gajp
Kotlin: https://qr18.cn/CdjtAF
Gradle: https://qr18.cn/DzrmMB
OkHttp Source Code Analysis Notes: https://qr18.cn/Cw0pBD
Flutter: https://qr18.cn/DIvKma
Android Eight Knowledge Body: https://qr18.cn/CyxarU
Android Core Notes: https://qr21.cn/CaZQLo
Android Perguntas da entrevista anterior: https://qr18.cn/CKV8OZ
Coleção de perguntas da entrevista mais recente do Android de 2023: https://qr18.cn/CgxrRy
Exercícios de entrevista de trabalho de desenvolvimento de veículo Android: https://qr18.cn/FTlyCJ
Perguntas de entrevista de áudio e vídeo:https://qr18.cn/AcV6Ap

Acho que você gosta

Origin blog.csdn.net/weixin_61845324/article/details/131700942
Recomendado
Clasificación