Android开发规范/手册
=========================
前序
Android 开发团队的集体智慧 结晶和经验总结,现将App的长期开发迭代和优化经验系统地整 理成册,以指导 Android 开发者更加高效、高质量地进行 App 开发,呈现给用户体验好、 性能优、稳定性佳、安全性高的产品。
目标
- 防患未然,提升质量意识,降低故障率和维护成本;
- 标准统一,提升协作效率;
- 追求卓越的工匠精神,打磨精品代码。
范围
- java 语言规范,
- Android 资源文件命名与使用,
- Android 基本组件,
- UI 与布局,进程、线程与消息通信,
- 文件与数据库,
- Bitmap、Drawable
- 动画
- 安全
- 其他
约束力等级
- 【强制】必须遵守,违反本约定或将会引起严重的后果;
- 【推荐】尽量遵守,长期遵守有助于系统稳定性和合作效率的提升;
- 【参考】充分理解,技术意识的引导,是个人学习、团队沟通、项目合作的方 向。
正文
一、java 语言规范
......
二、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 的行 layout 以 module_grid_item 开头【推荐】 drawable 资源名称以小写单词+下划线的方式命名,根据分辨率不同存放 在不同的 drawable 目录下,建议只使用一套,例如 drawable-xhdpi。采用规则如下:
模块名_业务功能描述_控件描述_控件状态限定词如:
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 资源采用小写单词+下划线方式命名,写入 module_styles.xml 文件中, 采用以下规则:
父 style 名称.当前 style 名称如:
<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
- 【推荐】
大分辨率图片(单维度超过 1000)大分辨率图片建议统一放在 xxhdpi 目录 下管理,否则将导致占用内存成倍数增加。
说明:
正例: 将 144144 的应用图标 PNG 文件放在 drawable-xxhdpi 目录为了支持多种屏幕尺寸和密度,Android 为多种屏幕提供不同的资源目录进行适配。 为不同屏幕密度提供不同的位图可绘制对象,可用于密度特定资源的配置限定符(在下面详述) 包括 ldpi(低)、mdpi(中)、 hdpi(高)、xhdpi(超高)、xxhdpi (超 超高)和 xxxhdpi(超超超高)。 例如,高密度屏幕的位图应使用 drawable-hdpi/。
反例: 将 144144 的应用图标 PNG 文件放在 drawable-mhdpi 目录
三、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) { try { startActivity(intent); } catch (ActivityNotFoundException e) { if (Config.LOGD) { Log.d(LOGTAG, "activity not found for " + mimeType + " over " + Uri.parse(url). getScheme(), e); } } } }反例:
Intent intent = new Intent(); intent.setAction("com.great.activity_intent.Intent_Demo1_Result3");【强制】 避免在 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, UseHomeActivity.class); this.startActivity(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,如需使用请注意上述问题。嵌套 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,且不要为服务声明IntentFilter, 保证应用的安全性。如果确实需要使用隐式调用,则可为 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(savedInstanceState); 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 } } }反例:
所有广播都使用全局广播
//In activity, sending broadcast Intent intent = new Intent("com.example.broadcastreceiver.SOME_ACTION"); sendBroadcast(intent);- 【推荐】 当前 Activity 的 onPause 方法执行结束后才会执行下一个 Activity 的 onCreate 方法,所以在 onPause 方法中不适合做耗时较长的工作,这会影响到页面之间的跳转效率。
- 【强制】不要在 Android 的 Application 对象中缓存数据。基础组件之间的数据共享,请使用 Intent 等机制,也可使用 SharedPreferences 等数据持久化机制。
反例:
class MyApplication extends Application { String username; String getUsername() { return username; } void setUsername(String username) { this.username = username; } } class SetUsernameActivity extends Activity { void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.set_username); MyApplication app = (MyApplication) getApplication(); app.setUsername("tester1"); startActivity(new Intent(this, GetUsernameActivity.class)); } } class GetUsernameActivity extends Activity { TextView tv; void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.get_username); tv = (TextView)findViewById(R.id.username); } void onResume() { super.onResume(); MyApplication app = (MyApplication) getApplication(); tv.setText("Welcome back ! " + app.getUsername().toUpperCase()); } } - 【推荐】 使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示 Toast 时不能取消上一次 Toast 消息的情况(如果你有连续弹出 Toast 的情况,避免 使用 Toast.makeText)。
- 【强制】
使用 Adapter 的时候,如果你使用了 ViewHolder 做缓存,在 getView()的 方法中无论这项 convertView 的每个子控件是否需要设置属性(比如某个 TextView 设置的文本可能为 null,某个按钮的背景色为透明,某控件的颜色为透明等),都需 要为其显式设置属性(Textview 的文本为空也需要设置 setText(""),背景透明也需要 设置),否则在滑动的过程中,因为 adapter item 复用的原因,会出现内容的显示错乱。
正例:
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder myViews; if (convertView == null) { myViews = new ViewHolder(); convertView = mInflater.inflate(R.layout.list_item, null); myViews.mUsername = (TextView)convertView.findViewById(R.id.username); convertView.setTag(myViews); } else { myViews = (ViewHolder)convertView.getTag(); } Info p = infoList.get(position); String dn = p.getDisplayName; myViews.mUsername.setText(StringUtils.isEmpty(dn) ? "" : dn); return convertView; } static class ViewHolder { private TextView mUsername; } - 【强制】
Activity 或者 Fragment 中动态注册 BroadCastReceiver 时,registerReceiver() 和 unregisterReceiver()要成对出现。
说明:
如果 registerReceiver()和 unregisterReceiver()不成对出现,则可能导致已经注册的 receiver 没有在合适的时机注销,导致内存泄漏,占用内存空间,加重 SystemService 负担。 部分华为的机型会对 receiver 进行资源管控,单个应用注册过多 receiver 会触发管 控模块抛出异常,应用直接崩溃。