欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android在多种设计下实现懒加载机制的方法

程序员文章站 2022-10-13 09:32:57
前言 前段时间在自己的练习项目中想用到懒加载机制,查看了大多数资料只介绍了在 view pager + fragment 组合的情况下实现的懒加载,但是现在大多数app更...

前言

前段时间在自己的练习项目中想用到懒加载机制,查看了大多数资料只介绍了在 view pager + fragment 组合的情况下实现的懒加载,但是现在大多数app更多的是 fragmentmanager 去管理主页面多个 fragment 的显示与隐藏,然后主界面的某个或多个 fragment 里又嵌套了多个 fragment + viewpager (详细见下图 ),对于这种情况,适用于第一种的方式是不能直接解决第二种的情况的,所以写下这篇文章,记录一下踩的几个坑,希望对同像我一样的初学者提供一种思考方式作为参考(如果有错误或者不合适的地方,希望各位前辈能在评论区指出,非常感谢!)。

关于懒加载

1. 什么是懒加载?

懒加载也叫延迟加载,在app中指的是每次只加载当前页面,是一种很好的优化app性能的一种方式。

2.为什么要用懒加载?

优化app性能,提升用户体验 :如果用户打开某页面,就会去预加载其它的页面时,数据集较小或者网络性能较优时还好,但是如果数据集过大或者网络性能不佳时,就会造成用户等待的时间较长,app界面产生明显的滞顿感的情况,严重影响到用户的体验。

减少无效资源的加载,减少服务器的压力,节省用户流量 :如果用户只想浏览或者经常浏览某个特定的页面,如果使用预加载的方式,就会造成资源浪费,增加服务器的压力等。

 实现懒加载

 1.viewpager+fragment情况

Android在多种设计下实现懒加载机制的方法 

1.1遇到的问题

在我们平时开发中,经常使用 viewpager+fragment 的组合来实现左右滑动的页面设计(如上图),但是 viewpger 有个 预加载 机制,默认会把 viewpager 当前位置的左右相邻页面预先初始化(俗称预加载),即使设置 setoffscreenpagelimit(0) 也无效果,也会预加载。通过点进源码中发现,如果不主动设置 setoffscreenpagelimit() 方法, moffscreenpagelimit 默认值为1,即使设置了0(小于1)的值了,但是还会按照 moffscreenpagelimit=limit=1 处理。

private int moffscreenpagelimit = 1;//即使不设置,默认值就为1

public int getoffscreenpagelimit() {
    return this.moffscreenpagelimit;
  }
  
public void setoffscreenpagelimit(int limit) {
    if (limit < 1) {//设置为0,还是会默认为1
      log.w("viewpager", "requested offscreen page limit " + limit + " too small; defaulting to " + 1);
      limit = 1;
    }
    if (limit != this.moffscreenpagelimit) {
      this.moffscreenpagelimit = limit;
      this.populate();
    }

1.2 解决思路

fragment 有一个非生命周期的 setuservisiblehint(boolean isvisibletouser) 回调方法, viewpager 嵌套 fragment 时会起作用 ,如果切换 viewpager 则该方法也会被调用,参数 isvisibletousertrue 代表当前 fragment 对用户可见,否则不可见。 所以最简单的思路: fragment 可见时才去加载数据,不可见时就不让它加载数据 。据我们创建抽象 basefragment ,对其进行封装。首先我们引入 isvisibletouser 变量,负责保存当前 fragment 对用户的可见状态。 同时还有几个值得注意的地方:

setuservisiblehint(boolean isvisibletouser) 方法的回调时机并没有与 fragment 的生命周期有确切的关联,比如说,回调时机有可能在 oncreateview() 方法之后,也可能在 oncreateview() 方法之前。因此,必须引入一个标志位 isprepareview 判断view是否创建完成,不然,很容易会造成空指针异常。我们初始化该变量为 false ,在 onviewcreated() 中,也就是view创建完成后,将其赋值为 true

数据初始化只应该加载一次,因此,引入第二个标志位, isinitdata ,初始为 false, 在数据加载完成之后,将其赋值为 true ,下次返回此页面时不会再自动加载。至此,我们的懒加载方法考虑了所有条件。也就是当 isvisibletousertrueisinitdatafalseisprepareviewtrue 时,进行数据加载,并且加载后为了防止重复调用,将 isinitdata 赋值为 true

将懒加载数据提取成一个方法,那么这个方法该何时调用呢?首先 setuservisiblehint(boolean isvisibletouser) 方法中是必须调用的,即当 fragment 由可见变为不可见和不可见变为可见时回调。 其次,很容易忽略的一点。对于第一个 fragment ,如果 setuservisiblehint(boolean isvisibletouser ) 方法在 oncreateview() 之前调用的话,如果懒加载方法只在 setuservisiblehint(boolean isvisibletouser ) 中调用,那么该 fragment 将只能在被主动切换一次之后才能加载数据,这肯定是不可能的,因此,我们需要在view创建完成之后,也进行一次调用。思来想去,在 onactivitycreated() 方法中是最合适的。我们在继承的时候,在 onviewcreated() 方法中进行一些初始化就行了,这样不会引起冲突。

1.3 basefragment代码实现

public abstract class basefragment extends fragment {

  private boolean isinitdata = false; //标志位,判断数据是否初始化
  private boolean isvisibletouser = false; //标志位,判断fragment是否可见
  private boolean isprepareview = false; //标志位,判断view已经加载完成 避免空指针操作

  @nullable
  @override
  public view oncreateview(@nonnull layoutinflater inflater, @nullable viewgroup container, @nullable bundle savedinstancestate) {
    return inflater.inflate(getlayoutid(),container,false);
  }

  @override
  public void onviewcreated(@nonnull view view, @nullable bundle savedinstancestate) {
    super.onviewcreated(view, savedinstancestate);
    isprepareview=true;//此时view已经加载完成,设置其为true
  }
  /**
   * 懒加载方法
   */
  public void lazyinitdata(){
    if(!isinitdata && isvisibletouser && isprepareview){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
      initdata();//加载数据
      isinitdata=true;//是否已经加载数据标志重新赋值为true
    }
  }

  @override
  public void setuservisiblehint(boolean isvisibletouser) {
    super.setuservisiblehint(isvisibletouser);
    this.isvisibletouser=isvisibletouser;//将fragment是否可见值赋给标志isvisibletouser
    lazyinitdata();//懒加载
  }

  /**
   * fragment生命周期中onviewcreated之后的方法 在这里调用一次懒加载 避免第一次可见不加载数据
   * @param savedinstancestate
   */
  @override
  public void onactivitycreated(@nullable bundle savedinstancestate) {
    super.onactivitycreated(savedinstancestate);
    lazyinitdata();//懒加载
  }

  /**
   * 由子类实现
   * @return 返回子类的布局id
   */
  abstract int getlayoutid();

  /**
   * 加载数据的方法,由子类实现
   */
  abstract void initdata();
}

2.fragment+viewpager+fragment情况

Android在多种设计下实现懒加载机制的方法 

2.1 遇到的问题

如图2,对于这种由 fragmentmanager 管理主页面的多个 fragment 的显示与隐藏,在其中的某个 fragment 中又嵌套了多个 fragment 的情况( 如上图 ),上面的方案是无法解决的,如果主页面的 fragment 直接继承上面的 basefragment ,就会出现主页的几个 fragment 都不会加载的现象,为什么会这样呢,按道理说 fragment 应该可见了,加载数据的判断逻辑应该没问题啊,而且上面那个demo也跑成功了。最终我发现,问题出在 setuservisiblehint() 这个方法上,点进去它的源码发现注释中有这么一句话:

this may be used by the system to prioritize operations such as fragment lifecycle updates or loader ordering behavior.

也就是说这个可能被用来在一组有序的 fragment 里 ,例如 fragment 生命周期的更新。告诉我们这个方法被调用希望在一个pager里 ,因此 fragmentpageradapter 所以可以使用这个,而主页面的几个 fragment 我们是通过 fragmentmanager 管理的,所以 setuservisiblehint() 是不会被调用,而我们设置的 isvisibletouser=false 默认值一直不会变,那么 lazyinitdata() 方法也就一直不会执行。

 /**
   * 懒加载方法
   */
  public void lazyinitdata(){
    if(!isinitdata && isvisibletouser && isprepareview){//因为isvisibletouser一直都是false,所以inidata()是不会被执行的
      initdata();//加载数据
      isinitdata=true;
    }
  }

2.2 解决思路

这里我的处理方式是,在lazyinitdata()中多加了一段处理逻辑,如下:

/**
   * 懒加载方法
   */
  public void lazyinitdata(){
    if(!isinitdata && isvisibletouser && isprepareview){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
      initdata();//加载数据
      isinitdata=true;//是否已经加载数据标志重新赋值为true
    }else if (!isinitdata && getparentfragment()==null && isprepareview){
      initdata();
      isinitdata=true;
    }
  }
  
  /**
   * fragment显示隐藏监听
   * @param hidden
   */
  @override
  public void onhiddenchanged(boolean hidden) {
    super.onhiddenchanged(hidden);
    if (!hidden) {
    lazyinitdata(); 
    }
  }

对于主页面的多个 fragment 只会在第二个判断逻辑处理(因为它的 isvisibletouser 值一直等于 false ),对于嵌套的 fragment 只会经过第一个处理逻辑(因为它的 getparentfragment()!=null ),然后通过 onhiddenchanged() 方法去加载 lazyinitdata() 方法,这样以来就能处理这种情况了。

但是这时候又会出现一个问题,如果一个app里第一种,第二种情况并存的话,这段代码又不适合第一种情况了,因为对于第一种的情况当判定 isvisibletouserfalse 时,虽然不走第一个处理逻辑,但是它的 getparentfragment() 一直是等于 null 的,那么它就会走第二个判断逻辑,这样又会预加载了。

对于这种情况,我的处理方式:给每个fragment设置一个标志值,当是第一种情况时,设为true,第二种情况时,设置false,然后再分别处理相应的判断逻辑。代码如下:

 /**
   * 懒加载方法
   */
  public void lazyinitdata(){
    if(setfragmenttarget()){
      if(!isinitdata && isvisibletouser && isprepareview){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
        initdata();//加载数据
        isinitdata=true;//是否已经加载数据标志重新赋值为true
      }
    }else {
      if(!isinitdata && isvisibletouser && isprepareview){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
        initdata();//加载数据
        isinitdata=true;//是否已经加载数据标志重新赋值为true
      }else if (!isinitdata && getparentfragment()==null && isprepareview ){
        initdata();
        isinitdata=true;
      }
    }
  }
  
   /**
   * 设置fragment target,由子类实现
   */
  abstract boolean setfragmenttarget();

经过这样的处理之后,第一种情况和第二种情况,或两者并存的情况下都能保证在继承一个base下,实现懒加载。

2.3 basefragmenttwo最终代码实现

public abstract class basefragmenttwo extends fragment {
  private boolean isinitdata = false; //标志位,判断数据是否初始化
  private boolean isvisibletouser = false; //标志位,判断fragment是否可见
  private boolean isprepareview = false; //标志位,判断view已经加载完成 避免空指针操作

  @nullable
  @override
  public view oncreateview(@nonnull layoutinflater inflater, @nullable viewgroup container, @nullable bundle savedinstancestate) {
    return inflater.inflate(getlayoutid(),container,false);
  }

  @override
  public void onviewcreated(@nonnull view view, @nullable bundle savedinstancestate) {
    super.onviewcreated(view, savedinstancestate);
    isprepareview=true;//此时view已经加载完成,设置其为true
  }
  /**
   * 懒加载方法
   */
  public void lazyinitdata(){
    if(setfragmenttarget()){
      if(!isinitdata && isvisibletouser && isprepareview){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
        initdata();//加载数据
        isinitdata=true;//是否已经加载数据标志重新赋值为true
      }
    }else {
      if(!isinitdata && isvisibletouser && isprepareview){//如果数据还没有被加载过,并且fragment已经可见,view已经加载完成
        initdata();//加载数据
        isinitdata=true;//是否已经加载数据标志重新赋值为true
      }else if (!isinitdata && getparentfragment()==null && isprepareview ){
        initdata();
        isinitdata=true;
      }
    }
  }



  @override
  public void onhiddenchanged(boolean hidden) {
    super.onhiddenchanged(hidden);
    if (!hidden) { lazyinitdata(); }
  }

  @override
  public void setuservisiblehint(boolean isvisibletouser) {
    super.setuservisiblehint(isvisibletouser);
    this.isvisibletouser=isvisibletouser;//将fragment是否可见值赋给标志isvisibletouser
    lazyinitdata();//加载懒加载
  }

  /**
   * fragment生命周期中onviewcreated之后的方法 在这里调用一次懒加载 避免第一次可见不加载数据
   * @param savedinstancestate
   */
  @override
  public void onactivitycreated(@nullable bundle savedinstancestate) {
    super.onactivitycreated(savedinstancestate);
    lazyinitdata();
  }

  /**
   * 由子类实现
   * @return 返回子类的布局id
   */
  abstract int getlayoutid();

  /**
   * 加载数据的方法,由子类实现
   */
  abstract void initdata();

  /**
   * 设置fragment target,由子类实现
   */
  abstract boolean setfragmenttarget();

}

其它需要注意:

①给 viewpager 设置 adapter 时,一定要传入 getchildfragmentmanager() ,否则 getparentfragment() 将会一直等于 null ,这会影响 lazyinitdata() 的判断,导致懒加载出现混乱甚至无效的情况。

②demo中我使用的是 viewpager+tablayout 的组合方式,在使用 tablayout 时一定要保证 styles.xml 中的主题应该使用 theme.appcompat.light.noactionbar 或者 theme.appcompat.lighttheme.appcompat.xxx 的主题。

项目地址

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。