筆記(十)——Android存儲知識
- 2020 年 3 月 27 日
- 筆記
——》個人平時筆記,看到的同學歡迎指正錯誤,文中多處摘錄於各大部落客精華、書籍
1、存儲相關詳解知識
Android起初早年是有內置SD卡和可擴展插拔TF卡區分的,但是近年手機內置SD卡的高記憶體導致越來越少的手機支援TF卡(外置SD卡)擴展了。並且現在默認都是將文件優先存儲於內置SD卡中。
以下項目app包名:com.fivefloor.bo.myview
(1)、記憶體(RAM)
記憶體與PC的記憶體是一樣的,是用來運行程式,不能用來永久存儲數據,手機一旦關機,在記憶體中的所有數據都將會丟失,記憶體也是現在人類製造的所有電子設備所必需擁有的。Android中的運行時記憶體RAM,每個app一般分配16M或24M或者通過系統底層設置可以更改自定義。
(2)、存儲(ROM)
內部存儲(ROM):
就是相當於是PC中的硬碟的私有存儲角色。用於存儲Andoid設備的作業系統和應用程式的存儲介質,Android設備中的Android系統和應用程式(APK文件)都是存在內部存儲區的。例如手機的/system/目錄、/data/目錄等。data文件夾就是我們常說的內部存儲區,當我們打開data文件夾之後(沒有root許可權的話,用戶也沒法操作內部存儲空間,不能打開該文件夾)。通過context.getCacheDir()、context.getFilesDir()等不帶External欄位獲取的文件路徑,如:/data/data/com.fivefloor.bo.myview/cache
外部存儲(ROM):
相當於PC中的硬碟、U盤或者移動硬碟。外部存儲一般就是我們看到的storage文件夾,當然也有可能是mnt文件夾,這個不同廠家有可能不一樣。storage或mnt文件夾即為外部存儲區,外部存儲中的文件是可以被用戶或者其他應用程式修改的,有兩種類型的文件(或者目錄):
>1.公共文件Public files:文件是可以被自由訪問,且文件的數據對其他應用或者用戶來說都是有意義的,當應用被卸載之後,其卸載前創建的文件仍然保留。比如camera應用,生成的照片大家都能訪問,而且camera不在了,照片仍然在。公有目錄有九大類,比如DCIM、DOWNLOAD、PICTURES等這種系統為我們創建的文件夾。如:/storage/emulated/0/Pictures >2.私有文件Private files:其實由於是外部存儲的原因即使得這種類型的文件也能被其他程式訪問,只不過一個應用私有的文件對其他應用其實是沒有訪問價值的(惡意程式除外)。外部存儲上的應用私有文件的價值,在於卸載之後這些文件也會被刪除。類似於內部存儲,只是和內部儲存不同的是這個部分可以給用戶和其他應用訪問,所以才叫外部儲存的私有部分嘛。私有目錄就是Android這個文件夾路徑下的,都是帶包名的。如:/storage/emulated/0/Android/data/com.fivefloor.bo.myview/cache >3.外部TF卡也是屬於外部存儲的,而要注意外置TF卡(外置SD卡)一般為:/storage/sdcard1或者/storage/sdcard2或 mnt/ext_sdcard,有些手機還可以擴展多張TF卡。
注意內部存儲不是記憶體。從用戶角度來說SD卡有內置SD卡和外置TF卡之分,通過Environment或者Context獲取的都是手機自帶的內置SD卡路徑,類似storage/emulated/0/加後綴。內部存儲和外部存儲並不是按是否存儲於SD卡來區分的,內部存儲是在data文件下且不可被訪問操作,外部存儲是在storage或者mnt文件夾下是可以被訪問操作的,****這些就是區別。內部存儲,我們稱為InternalStorage,外部存儲我們稱為ExternalStorage。內部存儲和外部存儲的私有文件(也就是app包名下的)都是屬於該app的,app卸載了他們也就跟著刪除了。

image
如果按照路徑的特徵,我們又可以將文件存儲的路徑分為兩大類,一類是路徑中含有包名的,一類是路徑中不含有包名的。含有包名的路徑,因為和某個app有關,所以對這些文件夾的訪問都是調用Context裡邊的方法;而不含有包名的路徑,和某一個app無關,如:九大共有目錄,我們可以通過Environment中的方法來訪問。如下圖:

image
/** * 獲取外置TF卡路徑/storage/sdcard1/或/storage/0F1C-240A/等 * * @param mContext * @return */ public static String getExtendedSDMemoryPath(Context mContext) { //ECOENDARY_STORAGE這個值,代表是第二儲存,即為外置可移動SD卡。EXTERNAL_STORAGE則對應的是手機內部的存儲。 StorageManager mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); Class<?> storageVolumeClazz = null; try { storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); Method getPath = storageVolumeClazz.getMethod("getPath"); Method isRemovable = storageVolumeClazz.getMethod("isRemovable"); Object result = getVolumeList.invoke(mStorageManager); final int length = Array.getLength(result); for (int i = 0; i < length; i++) { Object storageVolumeElement = Array.get(result, i); String path = (String) getPath.invoke(storageVolumeElement); boolean removable = (Boolean) isRemovable.invoke(storageVolumeElement); if (removable) { return path + File.separator; } } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null;}
2、SQLite是一個輕量級的、嵌入式的關係型資料庫,它遵守ACID的關聯式資料庫管理系統,是主要針對於嵌入式設備專門設計的資料庫。SQLite支援最大2TB的存儲空間,在Android中SQLite是受手機系統存儲空間(ROM)也就是機身記憶體大小限制的,不包括外置SD卡空間。所有app程式共用一個SQLite資料庫,但是資料庫表不同,多個app不共用,這個需要注意理清。
優秀的資料庫框架:GreenDao、OrmLite、Litepal等
要想創建一個SQLite資料庫,必須要構建一個SQLiteOpenHelper的實例,SQLiteOpenHelper中有兩個構造方法,使用參數少的那個即可,還要重寫onCreate(SQLiteDatabase db)、onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)方法,其中onUpgrade是為了資料庫升級提供的。當創建SQLiteOpenHelper實例時,如果已經存在舊資料庫(即就資料庫版本號存在)就會走onUpgrade方法而不會調用onCreate,這時可以添加表欄位或添加新表等操作升級資料庫。
//資料庫升級方案 public class DBHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "student.db"; //資料庫版本號 private static final int DATABASE_VERSION = 1002; private static DBHelper instance = null; /*創建表語句 語句對大小寫不敏感 create table 表名(欄位名 類型,欄位名 類型,…)*/ private final String CREATE_PERSON = "create table Student (" + VALUE_ID + " integer primary key," + VALUE_NAME + " text ," + VALUE_ISBOY + " integer," + VALUE_AGE + " ingeter," + VALUE_ADDRESS + " text," + VALUE_PIC + " blob" + ")"; public DBHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } public DBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); ///調用該方法,在onCreate()就不用調用onUpgrade(db, FIRST_DATABASE_VERSION, DATABASE_VERSION); } public synchronized static DBHelper getInstance(Context context) { if (instance == null) { instance = new DBHelper(context); } return instance; } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_PERSON); // 若不是第一個版本安裝,直接執行資料庫升級 // 請不要修改FIRST_DATABASE_VERSION的值,其為第一個資料庫版本大小,設置第一個資料庫版本號 final int FIRST_DATABASE_VERSION = 1000; onUpgrade(db, FIRST_DATABASE_VERSION, DATABASE_VERSION); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 使用for實現跨版本升級資料庫 for (int i = oldVersion; i < newVersion; i++) { switch (i) { case 1000: upgradeToVersion1001(db); break; case 1001: upgradeToVersion1002(db); break; default: break; } } } //升級資料庫表 private void upgradeToVersion1001(SQLiteDatabase db){ // student 表新增1個欄位 String sql1 = "ALTER TABLE Student ADD COLUMN age VARCHAR"; db.execSQL(sql1); } //升級資料庫表 private void upgradeToVersion1002(SQLiteDatabase db){ // student 表新增2個欄位, 添加新欄位只能一個欄位一個欄位加,sqlite有限制不予許一條語句加多個欄位 String sql1 = "ALTER TABLE Student ADD COLUMN tel VARCHAR"; String sql2 = "ALTER TABLE Student ADD COLUMN address VARCHAR"; db.execSQL(sql1); db.execSQL(sql2); } }
3、android應用程式(進程)記憶體(RAM)一般限制在16M,也有的是24M(早期的Android系統G1,就是只有16M),根據開發人員的定義也可以擴展;進程是作為資源分配的基本單位,可以創建多進程來獲取系統分配更多的資源記憶體,通過給四大組件指定android:process屬性,我們可以輕易地開啟多進程模式。Android為不同類型的進程分配了不同的記憶體使用上限,如果程式在運行過程中出現了記憶體泄漏的而造成應用進程使用的記憶體超過了這個上限,則會被系統視為記憶體泄漏,從而被kill掉。對於我們已經不需要使用的對象,我們可以把它設置為null,這樣當GC運行的時候,會遍歷到你這個對象已經沒有引用,就會自動把該對象佔用的記憶體回收。我們沒法像C++那樣馬上釋放不需要的記憶體,但是我們可以主動告訴系統,哪些記憶體可以回收了。也可以巧妙的運用弱引用和軟引用。
4、Java SE2開始,就提供了四種類型的引用:強引用、軟引用、弱引用和虛引用。Java中提供這四種引用類型主要有兩個目的:第一是可以讓程式設計師通過程式碼的方式決定某些對象的生命周期;第二是有利於JVM進行垃圾回收。
1.強引用就是指在程式程式碼之中普遍存在的,如我們常定義和實例化:String a="123";只要某個對象有強引用與之關聯,JVM必定不會回收這個對象,即使在記憶體不足的情況下,JVM寧願拋出OutOfMemory錯誤也不會回收這種對象如果想中斷強引用和某個對象之間的關聯,可以顯示地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該對象。一般用於服務端。在方法內部有一個強引用,這個引用保存在 java 棧 中,而真正的引用內容 (Object)保存在 java 堆中。當這個方法運行完成後,就會退出方法棧,則引用對象的引用數為 0 ,這個對象會被回收。
2.軟引用是用來描述一些有用但並不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。對於軟引用關聯著的對象,只有在記憶體不足的時候JVM才會回收該對象。因此,這一點可以很好地用來解決OOM的問題,並且這個特性很適合用來實現快取:比如網頁快取、圖片快取等。
import java.lang.ref.SoftReference; public class Main { public static void main(String[] args) { SoftReference<String> sr = new SoftReference<String>(new String("hello")); System.out.println(sr.get()); } }
3.弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。移動端記憶體緊缺推薦使用弱引用。
import java.lang.ref.WeakReference; public class Main { public static void main(String[] args) { WeakReference<String> sr = new WeakReference<String>(new String("hello")); System.out.println(sr.get()); System.gc(); //通知JVM的gc進行垃圾回收 System.out.println(sr.get()); } }
4.虛引用和前面的軟引用、弱引用不同,它並不影響對象的生命周期。在java中用java.lang.ref.PhantomReference類表示。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。
import java.lang.ref.PhantomReference; importjava.lang.ref.ReferenceQueue; publicclassMain { publicstaticvoidmain(String[] args) { ReferenceQueue<String> queue = new ReferenceQueue<String>(); PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue); System.out.println(pr.get()); } }
5、快取策略是一種思想,目前比較常用的快取策略是LruCache和DiskLruCache,其中LruCache常被用做記憶體快取,而DiskLruCache常被用做存儲快取。
BitmapFactory類提供了四類方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分別用於支援從文件系統、資源、輸入流以及位元組數組中載入出一個Bitmap對象,其中decodeFile和decodeResource又間接調用了decodeStream方法。載入圖片的四類方法都支援BitmapFactory.Options參數,通過它們就可以很方便地對一個圖片進行取樣縮放,來達到高效載入圖片,減少記憶體消耗。
6、SharedPreferences需要注意:
(1)、與commit方法相比,apply方法使用非同步方式將數據更新到文件。 apply沒有返回值而commit有返回boolean值表明修改是否提交成功。在單進程的環境下,apply()可以替代commit(),擁有更好的性能,但是apply()有可能會造成ANR。
(2)、SharedPreference 相關修改操作若使用 apply 方法進行提交是原子提交,會先寫入記憶體然後再非同步寫入磁碟。commit方法是直接同步提交到硬體磁碟。因此,在多個並發的提交commit的時候,後一個commit操作會先等待正在處理的commit保存到磁碟後再操作,從而降低了效率。而apply只是原子提交到內容,後面有調用apply的函數的將會直接覆蓋前面的記憶體數據,這樣從一定程度上提高了很多效率。
「原子提交」是SQLite這種支援事務的資料庫的一個重要特性。原子提交意味著某個事務中資料庫的變化會完整完成或者根本不完成。原子提交意味著不同的寫入分別寫入到資料庫的不同部分就似同時發生在同一個時間點一樣。 實際上硬體會連續的寫到海量存儲器中,只是寫一個扇區所用的時間非常少。所以,同時或瞬間寫入到數據文件的不同部分成為可能。SQLite的原子提交邏輯會使得一個事務中的變化就象同時發生的一樣。事務的原子是SQLite的重要特性,即使事務由於作業系統出錯或掉電發生中斷也能保持其原子性。
(3)、 如果希望立刻獲取存儲操作的結果,並據此做相應的其他操作,應當使用 commit。在不關心提交結果是否成功的情況下,優先考慮apply方法。
(4)、系統提供的 SharedPreferences 的應用場景是用來存儲一些非常簡單、輕量的數據。我們不要使用它存儲過於複雜的數據,例如 HTML、JSON 等。而且 SharedPreferences 的文件存儲性能與文件大小有關,每個 SP 文件不能過大,我們不要將毫無關聯的配置項保存在同一個文件中;同時考慮將頻繁修改的條目單獨隔離出來,存在一個新的SharedPreferences 文件中。
7、Android7.0使用Uri訪問本地文件添加了行為許可權,Android 框架執行StrictMode API 的政策禁止在應用外部公開 file://URI,分享私有文件內容需要通過使用FileProvider生成content://Uri來替代file://Uri:
Intent installIntent =new Intent(Intent.ACTION_VIEW); File apkPath =new File(Environment.getExternalStorageDirectory(),"appFile"); File apkFile =newFile(apkPath,"myapp.apk"); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Uri contentUri = FileProvider.getUriForFile(MainActivity.this,"com.example.myapp.fileprovider", apkFile); installIntent.setDataAndType(contentUri,"application/vnd.android.package-archive"); installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); }else{ installIntent.setDataAndType(Uri.fromFile(apkFile),"application/vnd.android.package-archive"); } installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if(installIntent.resolveActivity(getPackageManager()) !=null) { startActivity(installIntent); } AndroidManifest.xml清單文件: <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.myapp.fileprovider" android:grantUriPermissions="true" android:exported="false"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" /> </provider> filepaths .xml配置文件: <paths> <files-path path="images/" name="myimages" /></paths>