Mark Xu 的博客

记录精彩的程序人生

公司项目代码优化总结

公司的 Android 端项目已经历时五年有余了,经历了多名开发者编写,所以难免风格各异。我在接手项目后,在实现版本迭代的同时,也顺手对以前的代码做了部分优化。本文总结了在过程中遇到的一些问题,归纳为性能视图维护等几个方面。当然,有些问题见仁见智,欢迎讨论!

性能

ArrayMap、SparseArray 替代 HashMap

HashMap 是 Java 中的常用集合类型,大家都非常的熟悉。Android 平台上,Google 提供了 ArrayMap、SparseArray、LongSparseArray 等类型,在内存占用等方面做的更优。

对比 HashMap ArrayMap SparseArray
存储 数组 + 链表 数组(mHashes、mArray) 数组(mKeys、mValues)
扩容
key 类型 Object Object int 类型
存储、读取数据 获取数据通过遍历 Entry 数组 二分查找 二分查找
优点 避免基本数据类型自动装箱;不需要额外的结构体(Entry)
缺点 数据量大时,增删效率降低(需要复制数组)

1、HashMap 采用数组 + 链表的形式存储数据;ArrayMap 维护两个数组:mHashes 和 mArray,mHashes 存放 key 的 hashCode 值,mArray 存放 key 与 value 的值;SparseArray 维护两个数组

2、HashMap 默认的存储大小是一个容量为 16 的数组,即使没有元素,也要分配空间

3、SparseArray 避免了对 key 的自动装箱

ArrayMap 的使用场景:

  • item 数量小于 1000,且插入和删除数据不频繁时
  • Map 中包含子 Map 对象

SparseArray 使用场景:

  • item 数量小于 1000,且插入和删除数据不频繁时
  • key 为 int 类型时,可以避免自动装箱

类似的存放基本数据类型键值对的集合还有:

  • SparseIntArray -- int : int
  • SparseBooleanArray -- int : boolean
  • SparseLongArray -- int : long

ArrayList 的初始化容量

ArrayList 每次新增一个元素,就会检测 ArrayList 的当前容量是否已经到达临界点,如果到达临界点则会扩容 1.5 倍。然而 ArrayList 的扩容以及数组的拷贝生成新的数组是相当耗资源的。所以若我们事先已知集合的使用场景,知道集合的大概范围,我们最好是指定初始化容量,这样对资源的利用会更加好。

通过阅读源码会发现,ArrayList 在初始化时,如果没有指定初始化容量,添加第一个元素会自动扩容至 10。

整型常量代替枚举

先说结论:优先使用整型常量 + 注解,谨慎使用枚举。

枚举是 Java 中一种包含固定常数的类型,可以用来避免接收额外常量而引起的类型不安全问题。但每一个枚举值都是一个对象,相比于原生类型会占用更多的内存。

可以通过整型常量 + 自定义注解的方式来解决类型不安全问题,同时又节省内存资源,当参数类型不符时,编译器会自动提示我们。

public static final int MAN = 0;
public static final int WOMEN = 1;

@IntDef({MAN, WOMEN})
@Retention(RetentionPolicy.SOURCE)
public @interface Sex {
}

// 使用注解
public void setSex(@Sex int sex) {
    // do something
}

巨型 sp 文件

项目中的一个 Fragment 每次在加载时都会有一些卡顿,本来以为存在其他耗时操作,后面排查发现加载了巨型 SharedPreferences 文件。原先的代码将全国的城市列表 json 直接存储到默认的 sp 文件中,而加载页面时又会去默认 sp 文件中读取属性,造成卡顿。

关于巨型 sp 文件的详细介绍,可以参考请不要滥用 SharedPreference 这篇文章,讲解很详细!

这里粘贴下使用 sp 时的几个注意点:

1、不要存放大的 key 和 value,会引起界面卡顿、频繁 GC、占用内存等

2、毫不相关的配置不要放到一个 sp 文件中,大型 sp 配置避免直接放到默认 sp 文件

3、读取频繁的 key 和不易变动的 key 尽量不要放在一起,影响速度。(如果整个文件很小,可以忽略,为了这点性能添加维护成本得不偿失)

4、避免多次 edit 和 apply,尽量批量修改、一次操作

5、避免存放 JSON 和 HTML,可以直接存 JSON,否则许多特殊符号需要特殊处理,影响效率

6、不要跨进程通信

Handler 造成的内存泄漏

Handler 为我们提供了线程间通信的便捷方式,但是也是造成内存泄漏的主要原因。关于内存泄漏的情况分析,可以参考我的博文:Java 内存泄漏学习

下面展示一个可以随手优化的代码片段:

下面这是我们很常见的一个 Handler 书写方法,然而匿名内部类的形式是很容易造成内存泄漏的。

Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case 1:
                // do something
                break;
            case 2:
                // do something
                break;
            default:
                break;
        }
    }
};

Looper 从消息队列取出消息后,会调用 Handler 的 dispatchMessage 方法进行消息分发,默认策略如下:

  • Message 的 callback 不为空时,优先调用 Message 的 callback
  • Handler 的 mCallback 不为空时,调用 Handler 的 mCallback
  • 上面俩都为空时,才调用 handleMessage,也就是我们经常重写的那个方法

所以上面的代码我们可以优化成,在 Handler 的 mCallback 中来处理,实现如下:

Handler mHandler = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                // do something
                break;
            case 2:
                // do something
                break;
            default:
                break;
        }
        return true;
    }
});

简单的一个修改,IDE 也不会警告了。

视图

使用智图优化图片

1、APP 内部一般都会有很多图片素材,数量多了之后,对于 apk 的大小影响还是挺大的。关于这个,我们可以采用腾讯出品的智图 https://zhitu.isux.us/ 来对图片素材进行大小优化。

2、考虑使用 WebP 格式的图片。

WebP 文件具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。

此处安利一款 macOS 下快速查看 WebP 格式图片的 QuickLook 扩展插件:WebP

善于使用 tools: 命名空间

1、接手公司项目后,在阅读之前同事写的 xml 文件时,发现很多 view 的属性的可见性是 gone,很难通过效果图直接预览到想要修改的控件。

我个人的编码习惯是,对于默认 gone 的控件,为其增加 tools:visibility="visilble" 属性,一不会影响运行时显示,二方便了我们编码

tools 命名空间中的所有属性都不会影响运行时或 apk 的大小,它们会在 Gradle 打包应用时被剥离出去

2、很多人会使用 android:text="" 属性结合一些硬编码的假文本,目的是在预览窗口 中查看 TextView 或 EditText 的显示效果。但是 Lint 工具会检查出硬编码字符串,最后只能去定义 strings 来消除此问题,然而这样做对用户没有任何意义,还使 apk 中包含了没用的资源。

使用 tools 命名空间可以轻松解决上述问题,使用 tools:text"xiao ming" 来在预览窗口中查看预填���了数据的视图,并且对运行时毫无影响。

3、对于 android: 命名空间下的所有属性,都支持相应的 tools: 命名空间,方便我们在编码阶段进行开发。

布局文件层次太深

1、RelativeLayout 会对子 View 在横向、纵向上进行两次测量,LinearLayout 设置了 weight 属性时也需要测量两次,测量次数会对绘制性能产生较大影响

关于相对布局和线性布局的性能对比可以参考:Android 中 RelativeLayout 和 LinearLayout 性能分析

2、我更习惯于使用约束布局 ConstraintLayout,通过控件间的依赖可以使得视图层次更扁平,提高绘制效率

过度绘制

公司原项目代码很多地方存在过度绘制问题,过度绘制会导致某些像素在同一帧时间内被绘制多次,从而使 CPU 或 GPU 负载过重

多数设备的屏幕刷新频率是 60Hz,即每秒刷新 60 次,每隔 16.67 ms 刷新一次。如果下一帧能够在 16.67 ms 内渲染完成,每次刷新都能展示新的帧,在用户看来 app 流畅运行,否则第 N+1 次屏幕刷新将继续展示第 N 帧(第 N+1 帧尚未渲染完成),将出现掉帧、卡顿现象。

大量使用 GONE 视图

1、项目中存在很多 visibility 属性为 GONE 的控件,在后期其他人员维护、更新 xml 文件时,尤其是寻找控件的时候,就会发现很不方便。

2、可以善于使用 tools: 命名空间,设置该属性为 VISIBLE,这样对于既不影响业务逻辑,又方便了维护。

3、使用 ViewStub 控件,android.view.ViewStub 是一个大小为 0,默认不可见的控件,只有给它设置成了 View.Visible 或调用了它的 inflate() 方法之后才会填充布局资源,可以有效提高绘制性能

其他

switch case default 缺失问题

参考阿里巴巴 Java 开发手册:

在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止

在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使空代码。

快速编译问题

项目默认 Gradle 配置编译缓慢,每次编译需等待 2-3 分钟。采取引入 fastdex 三方库解决编译速度问题。具体使用方法及原理可参考作者 GitHub。

https://github.com/typ0520/fastdex

成员变量命名不规范,单词总是拼写错误问题

项目中存在许多拼音命名,并且拼音拼写错误。

大量使用已废弃方法

既然 API 的提供方已将该方法标记为 deprecated,通过查看该方法定义通常都可以看到作者的相关说明,及相应的推荐替代方法。个人认为我们应该尽可能减少已弃用方法的使用频率。

方法行数过多,职责不清晰

阿里巴巴的 Java 开发手册推荐方法体内的行数不超过 80 行,而目前项目中存在很多行数超过一百甚至两三百的方法,应该对方法体内部职责进行划分,该抽出的可以抽出为单独方法

代码格式混乱

缩进混乱、单行代码过长、= 两边无空格、硬编码、大量使用魔法值,也许每个人都会因为时间紧、项目赶等,各种原因而产生细节上问题。养成良好的代码规范,不但方便其他人阅读,其实对自己后面查阅代码也是很有帮助的。

参考链接

SparseArray 的使用及实现原理

深入剖析 Android 中的 ArrayMap

ArrayList 详解,看这篇就够了

Java 集合细节(一):请为集合指定初始容量

Android 之使用枚举 Enum 利弊及替代方案

请不要滥用 SharedPreference

精通 Android 中的 tools 命名空间

安卓帧渲染数据获取方式小结

留下你的脚步