Wstęp
Android
używa systemu plików opartego o dyski podobnego do rozwiązań znanych z wielu innych platform. Obiekty typu File
pozwalają na zarządzanie strukturą plików (zapis, odczyt) i mogą być wykorzystywane do operowania i przechowywania dużych jak i niewielkich informacji. Wszystkie urządzenia posiadają dwie przestrzenie pamięci: wewnętrzną (internal
) i zewnętrzną (external
). Takie rozróżnienie pochodzi z czasów w których większość urządzeń posiadała pamięć wbudowaną i wymienną (np. karta micro SD
). Obecnie jednak znaczna część urządzeń dzieli pamięć wbudowaną na internal
i external
oraz dostarcza możliwość rozszerzenia pamięci o pamięć przenośną. Z związku z czym lokalizacja pamięci może się różnić w zależności od konfiguracji urządzenia dlatego należy wystrzegać się ścieżek bezwzględnych.
Internal
Internal storage
jest zawsze dostępną pamięcią wbudowaną w urządzenie. Przechowuje pliki domyślnie prywatne dostępne tylko dla aplikacji. Kiedy aplikacja zostanie odinstalowana usunięte zostaną również jej pliki. Jest pewnym wyborem w sytuacji kiedy dane mają być widoczne tylko dla aplikacji i są powiązane z jej istnieniem. Warto dodać, że domyślnie aplikacje instalowane są w pamięci internal
. Przeważnie posiada mniejszy rozmiar niż external
. Nie wymaga żadnych dodatkowych uprawnień. Dostęp do pamięci aplikacji (katalogu) odbywa się przez getFilesDir
.
class InternalStorage {
fun readFile() {
//represents file system associated with the app
val dir = getFilesDir() //the name of directory is app package
val file = File(dir, "file.txt")
//do something with the file
}
fun writeFile() {
val content = "some file content"
//open file by constructor or by stream and write to it
//use MODE_PRIVATE to make it private for only this app
openFileOutput("file.txt", Context.MODE_PRIVATE).use {
//write data content
it.write(content.toByteArray())
}
//to make it public use FileProvider instead of MODE_WORLD_READABLE
}
fun deleteFile() {
val file = File(filesDir, "file.txt")
if(file.exists()) {
file.delete()
}
}
fun checkSpaceInStorage() {
val total = filesDir.totalSpace
val free = filesDir.freeSpace
}
fun listFilesInStorage() {
val list = fileList()
//do something with files names
}
//do more things with File class
}
External
External storage
nie jest zawsze dostępną pamięcią ponieważ może być powiązana z pamięcią wymienną, która w danej chwili nie jest zamontowana w urządzeniu. Zapisane dane są widoczne dla innych aplikacji i systemów, a po odinstalowaniu aplikacji pliki mogą, ale nie muszą być usuniętę. Wykorzystywana jest przede wszystkim do przechowywania danych niewrażliwych oraz takich, które mogą być udostępnione dla innych aplikacji lub dostępne dla użytkownika z poziomu eksploratora plików w komputerze. System dostarcza kilka publicznych katalogów dla różnych typów plików dostępnych z poziomu klasy Environment
. Dostęp do folderu publicznego odbywa się przez getExternalStoragePublicDirectory
natomiast do prywatnego przy użyciu getExternalFilesDir
.
class ExternalStorage {
fun doSomethingWithFile() {
if(hasPermissions()) {
//write or read to file depends on needs
if(isWriteable()) {
val file = getPrivateDirectory()
//write to file
}
else if(isReadable()) {
//get file and read it
}
}
}
//stay after uninstall, available for other apps
fun getPublicDirectoryFile() : File? {
//use one of Environment folder or pass null to get the root
val file = File(Environment.getExternalStoragePublicDirectory(null), "public.txt")
if(file.mkdirs()) {
//file has not been created
}
return file
}
//remove after uninstall, not visible for MediaStore
fun getPrivateDirectoryFile() : File? {
//use one of Environment folder or pass null to get the root
val file = File(getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "private.txt")
if(file.mkdirs()) {
//file has not been created
}
return file
}
//there can be more than one external storage, check it
fun listExternalDirectories() {
val list = getExternalFilesDirs(null)
if(list[0] != null) {
//primary external storage exists
}
if(list[1] != null) {
//secondary external storage exists (it should be removable storage like micro SD)
}
//in most cases there are no more than two directories
}
//it depends on API level if they are needed or not
fun hasPermissions() : Boolean {
//check if the app has READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE provided
return true
}
//check is storage writable or readable before do something
fun isWritable(): Boolean {
return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}
fun isReadable(): Boolean {
return Environment.getExternalStorageState() in
setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
}
}
Cache
Pamięć podręczna zlokalizowana jest w pamięci internal
i ograniczona jest do 1MB dlatego należy mieć na uwadzę regularne manualne czyszczenie nieużywanych już plików. W przypadku przekroczenia rozmiaru system może automatyczmnie usunąć niektóre pliki. Przeznaczona do przechowywania niedużych danych roboczych aplikacji. Może być także zamiennikiem przekazywania informacji między komponentami zamiast extras
w Intent
lub Bundle
(np. zapis kolekcji przy obrocie lub przekazaniu ich do kolejnego ekranu). Dostęp do niej odbywa się przy pomocy metody getCacheDir
lub getExternalCacheDir
(dla pamięci zewnętrznej) natomiast tworzenie nowego pliku metodą createTempFile
.
class Cache {
fun readCacheFile() {
//from getCacheDir
val file = File(getCacheDir(), "cache.txt")
//read cache, but be aware if still exists
}
fun writeCacheFile() {
val file = File.createTempFile("cache", "txt", getCacheDir())
//write cache, but be aware if still exists
//e.g. convert object collection to json
}
}
LruCache
Alternatywnym podejściem do implementacji pamięci podręcznej w stosunku do standardowego cache
opartego o system plików z lokalizacji getCacheDir
jest wykorzystanie także pamięci operacyjnej urządzenia do której dostęp jest szybszy niż do dysku. Przykładem realizacji może być LruCache
, który przechowuje obiekty w postaci kolejki klucz - wartość
. Dba o prawidłową kolejność wpisów (bajtów) w kolejce oraz automatyczne usuwanie obiektów w przypadku przekroczenia maksymalnego rozmiaru. Dodatkowo zastosowanie podobnego mechanizmu dla dyskowej pamięci tymczasowej DiskLruCache
pozwala na zoptymalizowanie zarządzania pamięcią podręczną.
class LruCacheActivity : AppCompatActivity() {
//set key and value type, it could be something heavy like Bitmap
private lateinit var memoryCache : LruCache<String, String>
private lateinit var diskCache : DiskLruCache //external dependency
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//init cache before
initMemoryCache()
initDiskCache()
//combine memory and disk cache
writeToCache("key", "value")
readFromCache("key")
}
fun initMemoryCache() {
//calculate max memory to 10% of available memory
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
memoryCache = object: LruCache<String, String>(maxMemory / 10) {
override fun sizeOf(key: String, value: String): Int {
return value.toByteArray().size
}
}
}
fun initDiskCache() {
val directory = File(cacheDir.path + File.separator + "lru") //use internal or external
val appVersion = 1 //
val valueCount = 1 //counter entries for every key
val maxSize = 1024L * 1024L * 10L //10MB of the cache
diskCache = DiskLruCache.open(directory, appVersion, valueCount, maxSize)
}
fun writeToCache(key : String, value : String) {
if(readFromMemoryCache(key) == null) {
writeToMemoryCache(key, value)
}
writeToDiskCache(key, value)
}
fun readFromCache(key : String) : String? {
val result = readFromMemoryCache(key)
return if(result == null) {
readFromDiskCache(key)
} else result
}
fun writeToMemoryCache(key : String, value : String) {
memoryCache.put(key, value)
}
fun readFromMemoryCache(key : String) : String? {
return memoryCache.get(key)
}
fun writeToDiskCache(key : String, value : String) {
val editor : DiskLruCache.Editor? = diskCache.edit(key) //returns null if there is active editing
//begin the transaction for this key
editor?.let {
//the entry is mark as being edited
val output = it.newOutputStream(0)
output.write(value.toByteArray())
it.commit() //transaction has been finished
}
}
fun readFromDiskCache(key : String) : String? {
var value : String? = null
val snapshot : DiskLruCache.Snapshot? = diskCache.get(key) //returns null if entries doesn't exists
snapshot?.let {
val input = it.getInputStream(0)
val bytes = input.readBytes()
value = String(bytes) //decode bytes
it.close()
}
return value
}
}
Bezpieczeństwo
Przechowywanie danych na urządzeniu jest narażone na niepowołany dostęp nawet w prywatnej lokalizacji plików. Podstawowym sposobem zachowania bezpieczeństwa jest zastosowanie szyfrów kryptograficznych dzięki czemu pomimo uzyskania dostępu zawartość pliku pozostanie zakodowana. Biblioteka Security
ułatwia to zadanie poprzez dostarczenie gotowego mechanizmu szyfrowania opartego o dwuczęściowy system zarządzania kluczami, gdzie klucze szyfrujące są również zaszyfrowane przez klucz główny znajdujący się w KeyStore
. Klasa EncryptedFile
dostarcza bezpieczną implementację dla FileInputStream
i FileOutputStream
.
class EncryptedActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
writeEncrypt()
readEncrypt()
//file is encrypted so reading in standard way returns encrypted content
val file = File(filesDir, "file.txt")
file.readBytes().toString(Charset.forName("UTF-8"))
}
fun writeEncrypt() {
val content = "some content"
val encryptedFile = getEncryptedFile()
encryptedFile.openFileOutput().apply {
write(content.toByteArray(Charset.forName("UTF-8")))
flush()
close()
}
}
fun readEncrypt() {
val encryptedFile = getEncryptedFile()
encryptedFile.openFileInput().apply {
val byteStream = ByteArrayOutputStream()
var byte = read()
while(byte != -1) {
byteStream.write(byte)
byte = read()
}
val content = byteStream.toByteArray()
close()
}
}
fun getEncryptedFile(): EncryptedFile {
return EncryptedFile.Builder(
File(filesDir, "file.txt"),
this,
getMasterKeyAlias(),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}
fun getMasterKeyAlias(): String {
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
return MasterKeys.getOrCreate(keyGenParameterSpec)
}
}