Android 10、11 存儲(chǔ)完全適配

一岁疼、存儲(chǔ)基本知識(shí)

先來看看存儲(chǔ)區(qū)域劃分:


其中阔涉,以下目錄無需存儲(chǔ)權(quán)限即可訪問:

1缆娃、App自身的內(nèi)部存儲(chǔ)

2、App自身的自帶外部存儲(chǔ)-私有目錄

剩下的都需要申請(qǐng)存儲(chǔ)權(quán)限瑰排,Android 10.0前后對(duì)于存儲(chǔ)作用域訪問的區(qū)別就體現(xiàn)在如何訪問剩余這些目錄內(nèi)的文件贯要。

重點(diǎn)在自帶外部存儲(chǔ)之共享存儲(chǔ)空間和其它目錄

2、Android 10.0 之前訪問方式

繼續(xù)細(xì)分為Android 6.0 之前和之后椭住。

Android 6.0 之前訪問方式

Android 6.0 之前是無需申請(qǐng)動(dòng)態(tài)權(quán)限的崇渗,在AndroidManifest.xml 里聲明存儲(chǔ)權(quán)限:

<?uses-permissionandroid:name=?"android.permission.WRITE_EXTERNAL_STORAGE"/>?

<?uses-permissionandroid:name=?"android.permission.READ_EXTERNAL_STORAGE"/>

就可以訪問共享存儲(chǔ)空間、其它目錄下的文件了京郑。

Android 6.0 之后的訪問方式動(dòng)態(tài)申請(qǐng)權(quán)限

Android 6.0 后需要?jiǎng)討B(tài)申請(qǐng)權(quán)限宅广,除了在AndroidManifest.xml 里聲明存儲(chǔ)權(quán)限外,還需要在代碼里動(dòng)態(tài)申請(qǐng)些举。

//檢查權(quán)限跟狱,并返回需要申請(qǐng)的權(quán)限列表

?private List?checkPermission(?Context context, String[] checkList)?{?

? ? ?List list =?new ArrayList();

? ? ?for(?inti =?0; i < checkList.length; i++) {?

? ? ?if( PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(context, checkList[i]))? ?{?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? list.?add(checkList[i]);

? ? ? ? }?

? ? ?}?

? ? ?return list;?

}

//申請(qǐng)權(quán)限

private void requestPermission(?Activity activity, String requestPermissionList[])?{

? ?ActivityCompat.requestPermissions(activity, requestPermissionList,?100);?

}

//用戶作出選擇后,返回申請(qǐng)的結(jié)果

@?Override

public void onRequestPermissionsResult(?intrequestCode, @NonNull String[] permissions, @NonNull?int[] grantResults?)?{?

if(requestCode ==?100) {?

? ? ? for(?inti =?0; i < permissions.length; i++) {

? ? ? ? ? ? ?if(permissions[i].?equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {?

? ? ? ? ? ? ? ? ? ?if(grantResults[i] == PackageManager.PERMISSION_GRANTED) {?

? ? ? ? ? ? ? ? ? ? ? ? Toast.makeText(MainActivity.?this,?"存儲(chǔ)權(quán)限申請(qǐng)成功", Toast.LENGTH_SHORT).show;?

? ? ? ? ? ? ? ? ? ? }?else{

? ? ? ? ? ? ? ? ? ? ? ? Toast.makeText(MainActivity.?this,?"存儲(chǔ)權(quán)限申請(qǐng)失敗", Toast.LENGTH_SHORT).show;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ?}

? ? ? ?}

?}

}

//測(cè)試申請(qǐng)存儲(chǔ)權(quán)限

private void testPermission(?Activity activity)?{?

? ? ? ?String[] checkList =?newString[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,? ? ? ? ? ? ? ? ?Manifest.permission.READ_EXTERNAL_STORAGE};?

? ? ? ? List<String> needRequestList = checkPermission(activity, checkList);

? ? ? ? if(needRequestList.isEmpty()) {?

? ? ? ? ? ? ?Toast.makeText(MainActivity.?this,?"無需申請(qǐng)權(quán)限", Toast.LENGTH_SHORT).show;?

? ? ? ? }?else{

? ? ? ? ? ? ? requestPermission(activity, needRequestList.toArray(?newString[needRequestList.size]));?

? ? ? ? }

?}

申請(qǐng)權(quán)限后户魏,提示用戶作出選擇:

訪問文件

權(quán)限申請(qǐng)成功后驶臊,即可對(duì)自帶外部存儲(chǔ)之共享存儲(chǔ)空間和其它目錄進(jìn)行訪問。

分別以共享存儲(chǔ)空間和其它目錄為例叼丑,闡述訪問方式:

訪問共享存儲(chǔ)空間

共享存儲(chǔ)空間分為兩類文件:媒體文件和文檔/其它文件关翎。

訪問媒體文件

目的是拿到媒體文件的路徑,有兩種方式獲取路徑:

1鸠信、直接構(gòu)造路徑

以圖片為例笤休,假設(shè)圖片存儲(chǔ)在/sdcard/Pictures/目錄下。

private void? testShareMedia(?)?{?

?// 獲取目錄:/storage/emulated/0/

?File rootFile = Environment.getExternalStorageDirectory;

?String imagePath = rootFile.getAbsolutePath + File.separator + Environment.DIRECTORY_PICTURES + File.separator +?"myPic.png";?

?Bitmap bitmap = BitmapFactory.decodeFile(imagePath);

}

如上症副,myPic.png的路徑:/storage/emulated/0/Pictures/myPic.png店雅,拿到路徑后就可以解析并獲取Bitmap。

2贞铣、通過MediaStore獲取路徑

沿用上篇的demo:

private void getImagePath(?Context context)?{?

? ? ? ?ContentResolver contentResolver = context.getContentResolver;

? ? ? ?Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,?null,?null,?null,?null);?

? ? ? ?while(cursor.moveToNext) {?

? ? ? ? ? ? ? ? String imagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));

? ? ? ? ? ? ? ? Bitmap bitmap = BitmapFactory.decodeFile(imagePath);

? ? ? ? ? ? ? ? break;?

? ? ? ?}

}

同樣的闹啦,也是拿到圖片路徑后獲取Bitmap。

還有一種不直接通過路徑訪問的方法:

3辕坝、通過MediaStore獲取Uri

private void getImagePath(?Context context)?{

? ? ?ContentResolver contentResolver = context.getContentResolver;

? ? ?Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,?null,?null,?null,?null);?

? ? ?while(cursor.moveToNext) {?

? ? ? ? ?//獲取唯一的id

? ? ? ? ? longid = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));

? ? ? ? ?//通過id構(gòu)造Uri

? ? ? ? ? Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);

? ? ? ? ? openUri(uri);

? ? ? ? ? ?break;?

? ? ? ?}

}

與直接拿到路徑不同的是窍奋,此處拿到的是Uri。圖片的信息封裝在Uri里酱畅,通過Uri構(gòu)造出InputStream琳袄,再進(jìn)行圖片解碼拿到Bitmap

訪問文檔和其它文件

1、直接構(gòu)造路徑

與媒體文件一樣纺酸,可以直接構(gòu)造路徑訪問窖逗。

2、通過SAF訪問

Storage Access Framework 簡(jiǎn)稱SAF:存儲(chǔ)訪問框架餐蔬。相當(dāng)于系統(tǒng)內(nèi)置了文件選擇器碎紊,通過它可以拿到想要訪問的文件信息佑附。

同樣的以獲取圖片為例:

private void startSAF{?

? ? ? ?Intent intent =?newIntent(Intent.ACTION_OPEN_DOCUMENT);?

? ? ? ?intent.addCategory(Intent.CATEGORY_OPENABLE);

? ? ? ?//選擇圖片intent.setType(?"image/jpeg");?

? ? ? ?startActivityForResult(intent,?100);?

}

@Override

protected void? onActivityResult(?intrequestCode,?intresultCode, @Nullable Intent data)?{?

? ? ? ? super.onActivityResult(requestCode, resultCode, data);

? ? ? ?if(requestCode ==?100) {??

? ? ? ?//選中返回的圖片封裝在uri里

? ? ? ? Uri uri =? data.getData;? ?

? ? ? ?openUri(uri);

? ? ? ? }

}

private void openUri(Uri uri){

? ? ? ?try{?

? ? ? ? ? ? //從uri構(gòu)造輸入流

? ? ? ? ? ?InputStream fis = getContentResolver.openInputStream(uri);

? ? ? ? ? ?Bitmap bitmap = BitmapFactory.decodeStream(fis);

? ? ? ? }?catch(Exception e) {

? ? ? ? }

}?


可以看出,通過SAF并不能直接拿到圖片的路徑仗考,圖片的信息封裝在Uri里音同,通過Uri構(gòu)造出InputStream,再進(jìn)行圖片解碼拿到Bitmap

訪問其它目錄

有兩種方式:

1秃嗜、直接構(gòu)造路徑

在/sdcard/目錄下直接創(chuàng)建目錄:

private void testPublicFile(?)?{

? ? ?File rootFile = Environment.getExternalStorageDirectory();

? ? ?String imagePath = rootFile.getAbsolutePath() + File.separator +?"myDir";?

? ? ?File myDir =?new File(imagePath);

? ? ?if(!myDir.exists()) {

? ? ? ? ? myDir.mkdir();

? ? ? }

}


可以看出权均,/sdcard/myDir/目錄創(chuàng)建成功。

二锅锨、通過SAF訪問

與共享存儲(chǔ)空間SAF訪問方式一致叽赊。

Android 10.0 之前訪問方式總結(jié)

由上面分析的共享存儲(chǔ)空間/其它目錄訪問方式可知,訪問目錄/文件可通過如下兩個(gè)方法:

1橡类、通過路徑訪問蛇尚。路徑可以直接構(gòu)造也可以通過MediaStore獲取。

2顾画、通過Uri訪問取劫。Uri可以通過MediaStore或者SAF獲取。

Android 6.0 以下訪問共享存儲(chǔ)空間/其它目錄步驟:

1研侣、AndroidManifest.xml里聲明存儲(chǔ)權(quán)限

2谱邪、通過路徑或者Uri訪問文件

Android 6.0(含)~Android 10.0(不含)訪問共享存儲(chǔ)空間/其它目錄步驟:

1、AndroidManifest.xml里聲明存儲(chǔ)權(quán)限

2庶诡、動(dòng)態(tài)申請(qǐng)存儲(chǔ)權(quán)限

3惦银、通過路徑或者Uri訪問文件

3、Android 10.0 訪問方式變更

為什么要變更

你可能已經(jīng)發(fā)現(xiàn)了上面訪問方式的弊端末誓,比如我們能夠直接在/sdcard/目錄下創(chuàng)建目錄/文件扯俱。事實(shí)上,很多App就是這么干的喇澡,看圖說話:



可以看出/sdcard/目錄下迅栅,如淘寶、qq晴玖、qq瀏覽器读存、微博、支付寶等都自己建了目錄呕屎。

這么看來让簿,導(dǎo)致目錄結(jié)構(gòu)很亂,而且App卸載后秀睛,對(duì)應(yīng)的目錄并沒有刪除尔当,于是就是遺留了很多"垃圾"文件,久而久之不處理琅催,用戶的存儲(chǔ)空間越來越小居凶。

總結(jié)弊端如下:

1虫给、在設(shè)置里"Clear storage"或者"Clear cache"并不能刪除該目錄下的文件

2藤抡、卸載App也不能刪除該目錄下的文件

3侠碧、App可以隨意修改其它目錄下的文件,如修改別的App創(chuàng)建的文件等缠黍,不安全

你也許會(huì)問弄兜,為什么要在/sdcard/目錄下新建自己的目錄呢?

大體有以下兩個(gè)原因:

1瓷式、此處新建的目錄不會(huì)被設(shè)置里的App存儲(chǔ)用量統(tǒng)計(jì)替饿,讓用戶"看起來"自己的App占用的存儲(chǔ)空間很小

2、方便操作文件

如何變更

面對(duì)眾多App不講"碼德"隨意新建目錄/文件的現(xiàn)象贸典,Google在Android 10.0上重拳出擊了视卢。

引入Scoped Storage

翻譯成中文有好幾個(gè)版本:作用域存儲(chǔ)、分區(qū)存儲(chǔ)廊驼、沙盒存儲(chǔ)据过。

具體中文翻譯不重要,下面以分區(qū)存儲(chǔ)指代妒挎。

分區(qū)存儲(chǔ)原理:

1绳锅、App訪問自身內(nèi)部存儲(chǔ)空間、訪問外部存儲(chǔ)空間-App私有目錄不需要任何權(quán)限(這個(gè)與Android 10.0之前一致)

2酝掩、外部存儲(chǔ)空間-共享存儲(chǔ)空間鳞芙、外部存儲(chǔ)空間-其它目錄 App無法通過路徑直接訪問,不能新建期虾、刪除原朝、修改目錄/文件等

3、外部存儲(chǔ)空間-共享存儲(chǔ)空間镶苞、外部存儲(chǔ)空間-其它目錄 需要通過Uri訪問

分區(qū)存儲(chǔ)的變更在于第二點(diǎn)喳坠、第三點(diǎn)。

為什么Uri能夠訪問

先來看為什么通過路徑無法直接訪問宾尚。

我們知道訪問文件最終是通過構(gòu)造InputStream/OutputStream來實(shí)現(xiàn)的丙笋,以InputStream為例,看看其構(gòu)造方法:

#FileInputStream.java

//文件描述符?

private final FileDeor fd;?

public FileInputStream(?File file)? throws? FileNotFoundException?{?

? ? ? String? name? =? file !=?null ? file.getPath() :?null;

? ? ? ? ...

? ? ? //傳入name煌贴,構(gòu)造FileDeor

? ? ? //沒有權(quán)限訪問御板,則此處拋出異常

? ? ? ?fd = IoBridge.open(name, O_RDONLY);

? ? ? ?...

}

可以看出,要想FileInputStream 能讀入文件牛郑,核心是需要構(gòu)造FileDeor怠肋,而對(duì)于Android 10.0,直接通過路徑構(gòu)造FileDeor 會(huì)拋出異常淹朋。

那么我們自然會(huì)想到笙各,有沒有通過構(gòu)造好的FileDeor 來生成FileInputStream對(duì)象钉答,進(jìn)而使用read(xx)方法讀取數(shù)據(jù)。

還真有杈抢,請(qǐng)看:通過Uri構(gòu)造InputStream数尿。

InputStream? fis = getContentResolver.openInputStream(uri);

進(jìn)入看其源碼:

#ContentResolver.java

public final @?Nullable? InputStream?openInputStream( @NonNull Uri uri)? throws FileNotFoundException{?

? ? ? ?...

? ? ? ?if(SCHEME_ANDROID_RESOURCE.?equals(scheme)) {

? ? ? ? ...

? ? ? ? }else if(SCHEME_FILE.?equals(scheme)) {

? ? ? ? ?...

? ? ? ? }?else{

? ? ? ? ? ? ?//通過Uri構(gòu)造fd是被允許的

? ? ? ? ? ? ?AssetFileDeor fd = openAssetFileDeor(uri,?"r",?null);

? ? ? ? ? ? ?try{

? ? ? ? ? ? ?//反過來創(chuàng)建 InputStream?

? ? ? ? ? ? ?return fd !=?null? fd.createInputStream() :?null;?

? ? ? ? ? ? }?catch(IOException e) {?

? ? ? ? ? ? throw? newFileNotFoundException(?"Unable to create stream");?

? ? ? ? ? ?}

? ? ? }

}

AssetFileDeor 持有ParcelFileDeor 引用,而ParcelFileDeor 持有FileDeor 引用惶楼。

同理也適用于FileOutputStream右蹦。因此,通過Uri能夠訪問文件歼捐。

四何陆、如何適配Android 10.0

從以上分析可知,適配Android 10.0 有點(diǎn)麻煩豹储,問題來了有沒有簡(jiǎn)單的方法繞過檢測(cè)贷盲。

第一種方法

1、Android 10.0 及其以后才會(huì)有分區(qū)存儲(chǔ)功能剥扣,只要Android 設(shè)備不升級(jí)系統(tǒng)到Android 10.0以后巩剖,就不會(huì)有問題。

2朦乏、可能覺得這是句廢話球及,其實(shí)不然,有些定制的設(shè)備系統(tǒng)一般都不會(huì)升級(jí)的呻疹。

如果不能使用第一種方法吃引,還可以采用第二種方法。

第二種方法

1刽锤、Android 一般升級(jí)功能的時(shí)候都會(huì)配合targetSdkVersion使用镊尺。只要targetSdkVersion<=28,分區(qū)存儲(chǔ)功能就不會(huì)開啟并思。

有關(guān)targetSdkVersion 作用請(qǐng)移步:targetSdkVersion庐氮、compileSdkVersion、minSdkVersion作用與區(qū)別

如果第二種方法也不能使用宋彼,則還有第三種方法弄砍。

第三種方法

在AndroidManifest.xml 里application標(biāo)簽下添加:

android:requestLegacyExternalStorage="true" 可禁用分區(qū)存儲(chǔ)

從長(zhǎng)遠(yuǎn)的角度看,以上三個(gè)方法都不是一勞永逸的方法输涕,其中第二種音婶、第三種方法是Google 留給App開發(fā)者適配的緩沖時(shí)間。

對(duì)于第二種方法:

Google 在App上架App Store 時(shí)候可能會(huì)強(qiáng)制要求升級(jí)targetSdkVersion莱坎,因此該方法不保險(xiǎn)衣式。

對(duì)于第三種方法:

在Android 11會(huì)忽略該字段,強(qiáng)制開啟分區(qū)存儲(chǔ),該字段也不怎么靠譜碴卧。

因此弱卡,最終還是需要老老實(shí)實(shí)按照Google 的要求適配Android 10.0,下篇將重點(diǎn)分析Android 10.0/11 該如何來適配住册。

本文基于Android 10.0婶博。

五、MediaStore 基本知識(shí)

上篇已經(jīng)分析得出結(jié)論界弧,Android 10.0 存儲(chǔ)訪問方式變更地方在于:

自帶外部存儲(chǔ)-共享存儲(chǔ)空間和自帶外部存儲(chǔ)-其它目錄

以上兩個(gè)地方不能通過路徑直接訪問文件凡蜻,而是需要通過Uri訪問搭综。

共享存儲(chǔ)空間

共享存儲(chǔ)空間存放的是圖片、視頻、音頻等文件溪厘,這些資源是公用的饰剥,所有App都能夠訪問它們。

系統(tǒng)里有external.db數(shù)據(jù)庫蒋歌,該數(shù)據(jù)庫里有files表帅掘,該表里存放著共享文件的諸多信息,如圖片有寬高堂油,經(jīng)緯度修档、存放路徑等,視頻寬高府框、時(shí)長(zhǎng)吱窝、存放路徑等。而文件真正存放的地方在于共享存儲(chǔ)空間迫靖。

1院峡、保存圖片到相冊(cè)

當(dāng)App1保存圖片到相冊(cè)時(shí),簡(jiǎn)單流程如下:

1系宜、將路徑信息寫入數(shù)據(jù)庫里照激,并獲取Uri

2、通過Uri構(gòu)造輸出流

3盹牧、將該圖片保存在/sdcard/Pictures/目錄下

2俩垃、從相冊(cè)獲取圖片

當(dāng)App2從相冊(cè)獲取圖片時(shí),簡(jiǎn)單流程如下:

1汰寓、先查詢數(shù)據(jù)庫口柳,找到對(duì)應(yīng)的圖片Cursor

2、從Cursor里構(gòu)造Uri

3踩寇、從Uri構(gòu)造輸入流讀取圖片

以上以圖片為例簡(jiǎn)單分析了共享存儲(chǔ)空間文件的寫入與讀取啄清,實(shí)際上對(duì)于視頻、音頻步驟亦是如此。

MediaStore作用

共享存儲(chǔ)空間里存放著圖片辣卒、視頻掷贾、音頻、下載的文件荣茫,App獲取或者插入文件的時(shí)候怎么區(qū)分這些類型呢想帅?

這個(gè)時(shí)候就需要MediaStore,來看看MediaStore.java


可以看出其內(nèi)部有Audio啡莉、Images等內(nèi)部類港准,這些內(nèi)部類里記錄著files表的各個(gè)字段名,通過構(gòu)造這些參數(shù)就可以插入相應(yīng)的字段值以及獲取對(duì)應(yīng)的字段值咧欣。

MediaStore 實(shí)際上就是相當(dāng)于給各個(gè)字段起了別名浅缸,我們編碼的時(shí)候更容易記住與使用:

//列舉一些字段:

//圖片類型? MediaStore.Images.Media.MIME_TYPE

//音頻時(shí)長(zhǎng)? MediaStore.Audio.Media.DURATION

//視頻時(shí)長(zhǎng)? MediaStore.Video.Media.DURATION

//等等,還有很多

MediaStore和Uri聯(lián)系

比如想要查詢共享存儲(chǔ)空間里的圖片文件:

Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,?null,?null,?null,?null);

MediaStore.Images.Media.EXTERNAL_CONTENT_URI 意思是指定查詢文件的類型是圖片魄咕,并構(gòu)造成Uri對(duì)象衩椒,Uri實(shí)現(xiàn)了Parcelable,能夠在進(jìn)程間傳遞哮兰。

接收方(另一個(gè)進(jìn)程收到后)毛萌,匹配Uri,解析出對(duì)應(yīng)的字段喝滞,進(jìn)行具體的操作阁将。

當(dāng)然,MediaStore是系統(tǒng)提供的方便操作共享存儲(chǔ)空間的類右遭,若是自己寫ContentProvider做盅,則也可以自定義類似MediaStore的類用來標(biāo)記自己的數(shù)據(jù)庫表的字段。

六狸演、通過Uri讀取和寫入文件

既然不能通過路徑直接訪問文件言蛇,那么來看看如何通過Uri訪問文件。在上篇文章里提到過:?Uri可以通過MediaStore或者SAF獲取宵距。(此處需要注意的是:雖然也可以通過文件路徑直接構(gòu)造Uri腊尚,但是此種方式構(gòu)造的Uri是沒有權(quán)限訪問文件的)

先來看看通過SAF獲取Uri。

從Uri讀取文件

現(xiàn)在/sdcard/目錄下存在一個(gè)文件名為:mytest.txt满哪。

該文件內(nèi)容是:

傳統(tǒng)的直接讀取mytest.txt方法:

//從文件讀取

private? void? readFile(?String filePath)?{?

? ? ?if( TextUtils.isEmpty( filePath) ) return;

? ? try{?

? ? ? ? ? ?File file =?newFile(filePath);?

? ? ? ? ? ?FileInputStream fileInputStream =?newFileInputStream(file);?

? ? ? ? ? ?BufferedInputStream? bis =?new? BufferedInputStream(fileInputStream);?

? ? ? ? ? ?byte[]? readContent =?new byte[?1024];?

? ? ? ? ? ?int? readLen =?0;?

? ? ? ? ? ?while(readLen !=?-1) {

? ? ? ? ? ? ? ? readLen = bis.read( readContent,?0, readContent.length);

? ? ? ? ? ? ? ?if ( readLen >?0) {?

? ? ? ? ? ? ? ? ? ?String content =?newString(readContent);?

? ? ? ? ? ? ? ? ? ?Log.d(?"test",?"read content:"+ content.substring(?0, readLen));?

? ? ? ? ? ? ? }

? ? ? ? ? }

? ? ? ? ? ?fileInputStream.close;

? ? ? }? catch(Exception e) {

? ? ? }

}

開啟分區(qū)存儲(chǔ)功能后婿斥,這種方法是不可取的,會(huì)報(bào)權(quán)限錯(cuò)誤哨鸭。

而mytest.txt不屬于共享存儲(chǔ)空間的文件民宿,是屬于其它目錄的,因此不能通過MediaStore獲取像鸡,只能通過SAF獲取活鹰,如下:

private void? startSAF{

? ? ?Intent intent =?new Intent( Intent.ACTION_OPEN_DOCUMENT);?

? ? ? intent.addCategory(Intent.CATEGORY_OPENABLE);

? ? ? //指定選擇文本類型的文件? ? ? ??

? ? ? intent.setType(?"text/plain");

? ? ? startActivityForResult(intent,?100);?

}

@Override

protected void? onActivityResult(?intrequestCode,?intresultCode, @Nullable Intent data)?{?

? ? ? ? ? ?super.onActivityResult(requestCode, resultCode, data);

? ? ? ? ? ?if(requestCode ==?100) {?

? ? ? ? ? ? // 選中返回的文件信息封裝在Uri?里

? ? ? ? ? ? Uri uri = data.getData;

? ? ? ? ? ?openUriForRead(uri);

? ? ? ? ? }

}

拿到Uri后,用來構(gòu)造輸入流讀取文件。

private void openUriForRead(?Uri uri)?{?

? ? ?if(uri ==?null)?return;

? ? ?try{?

? ? ?//獲取輸入流

? ? ?InputStream inputStream = getContentResolver.openInputStream(uri);

? ? ?byte[] readContent =?newbyte[?1024];?

? ? ?int len =?0;?

? ? ? ? ? do{?

? ? ? ? ? ?//讀文件

? ? ? ? ? ?len = inputStream.read(readContent);

? ? ? ? ? ? if(len !=?-1) {

? ? ? ? ? ? Log.d(?"test",?"read content:"+?newString(readContent).substring(?0, len));?

? ? ? ? ? ? ? }

? ? ? ? ? ? ?}?while(len !=?-1);

? ? ? ? ? ? ? ? inputStream.close;

? ? ? ? ? ?}?catch(Exception e) {

? ? ? ? ?Log.d(?"test", e.getLocalizedMessage);?

? ? ? }

}

最終輸出:

由此可以看出志群,mytest.txt屬于"其它目錄"下的文件着绷,因此需要通過SAF訪問,SAF返回Uri锌云,通過Uri構(gòu)造InputStream即可讀取文件荠医。

從Uri寫入文件

繼續(xù)來看看寫的過程,現(xiàn)在需要往mytest.txt寫入內(nèi)容桑涎。

同樣的彬向,還是需要通過SAF拿到Uri,拿到Uri后構(gòu)造輸出流:

private void? openUriForWrite(?Uri uri)?{?

? ? ? ? ?if(uri ==?null) {?return;?}

? ? ? ? ?try{?

? ? ? ? ? ? ? ?//從uri構(gòu)造輸出流

? ? ? ? ? ? ? ?OutputStream outputStream = getContentResolver.openOutputStream(uri);

? ? ? ? ? ? ? ? //待寫入的內(nèi)容

? ? ? ? ? ? ? ? String content =?"hello world I'm from SAF\n";

? ? ? ? ? ? ? ? //寫入文件

? ? ? ? ? ? ? ?outputStream.write(content.getBytes);

? ? ? ? ? ? ? ? outputStream.flush;

? ? ? ? ? ? ? ? outputStream.close;

? ? ? ? ? ? ?}?catch(Exception e) {

? ? ? ? ? ? ? Log.d(?"test", e.getLocalizedMessage);

? ? ? }

}

最后來看看文件是否寫入成功攻冷,通過SAF再次讀取mytest.txt娃胆,發(fā)現(xiàn)正好是之前寫入的內(nèi)容,說明寫入成功讲衫。

七缕棵、通過Uri 獲取圖片和插入相冊(cè)

上面列舉出了其它目錄下文件的讀寫,方法是通過SAF拿到Uri涉兽。

SAF好處是:

系統(tǒng)提供了文件選擇器,調(diào)用者只需要指定想要讀寫的文件類型篙程,比如文本類型枷畏、圖片類型、視頻類型等虱饿,選擇器就會(huì)過濾出相應(yīng)文件以供選擇拥诡。接入方便,選擇簡(jiǎn)單氮发。

想想另一種場(chǎng)景:

想要自己實(shí)現(xiàn)相冊(cè)選擇器渴肉,那么就需要獲得共享存儲(chǔ)空間下的文件信息。此種場(chǎng)景下使用SAF是無法做到的爽冕。

因此問題的關(guān)鍵是:?如何批量獲得共享存儲(chǔ)空間下圖片/視頻的信息仇祭?

答案是:ContentResolver+ContentProvider+MediaStore(ContentProvider對(duì)于調(diào)用者是透明的)。

以圖片為例颈畸,分析插入與查詢方式乌奇。

插入相冊(cè)

來看看圖片的插入過程:

//fileName為需要保存到相冊(cè)的圖片名

private void? insert2Album(InputStream inputStream,?StringfileName) {

? ? ? if(inputStream ==?null)?return;

? ? ?ContentValues contentValues =?new ContentValues;?

? ? ? contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName);

? ? ? if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {?

? ? ? ? ? ? ? //? RELATIVE_PATH 字段表示相對(duì)路徑-------->(1)

? ? ? ? ? ? ? contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);

? ? ? ?}?else{?

? ? ? ? ? ? ? Stringd? stPath = Environment.getExternalStorageDirectory + File.separator + Environment.DIRECTORY_PICTURES?+ File.separator + fileName;

? ? ? ? ? ?? ???//DATA字段在Android 10.0 之后已經(jīng)廢棄

? ? ? ? ? ? ? contentValues.put(MediaStore.Images.ImageColumns.DATA, dstPath);

? ? ? ?}

? ? //插入相冊(cè)------->(2)

? ? ? ? ? ? ? Uri uri = getContentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);

? ? //寫入文件------->(3)

? ? ? ? ? ?write2File(uri, inputStream);

}

重點(diǎn)說明三個(gè)點(diǎn):

(1)

Android 10.0之前,MediaStore.Images.ImageColumns.DATA 字段記錄的是圖片的絕對(duì)路徑眯娱,而Android 10.0(含)之后礁苗,DATA 被廢棄,取而代之的是使用MediaStore.Images.ImageColumns.RELATIVE_PATH徙缴,表示相對(duì)路徑试伙。比如指定RELATIVE_PATH為Environment.DIRECTORY_PICTURES,表示之后的圖片將會(huì)放到Environment.DIRECTORY_PICTURES目錄下。

(2)

調(diào)用ContentResolver里的方法插入相冊(cè)疏叨。

MediaStore.Images.Media.EXTERNAL_CONTENT_URI 指的是插入圖片表吱抚。

ContentValues 以Map的形式記錄了待寫入的字段值。

插入后返回Uri考廉。

(3)

以上兩步僅僅只是往數(shù)據(jù)庫里增加一條記錄秘豹,該記錄指向的新文件是空的,需要將圖片寫入到新文件昌粤。

而新文件位于/sdcard/Pictures/目錄下既绕,該目錄是不能直接通過路徑訪問的,因此需要通過第二步返回的Uri進(jìn)行訪問涮坐。

//? uri 關(guān)聯(lián)著待寫入的文件

//inputStream 表示原始的文件流

private void? write2File(?Uri uri, InputStream inputStream)?{?

? ? ? if(uri ==?null? || inputStream ==?null)?return;

? ? ? try{?

? ? ?//從Uri構(gòu)造輸出流

? ? ? ? OutputStream outputStream = getContentResolver.openOutputStream(uri);

? ? ? ? byte[]?in=?new byte[1024];?

? ? ? ? int len =?0;

? ? ? ?do{?

? ? ? ? ? ?//從輸入流里讀取數(shù)據(jù)

? ? ? ? ? ?len = inputStream.read(?in);?

? ? ? ? ? ? if(len !=?-1) {?

? ? ? ? ? ? ? outputStream.write(?in,?0, len);?

? ? ? ? ? ? ? outputStream.flush;

? ? ? ? ? ? }

? ? ? ? ?}?while(len !=?-1);

? ? ? ? ?inputStream.close;

? ? ? ? ?outputStream.close;

? ? }?catch(Exception e) {

? ? ? ? Log.d(?"test", e.getLocalizedMessage);?

? ? ?}

}

可以看出凄贩,目標(biāo)文件關(guān)聯(lián)的Uri有了,還需要原始的輸入文件袱讹。

測(cè)試上述的插入方法:

private? void? testInsert(?)?{

? ? ? String picName =?"mypic.jpg";?

? ? ? try{

? ? ? ? ? ?File? externalFilesDir = getExternalFilesDir(?null);?

? ? ? ? ? ?File? file =?newFile(externalFilesDir, picName);?

? ? ? ? ? ?FileInputStream fis =?newFileInputStream(file);?

? ? ? ? ? ?insert2Album(fis, picName);

? ? ? ? }?catch( Exception e) {?

? ? ? Log.d(?"test", e.getLocalizedMessage);?

? ? ? ?}

}

其中疲扎,原始文件(圖片)存放于自帶外部存儲(chǔ)-App私有目錄,如下:

需要注意的是:

1捷雕、讀取原始文件需要權(quán)限椒丧,上述例子里的原始文件存放在自帶外部存儲(chǔ)-App私有目錄,因此本App可以使用路徑直接讀取

2救巷、對(duì)于其他目錄則依然需要構(gòu)造Uri讀取壶熏,如通過SAF獲取Uri

獲取圖片

同樣的,想要從系統(tǒng)相冊(cè)中獲取圖片浦译,也需要通過Uri訪問棒假。

private? void? queryImageFromAlbum(?)?{?

? ? ? ? ?Cursor cursor = getContentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,?null,?null,?null,?null);

? ? ? ? ?if( cursor !=?null ) {?

? ? ? ? ? ? ? while( cursor.moveToNext() ) {

? ? ? ? ? ? ? ? ? ? ?//獲取唯一的id??

? ? ? ? ? ? ? ? ? ? ? long? id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));?

? ? ? ? ? ? ? ? ? ? ?//通過id構(gòu)造Uri??

? ? ? ? ? ? ? ? ? ? ? ?Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);

? ? ? ? ? ? ? ? ? ? ? ?//解析uri

? ? ? ? ? ? ? ? ? ? ?decodeUriForBitmap(uri);

? ? ? ? ? ? ? ? ?}

? ? ? ? ? ?}

?}

private? void? ?decodeUriForBitmap(?Uri uri)?{?

? ? ? ? ?if(uri ==?null)?return;

? ? ? ? ?try{?

? ? ? ? ? ? ? //構(gòu)造輸入流

? ? ? ? ? ? ? InputStream? inputStream = getContentResolver.openInputStream(uri);

? ? ? ? ? ? ?//解析Bitmap

? ? ? ? ? ? ? Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

? ? ? ? ? ? ? if( bitmap !=?null )?Log.d(?"test",?"bitmap width-width:"+ bitmap.getWidth +?"-"+ bitmap.getHeight);?

? ? ? ? ? }?catch(Exception e) {?

? ? ? ? ? ? ? ?Log.d(?"test", e.getLocalizedMessage);?

? ? ? ? ? }

}

與插入相冊(cè)過程類似,同樣需要拿到Uri精盅,再構(gòu)造輸入流帽哑,從輸入流讀取文件(圖片內(nèi)容)。

以上叹俏,通過Uri 獲取圖片和插入相冊(cè)分析完畢妻枕,共享存儲(chǔ)空間的其他文件類型如視頻、音頻她肯、下載文件也是同樣的流程佳头。

需要說明的是上述的ContentResolver .insert(xx)或ContentResolver.query(xx) 的參數(shù)取值還可以更豐富,但不是本篇重點(diǎn)晴氨,因此忽略了康嘉,實(shí)際使用過程中具體情況具體分析。

八籽前、Android 11.0 權(quán)限申請(qǐng)

通過Uri訪問文件似乎已經(jīng)滿足了Android 10.0適配要求亭珍,但是仔細(xì)想想還是有不足之處:

1敷钾、共享存儲(chǔ)空間只能通過MediaStore訪問,以前流行的訪問方式是直接通過路徑訪問肄梨。比如自己做的相冊(cè)管理器阻荒,先遍歷相冊(cè)拿到圖片/視頻的路徑,然后再解析成Bitmap展示众羡,現(xiàn)在需要先拿到Uri侨赡,再解析成Bitmap,多少有些不方便粱侣。此外羊壹,也許你依賴的第三方庫是直接通過路徑訪問文件的,而三方庫又沒有及時(shí)更新適配分區(qū)存儲(chǔ)齐婴,可能就會(huì)導(dǎo)致用不了相應(yīng)的功能油猫。

2、SAF雖然能夠訪問其它目錄的文件柠偶,但是每次都需要跳轉(zhuǎn)到新的頁面去選擇情妖,當(dāng)想要批量展示文件的時(shí)候,比如自己做的文件管理器诱担,就需要列出當(dāng)前目錄下有哪些目錄/文件毡证,這個(gè)時(shí)候需要有權(quán)限遍歷/sdcard/目錄。顯然该肴,SAF并不能勝任此工作情竹。

Android 11.0考慮到上面的問題,因此做了新的優(yōu)化匀哄。

共享存儲(chǔ)空間-媒體文件訪問變更

媒體文件可以通過路徑直接訪問:

private void? getImagePath(?Context context)?{?

? ? ? ? ?ContentResolver contentResolver = context.getContentResolver();

? ? ? ? ? Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,?null,?null,?null,?null);?

? ? ? ? ? while(cursor.moveToNext) {

? ? ? ? ? ? ? ? ?try{

? ? ? ? ? ? ? ? ? ? ?//取出路徑

? ? ? ? ? ? ? ? ? ? String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));

? ? ? ? ? ? ? ? ? ? ?Bitmap bitmap = BitmapFactory.decodeFile(path);

? ? ? ? ? ? ? ? ? ?}?catch(Exception e) {?

? ? ? ? ? ? ? ? ? ? ? ? Log.d(?"test", e.getLocalizedMessage);?

? ? ? ? ? ? ? ? ? ?}

? ? ? ? ? ? ? ? ? break;?

? ? ? ? ? ?}

}

可以看出,之前在Android 10.0上被禁用的訪問方式雏蛮,在Android 11.0上又被允許了涎嚼,這就解決了上面的第一個(gè)問題。

需要注意的是:此種方式只允許讀文件挑秉,寫文件依然不行

Google 官方指導(dǎo)意見是:

雖然可以通過路徑直接訪問媒體文件法梯,但是這些操作最終是被重定向到MediaStore API的,重定向過程可能會(huì)損耗一些性能犀概,并且直接通過路徑訪問不一定比MediaStore API 訪問快立哑。

總之建議非必要的話不要直接使用路徑訪問。

訪問所有文件

假若App開啟了分區(qū)存儲(chǔ)功能姻灶,當(dāng)App運(yùn)行在Android 10.0的設(shè)備上時(shí)铛绰,是沒法遍歷/sdcard/目錄的。而在Android 11.0上運(yùn)行時(shí)是可以遍歷的产喉,需要進(jìn)行如下幾個(gè)步驟捂掰。

1敢会、聲明管理權(quán)限

在AndroidManifest.xml添加權(quán)限聲明

<?uses-permissionandroid:name=?"android.permission.MANAGE_EXTERNAL_STORAGE"/>

2、動(dòng)態(tài)申請(qǐng)所有文件訪問權(quán)限

private? void? testAllFiles{?

? ? ? ? ? ?//運(yùn)行設(shè)備>=Android 11.0

? ? ? ? ? if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {?

? ? ? ? ? ? ? //檢查是否已經(jīng)有權(quán)限

? ? ? ? ? ? ? ? ? ? ?if(!Environment.isExternalStorageManager) {?

? ? ? ? ? ? ? ? ? ? ?//跳轉(zhuǎn)新頁面申請(qǐng)權(quán)限? ? ? ? ? ? ?

? ? ? ? ? ? ? ? ? ? ?startActivityForResult(?new? Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION),?101);?

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ?}

}

@Override

protected void? onActivityResult(?intrequestCode,?intresultCode, @Nullable Intent data)?{?

? ? ? super.onActivityResult(requestCode, resultCode, data);?

? ? ? //申請(qǐng)權(quán)限結(jié)果

? ? ?if(requestCode ==?101) {?

? ? ? ? ? ? if(Environment.isExternalStorageManager()) {?

? ? ? ? ? ? ? ? ? Toast.makeText(MainActivity.?this,?"訪問所有文件權(quán)限申請(qǐng)成功", Toast.LENGTH_SHORT).show();

? ? ? ? ? ? ? ? ?//遍歷目錄

? ? ? ? ? ? ? ? ?showAllFiles();

? ? ? ? ? ? ?}

? ? ? ?}

}

此處申請(qǐng)權(quán)限不是以對(duì)話框的形式提示用戶这嚣,而是跳轉(zhuǎn)到新的頁面鸥昏,說明該權(quán)限的管理更嚴(yán)格。

3姐帚、遍歷目錄吏垮、讀寫文件

擁有權(quán)限后,就可以進(jìn)行相應(yīng)的操作了罐旗。

private void showAllFiles(){?

? ? ? ? ? File file = Environment.getExternalStorageDirectory();

? ? ? ? ? File[]?list= file.listFiles();?

? ? ? ? ? for(?inti =?0; i <?list.length; i++) {?

? ? ? ? ? ? ? String name =?list[i].getName;

? ? ? ? ? ? ? Log.d(?"test",?"fileName:"+ name);?

? ? ? ? ?}

?}

文件管理器效果圖類似如下:

當(dāng)然讀寫文件也不在話下了膳汪,比如往/sdcard/目錄下寫入文件:

private? void? testPublicFile(?)?{?

? ? ? ? ?File rootFile = Environment.getExternalStorageDirectory();

? ? ? ? ?try{?

? ? ? ? ? ? ? ? File file =?newFile(rootFile,?"mytest.txt");?

? ? ? ? ? ? ? ? FileOutputStream fos =?newFileOutputStream(file);?

? ? ? ? ? ? ? ? String content =?"hello world\n";?

? ? ? ? ? ? ? ? fos.write(content.getBytes);

? ? ? ? ? ? ? ? fos.flush;

? ? ? ? ? ? ? ? fos.close;

? ? ? ? ? ?}?catch(Exception e) {

? ? ? ? ? ? ? ? ? Log.d(?"test", e.getLocalizedMessage);?

? ? ? ? ? ?}

}

ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 這個(gè)權(quán)限的名字看起來很唬人,感覺就像是能夠操作所有文件的樣子尤莺,這不就是打破了分區(qū)存儲(chǔ)的規(guī)則了嗎旅敷?其實(shí)不然:

即使擁有了該權(quán)限,依然不能訪問內(nèi)部存儲(chǔ)和外部存儲(chǔ)-App私有目錄

需要說明的是:

1颤霎、Environment.isExternalStorageManager()媳谁、Build.VERSION_CODES.R 等需要編譯版本>=30才能編譯通過。

2友酱、Google 提示當(dāng)使用MANAGE_EXTERNAL_STORAGE 申請(qǐng)權(quán)限時(shí)晴音,并且targetSdkVersion>=30,此種情況下App被禁止上架Google Play的缔杉,限制時(shí)間最早到2021年锤躁。因此,在此時(shí)間之前若是申請(qǐng)了MANAGE_EXTERNAL_STORAGE權(quán)限或详,最好不要升級(jí)targetSdkVersion到30以上系羞。

九、Android 10/11 存儲(chǔ)適配建議

好了霸琴,通過分析Android 10/11存儲(chǔ)適配方式椒振,了解到了不同的系統(tǒng)需要如何進(jìn)行適配,此時(shí)就需要一個(gè)統(tǒng)一的適配方案了梧乘。

適配核心

分區(qū)存儲(chǔ)是核心澎迎,App自身產(chǎn)生的文件應(yīng)該存放在自己的目錄下:

/sdcard/Android/data/packagename/ 和/data/data/packagename/

這兩個(gè)目錄本App無需申請(qǐng)?jiān)L問權(quán)限即可申請(qǐng),其它App無法訪問本App的目錄选调。

適配共享存儲(chǔ)

共享存儲(chǔ)空間里的文件需要通過Uri構(gòu)造輸入輸出流訪問夹供,Uri獲取方式有兩種:MediaStore和SAF。

適配其它目錄

在Android 11上需要申請(qǐng)?jiān)L問所有文件的權(quán)限仁堪。

具體做法

第一步

在AndroidManifest.xml里添加如下字段:

權(quán)限聲明:

?<?uses-permissionandroid:name=?"android.permission.WRITE_EXTERNAL_STORAGE"/>?

?<?uses-permissionandroid:name=?"android.permission.READ_EXTERNAL_STORAGE"/>?

?<?uses-permissionandroid:name=?"android.permission.MANAGE_EXTERNAL_STORAGE"/>

在<application/>標(biāo)簽下添加如下字段:

android:requestLegacyExternalStorage=?"true"

第二步

如果需要訪問共享存儲(chǔ)空間哮洽,則判斷運(yùn)行設(shè)備版本是否大于等于Android6.0,若是則需要申請(qǐng)WRITE_EXTERNAL_STORAGE 權(quán)限枝笨。拿到權(quán)限后袁铐,通過Uri訪問共享存儲(chǔ)空間里的文件揭蜒。

如果需要訪問其它目錄,則通過SAF訪問

第三步

如果想要做文件管理器剔桨、病毒掃描管理器等功能屉更。則判斷運(yùn)行設(shè)備版本是否大于等于Android 6.0,若是先需要申請(qǐng)普通的存儲(chǔ)權(quán)洒缀。若運(yùn)行設(shè)備版本為Android 10.0瑰谜,則可以直接通過路徑訪問/sdcard/目錄下文件(因?yàn)榻昧朔謪^(qū)存儲(chǔ));若運(yùn)行設(shè)備版本為Android 11.0树绩,則需要申請(qǐng)MANAGE_EXTERNAL_STORAGE 權(quán)限萨脑。

以上是Android 存儲(chǔ)權(quán)限適配的全部?jī)?nèi)容。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饺饭,一起剝皮案震驚了整個(gè)濱河市渤早,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瘫俊,老刑警劉巖鹊杖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異扛芽,居然都是意外死亡骂蓖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門川尖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來登下,“玉大人,你說我怎么就攤上這事叮喳”环迹” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵馍悟,是天一觀的道長(zhǎng)筐钟。 經(jīng)常有香客問我,道長(zhǎng)赋朦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任李破,我火速辦了婚禮宠哄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嗤攻。我一直安慰自己毛嫉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布妇菱。 她就那樣靜靜地躺著承粤,像睡著了一般暴区。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辛臊,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天仙粱,我揣著相機(jī)與錄音,去河邊找鬼彻舰。 笑死伐割,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的刃唤。 我是一名探鬼主播隔心,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼尚胞!你這毒婦竟也來了硬霍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤笼裳,失蹤者是張志新(化名)和其女友劉穎唯卖,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侍咱,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡耐床,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了楔脯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撩轰。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖昧廷,靈堂內(nèi)的尸體忽然破棺而出堪嫂,到底是詐尸還是另有隱情,我是刑警寧澤木柬,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布皆串,位于F島的核電站,受9級(jí)特大地震影響眉枕,放射性物質(zhì)發(fā)生泄漏恶复。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一速挑、第九天 我趴在偏房一處隱蔽的房頂上張望谤牡。 院中可真熱鬧,春花似錦姥宝、人聲如沸翅萤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽套么。三九已至培己,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間胚泌,已是汗流浹背省咨。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留诸迟,地道東北人茸炒。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像阵苇,于是被迫代替她去往敵國(guó)和親壁公。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容