以下內容摘自 阿里巴巴Android開發手冊
我們的目標是:
- 防患未然,提升質量意識,降低故障率和維護成本;
- 標準統一,提升協作效率;
- 追求卓越的工匠精神,打磨精品程式碼。
- 【強制】必須遵守,違反本約定或將會引起嚴重的後果;
- 【推薦】儘量遵守,長期遵守有助於系統穩定性和合作效率的提升;
- 【參考】充分理解,技術意識的引導,是個人學習、團隊溝通、專案合作的方向。
阿里Android開發規範:資原始檔命名與使用規範
阿里Android開發規範:四大基本元件
阿里Android開發規範:UI 與佈局
阿里Android開發規範:程式、執行緒與訊息通訊
阿里Android開發規範:檔案與資料庫
阿里Android開發規範:Bitmap、Drawable 與動畫
阿里Android開發規範:安全與其他
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;
}
複製程式碼
擴充套件參考:
2、【強制】當使用外部儲存時,必須檢查外部儲存的可用性。
正例:
// 讀/寫檢查
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;
}
複製程式碼
3、【強制】應用間共享檔案時,不要通過放寬檔案系統許可權的方式去實現,而應使用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);
}
複製程式碼
4、【推薦】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();
}
複製程式碼
5、【推薦】 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();
複製程式碼
擴充套件參考:
developer.android.com/reference/a…
6、【強制】資料庫 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 不關閉
}
複製程式碼
7、【強制】多執行緒操作寫入資料庫時,需要使用事務,以免出現同步問題。
說明:
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);
}
複製程式碼
擴充套件參考:
- nfrolov.wordpress.com/2014/08/16/…
- developer.android.com/reference/a…
- www.androiddesignpatterns.com/2012/05/cor…
- www.jianshu.com/p/57eb08fe0…
8、 【推薦】大資料寫入資料庫時,請使用事務或其他能夠提高 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();
}
}
複製程式碼
9、【強制】執行 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 語句中,可使用一個用於將 ? 作為可替換引數的選擇子句以及一個單獨的選擇引數陣列,會避免 SQL 注入。
正例:
// 使用一個可替換引數
String mSelectionClause = "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;
複製程式碼
反例:
// 拼接使用者輸入內容和列名
String mSelectionClause = "var = " + mUserInput;
複製程式碼
阿里Android開發規範:資原始檔命名與使用規範
阿里Android開發規範:四大基本元件
阿里Android開發規範:UI 與佈局
阿里Android開發規範:程式、執行緒與訊息通訊
阿里Android開發規範:檔案與資料庫
阿里Android開發規範:Bitmap、Drawable 與動畫
阿里Android開發規範:安全與其他