阿里巴巴Android编码规范___读书笔记

地址

http://oqt57ylgd.bkt.clouddn.com/阿里巴巴Java开发手册v1.2.0.pdf

资源文件命名

所有的资源文件都以模块名为前缀


布局文件:
Activity 的 layout 以 module_activity 开头
Fragment 的 layout 以 module_fragment 开头
Dialog 的 layout 以 module_dialog 开头
include 的 layout 以 module_include 开头
ListView 的行 layout 以 module_list_item 开头
RecyclerView 的 item layout 以 module_recycle_item 开头
GridView 的行 layout 以 module_grid_item 开头


drawable资源文件根据分辨率放在不同的drawable目录下,建议只使用一套drawable-xhdpi
drawable:
模块名业务功能控件_限定词,module_login_btn_pressed


属性动画:
模块名逻辑[方向|序号],module_fade_out
帧动画
模块名功能序号,module_loading_01


颜色:
保存在module_color.xml文件中
以#AARRGGBB编写
名称为模块_ 描述_颜色

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

dimen:
保存在module _dimen.xml中
以模块名_描述 为名称

<dimen name="module_horizontal_line_height">1dp</dimen>

Style:

父 style 名称.当前 style 名称
<style name="ParentTheme.ThisActivityTheme">
    ...
</style>

String:
字符串全部保存到module_string.xml文件中
规则: 模块名_描述


大分辨率图片:
单维度超过1000的图片统一存放在xxhdpi目录下

为了支持多种屏幕尺寸和密度,Android 为多种屏幕提供不同的资源目录进行适配。
为不同屏幕密度提供不同的位图可绘制对象,可用于密度特定资源的配置限定符(在
下面详述) 包括 ldpi(低)、mdpi(中)、 hdpi(高)、xhdpi(超高)、xxhdpi (超
超高)和 xxxhdpi(超超超高)。例如,高密度屏幕的位图应使用 drawable-hdpi/。
根据当前的设备屏幕尺寸和密度,将会寻找最匹配的资源,如果将高分辨率图片放
入低密度目录,将会造成低端机加载过大图片资源,又可能造成 OOM,同时也是资
源浪费,没有必要在低端机使用大图。

资源id:

控件缩写
LinearLayoutll
RelativeLayoutrv
ConstraintLayoutcl
ListViewlv
ScrollViewsv
TextViewtv
Buttonbtn
ImageViewiv
CheckBoxcb
RadioButtonrb
EditTextet
ProgressBarprogress_bar
DatePickerdate_picker

其余的以_进行分割

Android代码

大数据通信不要使用Intent!!!

当数据量较大时,避免使用Intent+parcelable的方式
可以使用EvenBus进行代替


持久化数据一般在OnPause() onStop()


隐式Intent使用前要检查!!!
//ctivity 间通过隐式 Intent 的跳转,在发出 Intent 之前必须通过 resolveActivity
//检查,避免找不到合适的调用组件,造成 ActivityNotFoundException 的异常。
if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_
ONLY) != null) {
    try {
        startActivity(intent);
    } catch (ActivityNotFoundException e) {
        if (Config.LOGD) {
            //找不到Activity
        }
    }
}

Service&&IntentService不要执行耗时操作!!!

避免在 Service#onStartCommand()/onBind()方法中执行耗时操作,如果确
实有需求,应改用 IntentService 或采用其他异步机制完成。

public class MyIntentService extends IntentService {
    public MyIntentService() {
        super("MyIntentService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        synchronized (this) {
            //todo
        }
    }
}

BroadCast不要执行耗时操作&&IntentService!!!

避免在 BroadcastReceiver#onReceive()中执行耗时操作,如果有耗时工作,
应该创建 IntentService 完成,而不应该在 BroadcastReceiver 内创建子线程去做。
广播超过十秒会被杀死!!!

new BroadcastReceiver() {
        @Override
    public void onReceive(Context context, Intent intent) {
        Intent userHomeIntent = new Intent();
        userHomeIntent.setClass(this, UseHomeActivity.class);
        this.startActivity(userHomeIntent);
    }
};

禁止使用全局广播!!!

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

添 加 Fragment 时

确 保 FragmentTransaction#commit() 在Activity#onPostResume()或者 FragmentActivity#onResumeFragments()内调用。
不要随意使用 FragmentTransaction#commitAllowingStateLoss()来代替.


不要在onDestroy中进行资源的释放

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

避免使用Fragment嵌套

嵌套 Fragment 是在 Android API 17 添加到 SDK 以及 Support 库中的功能,
Fragment 嵌套使用会有一些坑,容易出现 bug,比较常见的问题有如下几种:
1)  onActivityResult()方法的处理错乱,内嵌的 Fragment 可能收不到该方法的回调,
需要由宿主 Fragment 进行转发处理;
2)  突变动画效果;
3)  被继承的 setRetainInstance(),导致在 Fragment 重建时多次触发不必要的逻
辑。

只使用显式Intent来启动Service

总是使用显式 Intent 启动或者绑定 Service,且不要为服务声明 Intent Filter;

如果确实需要使用隐式调用,则可为 Service 提供 Intent Filter
并从 Intent 中排除相应的组件名称,但必须搭配使用 Intent#setPackage()方法设置
Intent 的指定包名,这样可以充分消除目标服务的不确定性。

避免在Activity#onPause()方法中进行耗时操作

当前Activity的onPause方法执行结束后才会执行下一个Activity的onCreate方法
所以为了app的流畅性,避免在onPause()中进行耗时操作

不要在 Android 的 Application 对象中缓存任何数据!!!


使用全局Toast

可以避免连续显示
Toast 时不能取消上一次 Toast 消息的情况

使用adapter时必须显式设置数据!!!

使用 Adapter 的时候,如果你使用了 ViewHolder 做缓存,在 getView()的
方法中无论这项 convertView 的每个子控件是否需要设置属性,都需
要为其显式设置属性(Textview 的文本为空也需要设置 setText(""),背景透明也需要
设置),否则在滑动的过程中,因为 adapter item 复用的原因,会出现内容的显示错
乱。

动态注册广播时必须在声明周期中成对注册和反注册!!!

部分华为的机型会对 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);
    }
}

布局中的多层嵌套不得使用LinearLayout!!!

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。

尽量使用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);

源文件统一使用UTF-8编码


单位

文本大小使用单位 dp,view 大小使用单位 dp。
对于 Textview,如果在文字大小确定的情况下推荐使用 wrap_content 布局避免出现文字显示不全的适配问题。

禁止在设计布局时多次设置子 view 和父 view 中为同样的背景造成页面过

度绘制!!!

灵活使用布局,推荐 Merge、ViewStub 来优化布局,尽可能多的减少 UI
布局层级,推荐使用 FrameLayout,LinearLayout、RelativeLayout 次之。

在需要时刻刷新某一区域的组件时

通过以下方式避免引发全局 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。


避免使用AnimationDrawable

尽量不要使用 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>

禁止在ScrollView中嵌套lv,rv!!!

会把 ListView 的所有 Item 都加载到内存中,要消耗巨大的内存和 cpu 去绘制图面。
ScrollView 中嵌套 List 或 RecyclerView 的做法官方明确禁止。除了开发过程中遇到
的各种视觉和交互问题,这种做法对性能也有较大损耗。

//推荐使用 NestedScrollView。
<LinearLayout>
    <android.support.v4.widget.NestedScrollView>
        <LinearLayout>
            <ImageView/>
            <android.support.v7.widget.RecyclerView/>
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>
</LinearLayout>

进程 线程与消息通信

application仅在需要的线程进行初始化

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

线程的创建

新建线程时,必须通过线程池提供(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() {
...
});

禁止使用默认线程池!!!

//这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回的线程池对象的弊端如下:
1)  FixedThreadPool 和 SingleThreadPool : 允 许 的 请 求 队 列 长 度 为
Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM;
2)  CachedThreadPool 和 ScheduledThreadPool : 允 许的 创建线 程 数量 为
Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

//以下做法禁止
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

进制在非UI线程中初始化ViewStub,否则返回null!!!


创建线程时候需要去设置线程名称

public class MyThread extends Thread {
    public MyThread(){+
        super.setName("ThreadName");
    }
}

创建线程池时需要设置存活时间,确保空闲线程能被释放


禁 止 在多 进 程 之 间 用 SharedPreferences 共 享数 据

 虽 然 可 以(MODE_MULTI_PROCESS),但官方已不推荐

谨慎使用多进程

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

文件与数据库

文件路径不要使用硬编码!!!

// 任何时候都不要硬编码文件路径,这不仅存在安全隐患,也让 app 更容易出现适配问题
Environment.getExternalStorageDirectory();
Environment.getExterStoragePublicDirectory();
Context.getFilesDir();
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.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

应用间使用FileProvider进行文件共享,而不是给权限!!!
<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.fileprovider",
    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);
}

SP.apply()优先于commit()

SharedPreference 提 交 数 据 时 , 尽 量 使 用 Editor#apply() , 而 非
Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使
用 Editor#commit()。
SharedPreference 相关修改使用 apply 方法进行提交会先写入内存,然后异步写入
磁盘,commit 方法是直接写入磁盘。如果频繁操作的话 apply 的性能会优于 commit,
apply 会将最后修改内容写入磁盘。但是如果希望立刻获取存储操作的结果,并据此
做相应的其他操作,应当使用 commit。


数据库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();
        }
    }
}

多线程操作数据库时应使用事务!!!

Android 的通过 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);
}

大数据写入数据库时,请使用事务

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();
    }
}

执行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);
}

使用数组进行sql语句拼接!!!

// 使用一个可替换参数
String mSelectionClause = "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;
//不要直接进行拼接
String mSelectionClause = "var = " + mUserInput;

Bitmap和Drawable与动画

加载大图和加载多张图片应该异步操作!!!

//应该异步操作
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    // 在后台进行图片解码
    @Override
    protected Bitmap doInBackground(Integer... params) {
    final Bitmap bitmap = BitmapFactory.decodeFile("some path");
    return bitmap;
    }
}
//可能引起卡顿
btnLoadImage.setOnClickListener(new OnClickListener(){
    public void onClick(View v) {
        Bitmap bitmap = BitmapFactory.decodeFile("some path");
    }
});

lv等控件中使用图片等缓存,建议使用图片加载框架!!!

//使用LruCache进行缓存
private LruCache<String, Bitmap> mMemoryCache;

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

//set and get
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
    mMemoryCache.put(key, bitmap);
    }
}
public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}

//使用时的逻辑操作,注意应该在子线程中
final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(),
params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap)

Png图片使用tinypng等类似工具进行压缩,减少包体积!!!


显示图片时根据需要对图片进行压缩

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

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

使用玩的图片应该及时进行回收!!!

// 使用结束,在 2.3.3 及以下需要调用 recycle()函数,在 2.3.3 以上 GC 会自动管理,除非你明
确不需要再用。
if (Build.VERSION.SDK_INT <= 10) {
bitmap.recycle();
}
bitmap = null;

在Activity.onPause()或onStop()中关闭正在正在指定的动画

view.clearAnimation();

异步回调应该判断Activity的状态!!!

在动画或者其他异步任务结束时,应该考虑回调时刻的环境是否还支持业
务处理。例如 Activity 的 onStop()函数已经执行,且在该函数中主动释放了资源,
此时回调中如果不做判断就会空指针崩溃。

@Override
public void onAnimationEnd(Animation arg0) {
    //判断一下资源是否被释放了
    if (mImageView != null) {
        mImageView.clearAnimation();
    }
}

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

使用RGB_565代替ARGB_8888

1)  ALPHA_8 代表 8 位 Alpha 位图;
2)  ARGB_4444 代表 16 位 ARGB 位图;
3)  ARGB_8888 代表 32 位 ARGB 位图;
4)  RGB_565 代表 8 位 RGB 位图
//但是一定要注意 RGB_565 是没有透明度的,如果图片本身需要保留透明度,那么
//就不能使用 RGB_565。

Config config = drawableSave.getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8888 :
Config.RGB_565;
Bitmap bitmap = Bitmap.createBitmap(w, h, config);

减少Bitmap(BitmapDrawable)的使用

尽量使用纯色(ColorDrawable)、
渐变色(GradientDrawable)、StateSelector(StateListDrawable)等与 Shape 结
合的形式构建绘图。


谨慎使用GIf图片,特别注意每个gif图片的大小以及每个页面中gif图片的数量


大图片资源不要直接打包到apk,通过文件仓库进行远程下载


使用postDelay代替onAnimationEnd

 onAnimationEnd  可 能 会 因 各 种 异 常 没 被 回 调
 ( 参 考 :https://stackoverflow.com
 /questions/5474923/onanimationend-is-not-getting-called-onanimationstart-works-fine
new Handler().postDelayed(new Runnable() {
    public void run() {
        if (v != null) {
            v.clearAnimation();
        }
    }
    //设置和动画事件一样长的延迟
}, anim.getDuration())

动画结束时释放相关资源

anim.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationEnd(Animation arg0) {
    //判断一下资源是否被释放了
        if (v != null) {
            v.clearAnimation();
        }
    }
});
v.startAnimation(anim);

安全

使用PendingIntent时禁止使用空intent和隐式intent!!!

1)  使用 PendingIntent 时,使用了空 Intent,会导致恶意用户劫持修改 Intent 的内
容。禁止使用一个空 Intent 去构造 PendingIntent,构造 PendingIntent 的 Intent
一定要设置 ComponentName 或者 action。
2)  PendingIntent 可以让其他 APP 中的代码像是运行自己 APP 中。PendingIntent
的intent接收方在使用该intent时与发送方有相同的权限。在使用PendingIntent
时,PendingIntent 中包装的 intent 如果是隐式的 Intent,容易遭到劫持,导致
信息泄露
//正确使用
Intent intent = new Intent(this, SomeActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_
UPDATE_CURRENT);
try {
    pendingIntent.send();
}

//错误使用  
Bundle addAccountOptions = new Bundle();
//todo添加一些数据
mPendingIntent = PendingTntent.getBroadcast(this, 0, new Intent, 0);
AccountManager.get(this).addAccount(
    accountType,
    null,
    null,
    addAccountOptions,
    null,
    mCallback,
    null);

//错误使用2
//PendingItent中包含的Intent为隐式intent,
//因此当 PendingIntent 触发执行时,发送的 intent 很可能被嗅探或者劫持,导致 intent
//内容泄漏
Intent intent = new Intent("com.test.test.pushservice.action.METHOD");
intent.addFlags(32);
intent.putExtra("app",
PendingIntent.getBroadcast(this, 0, intent, 0));

禁止使用常量初始化矢量参数构建 IvParameterSpec!!!

//正确使用
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,防止 adb backup 导出数据。!!!

在 AndroidManifest.xml 文件中为了方便对程序数据的备份和恢复在 Android API
level 8 以后增加了 android:allowBackup 属性值。默认情况下这个属性值为 true,故
当 allowBackup 标志值为 true 时,即可通过 adb backup 和 adb restore 来备份和恢
复应用程序数据。
<application
android:allowBackup="false"
 >

在实现的 HostnameVerifier 子类中,需要使用 verify 函数效验服务器主机

名的合法性!!!

//正确操作
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);
        }
    }
};
//错误操作
HostnameVerifier hnv = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        // 总是返回 true,接受任意域名服务器
        return true;
    }
};
HttpsURLConnection.setDefaultHostnameVerifier(hnv);

证书校验!!!

利用 X509TrustManager 子类中的 checkServerTrusted 函数效验服务器端
证书的合法性。
在实现的 X509TrustManager 子类中未对服务端的证书做检验,这样会导致不被信
任的证书绕过证书效验机制。

//错误示范
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)

META-INF 目录中不能包含如.apk,.odex,.so 等敏感文件!!!

该文件夹没有经
过签名,容易被恶意替换。


Receiver/Provider 不能在毫无权限控制的情况下,将 android:export 设置!!!

为 true。


数据存储在 Sqlite 或者轻量级存储需要对数据进行加密,取出来的时候进

行解密。!!!


禁止webview 通过 file:schema 方式访问本地敏感数据。!!!


只能在本应用使用 LocalBroadcast!!!

避免被别的应用收到,或者 setPackage 做限制。


不要把敏感信息打印到 log 中!!!

攻击者可以直接从 Logcat 中读取这些敏感信息。所以在产品的线上版本中
关闭调试接口,不要输出敏感信息。


对于内部使用的组件,显示设置组件的”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 的使用。!!!

API level>=17,允许 js 被调用的函数必须以@JavascriptInterface 进行注解,因此
不受影响; 对于 API level < 17,尽量不要使用 addJavascriptInterface,如果一定
要用,那么:

1)  使用 https 协议加载 URL,使用证书校验,防止访问的页面被篡改挂马;
2)  对加载 URL 做白名单过滤、完整性校验等防止访问的页面被篡改;
3)  如果加载本地 html,应该会 HTML 内置在 APK 中,以及对 HTML 页面进行完整
性校验

使用 Android 的 AES/DES/DESede 加密算法时,不要默认!!!

不要使用默认的加密模式ECB,应显示指定使用 CBC 或 CFB 加密模式。

加密模式 ECB、CBC、CFB、OFB 等,其中 ECB 的安全性较弱,会使相同的铭文
在不同的时候产生相同的密文,容易遇到字典攻击,建议使用 CBC 或 CFB 模式。
1)  ECB:Electronic codebook,电子密码本模式
2)  CBC:Cipher-block chaining,密码分组链接模式
3)  CFB:Cipher feedback,密文反馈模式
4)  OFB:Output feedback,输出反馈模式

不要使用 loopback 来通信敏感信息。

尽量关闭Wv加载File的协议!!!

对于不需要使用 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 通信,这会存在中间人攻击的风险,最终导致敏感信息可能
会被劫持,以及其他形式的攻击。

//错误示范
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。

5.0后尽量进制录屏!!!

Android5.0 以后安全性要求较高的应用应该使用 window.setFlag
(LayoutParam.FLAG_SECURE) 禁止录屏。

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();
    }
}

开放的 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 不能是” “!!!

日志的 tag 是空字符串没有任何意义,也不利于过滤日志


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值