阿里Android開發規範:檔案與資料庫

leeyh發表於2018-03-06

以下內容摘自 阿里巴巴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;
}
複製程式碼

擴充套件參考:

  1. developer.android.com/training/da…
  2. developer.android.com/reference/a…

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);
}
複製程式碼

擴充套件參考:

  1. nfrolov.wordpress.com/2014/08/16/…
  2. developer.android.com/reference/a…
  3. www.androiddesignpatterns.com/2012/05/cor…
  4. 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開發規範:安全與其他

相關文章