前言:
本文主要是針對(duì)在最近的一次版本迭代中,使用RecycleView實(shí)現(xiàn)一個(gè)版本首頁功能時(shí),整體的設(shè)計(jì)思路分享,還有遇到的一系列問題的梳理和總結(jié)。對(duì)作者本人來說是一次記錄的過程,以后遇到類似的問題時(shí)方便進(jìn)行回顧和復(fù)盤,如果同時(shí)能對(duì)其他人有所啟發(fā)和幫助,那就再好不過了。
主要涉及的問題點(diǎn):
- 后臺(tái)接口數(shù)據(jù)不確定時(shí)的數(shù)據(jù)實(shí)體映射方案
- RecycleView中組件狀態(tài)數(shù)據(jù)保存和恢復(fù)
- RecycleView中組件渲染刷新時(shí)機(jī)和臟數(shù)據(jù)的清理
- RecycleView中組件定時(shí)器創(chuàng)建和銷毀的生命周期管理:
- RecycleView中組件定時(shí)器相關(guān)性能優(yōu)化
- 首頁列表卡頓問題的性能優(yōu)化
- 內(nèi)存泄漏問題的排查和解決。
1、首頁設(shè)計(jì)方案:
首先我們先來看下設(shè)計(jì)出來的效果以及產(chǎn)品要求(截圖出來部分只是整個(gè)首頁的一部分,實(shí)際需要顯示模塊是比較多的,目前截圖已經(jīng)可以說明情況,就不再展示其他模塊的截圖)。

如圖所示只是整個(gè)首頁模塊的一部分,整體首頁有10到20個(gè)模塊,由后臺(tái)進(jìn)行配置顯示哪些模塊,而客戶端根據(jù)后臺(tái)返回的數(shù)據(jù)type映射數(shù)據(jù)進(jìn)行模塊展示。
現(xiàn)在我們先來明確下產(chǎn)品的要求:
1)最多達(dá)20個(gè)功能模塊,模塊比較多
2) 由后臺(tái)隨意配置顯示指定模塊
3)模塊位置可隨意變化,由后臺(tái)控制模塊展示的順序和位置
根據(jù)上述產(chǎn)品功能需求,我們基本就能確定實(shí)現(xiàn)方案。由后臺(tái)返回一個(gè)數(shù)據(jù)的數(shù)組,每個(gè)數(shù)組都包含不同模塊的數(shù)據(jù)實(shí)體,
這些數(shù)據(jù)實(shí)體有一些共同屬性,以及type??蛻舳烁鶕?jù)type來判斷對(duì)應(yīng)的是哪一個(gè)模塊并進(jìn)行顯示。
后臺(tái)數(shù)據(jù)定義:


根據(jù)和后臺(tái)確定的接口定義可以看出,后臺(tái)返回不同模塊數(shù)據(jù)的數(shù)組,每個(gè)模塊的數(shù)據(jù)有共同的屬性即父類BaseModule,但是每個(gè)模塊的數(shù)據(jù)類型并不相同,那么問題就來了,我們?cè)撊绾味x接口返回的字段呢。
這是一個(gè)問題。
眾所周知,我們使用網(wǎng)絡(luò)請(qǐng)求獲取數(shù)據(jù),在使用Gson FastJson等實(shí)體映射工具時(shí),需要事先定義好這個(gè)網(wǎng)絡(luò)請(qǐng)求需要返回的數(shù)據(jù)類型。但是本次的接口返回的數(shù)據(jù)是一個(gè)內(nèi)部數(shù)據(jù)類型無法確定的數(shù)組,我們并不知道具體的數(shù)據(jù)類型,那么要如何定義本次接口返回的數(shù)據(jù)實(shí)體呢?
2、數(shù)據(jù)實(shí)體映射實(shí)現(xiàn)
請(qǐng)先查看下圖:

考慮到數(shù)組內(nèi)數(shù)據(jù)無法確定,那么我們只能先定義一個(gè)已知的數(shù)據(jù)類型,因?yàn)槲覀兪褂玫膶?shí)體映射是Gson,所以首頁接口返回的實(shí)體定義為List<LinkedTreeMap>。 由于type和數(shù)據(jù)實(shí)體存在一個(gè)映射關(guān)系,所以先在Map中保存這種映射關(guān)系。比如registerMap.put<type , BaseHallModule.class>。比如type=1,就映射的是 EntityClassA實(shí)體。
在通過網(wǎng)絡(luò)請(qǐng)求得到List<LinkedTreeMap>的數(shù)據(jù)之后,我們需要將其映射為已知的各個(gè)模塊的實(shí)體類。
此時(shí)就需要對(duì)List<LinkedTreeMap> 進(jìn)行遍歷操作。在遍歷時(shí),通過linkedTreeMap.get("type"),就可以拿到該條數(shù)據(jù)的type。而由于之前我們已經(jīng)對(duì)type和實(shí)體的數(shù)據(jù)類型進(jìn)行了注冊(cè),那么此時(shí)再通過registerMap.get("type") 就可以拿到該條實(shí)體的Class。 然后通過JSON.parseObject(JSON.toJSONString(linkTreeMap.get(DATA_PARAM)), clazz),其中clazz= registerMap.get("type") 就可以直接映射得到該模塊數(shù)據(jù)的實(shí)體對(duì)象。
此時(shí)我們就將后臺(tái)返回的數(shù)據(jù)實(shí)體,由List<LinkedTreeMap> 轉(zhuǎn)化為了List<BaseHallModule>(其中各個(gè)模塊數(shù)據(jù)都是BaseHallModule的子類),就可以將其交給RecycleView去填充數(shù)據(jù)了。
3、RecycleView中組件狀態(tài)數(shù)據(jù)保存和恢復(fù)。

我們先來看這塊的功能,這里是一個(gè)選擇模塊,后臺(tái)可以指定選中哪幾個(gè)選項(xiàng),比如此時(shí)后臺(tái)默認(rèn)要求選中選項(xiàng)1 和選項(xiàng)3 。如果用戶選中了 選項(xiàng)2 和選項(xiàng)3, 如下圖:

用戶將該組件滾出屏幕,再滾回來時(shí),如果我們未做任何處理,那么此時(shí)該組件又會(huì)默認(rèn)恢復(fù)到選中的狀態(tài)。
當(dāng)然這是因?yàn)樵摻M件在列表中由不可見到可見時(shí),其Adpater的onBinderViewHolder方法會(huì)重新執(zhí)行,因?yàn)槲覀儾⑽磳?duì)用戶對(duì)選項(xiàng)的選中進(jìn)行數(shù)據(jù)的保存,就會(huì)導(dǎo)致onBinderViewHolder執(zhí)行時(shí),setData又會(huì)將最初后臺(tái)默認(rèn)選中的index設(shè)置給組件,組件又默認(rèn)選中選項(xiàng)1 和選項(xiàng)3 ,導(dǎo)致顯示混亂。
此時(shí)有兩種方式解決這個(gè)問題:
3.1 : 對(duì)選項(xiàng)的選擇狀態(tài)進(jìn)行狀態(tài)數(shù)據(jù)的保存,我們可以將用戶對(duì)組件操作的狀態(tài)數(shù)據(jù) ,比如這里選項(xiàng)選中的索引值保存在ViewHolder的原始數(shù)據(jù)中,當(dāng)用戶改變索引值時(shí),將原始數(shù)據(jù)中的索引值進(jìn)行更新,那么在下次該組件由不可見到可見時(shí),就可以恢復(fù)到用戶操作的正確的索引值上來。
3.2 : 在該組件在列表中,由不可見到可見時(shí),判斷該組件上一次操作的數(shù)據(jù)lastData 和 本次OnBindViewHolder時(shí),傳進(jìn)來的Data是否一致,如果不一致則進(jìn)行OnBindViewHolder的更新操作,即SelectView.setData()刷新界面,如果一致,則直接返回,不再刷新界面。
4、RecycleView中組件的渲染刷新時(shí)機(jī):
具體說明見 3.2。
先來看下HallBaseViewHolder的代碼(首頁所有ViewHolder的基類)
public abstract class HallBaseViewHolder<T extends HallBaseListItem>
extends CustomRecyclerView.BaseViewHolder {
protected Context mContext;
private T mLastData;
public HallBaseViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
mContext = itemView.getContext();
}
void preUpdate(T t, int position) {
if (!intercept() || mLastData == null || !mLastData.equals(t)) {
updateData(t, position);
}
mLastData = t;
}
/**
* 更新數(shù)據(jù)
*
* @param t t
*/
public abstract void updateData(T t, int position);
protected boolean intercept() {
return true;
}
}
HallBaseAdapter : RecycleView的Adapter:
public class HallBaseAdapter extends CustomRecyclerView.BaseAdapter<HallBaseListItem, HallBaseViewHolder> {
private Context mContext;
private CustomSwipeRefreshLayout mCustomSwipeRefreshLayout;
private CustomRecyclerView mCustomRecyclerView;
private ITimerContext mHallTimerContext;
public HallBaseAdapter(Context mContext, CustomRecyclerView customRecyclerView,
CustomSwipeRefreshLayout customSwipeRefreshLayout) {
this.mContext = mContext;
this.mCustomRecyclerView = customRecyclerView;
this.mCustomSwipeRefreshLayout = customSwipeRefreshLayout;
}
public void setmHallTimerContext(ITimerContext mHallTimerContext) {
this.mHallTimerContext = mHallTimerContext;
}
@Override
public void onSectionClick(View view, boolean isSelected, int position) {
}
@Override
public HallBaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
HallBaseViewHolder hallBaseViewHolder = null;
switch (viewType) {
case ModuleType.HALL_BANNER:
hallBaseViewHolder = BannerViewHolder.of(mContext, parent);
((BannerViewHolder) hallBaseViewHolder).setmHallRefreshWrapper
(mCustomSwipeRefreshLayout);
break;
case ModuleType.SERVICE_NOTICE:
hallBaseViewHolder = ServerNoticeViewHolder.of(mContext, parent);
break;
case ModuleType.ANNOUNCEMENT:
hallBaseViewHolder = AnnouncementViewHolder.of(mContext, parent);
break;
case ModuleType.DIGITAL_QUICK_BUY:
hallBaseViewHolder = DigitalQuickBuyViewHolder.of(mContext, parent);
break;
case ModuleType.ACTIVITY_ZONE:
hallBaseViewHolder = ActivityZoneViewHolder.of(mContext, parent);
break;
case ModuleType.IMAGE_ZONE:
hallBaseViewHolder = AdViewHolder.of(mContext, parent);
break;
case ModuleType.SPF_QUICK_BUY_SINGLE:
hallBaseViewHolder = SpfQuickBuyViewHolder.of(mContext, parent);
break;
......
......
......
}
return hallBaseViewHolder;
}
@Override
public void onBindViewHolder(HallBaseViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
HallBaseListItem hallBaseListItem = getShowItemData(position);
if (hallBaseListItem.ismNeedRefreshData()) {
holder.updateData(hallBaseListItem, position);
hallBaseListItem.setmNeedRefreshData(false);
} else {
holder.preUpdate(hallBaseListItem, position);
}
}
}
這里我們先只關(guān)注OnBindViewHolder中的方法,
我們注意到在該方法中,會(huì)首先判斷該條數(shù)據(jù)是否需要強(qiáng)制刷新即(hallBaseListItem.ismNeedRefreshData() ,這個(gè)判斷和定時(shí)器的單刷時(shí)機(jī)有關(guān),后面會(huì)詳細(xì)說明),如果不強(qiáng)制刷新,那么會(huì)執(zhí)行BaseHallViewHolder的preUpdate()方法。
void preUpdate(T t, int position) {
if (!intercept() || mLastData == null || !mLastData.equals(t)) {
updateData(t, position);
}
mLastData = t;
}
首先每個(gè)ViewHolder可以選擇是否攔截onBindViewHolder的刷新(即intercept()方法,默認(rèn)為true即攔截不刷新UI)。只有在不攔截,而且本次的onBindViewHolder的數(shù)據(jù)和上次保存的數(shù)據(jù)不一樣(即數(shù)據(jù)變化了,比如下拉刷新),才會(huì)進(jìn)行RecycleView里的組件View的刷新操作。
注意:
如果某個(gè)組件在列表中只存在一個(gè),那么通過這種方式(即使不做狀態(tài)數(shù)據(jù)的保存和恢復(fù)操作),是可以解決由不可見到可見時(shí)的用戶操作狀態(tài)的正確性的,因?yàn)榇藭r(shí)數(shù)據(jù)未變化,updateData()方法不執(zhí)行,列表不會(huì)進(jìn)行刷新操作。但是如果列表中存在多個(gè)組件,只通過preUpdate 數(shù)據(jù)相同性判斷,無法解決數(shù)據(jù)錯(cuò)亂的問題。
如下圖:

在滾動(dòng)列表時(shí),部分組件由不可見狀態(tài)到可見狀態(tài),此時(shí)由于OnBindViewHolder傳進(jìn)來的數(shù)據(jù)未變化,那么
updateData()并不會(huì)執(zhí)行。但是因?yàn)镽ecycleView的View在進(jìn)行復(fù)用時(shí),并不確定復(fù)用的是哪一個(gè)View,如果一頁顯示5個(gè)View時(shí),第6個(gè)View復(fù)用第1個(gè)View,其updateData()方法不執(zhí)行(原因見前面一句),那么第6個(gè)View顯示的選中狀態(tài)就完全是第1個(gè)View的狀態(tài),此時(shí)就會(huì)出現(xiàn)顯示混亂的問題。
組件復(fù)用結(jié)論:
1) RecycleView中的組件狀態(tài)數(shù)據(jù)保存和恢復(fù)是一定要做的流程,否則在多個(gè)View顯示時(shí),就會(huì)出現(xiàn)狀態(tài)數(shù)據(jù)顯示錯(cuò)亂的問題。
2)ViewHolder中preUpdate的判斷也是有必要的,當(dāng)data.equals(lastData)時(shí),不必刷新頁面(即使在多個(gè)View復(fù)用時(shí)也是如此,如果data一樣,說明其復(fù)用的是自己本身)。這樣可以減少列表滾動(dòng)時(shí)的UI渲染刷新的消耗,提升操作流暢性和性能。
5、RecycleView中組件復(fù)用時(shí)臟數(shù)據(jù)的清理:
在Recycleview中的自定義組件,某些可見的View如果進(jìn)行了一些計(jì)算產(chǎn)生的數(shù)據(jù),保存在View中時(shí)(自定義組件中的全局變量),在View進(jìn)行復(fù)用時(shí),如果不在初始化數(shù)據(jù)時(shí)對(duì)這些數(shù)據(jù)進(jìn)行清理,在下一個(gè)Item可見并復(fù)用上面的View時(shí),這些數(shù)據(jù)會(huì)對(duì)這個(gè)item產(chǎn)生影響,并有極大可能造成數(shù)據(jù)錯(cuò)亂的問題。所以在設(shè)計(jì)自定義組件時(shí),如果這個(gè)組件會(huì)在Recycleview中復(fù)用的話,需要考慮在setData時(shí),清理或者重置這些數(shù)據(jù)。
6、RecycleView中組件定時(shí)器生命周期的管理:

在ListView或者RecycleView中使用定時(shí)器是是一件麻煩事。
因?yàn)檫@涉及到幾個(gè)問題:
1)定時(shí)器由誰創(chuàng)建,由誰管理的問題
2)定時(shí)器在列表中何時(shí)創(chuàng)建,何時(shí)銷毀的問題
3)在整個(gè)頁面數(shù)據(jù)變化時(shí)(比如下拉刷新)定時(shí)器的管理和數(shù)據(jù)刷新的問題
4)單個(gè)定時(shí)器倒計(jì)時(shí)結(jié)束時(shí),如何進(jìn)行局部刷新的問題(整體頁面刷新相對(duì)浪費(fèi)資源和性能)。
針對(duì)上述問題,我們逐個(gè)進(jìn)行分析。
1)定時(shí)器由創(chuàng)建,由誰管理的問題
因?yàn)樵诹斜碇?,只有Item可見的時(shí)候,才會(huì)執(zhí)行adapter的onBindViewHolder的方法,進(jìn)行View的data填充以更新渲染UI。而當(dāng)?shù)谝淮蝿?chuàng)建且不可見時(shí),是不需要執(zhí)行定時(shí)器的。那么在adapter onBinderViewHodler時(shí),View.setData()中創(chuàng)建定時(shí)器是再好不過了。由于在列表中,View的生命周期并不是確定的(一次頁面刷新之后,也許這個(gè)View就不存在了,而且不能通過onDetachFromWindow時(shí)移除定時(shí)器,因?yàn)閂iew在列表中由可見到不可見時(shí),也會(huì)執(zhí)行這個(gè)方法。),所以組件中定時(shí)器的管理放在Adpater中進(jìn)行,又列表進(jìn)行管理。
當(dāng)然定時(shí)器在進(jìn)行倒計(jì)時(shí)時(shí),需要實(shí)時(shí)更新定時(shí)器的數(shù)據(jù)到數(shù)據(jù)源(否則再次onBindViewHolder時(shí)定時(shí)器就不準(zhǔn)了,跟上述狀態(tài)數(shù)據(jù)保存恢復(fù)原理一樣)。
可以將各個(gè)組件產(chǎn)生的定時(shí)器,統(tǒng)一放在一個(gè)全局的List中進(jìn)行管理,比如List<Timer> mTimerList;
2)定時(shí)器何時(shí)創(chuàng)建,何時(shí)銷毀的問題
3)頁面數(shù)據(jù)變化時(shí)(比如下拉刷新)定時(shí)器的管理和數(shù)據(jù)刷新的問題
定時(shí)器創(chuàng)建時(shí)機(jī): 在adapter onBinderViewHolder時(shí),View.setData時(shí)創(chuàng)建
定時(shí)器銷毀時(shí)機(jī):
a) 組件中 : 因?yàn)閂iew在RecycleView中會(huì)進(jìn)行復(fù)用。那么在adapter onBinderViewHolder時(shí),View.setData時(shí),如果數(shù)據(jù)源里指示該組件需要定時(shí)器,如果該View中已經(jīng)存在定時(shí)器,那么可以直接用這個(gè)定時(shí)器去執(zhí)行數(shù)據(jù)源里的倒計(jì)時(shí);如果該View中不存在定時(shí)器,那就需要?jiǎng)?chuàng)建一個(gè)定時(shí)器。如果該View中已經(jīng)存在了一個(gè)定時(shí)器,但是onBinderViewHolder時(shí),數(shù)據(jù)源中指示不再需要定時(shí)器,此時(shí)需要將這個(gè)定時(shí)器移除。
b)組件中: 當(dāng)組件中的倒計(jì)時(shí)結(jié)束時(shí),也要在View中移除這個(gè)定時(shí)器。
c) 在整個(gè)頁面列表數(shù)據(jù)發(fā)生變化時(shí)(比如下拉刷新),需要移除并銷毀整個(gè)mTimerList中的定時(shí)器。因?yàn)樵陧撁鏀?shù)據(jù)變化后,開發(fā)者不知道哪些View還在,哪些View已經(jīng)不存在了。那么此時(shí)比較便捷且穩(wěn)妥的做法就是,在頁面數(shù)據(jù)返回后,移除所有的定時(shí)器。放心,此時(shí)后臺(tái)會(huì)告訴我們需要的準(zhǔn)確的倒計(jì)時(shí)的數(shù)據(jù),后面只需要按照上述組件創(chuàng)建定時(shí)器的步驟執(zhí)行就可以繼續(xù)顯示不同組件需要顯示的定時(shí)器了。
d)在整個(gè)頁面銷毀時(shí),需要銷毀整個(gè)頁面里的所有定時(shí)器??梢酝ㄟ^上述說的操作全局mTimerList來進(jìn)行處理。
4) 定時(shí)器的局部刷新問題:
關(guān)于定時(shí)器的局部刷新: 首先服務(wù)器要有單刷某個(gè)模塊的單刷接口(如果后臺(tái)不提供單獨(dú)模塊的單刷接口,請(qǐng)直接刷新整個(gè)頁面,并把鍋推給后臺(tái)謝謝)
來來來,不說了,show the code。
@Override
public void onNext(StatusResponse<ResultDetailEntity> data) {
if (data.getCode() == 0) {
// 成功
ResultDetailEntity entity = data.getResult();
long countDownTime = entity.getSaleEndCountdownTime();
if (mHallBaseListItemList != null && mHallBaseListItemList.size() > 0) {
for (HallBaseListItem hallBaseListItem : mHallBaseListItemList) {
if (hallBaseListItem.getViewType() == ModuleType.KS_HZ_QUICK_BUY) {
Object object = hallBaseListItem.getData();
if (object != null && object instanceof K3hzRecommend) {
K3hzRecommend k3hzRecommend = (K3hzRecommend) object;
k3hzRecommend.setSaleEndCountdownTime(countDownTime);
hallBaseListItem.setmNeedRefreshData(true);
mHallBaseAdapter.notifyDataSetChanged();
}
}
}
}
}
}
其實(shí)就是在組件中的倒計(jì)時(shí)結(jié)束后,請(qǐng)求這個(gè)模塊的單刷接口,去單獨(dú)獲取這個(gè)模塊的數(shù)據(jù),然后從
mHallBaseListItemList中找出原來的數(shù)據(jù)替換掉,或者設(shè)置成單刷接口返回的數(shù)據(jù),notify adapter即可,這樣就不會(huì)像全刷一樣,浪費(fèi)性能和資源。
注意這里hallBaseListItem.setmNeedRefreshData(true);,即在ViewHolder preUpdate方法中,一定要執(zhí)行UI的刷新操作。
關(guān)于定時(shí)器的一個(gè)很重要的且易忽略的問題:
一個(gè)組件需要顯示倒計(jì)時(shí),這時(shí)后臺(tái)返回了倒計(jì)時(shí)是50秒,但是此時(shí)這個(gè)組件在列表中是不可見的。那么此時(shí)定時(shí)器是不執(zhí)行的,過了20秒后,用戶將這個(gè)組件滾動(dòng)到可見狀態(tài),此時(shí)倒計(jì)時(shí)應(yīng)該是還有30秒,但是數(shù)據(jù)源那里就還是50秒。這時(shí)就會(huì)出現(xiàn)倒計(jì)時(shí)數(shù)據(jù)不準(zhǔn)確的問題,解決方法就是在后臺(tái)接口返回倒計(jì)時(shí)數(shù)據(jù)時(shí),將相對(duì)數(shù)據(jù)50秒,轉(zhuǎn)化成時(shí)間戳,這樣即使出現(xiàn)上面的情況,倒計(jì)時(shí)就也就是準(zhǔn)確的了。
7、RecycleView中組件定時(shí)器相關(guān)性能優(yōu)化
理論上來說,首頁這里的配置是可以有無數(shù)個(gè)定時(shí)器存在的,所以對(duì)于定時(shí)器的性能管理是一個(gè)需要嚴(yán)肅思考的問題(對(duì)于所有定時(shí)器有一個(gè)統(tǒng)一的TimerManager來進(jìn)行創(chuàng)建和管理,也是非常有必要的)。
1)列表滾動(dòng)時(shí),定時(shí)器會(huì)執(zhí)行,但不渲染UI。只有在列表靜止時(shí)才會(huì)更新定時(shí)器的UI。
mCustomRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (mKsRecommendView != null) {
mKsRecommendView.resumeRenderCountDown();
}
} else {
if (mKsRecommendView != null) {
mKsRecommendView.pauseRenderCountDown();
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
}
});
即在定時(shí)器更新方法中,設(shè)置一個(gè)標(biāo)志位, 以判斷是否要執(zhí)行更新定時(shí)器UI的方法。
2) 擁有定時(shí)器的組件如果在列表中不可見時(shí),定時(shí)器會(huì)執(zhí)行,但不渲染UI。只有在該組件在列表中可見時(shí)
才會(huì)更新UI。
if (!mPauseRenderCountDown && mIsBelongPageVisible) {
if (mCustomRecyclerView != null) {
int firstVisiblePosition = mCustomRecyclerView.getmLinearLayoutManager()
.findFirstVisibleItemPosition();
int lastVisiblePosition = mCustomRecyclerView.getmLinearLayoutManager()
.findLastVisibleItemPosition();
if (mAdapterPosition < firstVisiblePosition || mAdapterPosition > lastVisiblePosition) {
L.verbose("ansen666", "Ks View在屏幕中不可見---->");
return;
}
}
mCountDownText.setText(TimeUtils.formatCoutDown(l,
false, true));
}
在RecycleView中,通過findFirstVisibleItemPosition 和 findLastVisibleItemPosition來判斷該組件在列表中是否可見。不可見就不setText()。
mPauseRenderCountDown 即為1)中提到的列表此時(shí)是否在滾動(dòng)的標(biāo)志位。
mIsBelongPageVisible 即為3)中要提到的,擁有定時(shí)器的頁面是否可見的標(biāo)志位。
3) 如果當(dāng)前頁面不可見時(shí),也不刷新定時(shí)器的UI。
首頁可能會(huì)存在大量的定時(shí)器,切換到其他頁面時(shí)其仍然在運(yùn)行并且渲染UI,首頁一直在onLayout 和onMeasure,比較耗費(fèi)資源,而且也會(huì)影響到其他頁面的流暢性和性能。所以當(dāng)首頁不可見時(shí),首頁的定時(shí)器就沒有渲染UI的必要,且最好也不再進(jìn)行UI的渲染。
8、首頁列表卡頓問題的性能優(yōu)化
上面也說過,首頁存在著十幾個(gè)模塊,有些模塊的布局是比較復(fù)雜的,所以在部分機(jī)器上就出現(xiàn)了一個(gè)很令人頭痛的性能問題: 滑動(dòng)卡頓。
沒辦法,出現(xiàn)了性能問題就需要排查解決啊,打開開發(fā)者選項(xiàng)中的Debug Gpu Overdraw,發(fā)現(xiàn)確實(shí)存在過度繪制的問題。


如上圖所示,是優(yōu)化后的版本,可以看到部分模塊繪制還是存在可以優(yōu)化的空間。但是相對(duì)于優(yōu)化之前,
嚴(yán)重過度繪制的情況,性能上已經(jīng)好了很多,也滿足了我們這邊一些低端機(jī)型(5 6年前手機(jī))流暢滑動(dòng)的需要。
針對(duì)首頁的列表卡頓的優(yōu)化,主要做了以下幾點(diǎn)保證了性能:
1) 針對(duì)過度繪制的問題 : 對(duì)列表中的各種組件View,尤其是卡頓嚴(yán)重的商品模塊(ViewPager+Panel +商品view)進(jìn)行了布局層級(jí)的優(yōu)化,減少所有可以減少的Layout布局層級(jí)。包括在ViewHolder中可以new出來的View,直接new出來,而不是通過xml inflater的方式(View 通過 xml inflater時(shí) 比new View(Context) 耗時(shí)增加很多,消耗也更大)。
2)經(jīng)過網(wǎng)上查閱的資料 Android 不同布局類型measure、layout、draw耗時(shí)對(duì)比(未做詳細(xì)驗(yàn)證,僅供參考,原文鏈接https://blog.csdn.net/a740169405/article/details/79037191)

在不影響布局層級(jí)深度的情況下,應(yīng)該盡量使用LinearLayout 和FrameLayout 來替換掉原來布局中的RelativeLayout 和ConstraintLayout(有時(shí)RelativeLayout是會(huì)減少布局層級(jí)的,作者本人在調(diào)試在售商品模塊時(shí),發(fā)現(xiàn)兩層LinearLayout的性能竟然比一層ConstraintLayout的性能還要高,涉及到層級(jí)取舍問題)。而本人在使用LinearLayout和FrameLayout后,配合減少布局層級(jí),使布局 在layout draw measure上時(shí)間大大減少,整個(gè)首頁列表的性能和流暢性也提升了很多。
3)除了上述在布局層面的優(yōu)化,還有就是上文提到過的在adapter的onBindViewHolder中,進(jìn)行preUpdate的判斷,如果本次ViewHolder接收到的數(shù)據(jù)和上次拿到的數(shù)據(jù)一致,說明數(shù)據(jù)并未刷新(比如列表Item只是從不可見到可見),此時(shí)不需要繼續(xù)直接View的UI刷新操作,直接return就好,這也節(jié)省無謂的刷新導(dǎo)致的內(nèi)存占用。
4) 還有就是上文提到過的定時(shí)器的優(yōu)化。列表中有定時(shí)器時(shí),每當(dāng)定時(shí)器觸發(fā)UI的更新時(shí),RecycleView都需要進(jìn)行onLayout 和OnMeasure的執(zhí)行操作。
在擁有定時(shí)器的組件不可見,以及列表滾動(dòng)時(shí),或者擁有定時(shí)器的頁面不可見時(shí),就停止定時(shí)器的渲染UI操作(定時(shí)器可以繼續(xù)運(yùn)行,只是不更新UI)。這樣就減少了因定時(shí)器更新UI而導(dǎo)致RecycleView onLayout onMeasure的執(zhí)行操作,減少了計(jì)算時(shí)間和性能上的消耗。
5)還有就是商品列表那里調(diào)試性能時(shí)發(fā)現(xiàn)的問題。每次下拉刷新后,force garbage collection之后,
增加的內(nèi)存并不能降下來。后來經(jīng)過分析之后,發(fā)現(xiàn)是因?yàn)樯唐妨斜硎褂肰iewPager+ PanelView實(shí)現(xiàn)。在每次下拉刷新后,上次生成的List<PanelView>是直接拋棄了,重新生成了一份新的List<PanelView>。這樣就導(dǎo)致了每下拉刷新一次,就重新生成了很多歌PanelView。解決方法就是將第一次生成的List<PanelView> 緩存下來,供下一次下拉刷新后使用。這樣force garbage collection之后,增加的內(nèi)存的就可以降到初始值了。
所以在構(gòu)建組件時(shí),注意進(jìn)行View的復(fù)用也可以極大的節(jié)省內(nèi)存占用和提升性能。
9、內(nèi)存泄漏問題的排查和解決。
如果說上文提到的卡頓問題,我們可以歸根于手機(jī)性能太差了(3 4 年前甚至5 6年前的手機(jī))。那么如果代碼存在內(nèi)存泄漏的問題,我們就不能再找任何借口了。除了心里暗暗罵Google不能幫開發(fā)者減少內(nèi)存泄漏的風(fēng)險(xiǎn)之外,我們只能逐個(gè)排查泄漏根源所在。
是的,這次內(nèi)存泄漏的問題,好巧不巧我們也遇到了(其實(shí)應(yīng)該之前版本就存在,只是之前版本因功能原因等內(nèi)存占用較低,此問題不突出)。測試同學(xué)反饋近期應(yīng)用經(jīng)常出現(xiàn)閃退的問題(不是崩潰,是直接閃退應(yīng)用),經(jīng)過初步分析和確認(rèn),有很大幾率是內(nèi)存泄漏的問題。打開Android Profiler進(jìn)行調(diào)試時(shí)發(fā)現(xiàn):

打開應(yīng)用進(jìn)入首頁,不斷切換App首頁的幾個(gè)tab,然后退出應(yīng)用。然后再進(jìn)入應(yīng)用,重復(fù)上述步驟。然后運(yùn)行Force Garbage Collection 執(zhí)行垃圾回收操作,然后執(zhí)行Dump Java Heap。然后發(fā)現(xiàn)首頁MainActivity仍然存在多個(gè)實(shí)例無法被釋放掉,那么很明顯了,內(nèi)存泄漏了,有對(duì)象持有了Activity的引用,無法被釋放掉,造成了多個(gè)MainAcitivty的實(shí)例存在,也就引發(fā)了一連串相關(guān)內(nèi)存無法釋放的問題,造成內(nèi)存泄漏和閃退。
明確了問題根源,接下來就是漫長而枯燥的排查工作,沒辦法,自己挖的坑,無論如何也要填完。
一般涉及到類似無法定位的問題排查時(shí),經(jīng)常使用二分查找法。而這次為了更準(zhǔn)確的定位是哪個(gè)模塊的問題,我們采用了逐個(gè)模塊進(jìn)行排查的方式(每次屏蔽其他模塊,只展示要排查的模塊),整個(gè)排查的過程比較漫長和枯燥,這里不再贅述,直接給出排查后的結(jié)論。
內(nèi)存泄漏的原因有很多,這里只給出這次排查涉及到的點(diǎn):
在全局變量比如單例中,如果使用到Context ,一定不要持有Activity的Context,盡量使用Application 的Context。
單例在應(yīng)用程序退出時(shí),一定要記得銷毀,普通的退出應(yīng)用時(shí),單例并不會(huì)自動(dòng)銷毀,需要手動(dòng)進(jìn)行釋放。如果單例中存在大量數(shù)據(jù),不主動(dòng)銷毀單例在應(yīng)用程序退出時(shí)仍然會(huì)占用大量內(nèi)存。
Listener Callback等如果持有View的引用,Activity退出時(shí),要記得取消相應(yīng)回調(diào)等,防止因Listener未釋放,導(dǎo)致View沒有被釋放而引發(fā)的內(nèi)存占用問題。
經(jīng)組內(nèi)測試發(fā)現(xiàn),Lambda表達(dá)式,在部分使用場景下,會(huì)生成static的數(shù)據(jù)變量,很難釋放和銷毀,后續(xù)會(huì)針對(duì)這塊做相應(yīng)處理,目前要盡力避免使用Lambda表達(dá)式(我們用的是apply plugin: 'me.tatarka.retrolambda' 這個(gè)庫)。
定時(shí)器啟動(dòng)后一定要注意銷毀,因其不銷毀造成的內(nèi)存泄漏問題是必然存在的。
在使用List<Activity> List<view> List<Fragment>時(shí),使用完成后,需要手動(dòng)clear掉,防止出現(xiàn)內(nèi)存泄漏的問題,尤其是List<Activity>,以前是發(fā)現(xiàn)過因?yàn)長ist<Activity> 未妥善處理導(dǎo)致的泄漏問題。
應(yīng)用退出時(shí),引發(fā)多個(gè)Activity實(shí)例存在且無法銷毀的最直接原因:
1)某個(gè)模塊在頁面退出時(shí)定時(shí)器未銷毀(很低級(jí)且致命的問題)
2) Listener持有view引用且Listener如果不能夠得到釋放的話,就會(huì)存在泄漏問題。 比如Listener被保存在Map中未被釋放(一個(gè)錯(cuò)誤的調(diào)用導(dǎo)致的很極端的泄漏問題)
而上述所列的其他原因,也會(huì)對(duì)內(nèi)存占用造成極大負(fù)擔(dān),且加大了內(nèi)存泄漏的風(fēng)險(xiǎn),在開發(fā)中一定要小心謹(jǐn)慎,并嚴(yán)格按照規(guī)范要求來使用。
結(jié)語:
本文主要針對(duì)在本次版本迭代使用RecycleView構(gòu)建首頁時(shí)產(chǎn)生的一系列問題的回顧與總結(jié)。本次迭代主要涉及到了后臺(tái)接口數(shù)據(jù)不確定時(shí)的數(shù)據(jù)映射方案的實(shí)現(xiàn);RecycleView中組件View的復(fù)用,狀態(tài)數(shù)據(jù)的保存和恢復(fù),臟數(shù)據(jù)的清理;列表中定時(shí)器創(chuàng)建和銷毀生命周期的管理;列表中定時(shí)器相關(guān)的性能優(yōu)化;列表卡頓相關(guān)的性能優(yōu)化,內(nèi)存泄漏問題的定位和排查等。希望其中涉及到的問題能對(duì)其他人有所啟發(fā)和幫助。
因時(shí)間關(guān)系文章難免有疏漏,歡迎提出指正,謝謝。