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:
- 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.
- Como um servidor Socket para monitorar o processo de criação de solicitação AMS
- 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:
- Inicie o pool de encadeamentos do Binder para se preparar para a comunicação entre processos
- 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.ActivityThread
e 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 H
pertence à 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 attachApplication
método AMS. attachApplication
O método sempre será chamado para ApplicationThread
o bindApplication
método e, finalmente, para ActivityThread.handleBindApplication
o 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, attachBaseContext
o 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 Instrumentation
chamado 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 mActivityMonitors
sã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.thread
Na verdade IApplicationThread
, como o IActivityManager anterior, também é um objeto proxy e o proxy é o processo do aplicativo ApplicationThread
. ApplicationThread
Pertence à classe interna de ActivityThread, podemos ver seu scheduleLaunchActivity
mé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á scheduleLaunchActivity
no encadeamento Binder quando é executado, portanto, precisamos H
alternar para o encadeamento principal.
H
Quando 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.makeApplication
na 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, ContextImpl
ambos ContextWrapper
herdam de Context e a classe de implementação específica de Context existe ContextImpl
como ContextWrapper
uma variável de membro de Context mBase
. Activity, Service e Application herdam e ContextWrapper
pertencem ao Context, mas todos dependem ContextImpl
da 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()
é Activity
um 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 ViewRootImpl
objeto, o ponto inicial do desenho da View é ViewRootImpl的perfromTraversals
o método, e esses três processos perfromTraversals
serão chamados internamente .measure、layout、draw
measure
Para 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.
layout
O 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 onLayout
recursivamente 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 onDraw
o método para desenhar a View na tela. dispatchDraw
Obviamente, a View filha também chamará métodos recursivamente (através de métodos) camada por camada onDraw
para descer.
Medir
O sentimento pessoal mais importante no processo de desenho é measure
o método, então deixe-me falar sobre isso. Quando olharmos para os métodos relacionados à medida, com certeza veremos MeasureSpec
esse parâmetro.
MeasureSpec
É um valor int de 32 bits, representado pelos 2 bits superiores SpecMode
e 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)
MeasureSpec
Mode
Size
Então, como o MeasureSpec é criado?
Existem três tipos de SpecMode, que são UNSPECIFIED、EXACTLY、AT_MOST
trê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 LayoutParams
está 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 MeasureSpec
for confirmada, onMeasure
o método medirá a View da próxima camada e obterá o MeasureSpec da sub-View . Podemos dar uma olhada no measureChildWithMargins
mé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 MeasureSpec
e . 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:padding
margin
MeasureSpec
measure
当父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_MOST
geralmente 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
layout
O 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 onLayout
para chamar seus layout
métodos. O método de layout é, na verdade , determinar a posição das quatro coordenadas de vértice .
Podemos ver que semelhante onLayout
ao onMeasure
mé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
onDraw
mé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ê precisaonDraw
desenhar o Padding depois de obtê-lo você mesmo. Além disso,onMeasure
você precisa considerar a situação de wrap_content e padding, conforme mencionado acima.
- Esse tipo geralmente é usado para desenhar alguns gráficos irregulares e seu
- Herdar ViewGroup
- Esse tipo é relativamente raro e geralmente é usado para implementar layouts personalizados. Isso significa regras de layout personalizadas, ou seja, custom
onMeasure
,onLayout
. EmonMeasure
, você também precisa lidar com wrap_content e padding. Além disso, emonMeasure
eonLayout
, também é necessário considerar o cenário em que o preenchimento e a margem do subelemento trabalham juntos.
- Esse tipo é relativamente raro e geralmente é usado para implementar layouts personalizados. Isso significa regras de layout personalizadas, ou seja, custom
- 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
onMeasure
ouonLayout
métodos.
- Esse tipo geralmente é usado para estender a função de uma View existente, que é relativamente fácil de implementar e não requer reescrita
- 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
onMeasure
ouonLayout
métodos.
- 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
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 PhoneWindow
e a Nota 2 é definida para Janela WindowManager
. WindowManager
, como o nome sugere é usado para gerenciar o Window, ele herda ViewManager
a 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 é WindowManager
a classe de implementação WindowManagerImpl
. Podemos observar WindowManagerImpl
esses 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);
}
addView
Pode-se ver que ele é realmente mGlobal
implementado pelo Go, WindowManagerImpl
apenas 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
. Type
O 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 addView
e 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 applyDefaultToken
o método e token
o 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 IBinder
objeto, 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 scheduleLauncherActivity
o 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#addView
um objeto será novo toda vez no método do comentário 2 ViewRootImpl
. ViewRootImpl
Estamos 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#setView
e 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);
...
}
IWindowSession
Na 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, addToDisplay
o primeiro parâmetro do método, mWindow, é ViewRootImpl
uma 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 WindowManagerGlobal
três listas são mantidas na Nota 3, uma é View
, uma é ViewRootImpl
, e uma é Params
parâ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 é WindowManagerGlobal
que 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á scheduleTraversals
o método para redesenhar a página e, finalmente, performTraversals
chamará IWindowSession#relayout
o 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.
ActivityThread
O 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 performResumeActivity
acionará internamente onResume
o ciclo de vida da Activity. A Nota 2 usa WindowManager#addView
o 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 createPopupLayoutParams
valores aos parâmetros do Window. Preste atenção especial aos dois campos da Nota 3 e Nota 4. type
O 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 token
não é mais o token da Activity, mas o token da View passada durante o show.
WindowParams.LayoutParams.type
Toast também é semelhante, podemos ver que o valor em seu código-fonte WindowManager.LayoutParams.TYPE_TOAST
corresponde à 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 é chamarlooper.quit
o 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.
- 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. handler.post
Ouhandler.sendMessage
envie uma mensagem e coloque-a na fila para MessageQueue.- Looper distribui as filas em MessageQueue para Handler de acordo com a prioridade.
- 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/send
uma mensagem atrasada. Como essa mensagem atrasada é percebida? Cada Mensagem possui um when
campo correspondente a quando ela precisa ser despachada. Ao entrar no MessageQueue, ele será when
organizado 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 postAtFrontOfQueue
e sendMessageAtFrontOfQueue
colocar 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的postSyncBarrier
enviar uma barreira de sincronização chamando o método reflexivamente e removeSyncBarrier
o 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.target
nula, 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