明天是周二,正好是咱们团队每周一次的技术分享,我会把前段时间花了几天在干其余活的同时,整的一套诡异的冷启动速度优化方案分享一下。html
我这边文章的内容不会涉及网上变地都是的常规的优化方案~ ,同时,平时工做的时候,工做内容杂且多,因此这个优化方案也不是特别成熟,仅供参考吧~android
在咱们的Android项目中,应用过了闪屏以后会进入到主屏 - MainActivity,这个地方我吐槽不少次了,广告闪屏做为launcher真的不是特别靠谱,最好的方式应该是从MainActivity里面来启动AdActivity,甚至是不用Activity,采用一个全屏的AdView均可以。web
先简单介绍一下咱们项目中MainActivity涉及到的结构:设计模式
简单的画了个图,简直是。。画图界的耻辱。。。缓存
大概看看意思就能够了,我在组内分享就是用的这个草图,急着下班,就不从新画了。。bash
当App冷启动的时候,肉眼可见的要初始化的东西太多了,自己Fragment就是一个相对重的东西。比Activity要轻量不少,可是比View又要重
网络
咱们首页大概是 4-5个tab,每一个tab都是一个Fragment,且第一个tab内嵌了4个Fragment,我这一次的优化主要将目标瞄准了首页的 tab1 以及tab1内嵌的四个tabapp
平时见到的懒加载:less
就是初始化fragment的时候,会连同咱们写的网络请求一块儿执行,这样很是消耗性能,最理想的方式是,只有用户点开或滑动到当前fragment时,才进行请求网络的操做。所以,咱们就产生了懒加载这样一个说法。
可是。。。。异步
因为咱们首屏4个子Tab都是继承自一个基类BaseLoadListFragment,数据加载的逻辑很是的死,按照上述的改法,影响面太大。后续可能会徒增烦恼
说到这里,又不得不提一个老生常谈的一个坑,由于咱们的首页是用的ViewPager + FragmentPagerAdapter来进行实现的。所以就出现了一个坑:
ViewPager + FragmentPagerAdapter组合使用,调用notifyDataSetChanged()方法无效,没法刷新Fragment列表
下面我会对这个问题进行一下详细的介绍
当咱们要使用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, considerFragmentStatePagerAdapter
.
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 toFragmentPagerAdapter
at the cost of potentially more overhead when switching between pages
总结:
咱们项目中就是使用的ViewPager+FragmentPagerAdapter。
正常状况,咱们使用adapter时,想要刷新数据只须要:
可是,这个在这个Adapter中是不适用的。由于(这一步没耐心的能够直接看后面的总结):
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。
重写getItemId()和getItemPosition()
class TabsAdapter extends FragmentPagerAdapter {
private ArrayList<Fragment> mFragmentList;
private ArrayList<String> mPageTitleList;
private int mCount;
TabsAdapter(FragmentManager fm, ArrayList<Fragment> fragmentList, ArrayList<String> 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<Fragment> 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:什么都没作
一次引发页面切换的滑动,state的顺序分别是: 1 -> 2 -> 0
1:开始滑动
2:滑动结束
(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;
}
}
}
复制代码
Android在启动过程当中可能涉及到的一些View的预加载方案:
直接看图,这个是首页四个子Tab Fragment的基类的layout,由于某些东西设计的不合理,致使层级是很是的深,直接致使了首页上的三个tab加上FeedMainFragment自身,光将这个View inflate出来的时间就很是长。所以咱们考虑在子线程中提早inflate layout
官方提供了一个类,能够来进行异步的inflate,可是有两个缺点:
所以决定本身封装一个AsyncInflateManager,内部使用线程池,且对于inflate完成的View有一套缓存机制。而其中最核心的LayoutInflater则直接copy出来就好。
先看AsyncInflateManager的实现,这里我直接将代码copy进来,而不是截图了,这样大家若是想用其中部分东西,能够直接copy:
/**
* @author zoutao
* <p>
* 用来提供子线程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<Runnable>());
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的一个单位,
/**
* @author zoutao
*/
public class AsyncInflateItem {
String inflateKey;
int layoutResId;
ViewGroup parent;
OnInflateFinishedCallback callback;
View inflatedView;
private boolean cancelled;
private boolean inflating;
//还有一些set get方法
}复制代码
以及最后inflate的回调callback:
public interface OnInflateFinishedCallback {
void onInflateFinished(AsyncInflateItem result);
}复制代码
通过这样的封装,外面能够直接在Application的onCreate中,开始异步的inflate view的任务。调用以下:
AsyncInflateUtil.startTask();复制代码
/**
* @author zoutao
*/
public class AsyncInflateUtil {
public static void startTask() {
Context context = new MutableContextWrapper(CommonContext.getApplication());
AsyncInflateManager.getInstance().asyncInflateViews(context,
new AsyncInflateItem(InflateKey.TAB_1_CONTAINER_FRAGMENT, R.layout.fragment_main),
new AsyncInflateItem(InflateKey.SUB_TAB_1_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_2_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_3_FRAGMENT, R.layout.fragment_load_list),
new AsyncInflateItem(InflateKey.SUB_TAB_4_FRAGMENT, R.layout.fragment_load_list));
}
public class InflateKey {
public static final String TAB_1_CONTAINER_FRAGMENT = "tab1";
public static final String SUB_TAB_1_FRAGMENT = "sub1";
public static final String SUB_TAB_2_FRAGMENT = "sub2";
public static final String SUB_TAB_3_FRAGMENT = "sub3";
public static final String SUB_TAB_4_FRAGMENT = "sub4";
}
}
复制代码
注意:这里会有一个坑。就是在Application的onCreate中,能拿到的Context只有Application,这样inflate的View,View持有的Context就是Application,这会致使一个问题。
若是用View.getContext()这个context去进行Activity的跳转就会。。抛异常
Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
而若是想要传入Activity来建立LayoutInflater,时机又太晚。众所周知,Context是一个抽象类,实现它的包装类就是ContextWrapper,而Activity、Appcation等都是ContextWrapper的子类,然而,ContextWrapper还有一个神奇的子类,
package android.content;
/**
* Special version of {@link ContextWrapper} that allows the base context to
* be modified after it is initially set.
*/
public class MutableContextWrapper extends ContextWrapper {
public MutableContextWrapper(Context base) {
super(base);
}
/**
* Change the base context for this ContextWrapper. All calls will then be
* delegated to the base context. Unlike ContextWrapper, the base context
* can be changed even after one is already set.
*
* @param base The new base context for this wrapper.
*/
public void setBaseContext(Context base) {
mBase = base;
}
}
复制代码
能够看到Android上Context的设计采用了装饰器模式,装饰器模式极大程度的提升了灵活性。这个例子对我最大的感觉就是,当官方没有提供MutableContextWrapper这个类时,其实咱们本身也彻底能够经过一样的方式去进行实现。思惟必定要灵活~
常见的启动速度优化的方案有:
这些均可以在网上找到大量的文章以及各个大佬的实现方案。
首先,优化的大方向确定先定好:
懒加载:
预加载:
这些方案不必定很是好使,因此仅供参考~~~
从ContextWrapper、MutableContextWrapper类的设计中学到了 ↓
个人简书 邹啊涛涛涛的简书
个人CSDN 邹啊涛涛涛的CSDN
个人掘金 邹啊涛涛涛的掘金