你如果拿到公司的一个大型app,首先肯定是研究一下它的技术,这个App到底用到什么框架,用什么方式来实现的,如果开发一款高性能app,考虑使用什么框架来构建这个app,现在大多数app都是在用MVVM,这里的MVVM是M-V-VM,UI上的框架大多使用viewPage+Fragment,头条、QQ等都是这么干,都是使用双层的ViewPage+Fragment,这个已经成为了app的标配,左右滑动,滑动切换界面,切换内容,符合我们用户的操作需求。
认真观察头条的app,在它栏目里左右滑动的时候会出现空白,这是使用了懒加载,这是对性能的考虑。
了解懒加载之前,先了解一下什么是预加载?
预加载就是预先加载,先加载,除了当前页面,前后两个页面也要加载出来。让界面可以加快显示出来。
数据结构中有两个知识点,空间复杂度、时间复杂度。
那么这里让界面能加快展示到用户面前,是有代价的。有句话是“用空间换时间”,“用时间换空间”,虽然我只显示了一个页面,当前页面,但是我已经在背后默默地在当前页面左右都缓存了两个页面的数据,者就是预加载,将它加载到内存里面,当我们从当前页面滑到另外一个页面时候,直接那个页面的数据就显示出来了,它不需要再去进行渲染这方面的操作,这动作已经完成了,所以这就能提升我们用户左右滑动这个效果的感觉,但是这个代价在,预加载都要放到内存中,内存的使用增加,数据加载增加,本来只加载1页页面的数据,现在要加载5页的数据,内存本来是缓存一个页面的,现在要加载5页的数据,内存5M->25M ,数据加载0.1s->0.5s
懒加载
预加载在数据量少的时候,确实能满足app的需求,但是随着用户的需求,页面的展示越来越高要求,页面需要展示高清图,视频等占内存大,请求数据大的。预加载占的内存太大,而且一次性请求的数据量也会随之而几倍增长。
懒加载,用一个空白页面,有页面没数据,节省内存,预加载一个空白页面只占5k内存,减少内存的使用,当滑动为当前页面时,加载数据。
都知道方法tansaction.attach(fragment)到这里时,Fragment的生命周期是不会马上执行的,等事务执行commit的时候才会走生命周期,这和数据库的管理很类似,都是用触发器去管理的,触发器管理有一个特征,它需要commit,你只有提交了,你的事件才会执行。
所以这里setUserVisibleHint会先于生命周期执行。
懒加载跟UI可见有关系
加载数据的操作要放到与UI相关的函数里面去,跟UI相关的函数是:
onCreateView -- >创建UI fragment页面的创建
onResume -- >界面可见的
onPause --> 是界面可见,但是不可交互
onDestroyView --> 界面的销毁
public abstract class LazyFragment extends Fragment
{
private static final String TAG = "LazyFragment";
/**
* Fragment生命周期 onAttach -> onCreate -> onCreatedView -> onActivityCreated
* -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy
* -> onDetach 对于 ViewPager + Fragment 的实现我们需要关注的几个生命周期有: onCreatedView +
* onActivityCreated + onResume + onPause + onDestroyView
*/
protected View rootView = null;
/**
* 布局是否创建完成
*/
protected boolean isViewCreated = false;
/**
* 当前可见状态
*/
protected boolean currentVisibleState = false;
/**
* 是否第一次可见
*/
protected boolean mIsFirstVisible = true;
@Override
public void onAttach(Context context)
{
super.onAttach(context);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
}
/**
* 修改Fragment的可见性 setUserVisibleHint 被调用有两种情况:
* 1)在切换tab的时候,会先于所有fragment的其他生命周期,先调用这个函数,可以看log 2)
* 对于之前已经调用过setUserVisibleHint方法的fragment后,让fragment从可见到不可见之间状态的变化
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser)
{
super.setUserVisibleHint(isVisibleToUser);
Logger.d("setUserVisibleHint: " + isVisibleToUser);
// 对于情况1)不予处理,用 isViewCreated 进行判断,如果isViewCreated false,说明它没有被创建
if (isViewCreated)
{
// 对于情况2,需要分情况考虑,如果是不可见 -> 可见 2.1
// 如果是可见 -> 不可见 2.2
// 对于2.1)我们需要如何判断呢?首先必须是可见的(isVisibleToUser
// 为true)而且只有当可见状态进行改变的时候才需要切换,否则会出现反复调用的情况
// 从而导致事件分发带来的多次更新
if (isVisibleToUser && !currentVisibleState)
{
// 从不可见 -> 可见
dispatchUserVisibleHint(true);
}
else if (!isVisibleToUser && currentVisibleState)
{
dispatchUserVisibleHint(false);
}
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
{
super.onCreateView(inflater, container, savedInstanceState);
if (null == rootView)
{
rootView = inflater.inflate(getLayoutRes(), container, false);
}
initView(rootView);
Logger.d("onCreateView: ");
// 初始化的时候,判断当前Fragment可见状态
// isHidden在使用FragmentTransaction的show/hidden时会调用,可见返回的是false
if (!isHidden() && getUserVisibleHint())
{
// 可见状态,进行事件分发
dispatchUserVisibleHint(true);
}
return rootView;
}
/**
* 用FragmentTransaction来控制fragment的hide和show时,
* 那么这个方法就会被调用。每当你对某个Fragment使用hide 或者是show的时候,那么这个Fragment就会自动调用这个方法。
*/
@Override
public void onHiddenChanged(boolean hidden)
{
Logger.d("onHiddenChanged: " + hidden);
super.onHiddenChanged(hidden);
// 这里的可见返回为false
if (hidden)
{
dispatchUserVisibleHint(false);
}
else
{
dispatchUserVisibleHint(true);
}
}
/**
* 统一处理用户可见事件分发
*/
private void dispatchUserVisibleHint(boolean isVisible)
{
Logger.d("dispatchUserVisibleHint: " + isVisible);
// 首先考虑一下fragment嵌套fragment的情况(只考虑2层嵌套)
if (isVisible && isParentInvisible())
{
// 父Fragmnet此时不可见,直接return不做处理
return;
}
// 为了代码严谨,如果当前状态与需要设置的状态本来就一致了,就不处理了
if (currentVisibleState == isVisible)
{
return;
}
currentVisibleState = isVisible;
if (isVisible)
{
if (mIsFirstVisible)
{
mIsFirstVisible = false;
// 第一次可见,进行全局初始化
onFragmentFirstVisible();
}
onFragmentResume();
// 分发事件给内嵌的Fragment
dispatchChildVisibleState(true);
}
else
{
onFragmentPause();
dispatchChildVisibleState(false);
}
}
/**
* 在双重ViewPager嵌套的情况下,第一次滑到Frgment 嵌套ViewPager(fragment)的场景的时候
* 此时只会加载外层Fragment的数据,而不会加载内嵌viewPager中的fragment的数据,因此,我们
* 需要在此增加一个当外层Fragment可见的时候,分发可见事件给自己内嵌的所有Fragment显示
*/
private void dispatchChildVisibleState(boolean visible)
{
FragmentManager fragmentManager = getChildFragmentManager();
List fragments = fragmentManager.getFragments();
if (null != fragments)
{
for (Fragment fragment : fragments)
{
if (fragment instanceof LazyFragment && !fragment.isHidden()
&& fragment.getUserVisibleHint())
{
((LazyFragment)fragment).dispatchUserVisibleHint(visible);
}
}
}
}
/**
* Fragment真正的Pause,暂停一切网络耗时操作
*/
protected void onFragmentPause()
{
Logger.d("onFragmentResume " + " 真正的resume,开始相关操作耗时");
}
/**
* Fragment真正的Resume,开始处理网络加载等耗时操作
*/
protected void onFragmentResume()
{
Logger.d("onFragmentPause" + " 真正的Pause,结束相关操作耗时");
}
private boolean isParentInvisible()
{
Fragment parentFragment = getParentFragment();
if (parentFragment instanceof LazyFragment)
{
LazyFragment fragment = (LazyFragment)parentFragment;
return !fragment.isSupportVisible();
}
return false;
}
private boolean isSupportVisible()
{
return currentVisibleState;
}
/**
* 在滑动或者跳转的过程中,第一次创建fragment的时候均会调用onResume方法
*/
@Override
public void onResume()
{
super.onResume();
// 如果不是第一次可见
if (!mIsFirstVisible)
{
// 如果此时进行Activity跳转,会将所有的缓存的fragment进行onResume生命周期的重复
// 只需要对可见的fragment进行加载,
if (!isHidden() && !currentVisibleState && getUserVisibleHint())
{
dispatchUserVisibleHint(true);
}
}
}
/**
* 只有当当前页面由可见状态转变到不可见状态时才需要调用 dispatchUserVisibleHint currentVisibleState &&
* getUserVisibleHint() 能够限定是当前可见的 Fragment 当前 Fragment 包含子 Fragment 的时候
* dispatchUserVisibleHint 内部本身就会通知子 Fragment 不可见 子 fragment 走到这里的时候自身又会调用一遍
*/
@Override
public void onPause()
{
super.onPause();
if (currentVisibleState && getUserVisibleHint())
{
dispatchUserVisibleHint(false);
}
}
@Override
public void onDestroyView()
{
super.onDestroyView();
Logger.d("onDestroyView");
isViewCreated = false;
mIsFirstVisible = false;
}
@Override
public void onDestroy()
{
super.onDestroy();
}
@Override
public void onDetach()
{
super.onDetach();
}
/**
* 第一次可见,根据业务进行初始化操作
*/
protected abstract void onFragmentFirstVisible();
/**
* 初始化页面,如果不需要进行懒加载则可直接在此处进行网络处理
*
* @param rootView
*/
protected abstract void initView(View rootView);
/**
* 获取布局资源
*/
protected abstract int getLayoutRes();
}
initView()和getLayoutRes()都声明为抽象函数,让子类去实现,这里是布局和findViewbyId等操作,交给具体的fragment去实现
为什么rootView要去判空创建呢?rootView不是一定会是空吗?
它的生命周期肯定不会只调用一次,当它重复去走onCreateView的时候,这个生命周期我们是控制不了的,是Activity去回调给我们的。
为什么onFragmentResume()和onFragmentPause()不是抽象方法?
这个LazyFragment是一个抽象类,如果你把它做成一个模板类BaseFragment,你就可以把它当父类,你把它当父类又不需要实现懒加载,那你就不需要实现这两个函数,是没关系的没这是正常的fragment,为了扩展,这是一个需求,面向对象的思路,选择和不选择懒加载都可以共用 这个父类。
加入标志位 isViewCreate
这个值为true时,才会分发事件
在首次加载的时候也要补发一句
// 初始化的时候,判断当前Fragment可见状态
// isHidden在使用FragmentTransaction的show/hidden时会调用,可见返回的是false
if (!isHidden() && getUserVisibleHint())
{
// 可见状态,进行事件分发
dispatchUserVisibleHint(true);
}
添加变量currentVisibleState
为了代码严谨,如果当前状态与需要设置的状态本来就一致了,就不处理了
fragment要显示数据,哪里来的?
fragment要显示数据,哪里来的?一般情况下在onResume里面,在onResume里面做数据加载操作,网络操作,线程,启动线程,去viewmodel里面取数据,但是懒加载不允许你数据加载在onResume里面,因为onRsume有种情况不需要加载,那就是预加载的情况,如果预加载也在onRsume里面,那么懒加载就没意义了,所以懒加载是用于控制数据加载的时机,所以不能在onResume里面加载,应该在UI可见的时候去setUserVisibleHint传入true,说明当前可见,就加载数据调用dispathUserVisibleHint(true);否者停止加载,调用dispatchUserVisibleHint(false),根据传进去的visibleState去判断选择使用onFragmentLoad还是OnFragmentLoadStop。
在onResume()和onPause()里面加处理
当这个Activity跳到另一个Activity的时候,不会去修改可见性,只会走生命周期的onPause(),而回来的时候也只会走onResume(),所以我们要主动去调用可见方法。dispatchUserVisibleHint(XXX);
什么是性能优化?
性能优化的本质是,做法是节省内存,减少流量,目的是在软件放牧减少不必要的CPU的浪费,减少对内存的不合理使用,这是根本。
懒加载是怎么优化的?
懒加载是针对预加载进行优化的,对ArrayList的优化,它其实根本是对ArrayList,以前保存的是一个fragment,这个fragment中有大量的view,现在我们通过arrayList里面保存的也是一个fragment,只不过这个fragment是一个空白的fragment,这就减少了内存,那空白的fragment的数据哪里来的呢?所以就出现了可见的时候加载,在可见的时候将空白的数据,把它替换掉,形成一个可见的漂亮的界面,然后放到ArrayList里面去,这是一个延迟加载,懒加载,优化的根本是对ArrayList原理里面的优化,ArrayList保存的是一个大量数据的,肯定会很多内存,如果保存的是一个空白页面,那么我们保存的数据就会越少。
预加载可以避免吗?
当然不可以,这里虽然加载的是一个空白页面,但是预加载这个模式依旧是存在的。