Scoped Storage คือรูปแบบการเข้าถึงระบบไฟล์ที่นำมาใช้ใน Android 10 (API 29) ซึ่งจำกัดการเข้าถึงพื้นที่จัดเก็บร่วมของอุปกรณ์โดยแอปพลิเคชันต่างๆ ตามเอกสารของนักพัฒนา Android ของ Google (2024) Scoped Storage แทนที่รูปแบบสิทธิ์แบบเก่าอย่าง READ_EXTERNAL_STORAGE และ WRITE_EXTERNAL_STORAGE โดยให้แอปพลิเคชันเข้าถึงได้เฉพาะไฟล์ของตนเองในแซนด์บ็อกซ์ที่แยกออกจากกัน และคอลเล็กชันที่ใช้ร่วมกันผ่าน MediaStore รูปแบบนี้ช่วยเพิ่มความเป็นส่วนตัวของข้อมูลผู้ใช้และป้องกันการรวบรวมข้อมูลโดยไม่ได้รับอนุญาตจากแอปพลิเคชัน
ประเด็นสำคัญ
Scoped Storage คือการเปลี่ยนแปลงทางสถาปัตยกรรมใน Android 10 (API 29) ที่เปลี่ยนแปลงวิธีการที่แอปพลิเคชันเข้าถึงระบบไฟล์ของอุปกรณ์อย่างสิ้นเชิง แทนที่จะเข้าถึงพื้นที่จัดเก็บร่วมได้อย่างเต็มที่ แอปจะเข้าถึงได้เฉพาะแซนด์บ็อกซ์ของตนเองและคอลเล็กชันมีเดียเฉพาะผ่าน MediaStore
ก่อน Scoped Storage แอปใดๆ ที่มีสิทธิ์ READ_EXTERNAL_STORAGE สามารถอ่านไฟล์ทั้งหมดในพื้นที่จัดเก็บร่วมได้ — รูปภาพ เอกสาร ดาวน์โหลด ข้อมูลของแอปอื่นๆ สิ่งนี้สร้างความเสี่ยงด้านความเป็นส่วนตัว: แอปที่ไม่น่าเชื่อถือสามารถรวบรวมข้อมูลผู้ใช้โดยที่ผู้ใช้ไม่รู้ตัว
ตาม Google I/O 2019 ผู้ใช้ Android กว่า 65% ถือว่าความเป็นส่วนตัวของข้อมูลเป็นปัจจัยสำคัญในการเลือกแอป Scoped Storage คือการตอบสนองโดยตรงต่อความต้องการนี้: แต่ละแอปทำงานในสภาพแวดล้อมที่แยกออกจากกัน และเข้าถึงข้อมูลของแอปอื่นได้ก็ต่อเมื่อได้รับความยินยอมอย่างชัดแจ้งจากผู้ใช้เท่านั้น
สำคัญ: Scoped Storage ใน Android 10 เป็นทางเลือก (แอปสามารถปิดใช้งานผ่าน requestLegacyExternalStorage) ตั้งแต่ Android 11 (API 30) เป็นต้นไป Scoped Storage กลายเป็นข้อบังคับสำหรับทุกแอป โดยไม่คำนึงถึง target SDK
Google นำ Scoped Storage มาใช้เพื่อแก้ปัญหาพื้นฐานสามประการของรูปแบบการเข้าถึงไฟล์แบบเก่า: ความเป็นส่วนตัว การจัดการสิทธิ์ และการทำความสะอาดไฟล์ที่เหลืออยู่
ในรูปแบบเก่า แอปที่มี READ_EXTERNAL_STORAGE สามารถสแกนพื้นที่จัดเก็บร่วมทั้งหมดและรวบรวมข้อมูลเมตาของไฟล์ผู้ใช้ — พิกัดทางภูมิศาสตร์ของรูปภาพ ชื่อเอกสาร โครงสร้างไดเรกทอรี Scoped Storage ขจัดความเป็นไปได้นี้: แม้จะมีสิทธิ์ แอปจะเห็นเฉพาะไฟล์ที่สร้างขึ้นเองและไฟล์ที่ผู้ใช้เลือกอย่างชัดแจ้งผ่าน SAF
ก่อน Scoped Storage แอปสามารถทิ้งไฟล์ไว้ในพื้นที่จัดเก็บร่วมหลังจากการถอนการติดตั้ง เมื่อเวลาผ่านไป ไดเรกทอรี ขยะ จะสะสมมากขึ้น Scoped Storage แก้ปัญหานี้: ไฟล์ทั้งหมดในแซนด์บ็อกซ์ของแอปจะถูกลบพร้อมกับแอป และไฟล์ผ่าน MediaStore มีเจ้าของและสามารถทำความสะอาดโดยระบบได้
| ด้าน | รูปแบบเก่า (Legacy) | Scoped Storage |
|---|---|---|
| การเข้าถึง พื้นที่จัดเก็บร่วม | เต็มที่ (มีสิทธิ์) | เฉพาะแซนด์บ็อกซ์ของตนเอง |
| ไฟล์มีเดีย | พาธไฟล์โดยตรง | ผ่าน MediaStore URI |
| ไฟล์ของ แอปอื่น | เข้าถึงได้ตลอดเวลา | ผ่าน SAF เท่านั้น |
| การทำความสะอาด เมื่อถอนการติดตั้ง | ไฟล์ยังคงอยู่ | แซนด์บ็อกซ์ถูกลบ |
| การควบคุม ของผู้ใช้ | น้อยที่สุด | ความยินยอมอย่างชัดแจ้งต่อไฟล์ |
Scoped Storage เป็นส่วนหนึ่งของกลยุทธ์ ความเป็นส่วนตัว โดยรวมของ Google ใน Android ซึ่งรวมถึงสิทธิ์ "เฉพาะขณะใช้งาน" ตัวบ่งชี้การเข้าถึงกล้องและไมโครโฟน และแผงควบคุมความเป็นส่วนตัว
แต่ละ แอปใน Android จะได้รับไดเรกทอรีส่วนตัวของตนเอง ซึ่งเข้าถึงได้ผ่าน Context.getFilesDir() และ context.getCacheDir() สำหรับการเข้าถึงไดเรกทอรีส่วนตัวภายนอก (ที่ใช้ร่วมกัน) จะใช้ Context.getExternalFilesDir()
ใน Scoped Storage แอปมี การเข้าถึงอย่างเต็มที่ ไปยังไดเรกทอรีส่วนตัวภายนอกโดยไม่ต้องมีสิทธิ์ใดๆ นี่คือสถานที่หลักสำหรับจัดเก็บไฟล์ที่ไม่ควรเข้าถึงได้โดยแอปอื่น หรือที่แอปสร้างขึ้นเพื่อการใช้งานของตนเอง
import android.os.Environment
import java.io.File
import java.io.IOException
class FileManager {
fun saveToAppStorage(context: Context, fileName: String, data: ByteArray) {
val appDir = context.getExternalFilesDir(null)
?: return
val file = File(appDir, fileName)
file.writeBytes(data)
}
fun readFromAppStorage(context: Context, fileName: String): ByteArray? {
val appDir = context.getExternalFilesDir(null)
?: return null
val file = File(appDir, fileName)
return file.takeIf { it.exists() }?.readBytes()
}
}
พาธไปยังไดเรกทอรีส่วนตัวภายนอก: /storage/emulated/0/Android/data/{packageName}/files/ ตั้งแต่ Android 11 เป็นต้นไป พาธไฟล์โดยตรงไม่สามารถใช้ได้ — ผ่าน API เท่านั้น นี่คือการเสริมความปลอดภัยอีกประการของ Scoped Storage
MediaStore คือ API หลักสำหรับการเข้าถึงไฟล์มีเดียที่ใช้ร่วมกัน (รูปภาพ วิดีโอ เสียง) ใน Scoped Storage แอปทำงานไม่ใช่กับพาธไฟล์ แต่กับ content URIs ที่จัดหาโดยผู้ให้บริการ MediaStore
MediaStore แบ่งออกเป็นสามคอลเล็กชันหลัก: Images, Video และ Audio แต่ละคอลเล็กชันรองรับการดำเนินการ CRUD ผ่าน ContentResolver ในการแทรกไฟล์ใหม่ลงในคอลเล็กชัน Images จะใช้ MediaStore.Images.Media ในการค้นหาไฟล์ที่มีอยู่ — query ด้วย URI ที่เกี่ยวข้อง
import android.content.ContentValues
import android.provider.MediaStore
import android.os.Environment
import java.io.OutputStream
fun saveImageToGallery(context: Context, bitmap: Bitmap, title: String) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$title.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/MyApp"
)
}
val uri = context.contentResolver
.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
?: return
context.contentResolver.openOutputStream(uri)?.use { output: OutputStream ->
bitmap.compress(
Bitmap.CompressFormat.JPEG,
95,
output
)
}
}
สำคัญ: MediaStore ไม่รองรับพาธไฟล์ต่างๆ — เฉพาะ content URIs การพยายามรับพาธไฟล์จาก URI ผ่าน MediaStore.Files.getContentUri() จะไม่ให้พาธโดยตรงไปยังไฟล์บน Android 11+ ให้ใช้ ContentResolver.openInputStream() และ openOutputStream() ในการทำงานกับเนื้อหาแทน
Storage Access Framework (SAF) คือ API สำหรับการเข้าถึงไฟล์และไดเรกทอรีต่างๆ นอกแซนด์บ็อกซ์ของแอป SAF จัดเตรียมอินเทอร์เฟซการเลือกไฟล์หรือไดเรกทอรีให้ผู้ใช้ หลังจากนั้นแอปจะได้รับ content URI ที่มีการเข้าชั่วคราว
SAF ใช้สำหรับสถานการณ์ที่ MediaStore ครอบคลุมไม่ถึง: การทำงานกับเอกสาร ต่างๆ (PDF, ZIP, APK) การเข้าถึงไดเรกทอรีบนการ์ด SD การนำเข้าและส่งออกไฟล์จากแอปอื่น ผู้ใช้เลือกไฟล์อย่างชัดแจ้งผ่านตัวเลือกไฟล์ของระบบ — ซึ่งรับประกันความยินยอมในการเข้าถึง
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
const val REQUEST_CODE_PICK_DIR = 1001
fun pickDirectory(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
activity.startActivityForResult(intent, REQUEST_CODE_PICK_DIR)
}
fun handlePickResult(requestCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_PICK_DIR && data != null) {
val treeUri: Uri = data.data ?: return
// Take persistent URI permission
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(treeUri, flags)
}
}
SAF ให้แอปเข้าถึงไดเรกทอรีที่ผู้ใช้เลือกจนกว่าจะรีบูตอุปกรณ์ (เมื่อใช้ takePersistableUriPermission) นี่เป็นวิธีเดียวที่จะเข้าถึงไฟล์ต่างๆ ในพื้นที่จัดเก็บร่วมบน Android 11+
การโยกย้ายแอป Android ที่มีอยู่ไปยัง Scoped Storage ต้องมีการเปลี่ยนแปลงในหลายพื้นที่สำคัญ Google แนะนำวิธีการแบบเป็นขั้นตอนพร้อมการทดสอบบน Android 11+
แทนที่การดำเนินการ File โดยตรงทั้งหมดในพื้นที่จัดเก็บร่วมด้วย ContentResolver.openInputStream / openOutputStream สำหรับไฟล์ในแซนด์บ็อกซ์ของแอป (getExternalFilesDir) พาธไฟล์ยังคงทำงานได้
ลบ READ_EXTERNAL_STORAGE และ WRITE_EXTERNAL_STORAGE หากไม่จำเป็นสำหรับสถานการณ์เฉพาะ (เช่น การจัดการไฟล์ — สำหรับนี้มีสิทธิ์พิเศษ MANAGE_EXTERNAL_STORAGE)
| สถานการณ์ | วิธีการเก่า | วิธีการของ Scoped Storage |
|---|---|---|
| บันทึก รูปภาพ | File(path).writeBytes() | MediaStore + ContentResolver |
| อ่าน PDF | File(path).inputStream() | SAF ACTION_OPEN_DOCUMENT |
| ไฟล์ ของตนเอง | Environment.getExternalStorageDirectory() | context.getExternalFilesDir() |
| แคช | File(cacheDir).writeBytes() | context.cacheDir (ไม่เปลี่ยนแปลง) |
หลังการโยกย้าย ให้ทดสอบแอปบน Android 11+ (API 30) โดยตั้งค่า targetSdk = 30 หรือสูงกว่าใน build.gradle ตรวจสอบให้แน่ใจว่าการดำเนินการไฟล์ทั้งหมดในพื้นที่จัดเก็บร่วมทำงานผ่าน MediaStore หรือ SAF ไม่ใช่ผ่านพาธไฟล์โดยตรง
import android.os.Build
fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
fun compatReadFile(context: Context, uri: Uri): ByteArray? {
return if (isScopedStorage()) {
context.contentResolver
.openInputStream(uri)?.readBytes()
} else {
File(uri.path ?: return null)
.takeIf { it.exists() }
?.readBytes()
}
}
ตาม Google Play Console (2024) อุปกรณ์ Android ที่ใช้งานอยู่กว่า 78% ทำงานบน Android 10+ ที่มี Scoped Storage การโยกย้ายเป็นขั้นตอนบังคับสำหรับการเผยแพร่อัปเดตบน Google Play: แอปใหม่ต้องกำหนดเป้าหมาย API 31+ อัปเดต — API 30+
คำถามที่พบบ่อย
ไม่ ตั้งแต่ Android 11 (API 30) เป็นต้นไป Scoped Storage เป็นข้อบังคับสำหรับทุกแอป แฟลก requestLegacyExternalStorage ที่มีใน Android 10 ไม่ทำงานบน API 30+ วิธีเดียวที่จะเข้าถึงระบบไฟล์อย่างกว้างขวางคือสิทธิ์ MANAGE_EXTERNAL_STORAGE แต่มีไว้สำหรับตัวจัดการไฟล์และแอนตี้ไวรัสเท่านั้น
MANAGE_EXTERNAL_STORAGE คือสิทธิ์พิเศษสำหรับแอปที่ต้องการเข้าถึงระบบไฟล์อย่างเต็มที่ (ตัวจัดการไฟล์ เครื่องมือสำรองข้อมูล) การขอสิทธิ์จะเปิดหน้าจอระบบพร้อมคำเตือนสำหรับผู้ใช้ เมื่อเผยแพร่บน Google Play คุณต้องกรอกแบบฟอร์มคำประกาศเกี่ยวกับความจำเป็นของสิทธิ์นี้
ใน Scoped Storage บน Android 11+ ไม่สามารถรับพาธไฟล์โดยตรงจาก content URI ได้ ให้ใช้ ContentResolver.openInputStream() สำหรับการอ่านและ openOutputStream() สำหรับการเขียนแทน หากคุณต้องการพาธไฟล์เพื่อความเข้ากันได้กับไลบรารีของบุคคลที่สาม ให้สร้างสำเนาของไฟล์ใน getCacheDir() และทำงานกับสำเนานั้น
FileProvider ยังคงทำงานได้โดยไม่มีการเปลี่ยนแปลง — ใช้สำหรับให้แอปอื่นเข้าถึงไฟล์จากแซนด์บ็อกซ์ของแอปผ่าน content URIs Scoped Storage ไม่ส่งผลต่อ FileProvider เนื่องจาก FileProvider ทำงานที่ระดับ content URIs ไม่ใช่พาธไฟล์โดยตรง
ตรวจสอบ Build.VERSION.SDK_INT: ถ้า >= Build.VERSION_CODES.Q (29) แสดงว่าอุปกรณ์รองรับ Scoped Storage อย่างไรก็ตาม บน Android 10 Scoped Storage อาจถูกปิดใช้งานผ่าน requestLegacyExternalStorage บน Android 11+ ให้ตรวจสอบว่าแฟลกในแมนิเฟสต์ requestLegacyExternalStorage ถูกตั้งค่าหรือไม่ — ถ้าไม่ Scoped Storage จะทำงานอยู่
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม