Autor: Cálculo_Xiao Wang
Este artigo começa com os três principais processos e distribuição de eventos do ViewTraversals, combina uso e experiência e se concentra na análise do mecanismo de reciclagem e reutilização do RecyclerView. O artigo inteiro tomará LinearLayoutManager como exemplo, girará em torno de vários métodos clássicos de reescrita diária de RecyclerView.Adapter e explicará o mecanismo de cache de RV
Este artigo é bastante longo, então sugiro que você o leia mais tarde, caso tenha esquecido alguma coisa, e depois leia o artigo anterior
Três etapas: medir, traçar, desenhar
RV (RecyclerView, referido no texto a seguir) como ViewGroup, devemos entender seus três principais estágios. Dentre eles, vamos focar no layout (por isso falaremos sobre isso no final)
medir
Na fase de medição, o foco principal está mLayout.isAutoMeasureEnabled()
no segmento de código, pois o LayoutManager normal está habilitado por padrão. Para a medição do ViewGroup, precisamos saber o tamanho da criança antes de podermos decidir, especialmente para RV. Por exemplo LinearLayoutManger.height = WRAP_CONTENT
, o tamanho específico deve ser conhecido após a colocação da criança. setMeasuredDimensionFromChildren
Finalmente chamado setMeasuredDimension
, também significa que dispatchLayoutStep2
é muito provável que determine a medida && layout do item filho
// RecyclerView.java
LayoutManager mLayout;
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
// 没有LayoutManager
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
// LayoutManager默认开启
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
mLastAutoMeasureSkippedDueToExact =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) {
// 如果尺寸固定EXACTLY或 没有adpater(那也将取不到child),所以测量结束
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// 现在可以真正取到child尺寸了,那也意味着dispatchLayoutStep2极有可能决定了child的摆放和测量
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// 在dispatchLayout中会进行是否需要二次布局
mLastAutoMeasureNonExactMeasuredWidth = getMeasuredWidth();
mLastAutoMeasureNonExactMeasuredHeight = getMeasuredHeight();
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
}
}
Para esta etapa, sem muita elaboração, precisamos nos atentar ao dispatchLayoutStep2
desempenho específico no acompanhamento. E podemos inferir que o tamanho fixo pode efetivamente acelerar a fase de medição. É claro que em alguns aninhamentos de listas grandes, isso pode ser inevitável
empate
Esta é a única entre as três fases principais do RV que possui um comportamento de iniciação de reescrita ativa (diferente do comportamento de resposta de onXX). A partir disso, aprendemos que ItemDecorations
o desenho de também está envolvido nesta etapa, e a ordem de seu onDraw e onDrawOver também é clara. Claro, a configuração de deslocamento de ItemDecorations deve estar no layout
estágio
// RecyclerView.java
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
// ……
if (needsInvalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
A partir das duas fases de medida e layout, e do uso básico do RV, percebemos que o RV divide diferentes responsabilidades para diferentes objetos (LayoutManager, ItemDecoration, Adapter, cujos nomes são adequados ao significado, então não vou explicar muito), e então a intervenção direcionada no elo principal é uma excelente prática de dissociação
disposição
Existem dispatchLayout
três métodos com quase o mesmo nome: dispatchLayoutStep1\2\3, e depois há chamadas correspondentes na fase Measure, que também possui um sinalizador de status mState.mLayoutStep
, então continuamos com as perguntas:
- Quais são as funções e o significado dos três métodos
- Como os sinalizadores de status mudam e o que eles fazem
// RecyclerView.java
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 题外话,这就是Trace工具捕捉的方式,begin和end成双入对
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
if (mAdapter == null) {
return;
}
if (mLayout == null) {
return;
}
boolean needsRemeasureDueToExactSkip = mLastAutoMeasureSkippedDueToExact
&& (mLastAutoMeasureNonExactMeasuredWidth != getWidth()
|| mLastAutoMeasureNonExactMeasuredHeight != getHeight());
mLastAutoMeasureNonExactMeasuredWidth = 0;
mLastAutoMeasureNonExactMeasuredHeight = 0;
mLastAutoMeasureSkippedDueToExact = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates()
|| needsRemeasureDueToExactSkip
|| mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
Da mesma forma, ajustamos a ordem de leitura para 1, 3 e finalmente 2
expediçãoLayoutStep1
Informação 1: STEP_START
Usado no início da fase 1 e definido comoSTEP_LAYOUT
Informação 2: As principais informações extraídas parecem estar Animation
relacionadas, e os dois julgamentos importantes mRunSimpleAnimations e mRunPredictiveAnimations são todos processAdapterUpdatesAndSetAnimationFlags()
processados, vamos dar uma olhada mais profunda
Informação 3: Chamado uma vez onLayoutChildren
, os detalhes específicos são liberados em dispatchLayoutStep2 , agora só falta saber que foi realizado um layout, normalmente chamamos预布局
// RecyclerView.java
private void dispatchLayoutStep1() {
// 状态校验
mState.assertLayoutStep(State.STEP_START);
// ……
// 这个我们展开看看吧,mInPreLayout预布局标记也在这个阶段设置了
processAdapterUpdatesAndSetAnimationFlags();
// ……
mState.mInPreLayout = mState.mRunPredictiveAnimations;
// ……
if (mState.mRunSimpleAnimations) {
// ……
mViewInfoStore.addToPreLayout(holder, animationInfo);
// ……
}
if (mState.mRunPredictiveAnimations) {
// ……
mLayout.onLayoutChildren(mRecycler, mState);
// ……
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
// ……
}
// ……
mState.mLayoutStep = State.STEP_LAYOUT;
}
Há muitos julgamentos e processamentos nesta área. Em resumo: RV suporta animação e mItemsAddedOrRemoved || mItemsChanged
é verdadeiro. também mRunPredictiveAnimations
dependemRunSimpleAnimations
Ponto-chave: mAdapterHelper.preProcess()
é um método quase necessário (a cena especial de remoção de animação não é considerada por enquanto) e trata das alterações do adaptador (como adicionar\remover\alterar), que registra o comportamento específico da mudança, e a cadeia de chamadas pode ser explorada por si só (não feito mPendingUpdates
aqui Expandir), em RecyclerView mObserver:RecyclerViewDataObserver
, ou seja, quando adaptador.notifyXX for chamado (qualquer método de atualização), ele responderá e o comportamento correspondente será adicionado amPendingUpdates
// RecyclerView.java
private void processAdapterUpdatesAndSetAnimationFlags() {
if (mDataSetHasChangedAfterLayout) {
// Processing these items have no value since data set changed unexpectedly.
// Instead, we just reset it.
mAdapterHelper.reset();
if (mDispatchItemsChangedEvent) {
mLayout.onItemsChanged(this);
}
}
if (predictiveItemAnimationsEnabled()) {
// predictiveItemAnimationsEnabled具体如下,如果不特殊设置去除动画的话,通常为true
// return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
mAdapterHelper.preProcess();
} else {
mAdapterHelper.consumeUpdatesInOnePass();
}
boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
}
De modo geral, a etapa 1 leva ao 预布局
conceito, e o pré-layout e a exibição da animação (adicionar\remover\alterar) são extremamente relevantes.
唐子玄
O significado de uma frase explicada no artigo citado pelo irmão mais velho 预布局
é simplesmente deixar um instantâneo antes da animação para comparação para determinar como a animação será executada
Existem dois itens (1, 2) na lista, exclua 2, agora 3 se moverá suavemente da parte inferior da tela e ocupará a posição original dos 2. Para conseguir esse efeito, a estratégia do RecyclerView é: primeiro realizar um pré-layout para o item da tabela antes da animação e carregar o item invisível da tabela 3 no layout para formar um instantâneo do layout (1, 2, 3). Em seguida, execute um pós-layout para o item da tabela após a animação e também forme um instantâneo do layout (1, 3). Compare a posição da entrada 3 nos dois instantâneos e você saberá como animá-la.
expediçãoLayoutStep3
Mensagem 1: STEP_ANIMATIONS
Utilizada no início da fase 3, e não necessária, pois reverte imediatamente paraSTEP_START
Mensagem 2: mViewInfoStore.process
A animação é finalmente acionada no meio. Este artigo não irá expandir os detalhes da animação, que geralmente é chamada de pós-layoutpostLayout
// RecyclerView.java
private void dispatchLayoutStep3() {
// ……
mState.assertLayoutStep(State.STEP_ANIMATIONS);
// 校验完后,就直接重置了状态标记了
mState.mLayoutStep = State.STEP_START;
// ……
mViewInfoStore.process(mViewInfoProcessCallback);
// ……
// scrap回收
mLayout.removeAndRecycleScrapInt(mRecycler);
}
expediçãoLayoutStep2
Esta é a única das três fases que pode ser chamada várias vezes e também estabelece o estado final das visualizações durante esta fase
Informação 1: STEP_LAYOUT
É usada no início da fase 2 e definida no final STEP_ANIMATIONS
. Como será chamada várias vezes, STEP_ANIMATIONS
também é um dos julgamentos iniciais
Informação 2: Diferente da chamada na fase de medida onLayoutChildren
, é definida neste momento mState.mInPreLayout = false
, por isso é chamada de fase de layout real
// RecyclerView.java
private void dispatchLayoutStep2() {
// ……
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
// ……
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
// ……
mState.mLayoutStep = State.STEP_ANIMATIONS;
// ……
}
A seguir , mLayout é tomado LinearLayoutManager
como exemplo. Primeiro, detachAndScrapAttachedViews
todas as visualizações anexadas existentes em RV são desvinculadas e descartadas. É claro que esse descarte é temporário (consulte Fill
o capítulo para obter detalhes) e o objetivo é evitar o impacto de pré- instantâneos durante a fase de pré-layout e prossiga para restaurar a tela. Em seguida, ele é preenchido e o isPreLayoutfill
é trazido aqui , o que será mencionado mais adiante.
// LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// ……
detachAndScrapAttachedViews(recycler);
mLayoutState.mIsPreLayout = state.isPreLayout();
// ……
fill(recycler, mLayoutState, state, false);
// ……
// 这里只有真正布局阶段才能进入,会将剩余的mAttachedScrap填充到屏幕中
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
}
expediçãoLayoutStepSummary
A partir disso, sabemos aproximadamente o significado e a função de cada estágio, e a marca de status é usada para processar o fluxo do estágio (observe que o estágio de medição também será chamado)
evento de slides
Durante o uso do RV, o deslizamento é o mais frequente e o ACTION_MOVE relacionado ao deslizamento não pode ser liberado. Entre eles scrollByInternal
está a lógica específica de seu próprio consumo deslizante. Com a cadeia de chamadas de código, finalmente chegou ao meio LinearLayoutManager.scrollBy
, e há mais novas descobertas, que na verdade são chamadas fill
(o preenchimento específico de RV quando falamos sobre layoutChildren anteriormente).
Você pode imaginar o seguinte: no processo de deslizar o dedo para cima, o item de cima é gradualmente removido e o novo item de baixo é adicionado. fill
Deve haver dois 回收、复用
links envolvidos, por isso o separamos em capítulos
// RecyclerView.java
public boolean onTouchEvent(MotionEvent e) {
// ……
case MotionEvent.ACTION_MOVE: {
// 这里作了多指兼容,并不是指多指手势,而具体指 当多个手指down下时,最终仅以最后一个手指为move基准
final int index = e.findPointerIndex(mScrollPointerId);
if (index < 0) {
Log.e(TAG, "Error processing scroll; pointer index for id "
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
return false;
}
// ……
// 嵌套滑动,优先询问parent
dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)
// ……
// 自己消费剩下的
scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e, TYPE_TOUCH)
// ……
}
break;
// ……
return true;
}
boolean scrollByInternal(int x, int y, MotionEvent ev, int type) {
// ……
scrollStep(x, y, mReusableIntPair);
// ……
}
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
// ……
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
// 下面以纵向举例
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
// ……
}
// LinearLayoutManager.java
// 注意,这里类不一样了喔
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
// ……
return scrollBy(dx, recycler, state);
}
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
// ……
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
// ……
}
Preencher reciclagem-reutilização
Esta será a principal prioridade na explicação do RV. É claro que nem todos os LayoutManagers possuem esse método, mas o LinearLayoutManager, por exemplo, onLayoutChildren
e scrollVerticallyBy
eventualmente mudou para esse método. E sua fonte deveria ser, ou seja layout
, onTouchEvent.ACTION_MOVE
os três principais processos e distribuição de eventos
// LinearLayoutManager.java
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// ……
// 剩余空间,循环过程中会不断减去新增item的尺寸,直到剩余空间不足结束
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ……
// 填充,会优先尝试复用
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// ……
// 当不需要ignore 或 不是预布局 或 正在摆放scrap时需要进行剩余空间计算
// 换言之,当且仅当 预布局阶段,mIgnoreConsumed标记为true的item会被跳过,因此会有多加载一个的情况
// 如【1、2】,remove 2,那么事前快照就应该需要【1、2、3】,对比事后【1、3】才能匹配动画
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
// 如向上滑动过程中,顶部的Item会被逐步回收
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
// ……
}
// ……
return start - layoutState.mAvailable;
}
Concentre-se principalmente em dois métodos, layoutChunk
e recycleByLayoutState
o conceito de espaço restante deve ser bem compreendido, como pode ser visto na ordem dos métodos, primeiro preencher e depois remover. Você pode pensar por que ele não é removido primeiro e depois preenchido?
Supondo que o preenchimento seja baseado na reutilização e a remoção seja baseada na reciclagem, se for removido primeiro e depois preenchido, então o cache que eu esperava que fosse reutilizado primeiro pode ser substituído pela visualização que é removida e adicionada ao cache primeiro, então reciclagem-reutilização não é tão eficiente
esconderijo
回收-复用
Este tópico geralmente é acompanhado por 缓存
este conceito, como cache ativo do Glide, cache de memória, cache de disco, cache de três níveis , mensagem , tipo de mídia. O cache multinível visa melhorar a eficiência da multiplexação e reduzir o custo de reciclagem
A estrutura de dados do cache usado pelo RV está estragada e é orientada a resultados para evitar confusão no texto de acompanhamento. Claro, o conceito será reforçado novamente a seguir. Os objetos armazenados em cache sãoViewHolder
Existem opiniões diferentes sobre se o cache de terceiro nível ou o cache de quarto nível é, porque mCachedViews na nota oficial éfirst-level cache
// RecyclerView.Recycler.java
// 两个Scrap(废弃)缓存,这个废弃为临时废弃之意,是预布局后为恢复画布而进行detach后快速布局使用,区别为changed是否污染,即需要bindView
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
// cache缓存,是RecycledViewPool的预备队列,默认size为2,可通过setViewCacheSize设置阈值
// 奉行先入先出原则,所以后来的缓存会顶替最老的缓存
static final int DEFAULT_CACHE_SIZE = 2;
int mViewCacheMax = DEFAULT_CACHE_SIZE;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
// 自定义缓存扩展,较少使用
private ViewCacheExtension mViewCacheExtension;
// 缓存池,每个viewType默认缓存5个,可通过setMaxRecycledViews进行扩容
RecycledViewPool mRecyclerPool;
public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
}
SparseArray<ScrapData> mScrap = new SparseArray<>();
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
}
Ao entrar oficialmente na reciclagem-reutilização, pense nisso
A prioridade do cache significa que o desempenho de multiplexação correspondente também varia de alto para baixo? (melhor desempenho de multiplexação significa operações menos dispendiosas)
1. Pior caso: recriar
ViewHodler
e religar dados2. Segundo melhor caso: reutilizar,
ViewHolder
mas religar dados3. Melhor caso: reutilizar
ViewHolder
sem religar os dadosCitação de - Autor: Tang Zixuan
Reciclar
Quanto à reciclagem, ela deve ser vista em duas partes
1. Conforme mencionado no artigo anterior, “Haverá um pré-layout para poluir a tela para obter um instantâneo de antemão”, fill
portanto uma aboliçãodetachAndScrapAttachedViews(recycler)
( onLayoutChildren
in) temporária foi realizada antes . Dois métodos de reciclagem foram encontrados ,recycleViewHolderInternal
scrapView
// RecyclerView.java
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
// 如果设置了hasStableIds,那也会放入scrap
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
2. Comportamento de reciclagemfill
que acompanha recycleByLayoutState
a remoção durante o enchimento . Seguindo a cadeia de chamadas , finalmente encontreirecycleByLayoutState >>> recycleViewsFromEnd(Start也是一样) >>> recycleChildren >>> RecyclerView.removeAndRecycleViewAt >>> recycleView
recycleViewHolderInternal
// LinearLayoutManager.java
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
} else {
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
// ……
final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace;
// ……
if (mOrientationHelper.getDecoratedStart(child) < limit
|| mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
// stop here
recycleChildren(recycler, 0, i);
return;
}
// ……
}
public void recycleView(@NonNull View view) {
// ……
recycleViewHolderInternal(holder);
// ……
}
Para entender o limite, você pode consultar esta imagem no artigo de Hongyang, eu a carreguei diretamente aqui
3. Como recycleViewHolderInternal
são duas chamadas, vamos falar primeiro sobre as regras antigas scrapView
. Descobrimos que até mesmo o Scrap distinguiu entre mAttached e mChanged e vinculou ViewHolder ao mesmo tempomScrapContainer
Por exemplo, remover deve ir para mAttached, mas alterar é atualizar ( mAdapterHelper.preProcess() mencionado em dispatchLayoutStep1 acima , que lida com as alterações de estado armazenadas em mPendingUpdates e o estado é monitorado e enviado por mObserver:RecyclerViewDataObserver ) está em mChanged.
A partir disso: sabemos que as funções dos dois caches de nível 1 de sucata são consistentes, mas precisamos distinguir se os dados estão poluídos (se precisarem ser atualizados, serão considerados poluídos)
// RecyclerView.Recycler.java
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
// ……
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
} else {
// ……
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
4. recycleViewHolderInternal
É semelhante ao nosso conceito de reciclagem diária. Se isRecyclable
for definido falso, não será reciclado, o que afetará seriamente o desempenho, o que significa que novas criações serão criadas a cada vez. Portanto, para o possível distúrbio de renderização de dados causado pela reciclagem e reutilização (não problema do RV, mas problema seu), deve ser por meio do comportamento de renderização correspondente if-else
das condições em onBindViewHolder .均
Preste atenção às principais condições: !holder.hasAnyOfTheFlags
, ou seja, o cache nos mCachedViews de segundo nível deve ser utilizável diretamente, sem religação. Ao mesmo tempo, quando mCachedViews estourar, o item mais antigo será recycleCachedViewAt
reciclado mRecyclerPool
, ou seja, o recurso de fila primeiro a entrar, primeiro a sair, portanto pode-se considerar que mCachedViews pertence a mRecyclerPool 预备队列
e seu cache é livre de poluição
// RecyclerView.Recycler.java
void recycleViewHolderInternal(ViewHolder holder) {
// ……
boolean cached = false;
boolean recycled = false;
// ……
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
// ……
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
// ……
}
// ……
}
Existem aproximadamente tantos conteúdos recuperados e, em retrospecto, parece inútil mViewCacheExtension
! ! ! Embora seja testado e recuperado durante a reutilização, não está envolvido no processo de reciclagem, ou seja, é totalmente mantido pelo desenvolvedor (ah, não ~).
Então também entendo duas distinções:
- Descartar sucata e cache
- Poluição
A seguir será explicado por que o descarte é temporário do ponto de vista da reutilização
reuso
Concluído em layoutChunk
// LinearLayoutManager.java
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
// ……
// 实际为attach,即建立child 和 RV 的绑定关系
addView\addDisappearingView(view);
// 测量
measureChildWithMargins(view, 0, 0);
// 测量完后就知道需要消耗多少空间了,remainingSpace的计算依赖于此
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
// 当然,这里的l\t\r\b计算也包含了ItemDecoration的附加
// measure完之后就layout,老生常谈
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (params.isItemRemoved() || params.isItemChanged()) {
// 这里是前面提到过的,预布局阶段计算剩余空间会跳过的场景,即会多加载下一个item
result.mIgnoreConsumed = true;
}
}
A reutilização vem do preenchimento de necessidades, layoutChunk
passo a passo ( layoutState.next >>> recycler.getViewForPosition >>> tryGetViewHolderForPositionByDeadline ), encontramos tryGetViewHolderForPositionByDeadline
. Um total de caches foram obtidos aqui 5次尝试
e também há dois métodos de adaptador com os quais estamos familiarizados createViewHolder>>>onCreateViewHolder
,tryBindViewHolderByDeadline>>>onBindViewHolder
- pré-layout
- Tome de acordo com a posição
- Pegue de acordo com Id e ViewType
- cache personalizado
- Do buffer pool de acordo com ViewType
- Criar ViewHolder
Tendo em vista a extensão do artigo, a lógica interna de cada método pode ser lida em profundidade, portanto não será ampliada uma a uma.
// RecyclerView.java
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
// ……
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 1. 仅用于预布局中的change,为了和mAttached区分,因为changed为执行动画前后需要不同的ViewHolder
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
}
// 2.根据Position取,依次从 mAttachedScrap、mHiddenViews(这个忘了提-_-,涉及到动画)、mCachedViews
// 会进行严格校验,如果失效了,会被再次回收
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (!validateViewHolderForOffsetPosition(holder)) {
// recycle holder (and unscrap if relevant) since it can't be used
if (!dryRun) {
// we would like to recycle this but need to make sure it is not used by
// animation logic etc.
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
}
// 3.根据Id和ViewType取,前提设置了hasStableIds,这里呼应了回收时,如果设置了也会相应回收进scrap,但仅对于detach场景
if (mAdapter.hasStableIds()) {
// 从 mAttachedScrap、mCachedViews取
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
holder.mPosition = offsetPosition;
}
// 4.自定义缓存
final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
holder = getChildViewHolder(view);
// 5. 从缓存池根据ViewType取
holder = getRecycledViewPool().getRecycledView(type);
// 这个重置操作很重要,意味着它必然需要re-bind。因为flag置0,下面!holder.isBound()一定为true
holder.resetInternal();
// 6.创建ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);
// ……
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
// 不走onBind也就是说Position不会更新
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 此处的判断决定了什么情况需要re-bind
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// ……
return holder;
}
Em 2. getScrapOrHiddenOrCachedHolderForPosition , a validade de position e Invalid é verificada . Ao mesmo tempo, mAttachedScrap e mCachedViews também são verificados quanto a não atualização durante a reciclagem, portanto, pode-se inferir que não há necessidade de religar após serem retirados aqui. (Observações: mHiddenViews não está relacionado neste artigo, está relacionado à animação)
Em seguida, combinado com a estratégia de reciclagem, especula-se que mAttachedScrap é usado para recuperação rápida após desanexação (depois remova-o, para onde foi no final? Na verdade, removeAndRecycleScrapInt realizou a reciclagem final em dispatchLayoutStep3 e irá para o pool de cache) , e mCachedViews é usado para Ao deslizar para cima e para baixo repetidamente, como [1, 2, 3], ao deslizar para cima [2, 3, 4] e depois deslizar para baixo [1, 2, 3], multiplexação direta
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
// scrap被取用后,不是remove掉,而是加了标记位。判断!holder.wasReturnedFromScrap()也是为了避免重复取用
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
// ……
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
// invalid view holders may be in cache if adapter has stable ids as they can be
// retrieved via getScrapOrCachedViewForId
if (!holder.isInvalid() && holder.getLayoutPosition() == position
&& !holder.isAttachedToTransitionOverlay()) {
if (!dryRun) {
mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
+ ") found match in cache: " + holder);
}
return holder;
}
}
return null;
}
Para 1.getChangedScrapViewForPosition , por que ele é usado apenas no pré-layout? Como alterado neste momento não significa que a religação seja necessária, significa apenas que será atualizado posteriormente, o que é usado para representar um estado de "pré-evento" e, no estágio de layout real, o item que precisa ser ser alterado irá reutilizar o cache no recyclerPool, ou seja, pegar um. O novo ViewHolder, com religação e layout real medido, representa um estado "após o fato". Sendo esse o caso, por que não colocá-lo em mAttachedScrap? Porque na fase de layout real, o mAttachedScrap restante que não foi disposto será onLayoutChildren
chamado no final layoutForPredictiveAnimations
para fazer o layout de todos os restantes, vou dar um exemplo para entender o propósito. Por exemplo, a tela pode acomodar apenas dois itens, [1,2], e a inserção precisa estar entre 1 e 2, então [1, novo2, antigo2] é a aparência real do instantâneo "pré-evento", e mChangedScrap evita que nesta fase seja preenchido
Este parágrafo pode ser um pouco complicado. É recomendável lê-lo várias vezes. Não tenho certeza se esse entendimento está correto. Bem-vindo a me corrigir
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
……
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
E 3. Qual é o significado de getScrapOrCachedViewForId ? hasStableIds
O uso de é um complemento para getChangedScrapViewForPosition . Ao mesmo tempo, notifyDataSetChanged
verifica-se na cadeia de chamadas de observação que hasStableIds pode evitar a reciclagem neste cenário mCachedViews
(o cache está poluído neste momento, o que é muito especial e precisa de atenção, ou seja, é necessária a religação para reutilização). Em alguns cenários específicos, melhora até certo ponto a eficácia e a eficiência da multiplexação
void markKnownViewsInvalid() {
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
final ViewHolder holder = mCachedViews.get(i);
if (holder != null) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
holder.addChangePayload(null);
}
}
if (mAdapter == null || !mAdapter.hasStableIds()) {
// we cannot re-use cached views in this case. Recycle them all
recycleAndClearCachedViews();
}
}
pergunta da alma
1. Quando usar a expansão do cache RV
As sugestões a seguir são baseadas em algumas de minha autoria após a leitura do código-fonte 猜想
e não foram testadas para obter o efeito de otimização. Bem-vindo ao comunicar. Como usá-lo precisa ser avaliado em combinação com cenários e interações reais
Tomemos como exemplo a coluna de classificação de segundo nível da Classificação Hema. Ao mudar a classificação de primeiro nível, é necessário atualizar e renderizar todas as classificações de segundo nível. Todas elas são consideradas visualizadores poluentes. Como o mesmo ViewType , apenas recyclerPool
5 são armazenados em cache por padrão e a figura está próxima de 10. Por um lado, não é suficiente. Com base em dados suficientes, quase todas as trocas requerem create
cerca de 5 espectadores. Em seguida, expanda o cache deste tipo ( setMaxRecycledViews
), então apenas será necessário religar
Então, mCachedViews
quando é recomendado expandir a capacidade, eu pessoalmente adivinhei, talvez como cachoeiras e grades, pode haver vários que são removidos ao mesmo tempo quando você desliza para cima em uma linha, e então você pode restaurá-lo rapidamente deslizando para baixo uma linha (como grades, grades, etc.) é definida como 2+spanCount por padrão) para evitar ser parcialmente reciclada no recyclerPool para religação devido ao estouro
2. Em que circunstâncias o cache obtido precisa do onBind e quando não?
Mencionamos anteriormente que aqueles RecycledViewPool
retirados precisam ser resetInternal
reiniciados e a religação deve ser realizada. E mChangedScrap
confirme que a atualização é necessária para ser diferenciada, portanto, a religação também é necessária
O resto mAttachedScrap
e a soma mCachedViews
são considerados não incluídos FLAG_UPDATE
, portanto, naturalmente, não são necessários. Claro, esta também é a sua limitação, porque só pode ser usado para multiplexação na mesma posição
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
3. Algumas outras sugestões de otimização para RV
- Se o tamanho do RV for fixo, pode-se
RecyclerView.setHasFixedSize(true)
evitar consumo desnecessáriorequestLayout
de desempenho por
// RecyclerViewDataObserver.java
void triggerUpdateProcessor() {
if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable);
} else {
mAdapterUpdateDuringMeasure = true;
requestLayout();
}
}
- Remova a animação padrão (dependendo da situação)
- onBindViewHolder vincula apenas o máximo possível para reduzir o processamento de dados
- Use carga útil para atualizar parcialmente, reduza outras ligações de dados onBindViewHolder desnecessárias, pode ser combinado com DiffUtil
- Monitore o deslizamento e cancele operações como carregamento de imagem ao deslizar, como
Glide.with(mContext).pauseRequests()
4. Não recomendado para notifyDataSetChanged
Chamar markKnownViewsInvalid
o método com todas ViewHolder
as marcas FLAG_INVALID
significa desanexar o link e todos os itens serão reciclados no recyclerViewPool.Se o pool transbordar, ele precisa ser criado para causar consumo no preenchimento
@Override
public void onChanged() {
assertNotInLayoutOrScroll(null);
mState.mStructureChanged = true;
processDataSetCompletelyChanged(true);
if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}
void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
mDispatchItemsChangedEvent |= dispatchItemsChanged;
mDataSetHasChangedAfterLayout = true;
markKnownViewsInvalid();
}
Notas de estudo do Android
Otimização de desempenho do Android: https://qr18.cn/FVlo89
Princípios subjacentes do Android Framework: https://qr18.cn/AQpN4J
Bucket da família Jetpack (incluindo Compose): https://qr18.cn/A0gajp
Carro Android: https://qr18.cn/F05ZCM
Notas de estudo de segurança reversa do Android: https://qr18.cn/CQ5TcL
Áudio e vídeo do Android: https://qr18.cn/Ei3VPD
Kotlin: https://qr18.cn/CdjtAF
Gradle: https://qr18.cn/DzrmMB
Notas de análise de código-fonte OkHttp: https://qr18.cn/Cw0pBD
Flutter: https://qr18.cn/DIvKma
Android Eight Corpo de conhecimento: https://qr18.cn/CyxarU
Android Core Notas: https://qr21.cn/CaZQLo
Perguntas anteriores da entrevista sobre Android: https://qr18.cn/CKV8OZ
Coleção mais recente de perguntas da entrevista sobre Android de 2023: https://qr18.cn/CgxrRy
Exercícios de entrevista de trabalho para desenvolvimento de veículos Android: https://qr18.cn/FTlyCJ
Perguntas da entrevista de áudio e vídeo:https://qr18.cn/AcV6Ap