20.常见代码优化实践
目录介绍
- 01.代码检测优化
- 02.控件异常优化
- 03.四大组件优化
- 04.Glide加载优化
- 4.1 绑定生命周期
- 4.2 避免使用圆角控件
- 4.3 滑动列表图片优化
- 05.编码规范优化
- 06.懒加载优化
- 07.其他场景优化
- 08.反射优化
01.性能调优因素
- CPU
- 大量复杂的计算,会长时间,频繁地占用cpu执行资源;例如,代码递归导致的无限循环,正则表达式引起的回溯,JVM 频繁的 FULL GC,以及多线程编程造成的大量上下文切换等,这些都有可能导致 CPU 资源繁忙。
- 内存
- Java 程序一般通过 JVM 对内存进行分配管理,主要是用 JVM 中的堆内存来存储 Java 创建的对象。系统堆内存的读写速度非常快,所以基本不存在读写性能瓶颈。但是由于内存成本要比磁盘高,相比磁盘,内存的存储空间又非常有限。所以当内存空间被占满,对象无法回收时,就会导致内存溢出、内存泄露等问题。
- 磁盘 I/O
- 磁盘相比内存来说,存储空间要大很多,但磁盘 I/O 读写的速度要比内存慢,虽然目前引入的 SSD 固态硬盘已经有所优化,但仍然无法与内存的读写速度相提并论。
- 网络
- 网络对于系统性能来说,也起着至关重要的作用。如果你购买过云服务,一定经历过,选择网络带宽大小这一环节。带宽过低的话,对于传输数据比较大,或者是并发量比较大的系统,网络就很容易成为性能瓶颈。
- 异常
- Java 应用中,抛出异常需要构建异常栈,对异常进行捕获和处理,这个过程非常消耗系统性能。如果在高并发的情况下引发异常,持续地进行异常处理,那么系统的性能就会明显地受到影响。
- 数据库
- 大部分系统都会用到数据库,而数据库的操作往往是涉及到磁盘 I/O 的读写。大量的数据库读写操作,会导致磁盘 I/O 性能瓶颈,进而导致数据库操作的延迟性。对于有大量数据库读写操作的系统来说,数据库的性能优化是整个系统的核心。
- 锁竞争
- 在并发编程中,我们经常会需要多个线程,共享读写操作同一个资源,这个时候为了保持数据的原子性(即保证这个共享资源在一个线程写的时候,不被另一个线程修改),我们就会用到锁。锁的使用可能会带来上下文切换,从而给系统带来性能开销。JDK1.6 之后,Java 为了降低锁竞争带来的上下文切换,对 JVM 内部锁已经做了多次优化,例如,新增了偏向锁、自旋锁、轻量级锁、锁粗化、锁消除等。而如何合理地使用锁资源,优化锁资源,就需要你了解更多的操作系统知识、Java 多线程编程基础,积累项目经验,并结合实际场景去处理相关问题。
04.Glide加载优化
4.1 绑定生命周期
- with()绑定生命周期
- with(Context context). 使用Application上下文,Glide请求将不受Activity/Fragment生命周期控制。
- with(Activity activity). 使用Activity作为上下文,Glide的请求会受到Activity生命周期控制。
- with(FragmentActivity activity). Glide的请求会受到FragmentActivity生命周期控制。
- with(android.app.Fragment fragment). Glide的请求会受到Fragment 生命周期控制。
- with(androidx.fragment.app.Fragment fragment). Glide的请求会受到Fragment生命周期控制。
- 为何要绑定生命周期
- with()方法可以接收Context、Activity或者Fragment类型的参数。也就是说我们选择的范围非常广,不管是在Activity还是Fragment中调用with()方法,都可以直接传this。
- 那如果调用的地方既不在Activity中也不在Fragment中呢?也没关系,我们可以获取当前应用程序的ApplicationContext,传入到with()方法当中。
- 注意with()方法中传入的实例会决定Glide加载图片的生命周期,如果传入的是Activity或者Fragment的实例,那么当这个Activity或Fragment被销毁的时候,图片加载也会停止。如果传入的是ApplicationContext,那么只有当应用程序被杀掉的时候,图片加载才会停止。
- with绑定生命周期原理
- RequestManager实现了LifecycleListener接口,这是一个与Context的生命周期绑定的接口,将request与生命周期绑定,这样就可以通过context的生命周期去操作网络请求的开始,暂停等;
- Glide 是如何解决图片加载生命周期的?(也是bug高发地带)
- 当一个界面离开之后,我们更希望当前的图片取消加载,那么 Glide 是怎么做到的呢?Glide 的使用方式上,一定需要传入一个 context 给它。它为什么需要拿上下文呢?原因就是可以根据不同的上下文进行处理,拿到 context (除了application context)之后,Glide做了一件很巧妙的事情,就是在这个界面上追加一个 fragment,由于 fragment 添加到了 activity 上,是可以捕获到生命周期的,因此可以在 destroy 的时候取消掉当前context下的 glide对象中的加载任务。
- 为什么标题后面说是 ‘也是bug高发地带’ 呢? 因为从实现方式上,它是巧妙的利用了fragment的生命周期来实现的‘销毁’动作,那么就类似于另外一个高发bug,延时的匿名内部类(网络请求callback回来),界面已经销毁,所以当前activity依附的glide也就销毁了的,此时再尝试加载图片的话,就会crash。具体源码中可以看到这里:https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/Glide.java
@NonNull private static RequestManagerRetriever getRetriever(@Nullable Context context) { Preconditions.checkNotNull( context, "You cannot start a load on a not yet attached View or a Fragment where getActivity() " + "returns null (which usually occurs when getActivity() is called before the Fragment " + "is attached or after the Fragment is destroyed)."); return Glide.get(context).getRequestManagerRetriever(); }
4.2 避免使用圆角控件
- 避免使用圆角的ImageView
- 在实际项目内,经常会用到一些带圆角的图片,或者直接就是圆形的图片。圆形的图片,多数用于一些用户的头像之类的显示效果。
- 而在 Android 下,也有大量的类似 XxxImageView 的开源控件,用于操作 Bitmap 以达到一个圆角图片的效果,例如 Github 上比较火的 RoundedImageView。
- 它们大部分的原理,是接收到你传递的 Bitmap ,然后再输出一个与原来 Bitmap 等大的新 Bitmap ,在此基础之上,进行圆角的一些处理,这就导致了,实际上会在内存中,多持有一个 Bitmap ,一下一张图片占用的内存就被加倍了。
- 选择使用Glide切割圆角
- 推荐使用glide-transformations ,利用 Glide 的 bitmapTransform() 接口,实现对加载的 Bitmap 的进行一些变换操作。
- glide-transformations提供一系类对加载的图片的变换操作,从形状变换到色彩变换,全部支持,基本上满足大部分开发需要,并且它会复用 Glide 的 BitmapPool ,来达到节约内存的目的。
4.3 滑动列表图片优化
- RecyclerView 滑动时不让 Glide 加载图片
//RecyclerView.SCROLL_STATE_IDLE //空闲状态 //RecyclerView.SCROLL_STATE_FLING //滚动状态 //RecyclerView.SCROLL_STATE_TOUCH_SCROLL //触摸后状态 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { LoggerUtils.e("initRecyclerView"+ "恢复Glide加载图片"); Glide.with(ImageBrowseActivity.this).resumeRequests(); }else { LoggerUtils.e("initRecyclerView"+"禁止Glide加载图片"); Glide.with(ImageBrowseActivity.this).pauseRequests(); } } });
01.代码优化有哪些
- 都是一些微优化,在性能方面看不出有什么显著的提升的。使用合适的算法和数据结构是优化程序性能的最主要手段。
02.lint检查去除无效代码
- lint去除无效资源和代码
- 如何检测哪些图片未被使用
- 点击菜单栏 Analyze -> Run Inspection by Name -> unused resources -> Moudule ‘app’ -> OK,这样会搜出来哪些未被使用到未使用到xml和图片,如下:
- 如何检测哪些无效代码
- 使用Android Studio的Lint,步骤:点击菜单栏 Analyze -> Run Inspection by Name -> unused declaration -> Moudule ‘app’ -> OK
- 如何检测哪些图片未被使用
03.代码规范优化
- 避免创建不必要的对象 不必要的对象应该避免创建:
- 如果有需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符的性能越低。
- 当一个方法的返回值是String的时候,通常需要去判断一下这个String的作用是什么,如果明确知道调用方会将返回的String再进行拼接操作的话,可以考虑返回一个StringBuffer对象来代替,因为这样可以将一个对象的引用进行返回,而返回String的话就是创建了一个短生命周期的临时对象。
- 尽可能地少创建临时对象,越少的对象意味着越少的GC操作。
- nDraw方法里面不要执行对象的创建
- 静态优于抽象
- 如果你并不需要访问一个对系那个中的某些字段,只是想调用它的某些方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,调用速度提升15%-20%,同时也不用为了调用这个方法去专门创建对象了,也不用担心调用这个方法后是否会改变对象的状态(静态方法无法访问非静态字段)。
- 对常量使用static final修饰符
- static int intVal = 42; static String strVal = "Hello, world!";
- 编译器会为上面的代码生成一个初始方法,称为方法,该方法会在定义类第一次被使用的时候调用。这个方法会将42的值赋值到intVal当中,从字符串常量表中提取一个引用赋值到strVal上。当赋值完成后,我们就可以通过字段搜寻的方式去访问具体的值了。
- final进行优化:
- static final int intVal = 42; static final String strVal = "Hello, world!";
- 这样,定义类就不需要方法了,因为所有的常量都会在dex文件的初始化器当中进行初始化。当我们调用intVal时可以直接指向42的值,而调用strVal会用一种相对轻量级的字符串常量方式,而不是字段搜寻的方式。
- 这种优化方式只对基本数据类型以及String类型的常量有效,对于其他数据类型的常量是无效的。
- 在没有特殊原因的情况下,尽量使用基本数据类型来代替封装数据类型,int比Integer要更加有效,其它数据类型也是一样。
- 基本数据类型的数组也要优于对象数据类型的数组。另外两个平行的数组要比一个封装好的对象数组更加高效,举个例子,Foo[]和Bar[]这样的数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效的多。
04.View异常优化
- view自定义控件异常销毁保存状态
- 经常容易被人忽略,但是为了追求高质量代码,这个也有必要加上。举个例子!
@Override protected Parcelable onSaveInstanceState() { //异常情况保存重要信息。 //return super.onSaveInstanceState(); final Bundle bundle = new Bundle(); bundle.putInt("selectedPosition",selectedPosition); bundle.putInt("flingSpeed",mFlingSpeed); bundle.putInt("orientation",orientation); return bundle; } @Override protected void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { final Bundle bundle = (Bundle) state; selectedPosition = bundle.getInt("selectedPosition",selectedPosition); mFlingSpeed = bundle.getInt("flingSpeed",mFlingSpeed); orientation = bundle.getInt("orientation",orientation); return; } super.onRestoreInstanceState(state); }
- 经常容易被人忽略,但是为了追求高质量代码,这个也有必要加上。举个例子!
05.去除淡黄色警告优化
- 淡黄色警告虽然不会造成崩溃,但是作为程序员还是要尽量去除淡黄色警告,规范代码
06.合理使用集合
- 使用优化过的数据集合
- Android提供了一系列优化过后的数据集合工具类,如SparseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效。HashMap工具类会相对比较低效,因为它需要为每一个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。
07.Activity不可见优化
- 当Activity界面不可见时释放内存
- 当用户打开了另外一个程序,我们的程序界面已经不可见的时候,我们应当将所有和界面相关的资源进行释放。重写Activity的onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发说明用户离开了程序,此时就可以进行资源释放操作了。
- 当时看到这个觉得很新奇的,但是具体还是没有用到,要是那个大神有具体操作方案,可以分享一下。
08.节制的使用Service
- 节制的使用Service
- 如果应用程序需要使用Service来执行后台任务的话,只有当任务正在执行的时候才应该让Service运行起来。当启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,系统可以在LRUcache当中缓存的进程数量也会减少,导致切换程序的时候耗费更多性能。我们可以使用IntentService,当后台任务执行结束后会自动停止,避免了Service的内存泄漏。
09.线程优化说明
9.1 创建Thread弊端
- 直接创建Thread实现runnable方法的弊端
- 大量的线程的创建和销毁很容易导致GC频繁的执行,从而发生内存抖动现象,而发生了内存抖动,对于移动端来说,最大的影响就是造成界面卡顿
- 线程的创建和销毁都需要时间,当有大量的线程创建和销毁时,那么这些时间的消耗则比较明显,将导致性能上的缺失
9.2 为何要用线程池
- 为什么要用线程池
- 重用线程池中的线程,避免频繁地创建和销毁线程带来的性能消耗;有效控制线程的最大并发数量,防止线程过大导致抢占资源造成系统阻塞;可以对线程进行一定地管理。
- 使用线程池管理的经典例子
- RxJava,RxAndroid,底层对线程池的封装管理特别值得参考
9.3 那些地方使用线程池
- 关于线程池,线程,多线程的具体内容
- 参考:轻量级线程池封装库,支持异步回调,可以检测线程执行的状态
- 该项目中哪里用到频繁new Thread
- 保存图片[注意,尤其是大图和多图场景下注意耗时太久];某些页面从数据库查询数据;设置中心清除图片,视频,下载文件,日志,系统缓存等缓存内容
- 使用线程池管理库好处,比如保存图片,耗时操作放到子线程中,处理过程中,可以检测到执行开始,异常,成功,失败等多种状态。
10.glide加载优化
- 在画廊中加载大图
- 假如你滑动特别快,glide加载优化就显得非常重要呢,具体优化方法如下所示
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { LoggerUtils.e("initRecyclerView"+ "恢复Glide加载图片"); Glide.with(ImageBrowseActivity.this).resumeRequests(); }else { LoggerUtils.e("initRecyclerView"+"禁止Glide加载图片"); Glide.with(ImageBrowseActivity.this).pauseRequests(); } } });
- 假如你滑动特别快,glide加载优化就显得非常重要呢,具体优化方法如下所示
11.不要用静态变量保存核心数据
- 尽量不使用静态变量保存核心数据。这是为什么呢?
- 这是因为android的进程并不是安全的,包括application对象以及静态变量在内的进程级别变量并不会一直呆着内存里面,因为它很有会被kill掉。
- 当被kill掉之后,实际上app不会重新开始启动。Android系统会创建一个新的Application对象,然后启动上次用户离开时的activity以造成这个app从来没有被kill掉的假象。而这时候静态变量等数据由于进程已经被杀死而被初始化,所以就有了不推荐在静态变量(包括Application中保存全局数据静态数据)的观点。
- 实际开发需求
- 有一个倒计时抢红包活动页面,App端有一个轮训请求,大概每隔一分钟会请求一次服务器,用于更新用户的抢红包人数,还剩多少个红包,部分已抢红包的金额(弹出后消失)等信息。用户可以在当前页面停留很长的时间。需要注意的是这里还有一个抢红包按钮,点击这个按钮,用户开始并且跳转到抢红包【只是用于展示抢红包后的信息】页面,而这时候需要传递一个参数:用户的ID,此时用户的ID保存在了系统的静态变量中。请求接口成功后,将用户抢到的红包信息展现在该页面。
- 出现的问题
- 偶发性,请求接口时,服务端报异常,说请求时没有带有用户ID。但是App端的代码是判断此时的ID是否为空,若不为空的话则请求接口
if (!TextUtils.isEmpty(id)) { requestNet(id); }
- 偶发性,请求接口时,服务端报异常,说请求时没有带有用户ID。但是App端的代码是判断此时的ID是否为空,若不为空的话则请求接口
- 思考?
- 这里的id是一个静态变量,而且没有手动的置空,为什么id会变为空了呢?
- 还原出现bug的场景
- 用户在某个时间段停留在当前形成页面;
- 用户锁屏,系统限制后台网络请求,轮训操作被限制;
- 系统内存吃紧,用户应用进程被杀死,进程的静态变量被初始化,activity界面被保留;
- 用户打开屏幕,点击开始抢红包,这时候由于进程已经被杀死,静态变量被初始化,所以上报给服务器的id为空
- 在用户打开屏幕的同时,系统重启应用进程,造成应用进程没有被杀死的假象;
- 解决问题
- 既然在内存中保存数据可能被系统杀死,那么可以有针对性的:
- 使用官方推荐的几种方式将数据持久化到磁盘上。
- 在使用数据的时候总是要对变量的值进行非空检查。
- 使用sp存储和取出数据,在此种场景下是比较好的。
- 既然在内存中保存数据可能被系统杀死,那么可以有针对性的:
12.如何查询重复库
- 我相信你看到了这里会有疑问,网上有许多博客作了这方面说明。但是我在这里想说,如何查找自己项目的所有依赖关系树
- 注意要点:其中app就是项目mudule名字。 正常情况下就是app!
gradlew app:dependencies
- 关于依赖关系树的结构图如下所示,此处省略很多代码
| | | | | | \--- android.arch.core:common:1.1.1 (*) | | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0 | +--- com.journeyapps:zxing-android-embedded:3.6.0 | | +--- com.google.zxing:core:3.3.2 | | \--- com.android.support:support-v4:25.3.1 | | +--- com.android.support:support-compat:25.3.1 -> 28.0.0 (*) | | +--- com.android.support:support-media-compat:25.3.1 | | | +--- com.android.support:support-annotations:25.3.1 -> 28.0.0 | | | \--- com.android.support:support-compat:25.3.1 -> 28.0.0 (*) | | +--- com.android.support:support-core-utils:25.3.1 -> 28.0.0 (*) | | +--- com.android.support:support-core-ui:25.3.1 -> 28.0.0 (*) | | \--- com.android.support:support-fragment:25.3.1 -> 28.0.0 (*) \--- com.android.support:multidex:1.0.2 -> 1.0.3
- 然后查看哪些重复jar
image
- 然后修改gradle配置代码
api (rootProject.ext.dependencies["zxing"]){ exclude module: 'support-v4' exclude module: 'appcompat-v7' }
13.使用注解限定传入类型
- 使用注解限定传入类型
- 比如,尤其是写第三方开源库,对于有些暴露给开发者的方法,需要限定传入类型是有必要的。举个例子:
- 刚开始的代码
/** * 设置播放器类型,必须设置 * 注意:感谢某人建议,这里限定了传入值类型 * 输入值:111 或者 222 * @param playerType IjkPlayer or MediaPlayer. */ public void setPlayerType(int playerType) { mPlayerType = playerType; }
- 优化后的代码,有效避免第一种方式开发者传入值错误
/** * 设置播放器类型,必须设置 * 注意:感谢某人建议,这里限定了传入值类型 * 输入值:ConstantKeys.IjkPlayerType.TYPE_IJK 或者 ConstantKeys.IjkPlayerType.TYPE_NATIVE * @param playerType IjkPlayer or MediaPlayer. */ public void setPlayerType(@ConstantKeys.PlayerType int playerType) { mPlayerType = playerType; } /** * 通过注解限定类型 * TYPE_IJK IjkPlayer,基于IjkPlayer封装播放器 * TYPE_NATIVE MediaPlayer,基于原生自带的播放器控件 */ @Retention(RetentionPolicy.SOURCE) public @interface IjkPlayerType { int TYPE_IJK = 111; int TYPE_NATIVE = 222; } @IntDef({IjkPlayerType.TYPE_IJK,IjkPlayerType.TYPE_NATIVE}) public @interface PlayerType{}
- 使用注解替代枚举,代码如下所示
@Retention(RetentionPolicy.SOURCE) public @interface ViewStateType { int HAVE_DATA = 1; int EMPTY_DATA = 2; int ERROR_DATA = 3; int ERROR_NETWORK = 4; }
- 枚举代码
public enum ViewStateType { HAVE_DATA, EMPTY_DATA, ERROR_DATA, ERROR_NETWORK, }
14.懒加载优化
14.1 什么是懒加载
- 1.1 什么是预加载
- ViewPager+Fragment的搭配在日常开发中也比较常见,可用于切换展示不同类别的页面,我们日常所见的咨询、购物、金融、社交等类型的APP都有机会用到这种控件组合
- ViewPager控件有个特有的预加载机制,即默认情况下当前页面左右两侧的1个页面会被加载,以方便用户滑动切换到相邻的界面时,可以更加顺畅的显示出来。
- 通过ViewPager的setOffscreenPageLimit(int limit)可以设置预加载页面数量,当前页面相邻的limit个页面会被预加载进内存
- 1.2 懒加载介绍
- 懒加载,其实也就是延迟加载,就是等到该页面的UI展示给用户时,再加载该页面的数据(从网络、数据库等),而不是依靠ViewPager预加载机制提前加载两三个,甚至更多页面的数据。这样可以提高所属Activity的初始化速度,也可以为用户节省流量.而这种懒加载的方式也已经/正在被诸多APP所采用。
- 但是通过ViewPager方法setOffscreenPageLimit(int limit)的源码可以发现,ViewPager通过一定的逻辑判断来确保至少会预加载左右两侧相邻的1个页面,也就是说无法通过简单的配置做到懒加载的效果。
- 1.3 懒加载概括
- 当页面可见的时候,才加载当前页面。
- 没有打开的页面,就不会预加载。
- 说白了,懒加载就是可见的时候才去请求数据。
- 实际应用中有哪些懒加载案例
- 1.1 ViewPager+Fragment的搭配使用懒加载
- 1.2 H5网页使用懒加载
14.2 关于ViewPager与Fragment懒加载
- ViewPager中setOffscreenPageLimit(int limit)相关源码
- 源码如下所示
//默认的缓存页面数量(常量) private static final int DEFAULT_OFFSCREEN_PAGES = 1; //缓存页面数量(变量) private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; public void setOffscreenPageLimit(int limit) { //当我们手动设置的limit数小于默认值1时,limit值会自动被赋值为默认值1(即DEFAULT_OFFSCREEN_PAGES) if (limit < DEFAULT_OFFSCREEN_PAGES) { Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "+ DEFAULT_OFFSCREEN_PAGES); limit = DEFAULT_OFFSCREEN_PAGES; } if (limit != mOffscreenPageLimit) { //经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit,用于 mOffscreenPageLimit = limit; populate(); } }
- ViewPager通过一定的逻辑判断来确保至少会预加载左右两侧相邻的1个页面,也就是说无法通过简单的配置做到懒加载的效果。
- 遇到的问题
- 在使用viewpager(或其他容器)与多个Fragment来组合使用,ViewPager 会默认一次加载当前页面前后隔一个页面,即使设置setofflimit(0)也无效果,也会预加载。这样把我们看不到的页面的数据也加载了,大大降低了性能,浪费初始化资源。然而我们就采用懒加载技术,只让用户看到的页面才会加载他的数据,大大提高效率。
- 主要的思路做法
- 主要的方法是Fragment中的setUserVisibleHint(),此方法会在onCreateView()之前执行,当viewPager中fragment改变可见状态时也会调用,当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法,使用getUserVisibleHint() 可以返回fragment是否可见状态。
- 在BaseMVPLazyFragment中需要在onActivityCreated()及setUserVisibleHint()方法中都调了一次lazyLoad() 方法。如果仅仅在setUserVisibleHint()调用lazyLoad(),当默认首页首先加载时会导致viewPager的首页第一次展示时没有数据显示,切换一下才会有数据。因为首页fragment的setUserVisible()在onActivityCreated() 之前调用,此时isPrepared为false 导致首页fragment 没能调用onLazyLoad()方法加载数据。
- 代码案例展示
public abstract class BaseMVPLazyFragment<T extends IBasePresenter> extends BaseMVPFragment<T> { /** * Fragment的View加载完毕的标记 */ protected boolean isViewInitiated; /** * Fragment对用户可见的标记 */ protected boolean isVisibleToUser; /** * 是否懒加载 */ protected boolean isDataInitiated; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } /** * 第一步,改变isViewInitiated标记 * 当onViewCreated()方法执行时,表明View已经加载完毕,此时改变isViewInitiated标记为true,并调用lazyLoad()方法 */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); isViewInitiated = true; //只有Fragment onCreateView好了, //另外这里调用一次lazyLoad() prepareFetchData(); //lazyLoad(); } /** * 第二步 * 此方法会在onCreateView()之前执行 * 当viewPager中fragment改变可见状态时也会调用 * 当fragment 从可见到不见,或者从不可见切换到可见,都会调用此方法 */ @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); this.isVisibleToUser = isVisibleToUser; prepareFetchData(); } /** * 第四步:定义抽象方法fetchData(),具体加载数据的工作,交给子类去完成 */ public abstract void fetchData(); /** * 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载 * 第一种方法 * 调用懒加载,getUserVisibleHint()会返回是否可见状态 * 这是fragment实现懒加载的关键,只有fragment 可见才会调用onLazyLoad() 加载数据 */ private void lazyLoad() { if (getUserVisibleHint() && isViewInitiated && !isDataInitiated) { fetchData(); isDataInitiated = true; } } /** * 第二种方法 * 调用懒加载 */ public void prepareFetchData() { prepareFetchData(false); } /** * 第三步:在lazyLoad()方法中进行双重标记判断,通过后即可进行数据加载 */ public void prepareFetchData(boolean forceUpdate) { if (isVisibleToUser && isViewInitiated && (!isDataInitiated || forceUpdate)) { fetchData(); isDataInitiated = true; } } }
- onLazyLoad()加载数据条件
- getUserVisibleHint()会返回是否可见状态,这是fragment实现懒加载的关键,只有fragment 可见才会调用onLazyLoad() 加载数据。
- isPrepared参数在系统调用onActivityCreated时设置为true,这时onCreateView方法已调用完毕(一般我们在这方法里执行findviewbyid等方法),确保 onLazyLoad()方法不会报空指针异常。
- isLazyLoaded确保ViewPager来回切换时BaseFragment的initData方法不会被重复调用,onLazyLoad在该Fragment的整个生命周期只调用一次,第一次调用onLazyLoad()方法后马上执行 isLazyLoaded = true。
- 然后再继承这个BaseMVPLazyFragment实现onLazyLoad() 方法就行。他会自动控制当fragment 展现出来时,才会加载数据
- Fragment的方法setUserVisibleHint
- 源码如下所示
image - 通过此方法来设置Fragment的UI对用户是否可见,当该页面对用户可见/不可见时,系统都会回调此方法。
- 我们可以重写此方法,然后根据回调的isVisibleToUser参数来进行相关的逻辑判断,以达到懒加载的效果,比如如果isVisibleToUser==true的话表示当前Fragment对用户可见,此时再去加载页面数据。
- setUserVisibleHint()调用时机
- ①在Fragment实例化,即在ViewPager中,由于ViewPager默认会预加载左右两个页面。此时预加载页面回调的生命周期流程:setUserVisibleHint() -->onAttach() --> onCreate()-->onCreateView()--> onActivityCreate() --> onStart() --> onResume()
- 此时,setUserVisibleHint() 中的参数为false,因为不可见。
- ②在Fragmetn可见时,即ViewPager中滑动到当前页面时,因为已经预加载过了,之前生命周期已经走到onResume() ,所以现在**只会回调:setUserVisibleHint() **。
- 此时,setUserVisibleHint() 中的参数为true,因为可见。
- ③在Fragment由可见变为不可见,即ViewPager由当前页面滑动到另一个页面,因为还要保持当前页面的预加载过程,所以只会回调:setUserVisibleHint()。
- 此时,setUserVisibleHint() 中的参数为false,因为不可见。
- ①在Fragment实例化,即在ViewPager中,由于ViewPager默认会预加载左右两个页面。此时预加载页面回调的生命周期流程:setUserVisibleHint() -->onAttach() --> onCreate()-->onCreateView()--> onActivityCreate() --> onStart() --> onResume()
- 懒加载Fragment为什么要继承BaseFragment
- 这个BaseFragment就是最基础的基类了,里面进行一些最底层的设置,定义抽象函数:比如布局文件的绑定,初始化视图控件,初始化数据,初始化Toolbar
- 所以这个懒加载LazyLoadFragment基本上是隔离开了。耦合度相当低,基本都可以这样使用吧。
- 还有一点,有些Fragment不需要懒加载,那么可以直接继承BaseFragment类;需要懒加载的直接继承BaseMVPLazyFragment类
15.四种引用优化
- 软引用使用场景
- 正常是用来处理大图片这种占用内存大的情况。其实看glide底层源码可知,也做了相关软引用的操作。这样使用软引用好处
- 通过软引用的get()方法,取得bitmap对象实例的强引用,发现对象被未回收。在GC在内存充足的情况下,不会回收软引用对象。此时view的背景显示
- 实际情况中,我们会获取很多图片.然后可能给很多个view展示, 这种情况下很容易内存吃紧导致oom,内存吃紧,系统开始会GC。这次GC后,bitmapSoftReference.get()不再返回bitmap对象,而是返回null,这时屏幕上背景图不显示,说明在系统内存紧张的情况下,软引用被回收。
- 使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。
- 弱引用使用场景
- 弱引用–>随时可能会被垃圾回收器回收,不一定要等到虚拟机内存不足时才强制回收。
- 对于使用频次少的对象,希望尽快回收,使用弱引用可以保证内存被虚拟机回收。比如handler,如果希望使用完后尽快回收,可以使用WeakReference
- 到底什么时候使用软引用,什么时候使用弱引用呢?
- 个人认为,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
- 还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
08.反射优化
- https://segmentfault.com/a/1190000020986852?utm_source=sf-related
- 浅谈App响应时间优化
- https://juejin.cn/post/7224147971834527799