Fragment极度懒加载-+-Layout子线程预加载,奇妙的APP启动速度优化思路

当App冷启动的时候,肉眼可见的要初始化的东西太多了,本身Fragment就是一个相对重的东西。比Activity要轻量很多,但是比View又要重

我们首页大概是 4-5个tab,每个tab都是一个Fragment,且第一个tab内嵌了4个Fragment,我这一次的优化主要将目标瞄准了首页的 tab1 以及tab1内嵌的四个tab

三、极致的懒加载

3.1 极致的懒加载

平时见到的懒加载:

就是初始化fragment的时候,会连同我们写的网络请求一起执行,这样非常消耗性能,最理想的方式是,只有用户点开或滑动到当前fragment时,才进行请求网络的操作。因此,我们就产生了懒加载这样一个说法。

但是。。。。

由于我们首屏4个子Tab都是继承自一个基类BaseLoadListFragment,数据加载的逻辑非常的死,按照上述的改法,影响面太大。后续可能会徒增烦恼

3.2 懒加载方案

  1. 首屏加载时,只往ViewPager中塞入默认要展示的tab,剩余的tab用空的占位Fragment代替
  2. 当用户滑动到其他tab时,比如滑动到好友动态tab,就用FriendFragment把当前的EmptyPlaceholderFragment替换掉,然后adapter.notifyDataSetChanged
  3. 当四个Tab全部替换为数据tab时,清除掉EmptyFragment的引用,释放内存

说到这里,又不得不提一个老生常谈的一个坑,因为我们的首页是用的ViewPager + FragmentPagerAdapter来进行实现的。因此就出现了一个坑:

ViewPager + FragmentPagerAdapter组合使用,调用notifyDataSetChanged()方法无效,无法刷新Fragment列表

下面我会对这个问题进行一下详细的介绍

3.3 FragmentPagerAdapter与FragmentStatePagerAdapter

当我们要使用ViewPager来加载Fragment时,官方为我们提供了这两种Adapter,都是继承自PagerAdapter。

区别,上官方描述:

FragmentPagerAdapter

This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider [FragmentStatePagerAdapter]( ).

FragmentStatePagerAdapter

This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to[FragmentPagerAdapter]( ) at the cost of potentially more overhead when switching between pages

总结:

  • 使用FragmentStatePagerAdapter时,如果tab对于用户不可见了,Fragment就会被销毁,FragmentPagerAdapter则不会,使用FragmentPagerAdapter时,所有的tab上的Fragment都会hold在内存里
  • 当tab非常多时,推荐使用FragmentStatePagerAdapter
  • 当tab不多,且固定时,推荐用FragmentPagerAdapter

我们项目中就是使用的ViewPager+FragmentPagerAdapter。

3.4 FragmentPagerAdapter的刷新问题

正常情况,我们使用adapter时,想要刷新数据只需要:

  1. 更新dataSet
  2. 调用notifyDataSetChanged()

但是,这个在这个Adapter中是不适用的。因为(这一步没耐心的可以直接看后面的总结):

  1. 默认的PagerAdapter的destoryItem只会把Fragment detach掉,而不会remove
  2. 当再次调用instantiateItem的时候,之前detach掉的Fragment,又会从mFragmentManager中取出,又可以attach了

3,ViewPager的dataSetChanged代码如下:

4,且adapter的默认实现

简单总结一下:

1,ViewPager的dataSetChanged()中会去用adapter.getItemPosition来判断是否要移除当前Item(position = POSITION_NONE时remove)

2,PagerAdapter的getItemPosition默认实现为POSITION_UNCHANGED

上述两点导致ViewPager构建完成Adapter之后,不会有机会调用到Adapter的instantiateItem了。

再者,即使重写了getItemPosition方法,每次返回POSITION_NONE,还是不会替换掉Fragment,这是因为instantiateItem方法中,会根据getItemId()去从FragmetnManager中找到已经创建好的Fragment返回回去,而getItemId()的默认实现是return position。

3.5 FragmentPagerAdapter刷新的正确姿势

重写getItemId()和getItemPosition()

class TabsAdapter extends FragmentPagerAdapter {

private ArrayList mFragmentList;
private ArrayList mPageTitleList;
private int mCount;

TabsAdapter(FragmentManager fm, ArrayList fragmentList, ArrayList pageTitleList) {
super(fm);
mFragmentList = fragmentList;
mCount = fragmentList.size();
mPageTitleList = pageTitleList;
}

@Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}

@Override
public CharSequence getPageTitle(int position) {
return mPageTitleList.get(position);
}

@Override
public int getCount() {
return mCount;
}

@Override
public long getItemId(int position) {
//这个地方的重写非常关键,super中是返回position,
//如果不重写,还是会继续找到FragmentManager中缓存的Fragment
return mFragmentList.get(position).hashCode();
}

@Override
public int getItemPosition(@NonNull Object object) {
//不在数据集合里面的话,return POSITION_NONE,进行item的重建
int index = mFragmentList.indexOf(object);
if (index == -1) {
return POSITION_NONE;
} else {
return mFragmentList.indexOf(object);
}
}

void refreshFragments(ArrayList fragmentList) {
mFragmentList = fragmentList;
notifyDataSetChanged();
}
}

其他的相关代码:

(1)实现ViewPager.OnPageChangeListener,来监控ViewPager的滑动状态,才可以在滑动到下一个tab的时候进行Fragment替换的操作,其中mDefaultTab是我们通过接口返回的当前启动展示的tab序号

@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}

@Override
public void onPageSelected(int position) {
mCurrentSelectedTab = position;
}

@Override
public void onPageScrollStateChanged(int state) {
if (!hasReplacedAllEmptyFragments && mCurrentSelectedTab != mDefaultTab && state == 0) {
//当满足: 1. 没有全部替换完 2. 当前tab不是初始化的默认tab(默认tab不会用空的Fragment去替换) 3. 滑动结束了,即state = 0
replaceEmptyFragmentsIfNeed(mCurrentSelectedTab);
}
}

备注:

onPageScrollStateChanged接滑动的状态值。一共有三个取值:

0:什么都没做
1:开始滑动
2:滑动结束

一次引起页面切换的滑动,state的顺序分别是: 1  ->  2  ->  0

2)进行Fragment的替换,这里因为我们的tab数量是可能根据全局config信息而改变的,所以这个地方写的稍微纠结了一些。

/**

  • 如果全部替换完了,直接return
  • 替换过程:
  • 1. 找到当前空的tab在mEmptyFragmentList 中的实际下标
  • @param tabId 要替换的tab的tabId - (当前空的Fragment在adapter数据列表mFragmentList的下标)
    */
    private void replaceEmptyFragmentsIfNeed(int tabId) {
    if (hasReplacedAllEmptyFragments) {
    return;
    }
    int tabRealIndex = mEmptyFragmentList.indexOf(mFragmentList.get(tabId)); //找到当前的空Fragment在 mEmptyFragmentList 是第几个
    if (tabRealIndex > -1) {
    if (Collections.replaceAll(mFragmentList, mEmptyFragmentList.get(tabRealIndex), mDataFragmentList.get(tabRealIndex))) {
    mTabsAdapter.refreshFragments(mFragmentList); //将mFragmentList中的相应empty fragment替换完成之后刷新数据
    boolean hasAllReplaced = true;
    for (Fragment fragment : mFragmentList) {
    if (fragment instanceof EmptyPlaceHolderFragment) {
    hasAllReplaced = false;
    break;
    }
    }
    if (hasAllReplaced) {
    mEmptyFragmentList.clear(); //全部替换完成的话,释放引用
    }
    hasReplacedAllEmptyFragments = hasAllReplaced;
    }
    }
    }

四、神奇的的预加载(预加载View,而不是data)

Android在启动过程中可能涉及到的一些View的预加载方案:

  1. WebView提前创建好,因为webview创建的耗时较长,如果首屏有h5的页面,可以提前创建好。
  2. Application的onCreate时,就可以开始在子线程中进行后面要用到的Layout的inflate工作了,最先想到的应该是官方提供的AsyncLayoutInflater
  3. 填充View的数据的预加载,今天的内容不涉及这一项

4.1 需要预加载什么

直接看图,这个是首页四个子Tab Fragment的基类的layout,因为某些东西设计的不合理,导致层级是非常的深,直接导致了首页上的三个tab加上FeedMainFragment自身,光将这个View inflate出来的时间就非常长。因此我们考虑在子线程中提前inflate layout

4.2 修改AsyncLayoutInflater

官方提供了一个类,可以来进行异步的inflate,但是有两个缺点:

  1. 每次都要现场new一个出来
  2. 异步加载的view只能通过callback回调才能获得(死穴)

因此决定自己封装一个AsyncInflateManager,内部使用线程池,且对于inflate完成的View有一套缓存机制。而其中最核心的LayoutInflater则直接copy出来就好。

先看AsyncInflateManager的实现,这里我直接将代码copy进来,而不是截图了,这样你们如果想用其中部分东西,可以直接copy:

/**

  • @author zoutao
  • 用来提供子线程inflate view的功能,避免某个view层级太深太复杂,主线程inflate会耗时很长,
  • 实就是对 AsyncLayoutInflater进行了抽取和封装
    */
    public class AsyncInflateManager {
    private static AsyncInflateManager sInstance;
    private ConcurrentHashMap<String, AsyncInflateItem> mInflateMap; //保存inflateKey以及InflateItem,里面包含所有要进行inflate的任务
    private ConcurrentHashMap<String, CountDownLatch> mInflateLatchMap;
    private ExecutorService mThreadPool; //用来进行inflate工作的线程池

private AsyncInflateManager() {
mThreadPool = new ThreadPoolExecutor(4, 4, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque());
mInflateMap = new ConcurrentHashMap<>();
mInflateLatchMap = new ConcurrentHashMap<>();
}

public static AsyncInflateManager getInstance() {
单例
}

/**

  • 用来获得异步inflate出来的view
  • @param context
  • @param layoutResId 需要拿的layoutId
  • @param parent container
  • @param inflateKey 每一个View会对应一个inflateKey,因为可能许多地方用的同一个 layout,但是需要inflate多个,用InflateKey进行区分
  • @param inflater 外部传进来的inflater,外面如果有inflater,传进来,用来进行可能的SyncInflate,
  • @return 最后inflate出来的view
    */
    @UiThread
    @NonNull
    public View getInflatedView(Context context, int layoutResId, @Nullable ViewGroup parent, String inflateKey, @NonNull LayoutInflater inflater) {
    if (!TextUtils.isEmpty(inflateKey) && mInflateMap.containsKey(inflateKey)) {
    AsyncInflateItem item = mInflateMap.get(inflateKey);
    CountDownLatch latch = mInflateLatchMap.get(inflateKey);
    if (item != null) {
    View resultView = item.inflatedView;
    if (resultView != null) {
    //拿到了view直接返回
    removeInflateKey(inflateKey);
    replaceContextForView(resultView, context);
    return resultView;
    }

if (item.isInflating() && latch != null) {
//没拿到view,但是在inflate中,等待返回
try {
latch.wait();
} catch (InterruptedException e) {
Log.e(TAG, e.getMessage(), e);
}
removeInflateKey(inflateKey);
if (resultView != null) {
replaceContextForView(resultView, context);
return resultView;
}

}
//如果还没开始inflate,则设置为false,UI线程进行inflate
item.setCancelled(true);
}
}
//拿异步inflate的View失败,UI线程inflate
return inflater.inflate(layoutResId, parent, false);
}

/**

  • inflater初始化时是传进来的application,inflate出来的view的context没法用来startActivity,
  • 因此用MutableContextWrapper进行包装,后续进行替换
    */
    private void replaceContextForView(View inflatedView, Context context) {
    if (inflatedView == null || context == null) {
    return;
    }
    Context cxt = inflatedView.getContext();
    if (cxt instanceof MutableContextWrapper) {
    ((MutableContextWrapper) cxt).setBaseContext(context);
    }
    }

@UiThread
private void asyncInflate(Context context, AsyncInflateItem item) {
if (item == null || item.layoutResId == 0 || mInflateMap.containsKey(item.inflateKey) || item.isCancelled() || item.isInflating()) {
return;
}
onAsyncInflateReady(item);
inflateWithThreadPool(context, item);
}

private void onAsyncInflateReady(AsyncInflateItem item) {

}

private void onAsyncInflateStart(AsyncInflateItem item) {

}

private void onAsyncInflateEnd(AsyncInflateItem item, boolean success) {
item.setInflating(false);
CountDownLatch latch = mInflateLatchMap.get(item.inflateKey);
if (latch != null) {
//释放锁
latch.countDown();
}

}

private void removeInflateKey(String inflateKey) {

}

private void inflateWithThreadPool(Context context, AsyncInflateItem item) {
mThreadPool.execute(new Runnable() {
@Override
public void run() {
if (!item.isInflating() && !item.isCancelled()) {
try {
onAsyncInflateStart(item);
item.inflatedView = new BasicInflater(context).inflate(item.layoutResId, item.parent, false);
onAsyncInflateEnd(item, true);
} catch (RuntimeException e) {
Log.e(TAG, “Failed to inflate resource in the background! Retrying on the UI thread”, e);
onAsyncInflateEnd(item, false);
}
}
}
});
}

/**

  • copy from AsyncLayoutInflater - actual inflater
    */
    private static class BasicInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = new String[]{“android.widget.”, “android.webkit.”, “android.app.”};

BasicInflater(Context context) {
super(context);
}

public LayoutInflater cloneInContext(Context newContext) {
return new BasicInflater(newContext);
}

protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = this.createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException ignored) {
}
}
return super.onCreateView(name, attrs);
}
}
}

这里我用一个AsyncInflateItem来管理一次要inflate的一个单位,

先自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以扫码领取!!!!

尾声

如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

架构篇

《Jetpack全家桶打造全新Google标准架构模式》

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!

录制的,相信这份视频能给你带来不一样的启发、收获。[外链图片转存中…(img-wmYDQHRL-1711288942293)]

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-s5W7ewB6-1711288942293)]

架构篇

《Jetpack全家桶打造全新Google标准架构模式》
[外链图片转存中…(img-Cdhy0QEI-1711288942293)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!

  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值