编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.崩溃捕获设计实践
  • 02.崩溃治理优化总结
  • 03.Native崩溃治理实践
  • 04.ANR监控设计实践
  • 05.CPU消耗优化实践
  • 06.卡顿监控设计实践
  • 07.卡顿治理优化实践
  • 08.网络分析与优化实践
  • 09.线程优化实践操作
  • 10.高性能图片优化方案
  • 11.OOM异常优化实践
  • 12.内存监控优化方案
  • 13.内存治理优化实践
  • 14.FPS监测设计实践
  • 15.进程优化设计实践
  • 16.App启动优化实践
  • 17.App页面UI优化实践
  • 18.App稳定性专项实践
  • 19.App瘦身优化实践
  • 20.常见代码优化实践
  • 21.移动端防抓包实践
  • 22.App磁盘沙盒实践
  • 23.Ping工具开发实践
  • 24.Gradle构建优化实践
  • 25.CodeReview实践总结

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();
              }
          }
      });

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是一个静态变量,而且没有手动的置空,为什么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
      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
      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为什么要继承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
贡献者: yangchong211
上一篇
19.App瘦身优化实践
下一篇
21.移动端防抓包实践