阿里巴巴Android开发手册:https://yq.aliyun.com/attachment/download/?id=5261
Android 资源文件命名与使用
1. 【推荐】 color 资源使用#AARRGGBB 格式
2. 【推荐】大分辨率图片(单维度超过 1000)建议统一放在 xxhdpi 目录下管理,否则将导致占用内存成倍数增加。
Android 基本组件
1. 【强制】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) {
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
if (Config.LOGD) {
Log.d(LOGTAG, "activity not found for " + mimeType + " over " +
Uri.parse(url). getScheme(), e);
}
}
}
}
2. 【强制】避免在 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) {
}
}
}
}
3. 【强制】避免在 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, UseHomeActivity.class);
this.startActivity(userHomeIntent);
}
};
4. 【强制】避免使用隐式 Intent 广播敏感信息,信息可能被其他注册了对应
BroadcastReceiver 的 App 接收。
如果广播仅限于应用内,则可以使用 LocalBroadcastManager#sendBroadcast()实现,避免敏感信息外泄和 Intent 拦截的风险。
Intent intent = new Intent("my-sensitive-event");
intent.putExtra("event", "this is a test event");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
5. 【推荐】不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的
销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在
Activity#onPause()/onStop()中结合 isFinishing()的判断来执行。
private boolean mIsRelease = false;
/**
* 释放资源
*/
private void release(){
if (mIsRelease){
return;
}
if (isFinishing()) {
if (mHttpList != null) {
mHttpList.release();
mHttpList = null;
}
}
mIsRelease = true;
}
@Override
protected void onPause() {
super.onPause();
//在onPause生命周期执行的瞬间,activity其实是还在前台的,所以有概率出现资源已经被释 放,但是activity里的View是还有被点击的机会导致空指针报错
}
@Override
protected void onStop() {
super.onStop();
release();
}
@Override
protected void onDestroy() {
super.onDestroy();
//SingleTask的清理会跳过isFinishing()判断,需要在onDestroy()再次兜底,保证不会因为SingleTask的原因没有释放资源
release();
}
6. 【推荐】使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示
Toast 时不能取消上一次 Toast 消息的情况(如果你有连续弹出 Toast 的情况,避免
使用 Toast.makeText)
UI 与布局
1. 【强制】布局中不得不使用 ViewGroup 多重嵌套时,不要使用 LinearLayout 嵌套,
改用 RelativeLayout,可以有效降低嵌套数。
2. 【推荐】在 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);
}
3. 【推荐】文本大小使用单位 dp,view 大小使用单位 dp。对于 Textview,如果在文
字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问
题。
4. 【推荐】灵活使用布局,推荐 Merge、ViewStub 来优化布局,尽可能多的减少 UI
布局层级,推荐使用 FrameLayout,LinearLayout、RelativeLayout 次之。
repeat_inclde.xml文件中使用<merge>标签作为layout布局的根节点。<merge/>标签帮助你排除把一个布局插入到另一个布局时产生的多余的View Group。
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/delete"/>
</merge>
要添加可复用布局的xml文件中,添加<include/>标签。系统会忽略merge标签,直接把两个Button替换到include标签的位置。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
<include layout="@layout/repeat_inclde"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
android:padding="10dp" />
</LinearLayout>
5. 【推荐】不能在 Activity 没有完全显示时显示 PopupWindow 和 Dialog。
6. 【强制】不能使用 ScrollView 包裹 ListView/GridView/ExpandableListVIew;因为这
样会把 ListView 的所有 Item 都加载到内存中,要消耗巨大的内存和 cpu 去绘制图
面。
推荐使用 NestedScrollView,通过xrecyclerView.setNestedScrollingEnabled(false);禁用自身的滚动功能
进程、线程与消息通信
1. 【强制】不要通过 Intent 在 Android 基础组件之间传递大数据(binder transaction
缓存为 1MB),可能导致 OOM。
可以使用EventBus等替代方案
2. 【强制】新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor
或者其他形式自定义的线程池),不允许在应用中自行显式创建线程。
int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
int KEEP_ALIVE_TIME = 1;
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>();
ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES,
NUMBER_OF_CORES*2, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, taskQueue,
new BackgroundThreadFactory(), new DefaultRejectedExecutionHandler());
//执行任务
executorService.execute(new Runnnable() {
...
});
- corePoolSize:核心线程数
当线程池中的线程数目达到 corePoolSize后,新来的任务将会被添加到缓存队列中,也就是那个workQueue。
-
maximumPoolSize:线程池中的最大线程数
当线程池中的线程数等于 corePoolSize 并且 workQueue 已满,这时就要看当前线程数是否大于maximumPoolSize,如果小于maximumPoolSize 定义的值,则会继续创建线程去执行任务, 否则将会调用去相应的任务拒绝策略来拒绝这个任务。
-
keepAliveTime:控制"idle Thread"的空闲存活时间
超过 corePoolSize的线程被称做"Idle Thread", 这部分线程会有一个最大空闲存活时间(keepAliveTime),如果超过这个空闲存活时间还没有任务被分配,则会将这部分线程进行回收。
keepAliveTime 共7种取值 TimeUnit.DAYS; //天 TimeUnit.HOURS; //小时 TimeUnit.MINUTES; //分钟 TimeUnit.SECONDS; //秒 TimeUnit.MILLISECONDS; //毫秒 TimeUnit.MICROSECONDS; //微妙 TimeUnit.NANOSECONDS; //纳秒
-
workQueue:阻塞队列
只要超过了 corePoolSize 就会把任务添加到该缓存队列,添加可能成功也可能不成功,如果成功的话就会等待空闲线程去执行该任务,若添加失败(一般是队列已满),就会根据当前线程池的状态决定如何处理该任务(若线程数 < maximumPoolSize 则新建线程;若线程数 >= maximumPoolSize,则会根据拒绝策略做具体处理)。
常见的阻塞队列: //基于数组的先进先出队列,此队列创建时必须指定大小; 1)ArrayBlockingQueue //基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE; 2)LinkedBlockingQueue //这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。 3)synchronousQueue
-
threadFactory:线程工厂
用来为线程池创建线程,当我们不指定线程工厂时,线程池内部会调用Executors.defaultThreadFactory()创建默认的线程工厂,其后续创建的线程优先级都是Thread.NORM_PRIORITY。
-
handler:拒绝执行策略
当线程池的缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。
通常有以下四种策略: // 丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.AbortPolicy: // 也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardPolicy: // 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.DiscardOldestPolicy: // 由调用线程处理该任务 ThreadPoolExecutor.CallerRunsPolicy:
3. 【强制】子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在
主线程中调用。
Message msg = new Message();
msg.obj = "需要返回的数据";
handler.sendMessage(msg);
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1) {
//执行操作
}
}
};
4. 【推荐】 禁 止 在 多 进 程 之 间 用 SharedPreferences 共 享 数 据 , 虽 然 可 以
(MODE_MULTI_PROCESS),但官方已不推荐。
使用SystemProperties
使用SettingProvider
文件与数据库
1. 【强制】任何时候不要硬编码文件路径,请使用 Android 文件系统 API 访问。
android.os.Environment#getExternalStorageDirectory()
android.os.Environment#getExternalStoragePublicDirectory()
android.content.Context#getFilesDir()
android.content.Context#getCacheDir()
2. 【强制】当使用外部存储时,必须检查外部存储的可用性。
// 读/写检查
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.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
Bitmap、Drawable 与动画
1. 【强制】加载大图片或者一次性加载多张图片,应该在异步线程中进行。图片的加
载,涉及到 IO 操作,以及 CPU 密集操作,很可能引起卡顿。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// 在后台进行图片解码
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = BitmapFactory.decodeFile("some path");
return bitmap;
}
...
}
2. 【强制】png 图片使用 tinypng 或者类似工具压缩处理,减少包体积。
3. 【推荐】谨慎使用 gif 图片,注意限制每个页面允许同时播放的 gif 图片,以及单个
gif 图片的大小。
4. 【参考】大图片资源不要直接打包到 apk,可以考虑通过文件仓库远程下载,减小包
体积。
安全
1. 【强制】使用 PendingIntent 时,禁止使用空 intent,同时禁止使用隐式 Intent
2. 【强制】将 android:allowbackup 属性设置为 false,防止 adb backup 导出数据。
3. 【强制】META-INF 目录中不能包含如.apk,.odex,.so 等敏感文件,该文件夹没有经
过签名,容易被恶意替换。
4. 【强制】Receiver/Provider 不能在毫无权限控制的情况下,将 android:export 设置
为 true。
5. 【参考】数据存储在 Sqlite 或者轻量级存储需要对数据进行加密,取出来的时候进
行解密。
6. 【强制】阻止 webview 通过 file:schema 方式访问本地敏感数据。
7. 【强制】对于内部使用的组件,显示设置组件的"android:exported"属性为 false。
8. 【强制】使用 Intent Scheme URL 需要做过滤。
// 将 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);
9. 【推荐】对于不需要使用 File 协议的应用,禁用 File 协议,显式设置 webView.
getSettings().setAllowFileAccess(false),对于需要使用 File 协议的应用,禁止 File
协议调用 JavaScript,显式设置 webView.getSettings().setJavaScriptEnabled(false)。
阿里巴巴Java开发手册:
https://yq.aliyun.com/attachment/download/?id=5585
编程规约
1. 【强制】bean 类中布尔类型的变量,都不要加 is 前缀。
2. 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用
单数形式,但是类名如果有复数含义,类名可以使用复数形式。
3. 【强制】if/for/while/switch/do 等保留字与括号之间都必须加空格。
4. 【强制】注释的双斜线与注释内容之间有且仅有一个空格。
5. 【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
- 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进。
- 运算符与下文一起换行。
- 方法调用的点符号与下文一起换行。
- 方法调用中的多个参数需要换行时,在逗号后进行。
- 在括号前不要换行。
6. 【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格。
正例:下例中实参的 args1,后边必须要有一个空格。
method(args1, args2, args3);
7. 【推荐】单个方法的总行数不超过 80 行。
说明:包括方法签名、结束右大括号、方法内代码、注释、空行、回车及任何不可见字符的总行数不超过 80 行。
8. 【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。
说明:任何情形,没有必要插入多个空行进行隔开。
9. 【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用
equals。
正例:"test".equals(object);
说明:推荐使用 java.util.Objects.equals("test",object)
10. 【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。
11. 关于基本数据类型与包装数据类型的使用标准如下:
- 【强制】所有的 bean 类属性必须使用包装数据类型。
- 【推荐】所有的局部变量使用基本数据类型。
12. 【推荐】使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无
内容的检查,否则会有抛 IndexOutOfBoundsException 的风险。
String str="a,b,c,,";
String []arr=str.split(",");
System.out.println("数据的长度"+arr.length+" 数组的内容"+Arrays.toString(arr));
//打印结果:数据的长度3 数组的内容[a, b, c]
String str="a,b,c,,";
String []arr=str.split(",",-1);
System.out.println("数据的长度"+arr.length+" 数组的内容"+Arrays.toString(arr));
// 打印结果:数据的长度5 数组的内容[a, b, c, , ]
13. 【推荐】循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
反例:
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
14. 【强制】使用工具类 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)也会随之修改。
15. 【强制】不要在 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.remove();
}
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的
结果吗?
16. 【推荐】集合初始化时,指定集合初始值大小。initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
17. 【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。如果是 JDK8,使用 Map.foreach 方法。
正例:
// 传统的Map迭代方式
for (Map.Entry<String, Object> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
// JDK8的迭代方式
map.forEach((key, value) -> {
System.out.println(key + ":" + value);
});
18. 【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的
contains 方法进行遍历、对比、去重操作。
正例:
Set set = new HashSet(list);
List alist = new ArrayList();
for (Object o : set) {
alist.add(o);
}
System.out.println("数据的长度为:"+alist.size());
List newList=new ArrayList();
newList.addAll(new HashSet(list));
System.out.println("数据的长度为:"+newList.size());
19. 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为
static,必须加锁,或者使用 DateUtils 工具类。
正例:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,
DateTimeFormatter 代替 SimpleDateFormat。
20. 【推荐】表达异常的分支时,少用 if-else 方式,【强制】如果非得使用 if()...else if()...else...方式表达逻辑,请勿超过 3 层。推荐使用卫语句、策略模式、状态模式等来实现。
正例:
if (condition) {
...
return obj;
}
// 接着写 else 的业务逻辑代码;
21. 【推荐】除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将
复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
正例:
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
...
}
22. 【推荐】及时清理不再使用的代码段或配置信息。
说明:对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由。