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 资源命名方式
模块名_业务功能描述_控件描述_控件状态限定词
如:module_login_btn_pressed,module_tabs_icon_home_normal
- anim 资源命名方式
模块名_逻辑名称_[方向|序号]
module_fade_in , module_fade_out , module_push_down_in
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。
基本组件规范
- 讨论Activity 间的数据通信,对于数据量比较大的,避免使用 Intent + Parcelable的方式,可以考虑EventBus等替代方案,以免造成TransactionTooLargeException.
- Activity#onSaveInstanceState()方法不是 Activity 生命周期方法,它是用来在 Activity 被意外销毁时保存 UI 状态的,只能用于保存临时性数据,例如UI控件的属性等,不能跟数据的持久化存储混为一谈。持久化存储应该在 Activity#onPause()/onStop()中实行.
- Activity 间通过隐式 Intent 的跳转,在发出 Intent 之前必须通过 resolveActivity 检查,避免找不到合适的调用组件,造成 ActivityNotFoundException 的异常。
- 避免在 Service#onStartCommand()/onBind()方法中执行耗时操作,如果有需求,应改用 IntentService 或采用其他异步机制完成.
- 避免在 BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作,应该创建 IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做.
- 避免使用隐式 Intent 广播敏感信息,信息可能被其他注册了对应 BroadcastReceiver 的 App 接收,如果广播仅限于应用内,则可以使用 LocalBroadcastManager#sendBroadcast()实现,避免敏感信息外泄和 Intent 拦截的风险.
- 添 加 Fragment 时,确保 FragmentTransaction#commit() 在 Activity#onPostResume()或者 FragmentActivity#onResumeFragments()内调用。不要随意使用 FragmentTransaction#commitAllowingStateLoss()来代替.
- 不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在 Activity#onPause()/onStop() 中结合 isFinishing()的判断来执行.
- 如非必须,避免使用嵌套的 Fragment.
-
总是使用显式 Intent 启动或者绑定 Service,且不要为服务声明 Intent Filter,
保证应用的安全性。如确实需要使用隐式调用,则可为 Service 提供 Intent Filter 并从 Intent 中排除相应的组件名称,必须搭配使用 Intent#setPackage()方法设置 Intent 的指定包名,消除目标服务的不确定性. - Service 需要以多线程来并发处理多个启动请求,建议使用 IntentService,避免复杂的设置。Service 组件一般运行主线程,避免耗时操作,如果有耗时操作应该在 Worker 线程执行,推荐使用 IntentService 执行后台任务.
- 对于只用于应用内的广播,优先使用 LocalBroadcastManager 来进行注册和发送,LocalBroadcastManager 安全性更好,同时拥有更高的运行效率.
- 当前 Activity 的 onPause 方法执行结束后才会创建(onCreate)或恢复(onRestart)别的 Activity,所以在 onPause 方法中不适合做耗时较长的工作,这会影响页面之间的跳转效率.(注:所有耗时任务都不应在主线程进行,在页面跳转的时候涉及到流畅性问题,所以需要格外注意).
- Activity或者Fragment中动态注册BroadCastReceiver时,registerReceiver() 和 unregisterReceiver()要成对(生命周期对应)出现.Activity 的生命周期不对应,可能出现多次 onResume 造成 receiver 注册多个,但最终只注销一个,其余 receiver 产生内存泄漏。
- Android 基础组件如果使用隐式调用,应在 AndroidManifest.xml 中使用 <intent-filter> 或在代码中使用 IntentFilter 增加过滤.
UI 与布局
- 使用 ConstraintLayout,尽量不要嵌套,降低布局层级(ViewStub,merge).
- 在 Activity 中显示对话框或弹出浮层时,尽量使用 DialogFragment,而非 Dialog/AlertDialog,这样便于随Activity生命周期管理对话框/弹出浮层的生命周期.
- 讨论: 文本大小使用单位 dp,View 大小使用单位 dp。对于 TextView,如果在文字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问题。
- 禁止在设计布局时多次为子 View 和父 View 设置同样背景进而造成页面过度绘制,无需显示的布局及时隐藏.
- 在需要时刻刷新某一区域的组件时,建议通过以下方式避免引发全局 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,推荐在 Activity#onAttachedToWindow()/Activity#onWindowFocusChanged() 之后创建对话框.
- 尽量不要使用 AnimationDrawable,它在初始化的时候就将所有图片加载到内存中,特别占内存,并且还不能释放,释放之后下次进入再次加载时会报错.
- 不能使用 ScrollView 包裹 ListView/GridView/ExpandableListVIew;这样会把 ListView 的所有 Item 加载到内存中,会消耗巨大的内存和 cpu 资源,推荐使用 NestedScrollView.
- 不要在 Android 的 Application 对象中缓存数据。
讨论:
1. 基础组件之间的数据共享使用 Intent 等机制
2. 使用 SharedPreferences 等数据持久化机制.
3. 使用kotlin object 单例全局对象.
- 使用 Toast 时,建议定义一个全局的 Toast 对象,这样可以避免连续显示 Toast 时不能取消上一次 Toast 消息的情况。即使需要连续弹出 Toast,也应避免直接调用 Toast#makeText.
- 使用 Adapter 的时候,如果使用 ViewHolder 做缓存,在 getView()方法中无论 convertView 的每个子控件是否需要设置属性(比如某个 TextView 设置的文本为 null,背景色为透明),都需要为其显式设置属性,否则在滑动过程中,因为 adapter item 复用的原因,会出现内容的显示错乱.
进程、线程与消息通信
- 不要通过 Intent 在 Android 基础组件之间传递大数据(binder transaction 缓存为 1MB),可能导致 OOM,TransactionTooLargeException等.
- 在 Application 的业务初始化代码加入进程判断,确保只在自己需要的进程初始化.
- 新建线程时,必须通过线程池提供(AsyncTask,ThreadPoolExecutor,Rxjava,kotlin 协程,或其他形式自定义线程池,不能直接使用Executors),不允许在应用中自行显式创建线程.
- 新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查.
- ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时线程能被释放.
- 禁止在多进程之间用 SharedPreferences 共享数据,虽然可以(MODE_MULTI_PROCESS),但官方已不推荐。
文件与数据库
- 不要硬编码文件路径,使用 Android 文件系统 API 访问
android.os.Environment#getExternalStorageDirectory()
android.os.Environment#getExternalStoragePublicDirectory()
android.content.Context#getFilesDir()
android.content.Context#getCacheDir
- 当使用外部存储时,必须检查外部存储的可用性
// 读/写检查
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(),commit 会直接读写磁盘,频繁操作性能不好.
- 数据库 Cursor 必须确保使用完后关闭,以免内存泄漏.
- 多线程操作写入数据库时,需要使用事务,以免出现同步问题.
- 大数据写入数据库时,使用事务或其他能够提高 I/O 效率的机制,保证执行速度.
- 执行 SQL 语句时,应使用 SQLiteDatabase#insert()、update()、delete(),不要使用 SQLiteDatabase#execSQL(),以免 SQL 注入风险.
Bitmap、Drawable 与动画
- 在 ListView,ViewPager,RecyclerView,GirdView 等组件中使用图片时,应做好图片的缓存,避免始终持有图片导致内存溢出,也避免重复创建图片,引起性能问题。建议 使 用 Fresco( https://github.com/facebook/fresco )、 Glide ( https://github.com/bumptech/glide )等图片库.
- png 图片使用 TinyPNG 或者类似工具压缩处理,减少包体积.
- 应根据实际展示需要,压缩图片,而不是直接显示原图.(Glide)
- 使用完毕的图片,应该及时回收,释放宝贵的内存.(Glide)
- 在 #onPause()或 #onStop()回调中,关闭当前正在执行的的动画.
- 在动画或者其他异步任务结束时,应该考虑回调时刻的环境是否还支持业务处理。例如 Activity 的 onStop()函数已经执行,且在该函数中主动释放了资源,此时回调中如果不做判断就会空指针崩溃(Kotlin ?. !!.)
- 使用 inBitmap 重复利用内存空间,避免重复开辟新内存(Glide 待确认)
public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int
reqHeight, ImageCache cache) {
final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...
// 如果在 Honeycomb 或更新版本系统中运行,尝试使用 inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}
private static void addInBitmapOptions(BitmapFactory.Options options,
ImageCache cache) {
// inBitmap 只处理可变的位图,所以强制返回可变的位图
options.inMutable = true;
if (cache != null) {
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
options.inBitmap = inBitmap;
}
}
}
- 使用 RGB_565 代替 RGB_888,在不怎么降低视觉效果的前提下,减少内存占用.(Glide).
- 尽量减少 Bitmap(BitmapDrawable)的使用,尽量使用纯色(ColorDrawable)、渐变色(GradientDrawable)、StateSelector(StateListDrawable)等与 Shape 结合的形式构建绘图.
- 谨慎使用 gif 图片,注意限制每个页面允许同时播放的 gif 图片,以及单个 gif 图片的大小.
- 非首次加载必须的大图片资源不要直接打包到 apk,可以考虑通过文件仓库远程下载,减小包体积.
- 在有强依赖 onAnimationEnd 回调的交互时,如动画播放完毕才能操作页面 , onAnimationEnd 可能会因各种异常没被回调 (参考:https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-called-onanimationstart-works-fine ),建议加上超时保护或通过 postDelay 替代 onAnimationEnd.
- View Animation 执行结束时,调用 View.clearAnimation()释放相关资源.
安全
- 将 android:allowbackup 属性必须设置为 false,阻止应用数据被导出.
<application
android:allowBackup="false">
- 如果使用自定义 HostnameVerifier 实现类,必须在 verify()方法中校验服务器主机名的合法性,否则可能受到中间人攻击.
- 如果使用自定义 X509TrustManager 实现类,必须在 checkServerTrusted() 方法中校验服务端证书的合法性,否则可能受到中间人攻击.
- 在 SDK 支持的情况下,Android 应用必须使用 V2 签名,这将对 APK 文件的修改做更多的保护(默认已开启,不能手动去关闭)
- 所有的 Android 基本组件(Activity、Service、BroadcastReceiver、阿里巴巴 Android 开发手册ContentProvider 等)都不应在没有严格权限控制的情况下,将 android:exported 设置为 true.
- WebView 应设置
WebView#getSettings()#setAllowFileAccess(false)、WebView#getSettings()#setAllowFileAccessFromFileURLs(false)、WebView#getSettings()#setAllowUniversalAccessFromFileURLs(false)
,阻止 filescheme URL 的访问.
- 不要把敏感信息打印到 log 中.
- 确保应用发布版本的 android:debuggable 属性设置为 false.
- 本地加密秘钥不能硬编码在代码中,更不能使用 SharedPreferences 等本地持久化机制存储。应选择 Android 自身的秘钥库(KeyStore)机制或者其他安全性更高的安全解决方案保存。
- 使用 Android 的 AES/DES/DESede 加密算法时,不要使用 ECB 加密模式,应使用 CBC 或 CFB 加密模式.
- Android APP 在 HTTPS 通信中,验证策略需要改成严格模式
SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
- zip 中不要包含 ../../file 这样的路径,可能被篡改目录结构,造成攻击。
//对重要的 Zip 压缩包文件进行数字签名校验,校验通过才进行解压
String entryName = entry.getName();
if (entryName.contains("..")){
throw new Exception("unsecurity zipfile!");
}
- MD5 和 SHA-1、SHA-256 等常用算法是 Hash 算法,有一定的安全性,但不能代替加密算法。敏感信息的存储和传输,需要使用专业的加密机制.
其他
- 不能使用 System.out.println 打印 log.
- Log 的 tag 不能是" ".
网友评论