Storage

Przechowywanie

  |   13 min czytania

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