Android开发手册
-
【强制】Activity间数据通信,数据量比较大的,避免使用Intent + Parcelable的方式,可以考虑EventBus等替代,一面造成TransactionTooLargeException.
-
【推荐】Activity中onSaveInstanceState()方法不是activity生命周期方法,也不保证一定会被调用。主要用于activity被意外销毁时保存UI的状态的,只能用于保存临时性数据,例如UI控件属性,不能跟数据的持久化存储混淆;
持久化存储应该再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 秒钟,可
能会被系统杀死。
-
【强制】避免使用隐式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()的判断来执行。
-
【强制】Activity 或者Fragment 中动态注册BroadCastReceiver 时,registerReceiver()
和unregisterReceiver()要成对出现。
部分华为的机型会对receiver 进行资源管控,单个应用注册过多receiver 会触发管
控模块抛出异常,应用直接崩溃。
-
【强制】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 线程进行View 相关操作。
-
【推荐】文本大小使用单位dp,View 大小使用单位dp。对于TextView,如果在文
字大小确定的情况下推荐使用wrap_content 布局避免出现文字显示不全的适配问
题。
文字用dp重点位置
说明:
之所以文本大小也推荐使用dp 而非sp,因为sp 是Android 早期推荐使用的,但其
实sp 不仅和dp 一样受屏幕密度的影响,还受到系统设置里字体大小的影响,所以
使用dp 对于应用开发会更加保证UI 的一致性和还原度。
-
【推荐】灵活使用布局,推荐merge、ViewStub 来优化布局,尽可能多的减少UI
布局层级,推荐使用FrameLayout,LinearLayout、RelativeLayout 次之。
此处重点需要复习
-
【推荐】在需要时刻刷新某一区域的组件时,建议通过以下方式避免引发全局layout
刷新:
-
设置固定的View 大小的宽高,如倒计时组件等;
-
调用View 的layout 方法修改位置,如弹幕组件等;
-
通过修改Canvas 位置并且调用invalidate(int l, int t, int r, int b)等方式限定刷新
区域;
- 通过设置一个是否允许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() 之后( 其实最好是
Activity#onWindowFocusChanged() 之后)才创建对话框。
以下两个方法需要进一步研究
onAttachedToWindow
onWindowFocusChanged
-
【强制】不能使用ScrollView 包裹ListView/GridView/ExpandableListVIew;因为这
样会把ListView 的所有Item 都加载到内存中,要消耗巨大的内存和cpu 去绘制图
面。
重点
说明:
ScrollView 中嵌套List 或RecyclerView 的做法官方明确禁止。除了开发过程中遇到
的各种视觉和交互问题,这种做法对性能也有较大损耗。ListView 等UI 组件自身有
垂直滚动功能,也没有必要在嵌套一层ScrollView。目前为了较好的UI 体验,更贴
近Material Design 的设计,推荐使用NestedScrollView。
-
【强制】不要在Android 的Application 对象中缓存数据。基础组件之间的数据共享
请使用Intent 等机制,也可使用SharedPreferences 等数据持久化机制。
-
【强制】使用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;
}
-
【强制】不要通过Intent 在Android 基础组件之间传递大数据(binder transaction
缓存为1MB),可能导致OOM。
-
【强制】在Application 的业务初始化代码加入进程判断,确保只在自己需要的进程
初始化。特别是后台进程减少不必要的业务初始化。
如果多个进程application中会初始化多次,所以需要进行进程的过滤判断
//正例: public class MyApplication extends Application { @Override public void onCreate() { //在所有进程中初始化 .... //仅在主进程中初始化 if (mainProcess) { ... } //仅在后台进程中初始化 if (bgProcess) { ... } } }
-
【强制】新建线程时,必须通过线程池提供(AsyncTask 或者ThreadPoolExecutor
或者其他形式自定义的线程池),不允许在应用中自行显式创建线程。
说明:
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解
决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致
消耗完内存或者“过度切换”的问题。另外创建匿名线程不便于后续的资源使用分析,
对性能分析等会造成困扰。
-
【强制】线程池不允许使用Executors 去创建,而是通过ThreadPoolExecutor 的方
式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:
Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和SingleThreadPool : 允许的请求队列长度为
阿里巴巴Android 开发手册
- 33 -
Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
- CachedThreadPool 和ScheduledThreadPool : 允许的创建线程数量为
Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
-
【强制】子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在
主线程中调用。
-
【推荐】禁止在多进程之间用SharedPreferences 共享数据, 虽然可以
(MODE_MULTI_PROCESS),但官方已不推荐。
-
【推荐】谨慎使用Android 的多进程,多进程虽然能够降低主进程的内存压力,但
会遇到如下问题:
- 首次进入新启动进程的页面时会有延时的现象(有可能黑屏、白屏几秒,是白
屏还是黑屏和新Activity 的主题有关);
- 应用内多进程时,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。
此处需要学习
//正例: <!-- AndroidManifest.xml --> <manifest> ... <application> ... <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> </provider> ... </application> </manifest> <!-- res/xml/provider_paths.xml --> <paths> <files-path path="album/" name="myimages" /> </paths> void getAlbumImage(String imagePath) { File image = new File(imagePath); Intent getAlbumImageIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Uri imageUri = FileProvider.getUriForFile(this,"com.example.provider",image); getAlbumImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); startActivityForResult(takePhotoIntent, REQUEST_GET_ALBUMIMAGE); } //反例: void getAlbumImage(String imagePath) { File image = new File(imagePath); Intent getAlbumImageIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); //不要使用file://的URI 分享文件给别的应用,包括但不限于Intent getAlbumImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(image)); startActivityForResult(takePhotoIntent, REQUEST_GET_ALBUMIMAGE); }
-
【推荐】SharedPreference 中只能存储简单数据类型(int、boolean、String 等),
复杂数据类型建议使用文件、数据库等其他方式存储。
-
【推荐】SharedPreference 提交数据时, 尽量使用Editor#apply() , 而非
Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使
用Editor#commit()。
说明:
SharedPreference 相关修改使用apply 方法进行提交会先写入内存,然后异步写入
磁盘, commit 方法是直接写入磁盘。如果频繁操作的话apply 的性能会优于commit,
apply 会将最后修改内容写入磁盘。但是如果希望立刻获取存储操作的结果,并据此
做相应的其他操作,应当使用commit。
-
【强制】数据库Cursor 必须确保使用完后关闭,以免内存泄漏。
-
【强制】禁止使用常量初始化矢量参数构建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,防止数据泄露。
-
【强制】在SDK 支持的情况下,Android 应用必须使用V2 签名,这将对APK 文
件的修改做更多的保护。
-
【强制】所有的 Android 基本组件(Activity、Service、BroadcastReceiver、
阿里巴巴Android 开发手册
- 57 -
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 通信中,验证策略需要改成严格模式。