说明:该文档由阿里《Java开发规范》和《Android开发规范》整理而来
【强制】必须遵守,违反本约定或将会引起严重的后果;
【推荐】尽量遵守,长期遵守有助于系统稳定性和合作效率的提升;
【参考】充分理解,技术意识的引导,是个人学习、团队沟通、项目合作的方向。
一、编码规范
(一)命名规范
- 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例:_name / __name / $name / name_ / name$ / name__
- 【强制】代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式也要避免采用。
正例:alibaba / taobao / youku / hangzhou 等国际通用的名称,可视同英文。
反例:DaZhePromotion [打折] / getPingfenByName() [评分] / int 某变量 = 3
- 【强制】类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion
- 【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。
正例: localValue / getHttpMessage() / inputUserId - 【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例:MAX_STOCK_COUNT
反例:MAX_COUNT
- 【强制】抽象类命名使用
Abstract
或Base
开头;异常类命名使用Exception
结尾;测试类命名以它要测试的类的名称开始,以Test
结尾。 - 【强制】类型与中括号紧挨相连来表示数组。
正例:定义整形数组 int[] arrayDemo;
反例:在 main 参数中,使用 String args[]来定义。
- 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
正例:应用工具类包名为 com.alibaba.ai.util、类名为 MessageUtils(此规则参考 spring 的框架结构) - 【强制】杜绝完全不规范的缩写,避免望文不知义。
反例:AbstractClass“缩写”命名成 AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性。
- 【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。
正例:在 JDK 中,表达原子更新的类名为:AtomicReferenceFieldUpdater。
反例:变量 int a 的随意命名方式。
- 【推荐】如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。
说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例:public class OrderFactory; public class LoginProxy;
public class ResourceObserver; - 【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。
正例:接口方法签名 void commit();
接口基础常量 String COMPANY = “alibaba”;
反例:接口方法定义 public abstract void f();
说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。 - 【参考】枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
说明:枚举其实就是特殊的类,域成员均为常量,且构造方法被默认强制是私有。
正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON
(二)常量定义
- 【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
反例:String key = "Id#taobao_" + tradeId; cache.put(key, value);
- 【强制】在 long 或者 Long 赋值时,数值后使用大写的 L,不能是小写的 l,小写容易跟数字 1 混淆,造成误解。
说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2? - 【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解和维护。
正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 ConfigConsts 下。 - 【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。
1) 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下。
2) 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下。
3) 子工程内部共享常量:即在当前子工程的 constant 目录下。
4) 包内共享常量:即在当前包下单独的 constant 目录下。
5) 类内共享常量:直接在类内部 private static final 定义。 - 【推荐】如果变量值仅在一个固定范围内变化用 enum 类型来定义。
说明:如果存在名称之外的延伸属性应使用 enum 类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节。
(三)代码格式
- 【强制】大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果是非空代码块则:
1) 左大括号前不换行。
2) 左大括号后换行。
3) 右大括号前换行。
4) 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行。 - 【强制】左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格;而左大括号前需要空格。详见第 5 条下方正例提示。
大括号前需要空格。详见第 5 条下方正例提示。 - 【强制】if/for/while/switch/do 等保留字与括号之间都必须加空格。
- 【强制】任何二目、三目运算符的左右两边都需要加一个空格。
说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。 - 【强制】采用 4 个空格缩进,禁止使用 tab 字符。
说明:如果使用 tab 缩进,必须设置 1 个 tab 为 4 个空格。IDEA 设置 tab 为 4 个空格时,请勿勾选 Use tab character;而在 eclipse 中,必须勾选 insert spaces for tabs。正例: (涉及 1-5 点)
public static void main(String[] args) {
// 缩进 4 个空格
String say = "hello";
// 运算符的左右必须有一个空格
int flag = 0;
// 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括号前加空格且不换行;左大括号后换行
if (flag == 1) {
System.out.println("world");
// 右大括号前换行,右大括号后有 else,不用换行
} else {
System.out.println("ok");
// 在右大括号后直接结束,则必须换行
}
}
- 强制】注释的双斜线与注释内容之间有且仅有一个空格。
正例:
// 这是示例注释,请注意在双斜线之后有一个空格
String ygb = new String();
- 【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
1) 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例。
2) 运算符与下文一起换行。
3) 方法调用的点符号与下文一起换行。
4) 方法调用中的多个参数需要换行时,在逗号后进行。
5) 在括号前不要换行,见反例。
正例:
StringBuffer sb = new StringBuffer();
// 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行 sb.append("zi").append("xin")...
.append("huang")...
.append("huang")...
.append("huang");
- 【推荐】单个方法的总行数不超过 80 行。
说明:包括方法签名、结束右大括号、方法内代码、注释、空行、回车及任何不可见字符的总行数不超过 80 行。 - 【推荐】没有必要增加若干空格来使某一行的字符与上一行对应位置的字符对齐。
- 【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。
说明:任何情形,没有必要插入多个空行进行隔开。
(四)OOP 规约
- 【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
- 【强制】所有的覆写方法,必须加@Override 注解。
说明:getObject()与 get0bject()的问题。一个是字母的 O,一个是数字的 0,加
@Override 可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。 - 【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用
Object。
说明:可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)
正例: public List listUsers(String type, Long… ids) {…} - 【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。
- 【强制】不能使用过时的类或方法。接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么。
- 【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
正例:“test”.equals(object);
反例:object.equals("test");
说明:推荐使用 java.util.Objects#equals(JDK7 引入的工具类) - 【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。 说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
- 【强制】定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。
反例:POJO 类的 gmtCreate 默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。 - 【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。 - 【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。
- 【强制】POJO 类必须写 toString 方法。使用 IDE 中的工具:source> generate
toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。 - 【推荐】使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险。说明:
String str = "a,b,c,,";
String[] ary = str.split(",");
// 预期大于 3,结果是 3
System.out.println(ary.length);
- 【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读,此条规则优先于第 14 条规则。
- 【推荐】 类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。
说明:公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为承载的信息价值较低,所有 Service 和 DAO 的 getter/setter 方法放在类体最后。 - 【推荐】setter 方法中,参数名称与类成员变量名称一致,this.成员名 = 参数名。在 getter/setter 方法中,不要增加业务逻辑,增加排查问题的难度。
- 【推荐】循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
说明:下例中,反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。 - 【推荐】final 可以声明类、成员变量、方法、以及本地变量,下列情况使用 final 关键字:
1) 不允许被继承的类,如:String 类。
2) 不允许修改引用的域对象。
3) 不允许被重写的方法,如:POJO 类的 setter 方法。
4) 不允许运行过程中重新赋值的局部变量。
5) 避免上下文重复使用一个变量,使用 final 描述可以强制重新定义一个变量,方便更好地进行重构。 - 【推荐】慎用 Object 的 clone 方法来拷贝对象。
说明:对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现域对象的深度遍历式拷贝。 - 【推荐】类成员与方法访问控制从严:
1) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。
2) 工具类不允许有 public 或 default 构造方法。
3) 类非 static 成员变量并且与子类共享,必须是 protected。
4) 类非 static 成员变量并且仅在本类使用,必须是 private。
5) 类 static 成员变量如果仅在本类使用,必须是 private。
6) 若是 static 成员变量,考虑是否为 final。
7) 类成员方法只供类内部调用,必须是 private。
8) 类成员方法只对继承类公开,那么限制为 protected。
说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。
思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 service 成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。
(五)集合处理
- 【强制】关于 hashCode 和 equals 的处理,遵循如下规则:
1) 只要重写 equals,就必须重写 hashCode。
2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。
3) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals。
说明:String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用。 - 【强制】 ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出
ClassCastException 异常,即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
说明: subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 而是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。 - 【强制】在 subList 场景中,高度注意对原集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。
- 【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()。
说明:使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素个数大于实际所需,下标为[ list.size() ] 的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
正例:
List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误。
- 【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。 说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。
Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { “you”, “wu” };
List list = Arrays.asList(str);
第一种情况:list.add(“yangguanbao”); 运行时异常。
第二种情况:str[0] = “gujin”; 那么 list.get(0)也会随之修改。 - 【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用 add 方法,而<? super T>不能使用 get 方法,作为接口调用赋值时易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super)原则:第一、频繁往外读取内容的,适合用<? extends T>。第二、经常往里插入的,适合用<? super T>。 - 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用
Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.re move();
}
}
- 【强制】 在 JDK7 版本及以上,Comparator 实现类要满足如下三个条件,不然
Arrays.sort,Collections.sort 会报 IllegalArgumentException 异常。
说明:三个条件如下
1) x,y 的比较结果和 y,x 的比较结果相反。
2) x>y,y>z,则 x>z。
3) x=y,则 x,z 比较结果和 y,z 比较结果相同。
反例:下例中没有处理相等的情况,实际使用中可能会出现异常:
new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.getId() > o2.getId() ? 1 : -1;
}
};
- 【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap 使用 HashMap(int initialCapacity) 初始化。
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能。
- 【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的 value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用 Map.foreach 方法。
正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。 - 【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
集合类 | Key | Value | Super | 说明 |
---|---|---|---|---|
Hashtable | 不允许为 null | 不允许为 null | Dictionary | 线程安全 |
ConcurrentHashMap | 不允许为 null | 不允许为 null | AbstractMap | 锁分段技术(JDK8:CAS) |
TreeMap | 不允许为 null | 允许为 null | AbstractMap | 线程不安全 |
HashMap | 允许为 null | 允许为 null | AbstractMap | 线程不安全 |
- 【参考】合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
说明:有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次 序是一定的。如:ArrayList 是 order/unsort;HashMap 是 unorder/unsort;TreeSet 是 order/sort。 - 【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains 方法进行遍历、对比、去重操作。
(六) 并发处理
- 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。说明:资源驱动类、工具类、单例工厂类都需要注意。
- 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
正例:
public class TimerTaskThread extends Thread {
public TimerTaskThread() {
super.setName("TimerTaskThread");
...
}
}
- 【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用
countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。
说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。 - 【参考】volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是 count++操作,使用如下类实现: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
二、Android资源文件与使用
- 【推荐】资源文件需带模块前缀。
- 【推荐】layout 文件的命名方式。
Activity 的 layout 以 module_activity 开头
Fragment 的 layout 以 module_fragment 开头 Dialog 的 layout 以 module_dialog 开头 include 的 layout 以 module_include 开头
ListView 的行 layout 以 module_list_item 开头
RecyclerView 的 item layout 以 module_recycle_item 开头
GridView 的 item layout 以 module_grid_item 开头 - 【推荐】drawable 资源名称以小写单词+下划线的方式命名,根据分辨率不同存放在不同的 drawable 目录下,如果介意包大小建议只使用一套,系统去进行缩放。采用规则如下: 模块名_业务功能描述_控件描述_控件状态限定词
如:module_login_btn_pressed,module_tabs_icon_home_normal - 【推荐】anim 资源名称以小写单词+下划线的方式命名,采用以下规则:
模块名_逻辑名称_[方向|序号]
Tween 动画(使用简单图像变换的动画,例如缩放、平移)资源:尽可能以通用的动画名称命名,如 module_fade_in , module_fade_out , module_push_down_in (动画+方向)。
Frame 动画(按帧顺序播放图像的动画)资源:尽可能以模块+功能命名+序号。如 module_loading_grey_001。 - 【推荐】color 资源使用#AARRGGBB 格式,写入 module_colors.xml 文件中,命名格式采用以下规则: 模块名_逻辑名称_颜色
如: <color name=“module_btn_bg_color”>#33b5e5e5</color> - 【推荐】dimen 资源以小写单词+下划线方式命名,写入 module_dimens.xml 文件中,采用以下规则: 模块名_描述信息
如: <dimen name=“module_horizontal_line_height”>1dp</dimen> - 【推荐】style 资源采 用“父 style 名称.当前 style 名称”方式 命名,写入module_styles.xml 文件中,首字母大写。如:
<style name=“ParentTheme.ThisActivityTheme”>
…
</style> - 【推荐】string 资源文件或者文本用到字符需要全部写入 module_strings.xml 文件中,字符串以小写单词+下划线的方式命名,采用以下规则:模块名_逻辑名称
如:moudule_login_tips,module_homepage_notice_desc - 【推荐】Id 资源原则上以驼峰法命名,View 组件的资源 id 建议以 View 的缩写作为前缀。常用缩写表如下:
控件 | 缩写 |
---|---|
LinearLayout | ll |
RelativeLayout | rl |
ConstraintLayout | cl |
ListView | lv |
ScollView | sv |
TextView | tv |
Button | btn |
ImageView | iv |
CheckBox | cb |
RadioButton | rb |
EditText | et |
其它控件的缩写推荐使用小写字母并用下划线进行分割,例如:ProgressBar 对应的缩写为 progress_bar;DatePicker 对应的缩写为 date_picker。
10. 【推荐】图片根据其分辨率,放在不同屏幕密度的 drawable 目录下管理,否则可能在低密度设备上导致内存占用增加,又可能在高密度设备上导致图片显示不够清晰。
说明: 为了支持多种屏幕尺寸和密度,Android 提供了多种通用屏幕密度来适配。
三、Android基本组件
Android 基本组件指 Activity 、 Fragment 、 Service 、 BroadcastReceiver 、 ContentProvider 等等。
- 【强制】Activity 间的数据通信,对于数据量比较大的,避免使用 Intent +
Parcelable 的 方 式 , 可 以 考 虑 EventBus 等 替 代 方 案 , 以 免 造 成
TransactionTooLargeException。 - 【推荐】Activity#onSaveInstanceState()方法不是 Activity 生命周期方法,也不保证一定会被调用。它是用来在 Activity 被意外销毁时保存 UI 状态的,只能用于保存临时性数据,例如 UI 控件的属性等,不能跟数据的持久化存储混为一谈。持久化存储应该在 Activity#onPause()/onStop()中实行。
- 【强制】Activity 间通过隐式 Intent 的跳转,在发出 Intent 之前必须通过resolveActivity 检 查 , 避 免 找 不 到 合 适 的 调 用 组 件 , 造 成 ActivityNotFoundException 的异常。正例:
public void viewUrl(String url, String mimeType) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(url), mimeType);
if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
startActivity(intent);
} else {
// 找不到指定的 Activity
}
}
反例:
Intent intent = new Intent();
intent.setAction("com.example.DemoIntent");
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
- 【强制】避免在 Service#onStartCommand()/onBind()方法中执行耗时操作,如果确实有需求,应改用 IntentService 或采用其他异步机制完成。
正例:
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
public void startIntentService(View source) {
Intent intent = new Intent(this, MyIntentService.class);
startService(intent);
}
}
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
synchronized (this) {
try {
//......
} catch (Exception e) {
}
}
}
}
- 【强制】避免在 BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作,应该创建 IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做。
说明: 由于该方法是在主线程执行,如果执行耗时操作会导致 UI 不流畅。可以使用
IntentService 、创建 HandlerThread 或者调用 Context#registerReceiver
(BroadcastReceiver, IntentFilter, String, Handler)方法等方式,在其他 Wroker 线程执行 onReceive 方法。BroadcastReceiver#onReceive()方法耗时超过 10 秒钟,可能会被系统杀死。
正例:
IntentFilter filter = new IntentFilter();
filter.addAction(LOGIN_SUCCESS);
this.registerReceiver(mBroadcastReceiver, filter);
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Intent userHomeIntent = new Intent();
userHomeIntent.setClass(this, UserHomeService.class);
this.startService(userHomeIntent);
}
};
反例:
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
MyDatabaseHelper myDB = new MyDatabaseHelper(context);
myDB.initData();
// have more database operation here
}
};
- 【强制】避免使用隐式 Intent 广播敏感信息,信息可能被其他注册了对应 BroadcastReceiver 的 App 接收。
说明: 通过 Context#sendBroadcast()发送的隐式广播会被所有感兴趣的 receiver 接收,恶意应用注册监听该广播的 receiver 可能会获取到 Intent 中传递的敏感信息,并进行其他危险操作。如果发送的广播为使用Context#sendOrderedBroadcast()方法发送的有序广播,优先级较高的恶意 receiver 可能直接丢弃该广播,造成服务不可用,或者向广播结果塞入恶意数据。
如果广播仅限于应用内,则可以使用 LocalBroadcastManager#sendBroadcast()实现,避免敏感信息外泄和 Intent 拦截的风险。
正例:
Intent intent = new Intent("my-sensitive-event");
intent.putExtra("event", "this is a test event");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
反例:
Intent intent = new Intent();
v1.setAction("com.sample.action.server_running");
v1.putExtra("local_ip", v0.h);
v1.putExtra("port", v0.i);
v1.putExtra("code", v0.g);
v1.putExtra("connected", v0.s);
v1.putExtra("pwd_predefined", v0.r);
if (!TextUtils.isEmpty(v0.t)) {
v1.putExtra("connected_usr", v0.t);
}
context.sendBroadcast(v1);
以上广播可能被其他应用的如下 receiver 接收导致敏感信息泄漏
final class MyReceiver extends BroadcastReceiver {
public final void onReceive(Context context, Intent intent) {
if (intent != null && intent.getAction() != null) {
String s = intent.getAction();
if(s.equals("com.sample.action.server_running"){
String ip = intent.getStringExtra("local_ip");
String pwd = intent.getStringExtra("code");
String port = intent.getIntExtra("port", 8888);
boolean status = intent.getBooleanExtra("connected", false);
}
}
}
}
- 【推荐】添加 Fragment 时,确保 FragmentTransaction#commit() 在
Activity#onPostResume()或者 FragmentActivity#onResumeFragments()内调用。不要随意使用 FragmentTransaction#commitAllowingStateLoss()来代替,任何 commitAllowingStateLoss()的使用必须经过 code review,确保无负面影响。
说明: Activity 可能因为各种原因被销毁, Android 支持页 面被销毁前通过
Activity#onSaveInstanceState() 保存自己的状态。但如果
FragmentTransaction.commit()发生在 Activity 状态保存之后,就会导致 Activity 重
建、恢复状态时无法还原页面状态,从而可能出错。为了避免给用户造成不好的体
验,系统会抛出 IllegalStateExceptionStateLoss 异常。推荐的做法是在 Activity 的
onPostResume() 或 onResumeFragments() ( 对 FragmentActivity )里执行
FragmentTransaction.commit(),如有必要也可在 onCreate()里执行。不要随意改用
FragmentTransaction.commitAllowingStateLoss() 或者直接使用 try-catch 避 免
crash,这不是问题的根本解决之道,当且仅当你确认 Activity 重建、恢复状态时,
本次 commit 丢失不会造成影响时才可这么做。
正例:
public class MainActivity extends FragmentActivity {
FragmentManager fragmentManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
fragmentManager = getSupportFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
MyFragment fragment = new MyFragment();
ft.replace(R.id.fragment_container, fragment);
ft.commit();
}
}
反例:
public class MainActivity extends FragmentActivity {
FragmentManager fragmentManager;
@Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
fragmentManager = getSupportFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
MyFragment fragment = new MyFragment();
ft.replace(R.id.fragment_container, fragment);
ft.commit();
}
}
- 【推荐】不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在 Activity#onPause()/onStop()中结合 isFinishing()的判断来执行。
- 【推荐】如非必须,避免使用嵌套的 Fragment。
说明:嵌套Fragment是在Android API 17添加到SDK以及Support库中的功能,Fragment 嵌套使用会有一些坑,容易出现 bug,比较常见的问题有如下几种:
1) onActivityResult()方法的处理错乱,内嵌的 Fragment 可能收不到该方法的回调,需要由宿主 Fragment 进行转发处理;
2) 突变动画效果;
3) 被继承的 setRetainInstance(),导致在 Fragment 重建时多次触发不必要的逻辑。
正例:
FragmentManager fragmentManager = getFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(FragmentB.TAG);
if (null == fragment) {
FragmentB fragmentB = new FragmentB();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.fragment_container, fragmentB, FragmentB.TAG).commit();
}
反例:
Fragment videoFragment = new VideoPlayerFragment();
FragmentTransaction transaction = currentFragment.getChildFragmentManager().beginTransaction();
transaction.add(R.id.video_fragment, videoFragment).commit();
- 【推荐】总是使用显式 Intent 启动或者绑定 Service,且不要为服务声明 Intent Filter,保证应用的安全性。如果确实需要使用隐式调用,则可为 Service 提供 Intent Filter 并从 Intent 中排除相应的组件名称,但必须搭配使用 Intent#setPackage()方法设置 Intent 的指定包名,这样可以充分消除目标服务的不确定性。
- 【推荐】Service 需要以多线程来并发处理多个启动请求,建议使用 IntentService,可避免各种复杂的设置。
说明: Service 组件一般运行主线程,应当避免耗时操作,如果有耗时操作应该在 Worker 线程执行。可以使用 IntentService 执行后台任务。
正例:
public class SingleIntentService extends IntentService {
public SingleIntentService() {
super("single-service thread");
}
@Override
protected void onHandleIntent(Intent intent) {
try {
//......
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
反例:
public class HelloService extends Service {
//...
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
//操作语句
}
}).start();
...
}
}
- 【推荐】对于只用于应用内的广播,优先使用 LocalBroadcastManager 来进行注册和发送,LocalBroadcastManager 安全性更好,同时拥有更高的运行效率。
说明:对于使用 Context#sendBroadcast()等方法发送全局广播的代码进行提示。如果该广播仅用于应用内,则可以使用 LocalBroadcastManager 来避免广播泄漏以及广播被拦截等安全问题,同时相对全局广播本地广播的更高效。
正例:
public class MainActivity extends ActionBarActivity {
private MyReceiver receiver;
private IntentFilter filter;
private Context context;
private static final
String MY_BROADCAST_TAG = "com.example.localbroadcast";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstsanceState);
context = this;
setContentView(R.layout.activity_main);
receiver = new MyReceiver();
filter = new IntentFilter();
filter.addAction(MY_BROADCAST_TAG);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent();
intent.setAction(MY_BROADCAST_TAG);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
});
}
@Override
protected void onResume() {
super.onResume();
LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter);
}
@Override
protected void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
}
class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context arg0, Intent arg1) {
// message received
}
}
}
反例:所有广播都使用全局广播
13. 【推荐】当前 Activity 的 onPause 方法执行结束后才会创建(onCreate)或恢复(onRestart)别的 Activity,所以在 onPause 方法中不适合做耗时较长的工作,这会影响到页面之间的跳转效率。
14.【 强 制 】 Activity 或 者 Fragment 中 动 态 注 册 BroadCastReceiver 时 , registerReceiver() 和 unregisterReceiver()要成对出现。
说明:如果 registerReceiver()和 unregisterReceiver()不成对出现,则可能导致已经注册的 receiver 没有在合适的时机注销,导致内存泄漏,占用内存空间,加重
SystemService 负担。 部分华为的机型会对 receiver 进行资源管控,单个应用注册过多 receiver 会触发管控模块抛出异常,应用直接崩溃。
正例:
public class MainActivity extends AppCompatActivity {
private static MyReceiver myReceiver = new MyReceiver();
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter("com.example.myservice");
registerReceiver(myReceiver, filter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(myReceiver);
}
//...
}
反例:
public class MainActivity extends AppCompatActivity {
private static MyReceiver myReceiver;
@Override
protected void onResume() {
super.onResume();
myReceiver = new MyReceiver();
IntentFilter filter = new IntentFilter("com.example.myservice");
registerReceiver(myReceiver, filter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(myReceiver);
}
}
Activity 的生命周期不对应,可能出现多次 onResume 造成 receiver 注册多个,但
最终只注销一个,其余 receiver 产生内存泄漏。
15. 【强制】Android 基础组件如果使用隐式调用,应在 AndroidManifest.xml 中使用 或在代码中使用 IntentFilter 增加过滤。
说明: 如果浏览器支持 Intent Scheme Uri 语法,如果过滤不当,那么恶意用户可能通过浏 览 器 js 代 码 进 行 一 些 恶 意 行 为 , 比 如 盗 取 cookie 等 。 如 果 使 用 了
Intent.parseUri 函数,获取的 intent 必须严格过滤。
正例:
// 将 intent scheme URL 转换为 intent 对象
Intent intent = Intent.parseUri(uri);
// 禁止没有 BROWSABLE category 的情况下启动 activity
intent.addCategory("android.intent.category.BROWSABLE");
intent.setComponent(null);
intent.setSelector(null);
// 使用 intent 启动 activity
context.startActivityIfNeeded(intent, -1)
反例:
Intent intent = Intent.parseUri(uri.toString().trim().substring(15), 0);
intent.addCategory("android.intent.category.BROWSABLE");
context.startActivity(intent);
四、UI与布局
- 【强制】布局中不得不使用 ViewGroup 多重嵌套时,不要使用 LinearLayout 嵌套,改用 RelativeLayout,可以有效降低嵌套数。
说明:
Android 应用页面上任何一个 View 都需要经过 measure、layout、draw 三个步骤才能被正确的渲染。从 xml layout 的顶部节点开始进行 measure,每个子节点都需要向自己的父节点提供自己的尺寸来决定展示的位置,在此过程中可能还会重新 measure(由此可能导致measure的时间消耗为原来的2-3倍)。节点所处位置越深,嵌套带来的 measure 越多,计算就会越费时。这就是为什么扁平的 View 结构会性能更好。
同时,页面拥上的 View 越多,measure、layout、draw 所花费的时间就越久。要缩短这个时间,关键是保持 View 的树形结构尽量扁平,而且要移除所有不需要渲染的
View。理想情况下,总共的 measure,layout,draw 时间应该被很好的控制在 16ms 以内,以保证滑动屏幕时 UI 的流畅。
要找到那些多余的 View(增加渲染延迟的 view),可以用 Android Studio Monitor 里的 Hierarchy Viewer 工具,可视化的查看所有的 view。 - 【推荐】在 Activity 中显示对话框或弹出浮层时,尽量使用 DialogFragment,而非 Dialog/AlertDialog,这样便于随Activity生命周期管理对话框/弹出浮层的生命周期。
正例:
public void showPromptDialog(String text) {
DialogFragment promptDialog = new DialogFragment() {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
View view = inflater.inflate(R.layout.fragment_prompt, container);
return view;
}
};
promptDialog.show(getFragmentManager(), text);
}
- 【推荐】源文件统一采用 UTF-8 的形式进行编码。
- 【强制】禁止在非 UI 线程进行 View 相关操作。
- 【推荐】文本大小使用单位 dp,View 大小使用单位 dp。对于 TextView,如果在文字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问题。
说明:
之所以文本大小也推荐使用 dp 而非 sp,因为 sp 是 Android 早期推荐使用的,但其实 sp 不仅和 dp 一样受屏幕密度的影响,还受到系统设置里字体大小的影响,所以使用 dp 对于应用开发会更加保证 UI 的一致性和还原度。 - 【强制】禁止在设计布局时多次为子 View 和父 View 设置同样背景进而造成页面过度绘制,推荐将不需要显示的布局进行及时隐藏。
- 【推荐】灵活使用布局,推荐 merge、ViewStub 来优化布局,尽可能多的减少 UI 布局层级,推荐使用 FrameLayout,LinearLayout、RelativeLayout 次之。
- 【推荐】在需要时刻刷新某一区域的组件时,建议通过以下方式避免引发全局 layout 刷新:
1设置固定的 View 大小的宽高,如倒计时组件等;
2调用 View 的 layout 方法修改位置,如弹幕组件等;
3通过修改 Canvas 位置并且调用 invalidate(int l, int t, int r, int b)等方式限定刷新区域;
4通过设置一个是否允许requestLayout的变量,然后重写控件的requestlayout、 onSizeChanged 方法,判断控件的大小没有改变的情况下,当进入 requestLayout 的时候,直接返回而不调用 super 的 requestLayout 方法。 - 【推荐】不能在 Activity 没有完全显示时显示 PopupWindow 和 Dialog。
说明:
Android Activity 创建时的生命周期,按照 onCreate() -> onStart() -> onResume() -> onAttachedToWindow() -> onWindowFocusChanged() 的顺序,其中在 Activity#onAttachedToWindow() 时,Activity 会与它的 Window 关联,这时 UI 才会开始绘制,在 Activity#onWindowFocusChanged() 时,UI 才变成可交互状态,可以提示用户使用。如果在 Window 未关联时就创建对话框,UI 可能显示异常。推荐的做法是在 Activity#onAttachedToWindow() 之后才创建对话框。 - 【推荐】尽量不要使用 AnimationDrawable,它在初始化的时候就将所有图片加载到内存中,特别占内存,并且还不能释放,释放之后下次进入再次加载时会报错。
说明:
Android 的帧动画可以使用 AnimationDrawable 实现,但是如果你的帧动画中如果包含过多帧图片,一次性加载所有帧图片所导致的内存消耗会使低端机发生 OOM 异常。帧动画所使用的图片要注意降低内存消耗,当图片比较大时,容易出现 OOM。图片数量较少的 AnimationDrawable 还是可以接受的。 - 【强制】不能使用 ScrollView 包裹 ListView/GridView/ExpandableListVIew;因为这样会把 ListView 的所有 Item 都加载到内存中,要消耗巨大的内存和 cpu 去绘制图面。
说明:
ScrollView 中嵌套 List 或 RecyclerView 的做法官方明确禁止。除了开发过程中遇到的各种视觉和交互问题,这种做法对性能也有较大损耗。ListView 等 UI 组件自身有垂直滚动功能,也没有必要在嵌套一层 ScrollView。目前为了较好的 UI 体验,更贴近 Material Design 的设计,推荐使用 NestedScrollView。 - 【强制】不要在 Android 的 Application 对象中缓存数据。基础组件之间的数据共享请使用 Intent 等机制,也可使用 SharedPreferences 等数据持久化机制。
- 【推荐】使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示 Toast 时不能取消上一次 Toast 消息的情况。即使需要连续弹出 Toast,也应避免直接调用 Toast#makeText。
- 【强制】使用 Adapter 的时候,如果你使用了 ViewHolder 做缓存,在 getView()的方法中无论这项 convertView 的每个子控件是否需要设置属性(比如某个 TextView 设置的文本可能为 null,某个按钮的背景色为透明,某控件的颜色为透明等),都需要为其显式设置属性(Textview 的文本为空也需要设置 setText(""),背景透明也需要设置),否则在滑动的过程中,因为 adapter item 复用的原因,会出现内容的显示错乱。
五、进程、线程与消息通知
- 【强制】不要通过 Intent 在 Android 基础组件之间传递大数据(binder transaction 缓存为 1MB),可能导致 OOM。
- 【强制】在 Application 的业务初始化代码加入进程判断,确保只在自己需要的进程初始化。特别是后台进程减少不必要的业务初始化。
正例:
public class MyApplication extends Application {
@Override
public void onCreate() {
//在所有进程中初始化
....
//仅在主进程中初始化
if (mainProcess) {
...
}
//仅在后台进程中初始化
if (bgProcess) {
...
}
}
}
- 【强制】新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定义的线程池),不允许在应用中自行显式创建线程。
说明:
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。另外创建匿名线程不便于后续的资源使用分析,对性能分析等会造成困扰。
反例:
new Thread(new Runnable() {
@Override
public void run() {
//操作语句
...
}
}).start();
- 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
- 【强制】子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在主线程中调用。
- 【推荐】尽量减少不同 APP 之间的进程间通信及拉起行为。拉起导致占用系统资源,影响用户体验。
- 【推荐】ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时线程能被释放。
- 【推荐】禁止在多进程之间用 SharedPreferences 共享数据,虽然可以
(MODE_MULTI_PROCESS),但官方已不推荐。 - 【推荐】谨慎使用 Android 的多进程,多进程虽然能够降低主进程的内存压力,但会遇到如下问题:
1首次进入新启动进程的页面时会有延时的现象(有可能黑屏、白屏几秒,是白屏还是黑屏和新 Activity 的主题有关);
2应用内多进程时,Application 实例化多次,需要考虑各个模块是否都需要在所有进程中初始化。
六、文件与数据库
- 【强制】任何时候不要硬编码文件路径,请使用 Android 文件系统 API 访问。
说明:
Android 应用提供内部和外部存储,分别用于存放应用自身数据以及应用产生的用户数据。可以通过相关 API 接口获取对应的目录,进行文件操作。
android.os.Environment#getExternalStorageDirectory() android.os.Environment#getExternalStoragePublicDirectory() android.content.Context#getFilesDir()
android.content.Context#getCacheDir()
正例:
public File getDir(String alName) {
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), alName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
反例:
public File getDir(String alName) {
// 任何时候都不要硬编码文件路径,这不仅存在安全隐患,也让 app 更容易出现适配问题
File file = new File("/mnt/sdcard/Download/Album", alName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
- 【强制】当使用外部存储时,必须检查外部存储的可用性。
正例:
// 读/写检查
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
// 只读检查
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
- 【强制】应用间共享文件时,不要通过放宽文件系统权限的方式去实现,而应使用
FileProvider。 - 【推荐】SharedPreference 中只能存储简单数据类型(int、boolean、String 等),复杂数据类型建议使用文件、数据库等其他方式存储。
- 【推荐】 SharedPreference 提交数据时,尽量使用 Editor#apply() ,而非 Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用 Editor#commit()。
说明:
SharedPreference 相关修改使用 apply 方法进行提交会先写入内存,然后异步写入磁盘, commit 方法是直接写入磁盘。如果频繁操作的话 apply 的性能会优于 commit, apply 会将 后修改内容写入磁盘。但是如果希望立刻获取存储操作的结果,并据此做相应的其他操作,应当使用 commit。 - 【强制】数据库 Cursor 必须确保使用完后关闭,以免内存泄漏。
说明:
Cursor 是对数据库查询结果集管理的一个类,当查询的结果集较小时,消耗内存不易察觉。但是当结果集较大,长时间重复操作会导致内存消耗过大,需要开发者在操作完成后手动关闭 Cursor。
数据库 Cursor 在创建及使用时,可能发生各种异常,无论程序是否正常结束,必须在 后确保 Cursor 正确关闭,以避免内存泄漏。同时,如果 Cursor 的使用还牵涉多线程场景,那么需要自行保证操作同步。 - 【强制】多线程操作写入数据库时,需要使用事务,以免出现同步问题。
说明:通过 SQLiteOpenHelper 获取数据库 SQLiteDatabase 实例,Helper 中会自动缓存已经打开的 SQLiteDatabase 实例,单个 App 中应使用 SQLiteOpenHelper 的单例模式确保数据库连接唯一。由于 SQLite 自身是数据库级锁,单个数据库操作是保证线程安全的(不能同时写入),transaction 是一次原子操作,因此处于事务中的操作是线程安全的。
若同时打开多个数据库连接,并通过多线程写入数据库,会导致数据库异常,提示数据库已被锁住。
正例:
public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("userId", userId);
cv.put("content", content);
db.beginTransaction();
try {
db.insert(TUserPhoto, null, cv);
// 其他操作
db.setTransactionSuccessful();
} catch (Exception e) {
// TODO
} finally {
db.endTransaction();
}
}
反例:
public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("userId", userId);
cv.put("content", content);
db.insert(TUserPhoto, null, cv);
}
- 【推荐】大数据写入数据库时,请使用事务或其他能够提高 I/O 效率的机制,保证执行速度。
正例:
public void insertBulk(SQLiteDatabase db, ArrayList<UserInfo> users) {
db.beginTransaction();
try {
for (int i = 0; i < users.size; i++) {
ContentValues cv = new ContentValues();
cv.put("userId", users[i].userId);
cv.put("content", users[i].content);
db.insert(TUserPhoto, null, cv);
}
// 其他操作
db.setTransactionSuccessful();
} catch (Exception e) {
// TODO
} finally {
db.endTransaction();
}
}
- 【强制】执行 SQL 语句时,应使用 SQLiteDatabase#insert()、update()、delete(),不要使用 SQLiteDatabase#execSQL(),以免 SQL 注入风险 。
正例:
public int updateUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("content", content);
String[] args = {String.valueOf(userId)};
return db.update(TUserPhoto, cv, "userId=?", args);
}
- 【强制】如果 ContentProvider 管理的数据存储在 SQL 数据库中,应该避免将不受信任的外部数据直接拼接在原始 SQL 语句中。
七、Bitmap、Drawable与动画
- 【强制】加载大图片或者一次性加载多张图片,应该在异步线程中进行。图片的加载,涉及到 IO 操作,以及 CPU 密集操作,很可能引起卡顿。
- 【强制】在 ListView,ViewPager,RecyclerView,GirdView 等组件中使用图片时,应做好图片的缓存,避免始终持有图片导致内存溢出,也避免重复创建图片,引起性能问题。
- 【强制】png 图片使用 TinyPNG 或者类似工具压缩处理,减少包体积。
- 【推荐】应根据实际展示需要,压缩图片,而不是直接显示原图。手机屏幕比较小,直接显示原图,并不会增加视觉上的收益,但是却会耗费大量宝贵的内存。
- 【强制】使用完毕的图片,应该及时回收,释放宝贵的内存。
正例:
// 使用结束,在 2.3.3 及以下需要调用 recycle()函数,在 2.3.3 以上 GC 会自动管理,除非你明确不需要再用。
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
bitmap.recycle();
}
bitmap = null;
- 【强制】在 Activity#onPause()或 Activity#onStop()回调中,关闭当前 activity 正在执行的的动画。
- 【推荐】在动画或者其他异步任务结束时,应该考虑回调时刻的环境是否还支持业务处理。例如 Activity 的 onStop()函数已经执行,且在该函数中主动释放了资源,此时回调中如果不做判断就会空指针崩溃。
正例:
public class MyActivity extends Activity {
private ImageView mImageView;
private Animation mAnimation;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mImageView = (ImageView) findViewById(R.id.ImageView01);
mAnimation = AnimationUtils.loadAnimation(this, R.anim.anim);
mAnimation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationEnd(Animation arg0) {
//判断一下资源是否被释放了
if (mImageView != null) {
mImageView.clearAnimation();
}
}
});
mImageView.startAnimation(mAnimation);
}
}
反例:动画结束回调中,直接使用资源不加判断,导致异常。
8. 【推荐】使用 inBitmap 重复利用内存空间,避免重复开辟新内存。
正例:
9. 【推荐】使用 RGB_565 代替 RGB_888,在不怎么降低视觉效果的前提下,减少内存占用。
10. 【推荐】. 尽量减少 Bitmap(BitmapDrawable)的使用,尽量使用纯色(ColorDrawable)、渐变色(GradientDrawable)、StateSelector(StateListDrawable)等与 Shape 结合的形式构建绘图。
11. 【推荐】谨慎使用 gif 图片,注意限制每个页面允许同时播放的 gif 图片,以及单个 gif 图片的大小。
12. 【推荐】在有强依赖 onAnimationEnd 回调的交互时,如动画播放完毕才能操作页面, onAnimationEnd 可能会因各种异常没被回调(参考:
https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-calle
d-onanimationstart-works-fine),建议加上超时保护或通过 postDelay 替代 onAnimationEnd。
正例:
View v = findViewById(R.id.xxxViewID);
final FadeUpAnimation anim = new FadeUpAnimation(v);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration(1000);
anim.setFillAfter(true);
new Handler().postDelayed(new Runnable() {
public void run() {
if (v != null) {
v.clearAnimation();
}
}
}, anim.getDuration());
v.startAnimation(anim);
- 【推荐】当 View Animation 执行结束时,调用 View.clearAnimation()释放相关资源。
八、安全
- 【强制】禁止使用常量初始化矢量参数构建 IvParameterSpec,建议 IV 通过随机方式产生。
说明:
使用常量初始化向量,密码文本的可预测性会高得多,容易受到字典式攻击。iv 的作用主要是用于产生密文的第一个 block,以使 终生成的密文产生差异(明文相同的情况下),使密码攻击变得更为困难。
正例:
byte[] rand = new byte[16];
SecureRandom r = new SecureRandom();
r.nextBytes(rand);
IvParameterSpec iv = new IvParameterSpec(rand);
反例:
IvParameterSpec iv_ = new IvParameterSpec("1234567890".getBytes());
System.out.println(iv.getIV());
- 【强制】将 android:allowbackup 属性必须设置为 false,阻止应用数据被导出。
说明:
android:allowBackup 原本是 Android 提供的 adb 调试功能,如果设置为 true,可以导出应用数据备份并在任意设备上恢复。这对应用安全性和用户数据隐私构成极大威胁,所以必须设置为 false,防止数据泄露。 - 【强制】如果使用自定义 HostnameVerifier 实现类,必须在 verify()方法中校验服务器主机名的合法性,否则可能受到中间人攻击。
说明:
在与服务器建立 https 连接时,如果 URL 的主机名和服务器的主机名不匹配,则可通过该回调接口来判断是否应该允许建立连接。如果回调内实现不恰当,没有有效校验主机名,甚至默认接受所有主机名,会大大增加安全风险。 - 【强制】如果使用自定义 X509TrustManager 实现类,必须在 checkServerTrusted() 方法中校验服务端证书的合法性,否则可能受到中间人攻击。
说明:
常见误区是 checkServerTrusted()方法根本没有实现,这将导致 X509TrustManager 形同虚设。该方法中需要实现完备的校验逻辑,对于证书错误抛出CertificateException 。
正例:
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, if("yourhostname".equals(hostname))
{
return true;
} else
{
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify(hostname, session);
}
};
- 【强制】在 SDK 支持的情况下,Android 应用必须使用 V2 签名,这将对 APK 文件的修改做更多的保护。
- 【强制】所有的 Android 基本组件(Activity、Service、BroadcastReceiver、 ContentProvider 等)都不应在没有严格权限控制的情况下,将 android:exported 设置为 true。
- 【强制】WebView 应设置 WebView#getSettings()#setAllowFileAccess(false)、 WebView#getSettings()#setAllowFileAccessFromFileURLs(false) 、 WebView#getSettings()#setAllowUniversalAccessFromFileURLs(false),阻止 file scheme URL 的访问。
- 【强制】不要把敏感信息打印到 log 中。
- 【强制】确保应用发布版本的 android:debuggable 属性设置为 false。
- 【强制】本地加密秘钥不能硬编码在代码中,更不能使用 SharedPreferences 等本地持久化机制存储。应选择 Android 自身的秘钥库(KeyStore)机制或者其他安全性更高的安全解决方案保存。
说明:
应用程序在加解密时,使用硬编码在程序中的密钥,攻击者通过反编译拿到密钥可以轻易解密 APP 通信数据。 - 【强制】Android APP 在 HTTPS 通信中,验证策略需要改成严格模式。
说明:
Android APP 在 HTTPS 通信中,使用 ALLOW_ALL_HOSTNAME_VERIFIER,表示允许和所有的 HOST 建立 SSL 通信,这会存在中间人攻击的风险, 终导致敏感信息可能会被劫持,以及其他形式的攻击。 - 【推荐】. 在 Android 4.2(API Level 17)及以上,对安全性要求较高的应用可在 Activity 中,对 Activity 所关联的 Window 应用 WindowManager.LayoutParams.FLAG_ SECURE,防止被截屏、录屏。但要注意的是,一个 Activity 关联的 Window 可能不止一个,如果使用了 Dialog / DialogFragment 等控件弹出对话框,它们本身也会创建一个新的 Window,也一样需要保护。
- 【推荐】zip 中不要包含 …/…/file 这样的路径,可能被篡改目录结构,造成攻击。
说明:
当 zip 压缩包中允许存在"…/“的字符串,攻击者可以利用多个”…/"在解压时改变 zip 文件存放的位置,当文件已经存在是就会进行覆盖,如果覆盖掉的文件是 so、dex 或者 odex 文件,就有可能造成严重的安全问题。 - 【推荐】MD5 和 SHA-1、SHA-256 等常用算法是 Hash 算法,有一定的安全性,但不能代替加密算法。敏感信息的存储和传输,需要使用专业的加密机制。