Android编码规范

为什么需要编码规范?

编码规范对于程序员而言尤为重要,主要原因有:

  • 一个软件的生命周期中,80%的花费在于维护
  • 几乎没有任何一个软件,在其整个生命周期中,均由最初的开发人员来维护
  • 编码规范可以改善软件的可读性,可以让程序员尽快而彻底地理解新的代码

基本上每个大厂都有自已的Android编码规范,虽然细节上会有些差异,但总体上都差不多,以下为《阿里巴巴Android开发规范》,重温、回炉:

一、Android 资源文件命名与使用

  1. layout 命名方式:模块+下划线+功能 如: module_activity,module_fragment,module_recycle_item。
  2. drawable 命名方式:模块名业务功能描述控件描述_控件状态限定词 如:module_login_btn_pressed
  3. anim 命名方式:模块名逻辑名称[方向|序号] 如:module_fade_in ,module_fade_out
  4. color 命名方式:模块名逻辑名称颜色 如: < color name="module_btn_bg_color" > #33b5e5e5 </ color>
  5. dimen 命名方式:module_dimens.xml 模块名_描述信息 如: < dimen name="module_horizontal_line_height" >1dp</ dimen>
  6. style 命名方式:module_styles.xml 父style 名称点当前 style 名称 如:ParentTheme.ThisActivityTheme
  7. string 命名方式:module_string.xml 模块名_逻辑名称 如:moudule_login_tips
  8. Id 资源原则上以驼峰法命名,View组件的资源 id 需要以 View 的缩写作为前缀
  9. Android为多种屏幕提供不同的资源目录进行适配。为不同屏幕密度提供不同的位图可绘制对象,可用于密度特定资源的配置限定符(在下面详述) 包括:ldpi(低)、mdpi(中)、 hdpi(高)、xhdpi(超高)、xxhdpi(超超高)和 xxxhdpi(超超超高)

二、Android 基本组件

Android 基本组件指Activity 、Fragment 、Service 、BroadcastReceiver 、
ContentProvider 等等。

  1. Activity

Activity间的数据通信,对于数据量比较大的,避免使用 Intent + Parcelable的方式,可以考虑EventBus等替代方案,以免造TransactionTooLargeException。

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_DEFAULONLY) != null) {
        try {
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            if (Config.LOGD) {
                Log.d(LOGTAG, "activity not found for " + mimeType + " over " +
                Uri.parse(url). getScheme(), e);
            }
        }
    }
}
  • Activity#onSaveInstanceState()方法不是Activity生命周期方法,也不保证一定会被调用。它是用来在 Activity 被意外销毁时保存 UI 状态的,只能用于保存临时性数据,例如 UI控件的属性等,不能跟数据的持久化存储混为一谈。持久化存储应该在Activity#onPause()/onStop()中实行。

  • 不要在 Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的销毁和停止,因为 onDestroy()执行的时机可能较晚。可根据实际需要,在Activity#onPause()/onStop()中结合isFinishing()的判断来执行。

  • Activity或者Fragment中动态注册BroadCastReceiver时,registerReceiver()和unregisterReceiver()要成对出现。

  1. Service

避免在 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) { 
            } 
      }
   }
}

总是使用显式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(); 
        } 
    } 
} 

避免在BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作,应该创建IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做。
说明:由于该方法是在主线程执行,如果执行耗时操作会导致 UI 不流畅。

例:

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); 
    } 
};
  1. 广播(BroadcastReceiver)

避免使用隐式 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);

对于只用于应用内的广播,优先使用 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 
        } 
    } 
}

  1. 其他注意
  • 不要在 Android 的 Application 对象中缓存数据。基础组件之间的数据共享请使用Intent 等机制,也可使用 SharedPreferences 等数据持久化机制。
  • 添加 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(); 
    } 
} 

如非必须,避免使用嵌套的 Fragment。
说明:嵌套 Fragment 是在 Android API 17 添加到 SDK 以及 Support 库中的功能,

Fragment 嵌套使用会有一些坑,容易出现 bug,比较常见的问题有如下几种:

  • onActivityResult()方法的处理错乱,内嵌的 Fragment 可能收不到该方法的回调需要由宿主 Fragment进行转发处理;
  • 突变动画效果;
  • 被继承的 setRetainInstance(),导致在 Fragment 重建时多次触发不必要的逻辑。

非必须的场景尽可能避免使用嵌套 Fragment,如需使用请注意上述问题。

例:

FragmentManager fragmentManager = getFragmentManager(); 
Fragment fragment = fragmentManager.findFragmentByTag(FragmentB.TAG); 
if (null == fragment) { 
    FragmentB fragmentB = new FragmentB(); 
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransactio
    fragmentTransaction.add(R.id.fragment_container, fragmentB, 
    FragmentB.TAG).commit(); 
} 

使用 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; 
} 

三、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里的Hierarachy 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); 
}
  • 禁止在非ui线程进行view相关操作。
  • 文本大小使用单位 dp,view 大小使用单位 dp。对于 Textview,如果在文字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问题。
  • 禁止在设计布局时多次设置子 view 和父 view 中为同样的背景造成页面过度绘制,推荐将不需要显示的布局进行及时隐藏。
  • 灵活使用布局,推荐 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。

尽量不要使用 AnimationDrawable,它在初始化的时候就将所有图片加载到内存中,特别占内存,并且还不能释放,释放之后下次进入再次加载时会报错。
**说明: **Android 的帧动画可以使用 AnimationDrawable 实现,但是如果你的帧动画中如果包含过多帧图片,一次性加载所有帧图片所导致的内存消耗会使低端机发生OOM异常。帧动画所使用的图片要注意降低内存消耗,当图片比较大时,容易出现OOM。

不能使用 ScrollView 包裹 ListView/GridView/ExpandableListVIew;因为这样会把 ListView 的所有 Item 都加载到内存中,要消耗巨大的内存和 cpu 去绘制图面。
说明:ScrollView中嵌套 List 或RecyclerView的做法官方明确禁止。除了开发过程中遇到的各种视觉和交互问题,这种做法对性能也有较大损耗。ListView等UI 组件自身有垂直滚动功能,也没有必要在嵌套一层 ScrollView。目前为了较好的 UI 体验,更贴近Material Design 的设计,推荐使用 NestedScrollView。

四、进程、线程与消息通信

不要通过 Intent 在 Android 基础组件之间传递大数据(binder transaction缓存为1MB),可能导致 OOM。

在 Application 的业务初始化代码加入进程判断,确保只在自己需要的进程初始化。特别是后台进程减少不必要的业务初始化。

例:

public class MyApplication extends Application { 
    @Override 
    public void onCreate() { 
        //在所有进程中初始化 
        .... 
        //仅在主进程中初始化 
        if (mainProcess) { 
        ... 
        }  
        //仅在后台进程中初始化 
        if (bgProcess) { 
            ... 
        } 
    } 
} 

新建线程时,必须通过线程池提供(AsyncTask 或者 ThreadPoolExecutor或者其他形式自定义的线程池),不允许在应用中自行显式创建线程。
说明: 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。另外创建匿名线程不便于后续的资源使用分析,对性能分析等会造成困扰。

线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
**说明: **Executors 返回的线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadPool : 允 许 的 请 求 队 列 长 度 为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
  • CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在主线程中调用。
不要在非UI 线程中初始化ViewStub,否则会返回null。

尽量减少不同 APP 之间的进程间通信及拉起行为。拉起导致占用系统资源,影响用户体验。
新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查

ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时线程能被释放。
禁止在多进程之间用 SharedPreferences 共享数据,虽然可以(MODE_MULTI_PROCESS),但官方已不推荐。

谨慎使用 Android 的多进程,多进程虽然能够降低主进程的内存压力,但会遇到如下问题:

  • 不能实现完全退出所有 Activity的功能;
  • 首次进入新启动进程的页面时会有延时的现象(有可能黑屏、白屏几秒,是白屏还是黑屏和新 Activity的主题有关);
  • 应用内多进程时,Application 实例化多次,需要考虑各个模块是否都需要在所有进程中初始化;
  • 多进程间通过 SharedPreferences 共享数据时不稳定。

五、文件与数据库

任何时候不要硬编码文件路径,请使用 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 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.equ
        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 的使用还牵涉多线程场景,那么需要自行保证操作同步。
  • 多线程操作写入数据库时,需要使用事务,以免出现同步问题。Android 的通过SQLiteOpenHelper 获取数据库 SQLiteDatabase 实例,Helper中会自动缓存已经打开的SQLiteDatabase实例,单个App中应使用SQLiteOpenHelper的单例模式确保数据库连接唯一。由于 SQLite 自身是数据库级锁,单个数据库操作是保证线程安全的(不能同时写入),transaction 时一次原子操作,因此处于事务中的操作是线程安全的。若同时打开多个数据库连接,并通过多线程写入数据库,会导致数据库异常,提示数据库已被锁住。
  • 大数据写入数据库时,请使用事务或其他能够提高 I/O 效率的机制,保证执行速度。
  • 执行 SQL 语句时,应使用 SQLiteDatabase#insert()、update()、delete(),不要使用SQLiteDatabase#execSQL(),以免SQL 注入风险。
  • 如果 ContentProvider 管理的数据存储在 SQL 数据库中,应该避免将不受信任的外部数据直接拼接在原始 SQL 语句中,可使用一个用于将 ? 作为可替换参数的选择子句以及一个单独的选择参数数组,会避免 SQL 注入。

六、Bitmap、Drawable与动画

  • 加载大图片或者一次性加载多张图片,应该在异步线程中进行。图片的加载,涉及到IO 操作,以及 CPU密集操作,很可能引起卡顿。
  • 在ListView,ViewPager,RecyclerView,GirdView等组件中使用图片时,应做好图片的缓存,避免始终持有图片导致内存泄露,也避免重复创建图片,引起性 能 问题 。建 议 使用 FrescoGlide 等图片库
  • png图片使用 tinypng或者类似工具压缩处理,减少包体积。
  • 应根据实际展示需要,压缩图片,而不是直接显示原图。手机屏幕比较小,直接显示原图,并不会增加视觉上的收益,但是却会耗费大量宝贵的内存。
  • 使用完毕的图片,应该及时回收,释放宝贵的内存。
  • 针对不同的屏幕密度,提供对应的图片资源,使内存占用和显示效果达到合理的平衡。如果为了节省包体积,可以在不影响 UI 效果的前提下,省略低密度图片。
  • 在 Activity.onPause()或 Activity.onStop()回调中,关闭当前 activity 正在执行的的动画。
  • 在动画或者其他异步任务结束时,应该考虑回调时刻的环境是否还支持业务处理。例如 Activity 的 onStop()函数已经执行,且在该函数中主动释放了资源,此时回调中如果不做判断就会空指针崩溃。
  • 使用 inBitmap 重复利用内存空间,避免重复开辟新内存。
  • 使用ARGB_565 代替ARGB_888,在不怎么降低视觉效果的前提下,减少内存占用。
  • 尽量减少 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()释放相关资源。

七、安全

使用PendingIntent 时,禁止使用空 intent,同时禁止使用隐式 Intent
说明:

  • 使用 PendingIntent 时,使用了空 Intent,会导致恶意用户劫持修改 Intent 的内容。禁止使用一个空 Intent 去构造 PendingIntent,构造PendingIntent 的Intent一定要设置 ComponentName 或者action。
  • PendingIntent 可以让其他APP 中的代码像是运行自己 APP 中。PendingIntent的intent接收方在使用该intent时与发送方有相同的权限。在使用PendingIntent时,PendingIntent 中包装的 intent 如果是隐式的 Intent,容易遭到劫持,导致信息泄露。
  • 禁止使用常量初始化矢量参数构建 IvParameterSpec,建议 IV 通过随机方式产生。 使用固定初始化向量,结果密码文本可预测性会高得多,容易受到字典式攻击。iv的作用主要是用于产生密文的第一个 block,以使最终生成的密文产生差异(明文相同的情况下),使密码攻击变得更为困难,除此之外 iv 并无其它用途。因此 iv 通过随机方式产生是一种十分简便、有效的途径。
  • 将android:allowbackup 属性设置为 false,防止adb backup 导出数据。 在AndroidManifest.xml 文件中为了方便对程序数据的备份和恢复在 Android APIlevel 8 以后增加了 android:allowBackup 属性值。默认情况下这个属性值为true,故当allowBackup 标志值为true 时,即可通过 adb backup 和adb restore 来备份和恢复应用程序数据。
  • 在实现的 HostnameVerifier 子类中,需要使用 verify 函数效验服务器主机名的合法性,否则会导致恶意程序利用中间人攻击绕过主机名效验。在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。如果回调内实现不恰当,默认接受所有域名,则有安全风险。
  • 利用 X509TrustManager 子类中的 checkServerTrusted 函数效验服务器端证书的合法性。在实现的 X509TrustManager 子类中未对服务端的证书做检验,这样会导致不被信任的证书绕过证书效验机制。
  • META-INF 目录中不能包含如.apk,.odex,.so 等敏感文件,该文件夹没有经过签名,容易被恶意替换。
  • Receiver/Provider 不能在毫无权限控制的情况下,将 android:export 设置为true。
  • 数据存储在 Sqlite 或者轻量级存储需要对数据进行加密,取出来的时候进行解密。
  • 阻止webview通过file:schema 方式访问本地敏感数据。
  • 不要广播敏感信息,只能在本应用使用 LocalBroadcast,避免被别的应用收到,或者setPackage 做限制。
  • 不要把敏感信息打印到 log中。
  • 对于内部使用的组件,显示设置组件的"android:exported"属性为false。Android应用使用 Intent 机制在组件之间传递数据,如果应用在使用 getIntent(),getAction(),Intent.getXXXExtra()获取到空数据、异常或者畸形数据时没有进行异常捕获,应用就会发生 Crash,应用不可使用(本地拒绝服务)。恶意应用可通向受害者应用发送此类空数据、异常或者畸形数据从而使应用产生本地拒绝服务。
  • 应用发布前确保 android:debuggable 属性设置为false。
  • 使用Intent Scheme URL 需要做过滤。如果浏览器支持 Intent Scheme Uri 语法,如果过滤不当,那么恶意用户可能通过浏览器 js 代码进行一些恶意行为,比如盗取 cookie 等。如果使用了 Intent.parseUri函 数 , 获 取 的 intent 必须 严格过滤, intent 至少包含addCategory(“android.intent.category.BROWSABLE”) , setComponent(null) ,setSelector(null)3 个策略。
  • 密钥加密存储或者经过变形处理后用于加解密运算,切勿硬编码到代码中。应用程序在加解密时,使用硬编码在程序中的密钥,攻击者通过反编译拿到密钥可以轻易解密APP 通信数据。
  • 将所需要动态加载的文件放置在 apk 内部,或应用私有目录中,如果应用必须要把所加载的文件放置在可被其他应用读写的目录中(比如 sdcard),建议对不可信的加载源进行完整性校验和白名单处理,以保证不被恶意代码注入。
  • 除非min API level >=17,请注意 addJavascriptInterface 的使用,APIlevel>=17,允许js 被调用的函数必须以@JavascriptInterface 进行注解,因此不受影响; 对于 API level < 17,尽量不要使用 addJavascriptInterface,如果一定要用,那么:
  • 使用https 协议加载 URL,使用证书校验,防止访问的页面被篡改挂马;
  • 对加载URL 做白名单过滤、完整性校验等防止访问的页面被篡改;
  • 如果加载本地 html,应该会HTML 内置在APK 中,以及对 HTML 页面进行完整性校验。
  • 使用Android 的AES/DES/DESede 加密算法时,不要使用默认的加密模式ECB,应显示指定使用 CBC或CFB加密模式。 加密模式ECB、CBC、CFB、OFB等,其中 ECB 的安全性较弱,会使相同的铭文在不同的时候产生相同的密文,容易遇到字典攻击,建议使用 CBC或CFB模式。
  • ECB:Electronic codebook,电子密码本模式
  • CBC:Cipher-block chaining,密码分组链接模式
  • CFB:Cipher feedback,密文反馈模式
  • OFB:Output feedback,输出反馈模式
  • 不要使用 loopback 来通信敏感信息。
  • 对于不需要使用 File 协议的应用,禁用 File 协议,显式设置 webView. getSettings().setAllowFileAccess(false),对于需要使用File 协议的应用,禁止File协议调用JavaScript,显式设置webView.getSettings().setJavaScriptEnabled(false)。
  • Android APP 在HTTPS 通信中,验证策略需要改成严格模式。 Android APP 在HTTPS 通信中,使用ALLOW_ALL_HOSTNAME_VERIFIER,表示允许和所有的HOST 建立 SSL 通信,这会存在中间人攻击的风险,最终导致敏感信息可能会被劫持,以及其他形式的攻击。
  • Android5.0 以后安全性要求较高的应用应该使用 window.setFlag (LayoutParam.FLAG_SECURE) 禁止录屏
  • zip 中不建议允许../../file 这样的路径,可能被篡改目录结构,造成攻击。 说明:当 zip 压缩包中允许存在"../"的字符串,攻击者可以利用多个"../"在解压时改变zip 文件存放的位置,当文件已经存在是就会进行覆盖,如果覆盖掉的文件是 so、dex或者odex文件,就有可能造成严重的安全问题。
  • 开放的 activity/service/receiver 等需要对传入的 intent 做合法性校验
  • 加密算法:使用不安全的 Hash 算法(MD5/SHA-1)加密信息,存在被破解的风险,建议使用 SHA-256 等安全性更高的Hash 算法。
  • Android WebView组件加载网页发生证书认证错误时,采用默认的处理方法handler.cancel(),停止加载问题页面。Android WebView组件加载网页发生证书认证错误时,会调用WebViewClient 类的onReceivedSslError 方法,如果该方法实现调用了 handler.proceed()来忽略该证书错误,则会受到中间人攻击的威胁,可能导致隐私泄露.
  • 直接传递命令字或者间接处理有敏感信息或操作时,避免使用 socket 实现,使用能够控制权限校验身份的方式通讯。

八、其他

  • 不要通过Msg传递大的对象,会导致内存问题。
  • 不能使用System.out.println 打印 log。
  • Log的 tag不能是" "。