Alibaba Android 开发手册总结

Android 资源文件命名与使用

  1. 【推荐】资源文件需带模块前缀。

  2. 【推荐】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 开头

  3. 【推荐】drawable 资源名称以小写单词+下划线的方式命名,根据分辨率不同存放在
    不同的drawable 目录下,如果介意包大小建议只使用一套,系统去进行缩放。采用
    规则如下:
    模块名_业务功能描述_控件描述_控件状态限定词
    如:module_login_btn_pressed,module_tabs_icon_home_normal

  4. 【推荐】anim 资源名称以小写单词+下划线的方式命名,采用以下规则:
    模块名_逻辑名称_[方向|序号]
    Tween 动画(使用简单图像变换的动画,例如缩放、平移)资源:尽可能以通用的
    动画名称命名,如module_fade_in , module_fade_out, module_push_down_in (动 画+方向)
    Frame 动画(按帧顺序播放图像的动画)资源:尽可能以模块+功能命名+序号。如
    module_loading_grey_001

  5. 【推荐】color 资源使用#AARRGGBB 格式,写入module_colors.xml 文件中,命名
    格式采用以下规则:
    模块名_逻辑名称_颜色

如:
<color name="module_btn_bg_color">#33b5e5e5</color>

  1. 【推荐】dimen 资源以小写单词+下划线方式命名,写入module_dimens.xml 文件中,
    采用以下规则:
    模块名_描述信息
    如:
    <dimen name="module_horizontal_line_height">1dp</dimen>

  2. 【推荐】style 资源采用父style 名称.当前style 名称方式命名,写入
    module_styles.xml 文件中,首字母大写。如:
    <style name="ParentTheme.ThisActivityTheme"> … </style>

  3. 【推荐】string资源文件或者文本用到字符需要全部写入module_strings.xml 文件中,
    字符串以小写单词+下划线的方式命名,采用以下规则:
    模块名_逻辑名称
    如:moudule_login_tips,module_homepage_notice_desc

  4. 【推荐】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。

10.【推荐】图片根据其分辨率,放在不同屏幕密度的drawable 目录下管理,否则可能
在低密度设备上导致内存占用增加,又可能在高密度设备上导致图片显示不够清晰。
说明:
为了支持多种屏幕尺寸和密度,Android 提供了多种通用屏幕密度来适配。常用的
如下。
ldpi - 120dpi
mdpi - 160dpi
hdpi - 240dpi
xhdpi - 320dpi
xxhdpi - 480dpi
xxxhdpi - 640dpi

Android 的屏幕分辨率和密度并不存在严格的对应关系,应尽量避免直接基于分辨
率来开发,而是通过适配不同的屏幕密度来保证控件和图片的显示效果。不同密度
drawable 目录中的图片分辨率设置,参考不同密度的dpi 比例关系。
正例:
为显示某个图标,将48 x 48 的图标文件放在drawable-mdpi 目录(160dpi)下;
将72 x 72 的图标文件放在drawable-hdpi 目录(240dpi)下;将96 x 96 的图标
文件放在drawable-xhdpi 目录(320dpi)下;将144 x 144 的图标文件放在
drawable-xxhdpi 目录(480dpi)下。
反例:
上述图标,只有一个144 x 144 的图标文件放在drawable 目录下。

Android 基本组件

Android 基本组件指ActivityFragmentServiceBroadcastReceiver
ContentProvider 等等。

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

  2. 【推荐】Activity#onSaveInstanceState()方法不是Activity 生命周期方法,也不保证
    一定会被调用。它是用来在Activity 被意外销毁时保存UI 状态的,只能用于保存临
    时性数据,例如UI 控件的属性等,不能跟数据的持久化存储混为一谈。持久化存储
    应该在Activity#onPause()/onStop()中实行。

  3. 【强制】Activity 间通过隐式Intent 的跳转,在发出Intent 之前必须通过resolveActivity
    检查,避免找不到合适的调用组件,造成ActivityNotFoundException 的异常。
    正例:

public void viewUrl(String action, String url, String mimeType) {
    Intent intent = new Intent(!TextUtils.isEmpty(action) ? action : Intent.ACTION_VIEW);
    if (!TextUtils.isEmpty(url) && !TextUtils.isEmpty(mimeType)) {
        intent.setDataAndType(Uri.parse(url), mimeType);
    } else if (!TextUtils.isEmpty(url)) {
        intent.setData(Uri.parse(url));
    } else if (!TextUtils.isEmpty(mimeType)) {
        intent.setType(mimeType);
    }
    if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
        startActivity(intent);
    } else {
        // 找不到指定的 Activity
        Toast.makeText(this, "找不到指定的Activity", Toast.LENGTH_SHORT).show();
    }
}

反例:

Intent intent = new Intent();
intent.setAction("com.example.DemoIntent ");
try {
    startActivity(intent);
} catch (ActivityNotFoundException e) {
    e.printStackTrace();
}
  1. 【强制】避免在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) {
            }
        }
    }
}
  1. 【强制】避免在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, UserHomeService.class);
        this.startService(userHomeIntent);
    }
};

反例

mBroadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        MyDatabaseHelper myDB = new MyDatabaseHelper(context);
        myDB.initData();
        // have more database operation here
    }
};
  1. 【强制】避免使用隐式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);
            }
        }
    }
}
  1. 【推荐】添加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();
    }
}
  1. 【推荐】不要在Activity#onDestroy()内执行释放资源的工作,例如一些工作线程的
    销毁和停止,因为onDestroy()执行的时机可能较晚。可根据实际需要,在
    Activity#onPause()/onStop()中结合isFinishing()的判断来执行。

  2. 【推荐】如非必须,避免使用嵌套的Fragment。
    说明:
    嵌套Fragment 是在Android API 17添加到SDK以及Support 库中的功能,Fragment
    嵌套使用会有一些坑,容易出现bug,比较常见的问题有如下几种:

  1. onActivityResult()方法的处理错乱,内嵌的Fragment 可能收不到该方法的回调,
    需要由宿主Fragment 进行转发处理;
  2. 突变动画效果;
  3. 被继承的setRetainInstance(),导致在Fragment 重建时多次触发不必要的逻
    辑。
    非必须的场景尽可能避免使用嵌套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();
  1. 【推荐】总是使用显式Intent 启动或者绑定Service,且不要为服务声明Intent Filter,
    保证应用的安全性。如果确实需要使用隐式调用,则可为Service 提供Intent Filter
    并从Intent 中排除相应的组件名称,但必须搭配使用Intent#setPackage()方法设置
    Intent 的指定包名,这样可以充分消除目标服务的不确定性。

11.【推荐】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();
        ...
    }
}

12.【推荐】对于只用于应用内的广播,优先使用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(savedInstsanceState);
        context = this;
        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);
  1. 【推荐】当前Activity 的onPause 方法执行结束后才会创建(onCreate)或恢复
    (onRestart)别的Activity,所以在onPause 方法中不适合做耗时较长的工作,这
    会影响到页面之间的跳转效率。

  2. 【强制】Activity 或者Fragment 中动态注册BroadCastReceiver 时,registerReceiver()
    unregisterReceiver()要成对出现。
    说明:
    如果registerReceiver()unregisterReceiver()不成对出现,则可能导致已经注册的
    receiver 没有在合适的时机注销,导致内存泄漏,占用内存空间,加重SystemService
    负担。
    部分华为的机型会对receiver 进行资源管控,单个应用注册过多receiver 会触发管
    控模块抛出异常,应用直接崩溃。
    正例:

public class MainActivity extends AppCompatActivity {
    private static MyReceiver myReceiver = new MyReceiver();
    ...
    @Override
    protected void onResume() {
        super.onResume();
        IntentFilter filter = new IntentFilter("com.example.myservice");
        registerReceiver(myReceiver, filter);
    }
    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver(myReceiver);
    }
    ...
}

反例:

public class MainActivity extends AppCompatActivity {
    private static MyReceiver myReceiver;
    @Override
    protected void onResume() {
        super.onResume();
        myReceiver = new MyReceiver();
        IntentFilter filter = new IntentFilter("com.example.myservice");
        registerReceiver(myReceiver, filter);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(myReceiver);
    }
}

Activity 的生命周期不对应,可能出现多次onResume 造成receiver 注册多个,但
最终只注销一个,其余receiver 产生内存泄漏。

15.【强制】Android 基础组件如果使用隐式调用,应在 AndroidManifest.xml 中使用
<intent-filter> 或在代码中使用 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 与布局

  1. 【强制】布局中不得不使用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
    里的Hierarchy Viewer 工具,可视化的查看所有的view。

  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);
}
  1. 【推荐】源文件统一采用UTF-8 的形式进行编码。

  2. 【强制】禁止在非UI 线程进行View 相关操作。

  3. 【推荐】文本大小使用单位dp,View 大小使用单位dp。对于TextView,如果在文
    字大小确定的情况下推荐使用wrap_content 布局避免出现文字显示不全的适配问
    题。
    说明:
    之所以文本大小也推荐使用dp 而非sp,因为sp 是Android 早期推荐使用的,但其
    实sp 不仅和dp 一样受屏幕密度的影响,还受到系统设置里字体大小的影响,所以
    使用dp 对于应用开发会更加保证UI 的一致性和还原度。

  4. 【强制】禁止在设计布局时多次为子View 和父View 设置同样背景进而造成页面过
    度绘制,推荐将不需要显示的布局进行及时隐藏。
    正例:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />
    <Button
        android:id="@+id/btn_mybuttom"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="click it !" />
    <ImageView
        android:id="@+id/img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:src="@drawable/youtube" />
    <TextView
        android:text="it is an example!"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

反例:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int width = getWidth();
    int height = getHeight();
    mPaint.setColor(Color.GRAY);
    canvas.drawRect(0, 0, width, height, mPaint);
    mPaint.setColor(Color.CYAN);
    canvas.drawRect(0, height/4, width, height, mPaint);
    mPaint.setColor(Color.DKGRAY);
    canvas.drawRect(0, height/3, width, height, mPaint);
    mPaint.setColor(Color.LTGRAY);
    canvas.drawRect(0, height/2, width, height, mPaint);
}
  1. 【推荐】灵活使用布局,推荐mergeViewStub 来优化布局,尽可能多的减少UI
    布局层级,推荐使用FrameLayoutLinearLayoutRelativeLayout 次之。

  2. 【推荐】在需要时刻刷新某一区域的组件时,建议通过以下方式避免引发全局layout
    刷新:

  1. 设置固定的View 大小的宽高,如倒计时组件等;
  2. 调用View 的layout 方法修改位置,如弹幕组件等;
  3. 通过修改Canvas 位置并且调用invalidate(int l, int t, int r, int b)等方式限定刷新
    区域;
  4. 通过设置一个是否允许requestLayout 的变量,然后重写控件的requestlayout、
    onSizeChanged 方法, 判断控件的大小没有改变的情况下, 当进入
    requestLayout 的时候,直接返回而不调用super 的requestLayout 方法。
  1. 【推荐】不能在Activity 没有完全显示时显示PopupWindow 和Dialog。
    说明:
    Android Activity 创建时的生命周期,按照onCreate() -> onStart() -> onResume() -> onAttachedToWindow() -> onWindowFocusChanged() 的顺序, 其中在
    Activity#onAttachedToWindow() 时,Activity 会与它的 Window 关联,这时 UI 才
    会开始绘制,在 Activity#onWindowFocusChanged() 时,UI 才变成可交互状态,
    可以提示用户使用。如果在 Window 未关联时就创建对话框,UI 可能显示异常。
    推荐的做法是在 Activity#onAttachedToWindow() 之后( 其实最好是
    Activity#onWindowFocusChanged() 之后)才创建对话框。

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

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" 
    android:oneshot ="true">
    <item android:duration="500" android:drawable="@drawable/ic_heart_100"/>
    <item android:duration="500" android:drawable="@drawable/ic_heart_75"/>
    <item android:duration="500" android:drawable="@drawable/ic_heart_50"/>
    <item android:duration="500" android:drawable="@drawable/ic_heart_25"/>
    <item android:duration="500" android:drawable="@drawable/ic_heart_0"/>
</animation-list>

反例:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android" 
    android:oneshot ="false">
    <item android:drawable="@drawable/soundwave_new_1_40" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_41" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_42" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_43" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_44" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_45" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_46" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_47" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_48" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_49" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_50" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_51" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_52" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_53" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_54" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_55" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_56" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_57" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_58" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_59" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_60" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_61" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_62" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_63" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_64" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_65" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_66" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_67" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_68" android:duration="100" />
    <item android:drawable="@drawable/soundwave_new_1_69" android:duration="100" />
</animation-list>

上述如此多图片的动画就不建议使用AnimationDrawable 了。

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout>
    <android.support.v4.widget.NestedScrollView>
        <LinearLayout>

            <ImageView/>
            ...

            <android.support.v7.widget.RecyclerView/>
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>
</LinearLayout>

反例:

<ScrollView>
    <LinearLayout>
        <TextView/>
        ...
        <ListView/>
        <TextView />
    </LinearLayout>
</ScrollView>

12.【强制】不要在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;

    @Override
    void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.get_username);
        tv = (TextView) findViewById(R.id.username);
    }

    @Override
    void onResume() {
        super.onResume();
        MyApplication app = (MyApplication) getApplication();
        tv.setText("Welcome back ! " + app.getUsername().toUpperCase());
    }
}

13.【推荐】使用Toast 时,建议定义一个全局的Toast 对象,这样可以避免连续显示
Toast 时不能取消上一次Toast 消息的情况。即使需要连续弹出Toast,也应避免直
接调用Toast#makeText。

14.【强制】使用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;
}

进程、线程与消息通信

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

  2. 【强制】在Application 的业务初始化代码加入进程判断,确保只在自己需要的进程
    初始化。特别是后台进程减少不必要的业务初始化。
    正例:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        //在所有进程中初始化
        ....
        //仅在主进程中初始化
        if (mainProcess) {
            ...
        }
        //仅在后台进程中初始化
        if (bgProcess) {
            ...
        }
    }
}
  1. 【强制】新建线程时,必须通过线程池提供(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() {
    ...
});

反例:

new Thread(new Runnable() {
    @Override
    public void run() {
        //操作语句
        ...
    }
}).start();
  1. 【强制】线程池不允许使用Executors 去创建,而是通过ThreadPoolExecutor 的方
    式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
    说明:
    Executors 返回的线程池对象的弊端如下:
  1. FixedThreadPool 和SingleThreadPool : 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
  2. CachedThreadPool 和ScheduledThreadPool : 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
    正例:
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 cachedThreadPool = Executors.newCachedThreadPool();
  1. 【强制】子线程中不能更新界面,更新界面必须在主线程中进行,网络操作不能在
    主线程中调用。

  2. 【推荐】尽量减少不同APP 之间的进程间通信及拉起行为。拉起导致占用系统资源,
    影响用户体验。

  3. 【推荐】新建线程时,定义能识别自己业务的线程名称,便于性能优化和问题排查。
    正例:

public class MyThread extends Thread {
    public MyThread(){
        super.setName("ThreadName");
        …
    }
}
  1. 【推荐】ThreadPoolExecutor 设置线程存活时间(setKeepAliveTime),确保空闲时
    线程能被释放。

  2. 【推荐】禁止在多进程之间用SharedPreferences 共享数据, 虽然可以
    (MODE_MULTI_PROCESS),但官方已不推荐。

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

  1. 首次进入新启动进程的页面时会有延时的现象(有可能黑屏、白屏几秒,是白
    屏还是黑屏和新Activity 的主题有关);
  2. 应用内多进程时,Application 实例化多次,需要考虑各个模块是否都需要在所
    有进程中初始化。

文件与数据库

  1. 【强制】任何时候不要硬编码文件路径,请使用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;
}
  1. 【强制】当使用外部存储时,必须检查外部存储的可用性。
    正例:
// 读/写检查
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;
}
  1. 【强制】应用间共享文件时,不要通过放宽文件系统权限的方式去实现,而应使用
    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);
}
  1. 【推荐】SharedPreference 中只能存储简单数据类型(int、boolean、String 等),
    复杂数据类型建议使用文件、数据库等其他方式存储。
    正例:
public void updateSettings() {
    SharedPreferences mySharedPreferences = getSharedPreferences("settings",Activity.MODE_PRIVATE);
    SharedPreferences.Editor editor = mySharedPreferences.edit();
    editor.putString("id", "foo");
    editor.putString("nick", "bar");
    //不要把复杂数据类型转成String 存储
    editor.apply();
}
  1. 【推荐】SharedPreference 提交数据时, 尽量使用Editor#apply() , 而非
    Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使
    用Editor#commit()。
    说明:
    SharedPreference 相关修改使用apply 方法进行提交会先写入内存,然后异步写入
    磁盘, commit 方法是直接写入磁盘。如果频繁操作的话apply 的性能会优于commit,
    apply 会将最后修改内容写入磁盘。但是如果希望立刻获取存储操作的结果,并据此
    做相应的其他操作,应当使用commit。
    正例:
public void updateSettingsAsync() {
    SharedPreferences mySharedPreferences = getSharedPreferences("settings",
            Activity.MODE_PRIVATE);
    SharedPreferences.Editor editor = mySharedPreferences.edit();
    editor.putString("id", "foo");
    editor.apply();
}

public void updateSettings() {
    SharedPreferences mySharedPreferences = getSharedPreferences("settings",
            Activity.MODE_PRIVATE);
    SharedPreferences.Editor editor = mySharedPreferences.edit();
    editor.putString("id", "foo");
    if (!editor.commit()) {
        Log.e(LOG_TAG, "Failed to commit setting changes");
    }
}

反例:

editor.putLong("key_name", "long value");
editor.commit();
  1. 【强制】数据库Cursor 必须确保使用完后关闭,以免内存泄漏。
    说明:
    Cursor 是对数据库查询结果集管理的一个类,当查询的结果集较小时,消耗内存不
    易察觉。但是当结果集较大,长时间重复操作会导致内存消耗过大,需要开发者在
    操作完成后手动关闭Cursor。
    数据库Cursor 在创建及使用时,可能发生各种异常,无论程序是否正常结束,必须
    在最后确保Cursor 正确关闭,以避免内存泄漏。同时,如果Cursor 的使用还牵涉
    多线程场景,那么需要自行保证操作同步。
    正例:
public void handlePhotos(SQLiteDatabase db, String userId) {
    Cursor cursor;
    try {
        cursor = db.query(TUserPhoto, new String[]{"userId", "content"}, "userId=?", new
                String[]{userId}, null, null, null);
        while (cursor.moveToNext()) {
            // TODO
        }
    } catch (Exception e) {
        // TODO
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
}

反例:

public void handlePhotos(SQLiteDatabase db, String userId) {
    Cursor cursor = db.query(TUserPhoto, new String[] { "userId", "content" }, "userId=?", new String[] { userId }, null, null, null);
    while (cursor.moveToNext()) {
        // TODO
    }
    // 不能放任cursor 不关闭
}
  1. 【强制】多线程操作写入数据库时,需要使用事务,以免出现同步问题。
    说明:
    通过SQLiteOpenHelper 获取数据库SQLiteDatabase 实例,Helper 中会自动缓存已经打开的SQLiteDatabase 实例,单个App 中应使用SQLiteOpenHelper 的单例模式确保数据库连接唯一。由于SQLite 自身是数据库级锁,单个数据库操作是保证线程安全的(不能同时写入),transaction 是一次原子操作,因此处于事务中的操作是线程安全的。
    若同时打开多个数据库连接,并通过多线程写入数据库,会导致数据库异常,提示数据库已被锁住。
    正例:
public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
    ContentValues cv = new ContentValues();
    cv.put("userId", userId);
    cv.put("content", content);
    db.beginTransaction();
    try {
        db.insert(TUserPhoto, null, cv);
        // 其他操作
        db.setTransactionSuccessful();
    } catch (Exception e) {
        // TODO
    } finally {
        db.endTransaction();
    }
}

反例:

public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
    ContentValues cv = new ContentValues();
    cv.put("userId", userId);
    cv.put("content", content);
    db.insert(TUserPhoto, null, cv);
}
  1. 【推荐】大数据写入数据库时,请使用事务或其他能够提高I/O 效率的机制,保证执
    行速度。
    正例:
public void insertBulk(SQLiteDatabase db, ArrayList<UserInfo> users) {
    db.beginTransaction();
    try {
        for (int i = 0; i < users.size; i++) {
            ContentValues cv = new ContentValues();
            cv.put("userId", users[i].userId);
            cv.put("content", users[i].content);
            db.insert(TUserPhoto, null, cv);
        }
// 其他操作
        db.setTransactionSuccessful();
    } catch (Exception e) {
// TODO
    } finally {
        db.endTransaction();
    }
}
  1. 【强制】执行SQL 语句时,应使用SQLiteDatabase#insert()、update()、delete(),
    不要使用SQLiteDatabase#execSQL(),以免SQL 注入风险。
    正例:
public int updateUserPhoto(SQLiteDatabase db, String userId, String content) {
    ContentValues cv = new ContentValues();
    cv.put("content", content);
    String[] args = {String.valueOf(userId)};
    return db.update(TUserPhoto, cv, "userId=?", args);
}

反例:

public void updateUserPhoto(SQLiteDatabase db, String userId, String content) {
    String sqlStmt = String.format("UPDATE %s SET content=%s WHERE userId=%s",TUserPhoto, userId, content);
//请提高安全意识,不要直接执行字符串作为SQL 语句
    db.execSQL(sqlStmt);
}

10.【强制】如果ContentProvider 管理的数据存储在SQL 数据库中,应该避免将不受
信任的外部数据直接拼接在原始SQL 语句中。
正例:

// 使用一个可替换参数
String mSelectionClause = "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;

反例:

// 拼接用户输入内容和列名
String mSelectionClause = "var = " + mUserInput;

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;
    }
    ...
}

反例:

Button btnLoadImage = (Button) findViewById(R.id.btn);
btnLoadImage.setOnClickListener(new OnClickListener(){
    public void onClick(View v) {
        Bitmap bitmap = BitmapFactory.decodeFile("some path");
    }
});
  1. 【强制】在ListView,ViewPager,RecyclerView,GirdView 等组件中使用图片时,
    应做好图片的缓存,避免始终持有图片导致内存溢出,也避免重复创建图片,引起
    性能问题。建议使用Fresco ( https://github.com/facebook/fresco )、Glide
    https://github.com/bumptech/glide)等图片库。
    正例:
    例如使用系统LruCache 缓存,参考:
    https://developer.android.com/topic/performance/graphics/cache-bitmap.html
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // 获取可用内存的最大值,使用内存超出这个值将抛出OutOfMemory 异常。LruCache 通
    过构造函数传入缓存值,以KB 为单位。
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    // 把最大可用内存的1/8 作为缓存空间
    final int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}
public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);
    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 在后台进行图片解码
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(),
                params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

反例:
没有存储,每次都需要解码,或者有缓存但是没有合适的淘汰机制,导致缓存效果
很差,依然经常需要重新解码。

  1. 【强制】png 图片使用TinyPNG 或者类似工具压缩处理,减少包体积。

  2. 【推荐】应根据实际展示需要,压缩图片,而不是直接显示原图。手机屏幕比较小,
    直接显示原图,并不会增加视觉上的收益,但是却会耗费大量宝贵的内存。
    正例:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
    // 首先通过inJustDecodeBounds=true 获得图片的尺寸
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 然后根据图片分辨率以及我们实际需要展示的大小,计算压缩率
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 设置压缩率,并解码
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

反例:
不经压缩显示原图。

  1. 【强制】使用完毕的图片,应该及时回收,释放宝贵的内存。
    正例:
Bitmap bitmap = null;
loadBitmapAsync(new OnResult(result){
    bitmap = result;
});
...使用该bitmap...
// 使用结束,在2.3.3 及以下需要调用recycle()函数,在2.3.3 以上GC 会自动管理,除非你明
确不需要再用。
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
    bitmap.recycle();
}
bitmap = null;

反例:
使用完成图片,始终不释放资源。

  1. 【强制】在Activity#onPause()或Activity#onStop()回调中,关闭当前activity 正在执
    行的的动画。
    正例:
public class MyActivity extends Activity {
    ImageView mImageView;
    Animation mAnimation;
    Button mBtn;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mImageView = (ImageView) findViewById(R.id.ImageView01);
        mAnimation = AnimationUtils.loadAnimation(this, R.anim.anim);
        mBtn = (Button) findViewById(R.id.Button01);
        mBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mImageView.startAnimation(mAnimation);
            }
        });
    }

    @Override
    public void onPause() {
        //页面退出,及时清理动画资源
        mImageView.clearAnimation()
    }
}

反例:
页面退出时,不关闭该页面相关的动画。

  1. 【推荐】在动画或者其他异步任务结束时,应该考虑回调时刻的环境是否还支持业
    务处理。例如Activity 的onStop()函数已经执行,且在该函数中主动释放了资源,
    此时回调中如果不做判断就会空指针崩溃。
    正例:
public class MyActivity extends Activity {
    private ImageView mImageView;
    private Animation mAnimation;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mImageView = (ImageView) findViewById(R.id.ImageView01);
        mAnimation = AnimationUtils.loadAnimation(this, R.anim.anim);
        mAnimation.setAnimationListener(new AnimationListener() {
            @Override
            public void onAnimationEnd(Animation arg0) {
                //判断一下资源是否被释放了
                if (mImageView != null) {
                    mImageView.clearAnimation();
                }
            }
        });
        mImageView.startAnimation(mAnimation);
    }
}

反例:
动画结束回调中,直接使用资源不加判断,导致异常。

  1. 【推荐】使用 inBitmap 重复利用内存空间,避免重复开辟新内存。
    正例:
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;
        }
    }
}

9.【推荐】使用RGB_565 代替RGB_888,在不怎么降低视觉效果的前提下,减少内
存占用。
说明:
android.graphics.Bitmap.Config 类中关于图片颜色的存储方式定义:

  1. ALPHA_8 代表8 位Alpha 位图;
  2. ARGB_4444 代表16 位ARGB 位图;
  3. ARGB_8888 代表32 位ARGB 位图;
  4. RGB_565 代表8 位RGB 位图。
    位图位数越高,存储的颜色信息越多,图像也就越逼真。大多数场景使用的是
    ARGB_8888 和RGB_565,RGB_565 能够在保证图片质量的情况下大大减少内存
    的开销,是解决OOM 的一种方法。
    但是一定要注意RGB_565 是没有透明度的,如果图片本身需要保留透明度,那么
    就不能使用RGB_565。
    正例:
Config config = drawableSave.getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8565 :
Config.RGB_565;
Bitmap bitmap = Bitmap.createBitmap(w, h, config);

反例:

Bitmap newb = Bitmap.createBitmap(width, height, Config.ARGB_8888);

10【. 推荐】尽量减少 Bitmap(BitmapDrawable)的使用,尽量使用纯色(ColorDrawable)、
渐变色(GradientDrawable)、StateSelector(StateListDrawable)等与Shape 结
合的形式构建绘图。
11.【推荐】谨慎使用gif 图片,注意限制每个页面允许同时播放的gif 图片,以及单个
gif 图片的大小。
12.【参考】大图片资源不要直接打包到apk,可以考虑通过文件仓库远程下载,减小包
体积。
13.【推荐】根据设备性能,选择性开启复杂动画,以实现一个整体较优的性能和体验;
14.【推荐】在有强依赖 onAnimationEnd 回调的交互时,如动画播放完毕才能操作页
面, onAnimationEnd 可能会因各种异常没被回调( 参考:
https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-calle
d-onanimationstart-works-fine ), 建议加上超时保护或通过 postDelay 替代
onAnimationEnd。
正例:

View v = findViewById(R.id.xxxViewID);
final FadeUpAnimation anim = new FadeUpAnimation(v);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration(1000);
anim.setFillAfter(true);
new Handler().postDelayed(new Runnable() {
    public void run() {
        if (v != null) {
            v.clearAnimation();
        }
    }
}, anim.getDuration());
v.startAnimation(anim);

15.【推荐】当View Animation 执行结束时,调用View.clearAnimation()释放相关资源。
正例:

View v = findViewById(R.id.xxxViewID);
final FadeUpAnimation anim = new FadeUpAnimation(v);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration(1000);
anim.setFillAfter(true);
anim.setAnimationListener(new AnimationListener() {
    @Override
    public void onAnimationEnd(Animation arg0) {
        //判断一下资源是否被释放了
        if (v != null) {
            v.clearAnimation();
        }
    }
});
v.startAnimation(anim);

安全

  1. 【强制】禁止使用常量初始化矢量参数构建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());
  1. 【强制】将android:allowbackup 属性必须设置为false,阻止应用数据被导出。
    说明:
    android:allowBackup 原本是 Android 提供的 adb 调试功能,如果设置为 true,
    可以导出应用数据备份并在任意设备上恢复。这对应用安全性和用户数据隐私构成
    极大威胁,所以必须设置为 false,防止数据泄露。
    正例:
<application
    android:allowBackup="false"
    android:largeHeap="true"
    android:icon="@drawable/test_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
  1. 【强制】如果使用自定义HostnameVerifier 实现类,必须在verify()方法中校验服务
    器主机名的合法性,否则可能受到中间人攻击。
    说明:
    在与服务器建立 https 连接时,如果 URL 的主机名和服务器的主机名不匹配,则
    可通过该回调接口来判断是否应该允许建立连接。如果回调内实现不恰当,没有有
    效校验主机名,甚至默认接受所有主机名,会大大增加安全风险。
    反例:
HostnameVerifier hnv = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        // 不做校验,接受任意域名服务器
        return true;
    }
};
HttpsURLConnection.setDefaultHostnameVerifier(hnv);
  1. 【强制】如果使用自定义X509TrustManager 实现类,必须在checkServerTrusted()
    方法中校验服务端证书的合法性,否则可能受到中间人攻击。
    说明:
    常见误区是checkServerTrusted()方法根本没有实现,这将导致 X509TrustManager
    形同虚设。该方法中需要实现完备的校验逻辑, 对于证书错误抛出
    CertificateException 。
    正例:
HostnameVerifier hnv = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        if("yourhostname".equals(hostname)){
            return true;
        } else {
            HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
            return hv.verify(hostname, session);
        }
    }
};

反例:

TrustManager tm = new X509TrustManager() {
    public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
        //do nothing,接受任意客户端证书
    }
    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
        //do nothing,接受任意服务端证书
    }
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
};
sslContext.init(null, new TrustManager[] { tm }, null);
  1. 【强制】在SDK 支持的情况下,Android 应用必须使用V2 签名,这将对APK 文件的修改做更多的保护。

  2. 【强制】所有的 Android 基本组件(Activity、Service、BroadcastReceiver、ContentProvider 等)都不应在没有严格权限控制的情况下,将 android:exported 设置为 true。

  3. 【强制】WebView 应设置 WebView#getSettings()#setAllowFileAccess(false)、
    WebView#getSettings()#setAllowFileAccessFromFileURLs(false) 、
    WebView#getSettings()#setAllowUniversalAccessFromFileURLs(false),阻止 file
    scheme URL 的访问。

8.【强制】不要把敏感信息打印到log 中。
说明:
在开发过程中,为了方便调试,通常会使用log 函数输出一些关键流程的信息,这
些信息中通常会包含敏感内容,让攻击者更加容易了解APP 内部结构,方便破解和
攻击,甚至直接获取到有价值的敏感信息。
反例:

String username = "log_leak";
String password = "log_leak_pwd";
Log.d("MY_APP", "usesname" + username);
Log.v("MY_APP", "send message to server ");

以上代码使用Log.d Log.v 打印程序的执行过程的username 等调试信息,日志没有
关闭,攻击者可以直接从Logcat 中读取这些敏感信息。所以在产品的线上版本中关
闭调试接口,不要输出敏感信息。

9.【强制】确保应用发布版本的android:debuggable 属性设置为false。

10.【强制】本地加密秘钥不能硬编码在代码中,更不能使用 SharedPreferences 等本
地持久化机制存储。应选择Android 自身的秘钥库(KeyStore)机制或者其他安全
性更高的安全解决方案保存。
说明:
应用程序在加解密时,使用硬编码在程序中的密钥,攻击者通过反编译拿到密钥可
以轻易解密APP 通信数据。

11.【建议】addJavascriptInterface() 可以添加JS 对本地Java 方法的调用,但这本身
会导致恶意代码的攻击。在Android 4.2(API Level 17)以下,不应再使用这样的
调用方式。在Android 4.2 及以上,需要对本地被远程调用的方法显式添加
@JavascriptInterface annotation。

12.【强制】使用Android 的AES/DES/DESede 加密算法时,不要使用ECB 加密模式,
应使用CBC 或CFB 加密模式。
说明:
加密模式有 ECB、CBC、CFB、OFB 等,其中 ECB 的安全性较弱,如果使用固
定的密钥,相同的明文将会生成相同的密文,容易受到字典攻击,建议使用 CBC、
CFB 或OFB 等模式。

  1. ECB:Electronic codebook,电子密码本模式
  2. CBC:Cipher-block chaining,密码分组链接模式
  3. CFB:Cipher feedback,密文反馈模式
  4. OFB:Output feedback,输出反馈模式

13.【强制】Android APP 在HTTPS 通信中,验证策略需要改成严格模式。
说明:
Android APP 在HTTPS 通信中,使用ALLOW_ALL_HOSTNAME_VERIFIER,表
示允许和所有的HOST 建立SSL 通信,这会存在中间人攻击的风险,最终导致敏感
信息可能会被劫持,以及其他形式的攻击。
反例:

SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

ALLOW_ALL_HOSTNAME_VERIFIER 关闭host 验证,允许和所有的host 建立
SSL 通信,BROWSER_COMPATIBLE_HOSTNAME_VERIFIER 和浏览器兼容的
验证策略,即通配符能够匹配所有子域名 ,STRICT_HOSTNAME_VERIFIER 严
格匹配模式,hostname 必须匹配第一个CN 或者任何一个subject-alts,以上例子
使用了ALLOW_ALL_HOSTNAME_VERIFIER,需要改成STRICT_HOSTNAME_
VERIFIER。

14.【推荐】在Android 4.2(API Level 17)及以上,对安全性要求较高的应用可在Activity
中,对 Activity 所关联的 Window 应用 WindowManager.LayoutParams.FLAG_
SECURE,防止被截屏、录屏。但要注意的是,一个 Activity 关联的 Window 可
能不止一个,如果使用了 Dialog / DialogFragment 等控件弹出对话框,它们本身
也会创建一个新的 Window,也一样需要保护。

15.【推荐】zip 中不要包含 ../../file 这样的路径,可能被篡改目录结构,造成攻击。
说明:
当zip 压缩包中允许存在"../"的字符串,攻击者可以利用多个"../"在解压时改变zip 文
件存放的位置,当文件已经存在是就会进行覆盖,如果覆盖掉的文件是so、dex 或
者odex 文件,就有可能造成严重的安全问题。
正例:
对路径进行判断,存在".."时抛出异常。

//对重要的Zip 压缩包文件进行数字签名校验,校验通过才进行解压
String entryName = entry.getName();
if (entryName.contains("..")){
    throw new Exception("unsecurity zipfile!");
}

反例:

BufferedOutputStream dest = null;
try {
    ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream
            ("/Users/yunmogong/Documents/test/test.zip")));
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        int count;
        byte data[] = new byte[BUFFER];
        String entryName = entry.getName();
        FileOutputStream fos = new FileOutputStream(entryName);
        //System.out.println("Extracting:" + entry);
        dest = new BufferedOutputStream(fos, BUFFER);
        while ((count = zis.read(data, 0, BUFFER)) != -1) {
            dest.write(data, 0, count);
        }
        dest.flush();
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        dest.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

16.【推荐】MD5 和SHA-1、SHA-256 等常用算法是Hash 算法,有一定的安全性,
但不能代替加密算法。敏感信息的存储和传输,需要使用专业的加密机制。

其他

  1. 【强制】不能使用System.out.println 打印log。
    正例:
Log.d(TAG, "Some Android Debug info ...");

反例:

System.out.println("System out println ...");
  1. 【强制】Log 的tag 不能是" "。
    说明:
    日志的tag 是空字符串没有任何意义,也不利于过滤日志。
    正例:
private static String TAG = "LoginActivity";
Log.e(TAG, "Login failed!");

反例:

Log.e("", "Login failed!");

参考文献

[1] Google. Developer Guides [EB/OL].
https://developer.android.com/guide/index.html
[2] Google. Class Index [EB/OL].
https://developer.android.com/reference/classes.html
[3] Alex Lockwood. Android Design Patterns [EB/OL].
https://www.androiddesignpatterns.com/
[4] O'Reilly. High Performance Android Apps by Doug Sillars [EB/OL].
https://www.safaribooksonline.com/library/view/high-performance-android/97814
91913994/ch04.html#figure-story_tree
[5] Takeshi Terada. Whitepaper – Attacking Android browsers via intent scheme
URLs [EB/OL].
https://www.mbsd.jp/Whitepaper/IntentScheme.pdf
[6] 张明云. Android 开发中,有哪些坑需要注意? [EB/OL].
https://zhuanlan.zhihu.com/p/20309921
[7] MegatronKing. Android 多个Fragment 嵌套导致的三大BUG [EB/OL].
http://blog.csdn.net/megatronkings/article/details/51417510
[8] Nfrolov. Android: SQLiteDatabase locking and multi-threading [EB/OL].
https://nfrolov.wordpress.com/2014/08/16/android-sqlitedatabase-locking-and-m
ulti-threading
[9] gcoder_io. Android 数据库模块搭建方案 [EB/OL].
https://www.jianshu.com/p/57eb08fe071d

---The end---

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容