日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

安卓 sharedpreferences可以被其它activity读取_Google|再见 SharedPreferences 拥抱 Jetpack DataStore...

發布時間:2023/12/2 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 安卓 sharedpreferences可以被其它activity读取_Google|再见 SharedPreferences 拥抱 Jetpack DataStore... 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Google 新增加了一個新 Jetpack 的成員 DataStore,主要用來替換 SharedPreferences, DataStore 應該是開發者期待已久的庫,DataStore 是基于 Flow 實現的,一種新的數據存儲方案,它提供了兩種實現方式:

  • Proto DataStore:存儲類的對象(typed objects ),通過 protocol buffers 將對象序列化存儲在本地,protocol buffers 現在已經應用的非常廣泛,無論是微信還是阿里等等大廠都在使用,我們在部分業務場景中也用到了 protocol buffers,會在后續的文章詳細分析
  • Preferences DataStore:以鍵值對的形式存儲在本地和 SharedPreferences 類似,但是 DataStore 是基于 Flow 實現的,不會阻塞主線程,并且保證類型安全

Jetpack DataStore 將會分為至少 2 篇文章來分析,今天這篇文章主要來介紹 Jetpack DataStore 其中一種實現方式 Preferences DataStore,文章中的示例代碼,已經上傳到 GitHub 歡迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple。

GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

這篇文章會涉及到 Koltin flow 相關內容,如果不了解可以先去看另外一篇文章 Kotlin Flow 是什么?Channel 是什么?

通過這篇文章你將學習到以下內容:

  • 那些年我們所經歷的 SharedPreferences 坑?
  • 為什么需要 DataStore?它為我們解決了什么問題?
  • 如何在項目中使用 DataStore?
  • 如何遷移 SharedPreferences 到 DataStore?
  • MMKV、DataStore、SharedPreferences 的不同之處?

一個新庫的出現必定為我們解決了一些問題,那么 Jetpack DataStore 為我們解決什么問題呢,在分析之前,我們需要先來了解 SharedPreferences 都有那些坑。

那些年我們所經歷的 SharedPreferences 坑

SharedPreference 是一個輕量級的數據存儲方式,使用起來也非常方便,以鍵值對的形式存儲在本地,初始化 SharedPreference 的時候,會將整個文件內容加載內存中,因此會帶來以下問題:

  • 通過 getXXX() 方法獲取數據,可能會導致主線程阻塞
  • SharedPreference 不能保證類型安全
  • SharedPreference 加載的數據會一直留在內存中,浪費內存
  • apply() 方法雖然是異步的,可能會發生 ANR,在 8.0 之前和 8.0 之后實現各不相同
  • apply() 方法無法獲取到操作成功或者失敗的結果

接下來我們逐個來分析一下 SharedPreferences 帶來的這些問題,在文章中 SharedPreference 簡稱 SP。

getXXX() 方法可能會導致主線程阻塞

所有 getXXX() 方法都是同步的,在主線程調用 get 方法,必須等待 SP 加載完畢,會導致主線程阻塞,下面的代碼,我相信小伙伴們并不陌生。

val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 異步加載 SP 文件內容 sp.getString("jetpack", ""); // 等待 SP 加載完畢

調用 getSharedPreferences() 方法,最終會調用 SharedPreferencesImpl#startLoadFromDisk() 方法開啟一個線程異步讀取數據。 frameworks/base/core/java/android/app/SharedPreferencesImpl.java

private final Object mLock = new Object(); private boolean mLoaded = false; private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start(); }

正如你所看到的,開啟一個線程異步讀取數據,當我們正在讀取一個比較大的數據,還沒讀取完,接著調用 getXXX() 方法。

public String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;} }private void awaitLoadedLocked() {......while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}...... }

在同步方法內調用了 wait() 方法,會一直等待 getSharedPreferences() 方法開啟的線程讀取完數據才能繼續往下執行,如果讀取幾 KB 的數據還好,假設讀取一個大的文件,勢必會造成主線程阻塞。

SP 不能保證類型安全

調用 getXXX() 方法的時候,可能會出現 ClassCastException 異常,因為使用相同的 key 進行操作的時候,putXXX 方法可以使用不同類型的數據覆蓋掉相同的 key。

val key = "jetpack" val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 異步加載 SP 文件內容sp.edit { putInt(key, 0) } // 使用 Int 類型的數據覆蓋相同的 key sp.getString(key, ""); // 使用相同的 key 讀取 Sting 類型的數據

使用 Int 類型的數據覆蓋掉相同的 key,然后使用相同的 key 讀取 Sting 類型的數據,編譯正常,但是運行會出現以下異常。

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

SP 加載的數據會一直留在內存中

通過 getSharedPreferences() 方法加載的數據,最后會將數據存儲在靜態的成員變量中。

// 調用 getSharedPreferences 方法,最后會調用 getSharedPreferencesCacheLocked 方法 public SharedPreferences getSharedPreferences(File file, int mode) {......final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();return sp; }// 通過靜態的 ArrayMap 緩存 SP 加載的數據 private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;// 將數據保存在 sSharedPrefsCache 中 private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {......ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);if (packagePrefs == null) {packagePrefs = new ArrayMap<>();sSharedPrefsCache.put(packageName, packagePrefs);}return packagePrefs; }

通過靜態的 ArrayMap 緩存每一個 SP 文件,而每個 SP 文件內容通過 Map 緩存鍵值對數據,這樣數據會一直留在內存中,浪費內存。

apply() 方法是異步的,可能會發生 ANR

apply() 方法是異步的,為什么還會造成 ANR 呢?曾今的字節跳動就出現過這個問題,具體詳情可以點擊這里前去查看 剖析 SharedPreference apply 引起的 ANR 問題 而且 Google 也明確指出了 apply() 的問題。

簡單總結一下:apply() 方法是異步的,本身是不會有任何問題,但是當生命周期處于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的時候會一直等待 apply() 方法將數據保存成功,否則會一直等待,從而阻塞主線程造成 ANR,一起來分析一下為什么異步方法還會阻塞主線程,先來看看 apply() 方法的實現。 frameworks/base/core/java/android/app/SharedPreferencesImpl.java

public void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {mcr.writtenToDiskLatch.await(); // 等待......}};// 將 awaitCommit 添加到隊列 QueuedWork 中QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};// 8.0 之前加入到一個單線程的線程池中執行// 8.0 之后加入 HandlerThread 中執行寫入任務SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); }
  • 將一個 awaitCommit 的 Runnable 任務,添加到隊列 QueuedWork 中,在 awaitCommit 中會調用 await() 方法等待,在 handleStopService 、 handleStopActivity 等等生命周期會以這個作為判斷條件,等待任務執行完畢
  • 將一個 postWriteRunnable 的 Runnable 寫任務,通過 enqueueDiskWrite 方法,將寫入任務加入到隊列中,而寫入任務在一個線程中執行

注意:在 8.0 之前和 8.0 之后 enqueueDiskWrite() 方法實現邏輯各不相同

在 8.0 之前調用 enqueueDiskWrite() 方法,將寫入任務加入到 單個線程的線程池 中執行,如果 apply() 多次的話,任務將會依次執行,效率很低,android-7.0.0_r34 源碼如下所示。

// android-7.0.0_r34: frameworks/base/core/java/android/app/SharedPreferencesImpl.java private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {......QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); }// android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java public static ExecutorService singleThreadExecutor() {synchronized (QueuedWork.class) {if (sSingleThreadExecutor == null) {sSingleThreadExecutor = Executors.newSingleThreadExecutor();}return sSingleThreadExecutor;} }

通過 Executors.newSingleThreadExecutor() 方法創建了一個 單個線程的線程池,因此任務是串行的,通過 apply() 方法創建的任務,都會添加到這個線程池內。

在 8.0 之后將寫入任務加入到 LinkedList 鏈表中,在 HandlerThread 中執行寫入任務,android-10.0.0_r14 源碼如下所示。

// android-10.0.0_r14: frameworks/base/core/java/android/app/SharedPreferencesImpl.java private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {......QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }// android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.javaprivate static final LinkedList<Runnable> sWork = new LinkedList<>();public static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler(); // 獲取 handlerThread.getLooper() 生成 Handler 對象synchronized (sLock) {sWork.add(work); // 將寫入任務加入到 LinkedList 鏈表中if (shouldDelay && sCanDelay) {handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);} else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);}} }

在 8.0 之后通過調用 handlerThread.getLooper() 方法生成 Handler,任務都會在 HandlerThread 中執行,所有通過 apply() 方法創建的任務,都會添加到 LinkedList 鏈表中。

當生命周期處于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的時候會調用 QueuedWork.waitToFinish() 會等待寫入任務執行完畢,我們以其中 handlePauseActivity() 方法為例。

public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,int configChanges, PendingTransactionActions pendingActions, String reason) {......// 確保寫任務都已經完成QueuedWork.waitToFinish();......} }

正如你所看到的在 handlePauseActivity() 方法中,調用了 QueuedWork.waitToFinish() 方法,會等待所有的寫入執行完畢,Google 在 8.0 之后對這個方法做了很大的優化,一起來看一下 8.0 之前和 8.0 之后的區別。

注意:在 8.0 之前和 8.0 之后 waitToFinish() 方法實現邏輯各不相同

在 8.0 之前 waitToFinish() 方法只做了一件事,會一直等待寫入任務執行完畢,我先來看看在 android-7.0.0_r34 源碼實現。

android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java

private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =new ConcurrentLinkedQueue<Runnable>();public static void waitToFinish() {Runnable toFinish;while ((toFinish = sPendingWorkFinishers.poll()) != null) {toFinish.run(); // 相當于調用 `mcr.writtenToDiskLatch.await()` 方法} }
  • sPendingWorkFinishers 是 ConcurrentLinkedQueue 實例,apply 方法會將寫入任務添加到 sPendingWorkFinishers 隊列中,在 單個線程的線程池 中執行寫入任務,線程的調度并不由程序來控制,也就是說當生命周期切換的時候,任務不一定處于執行狀態
  • toFinish.run() 方法,相當于調用 mcr.writtenToDiskLatch.await() 方法,會一直等待
  • waitToFinish() 方法就做了一件事,會一直等待寫入任務執行完畢,其它什么都不做,當有很多寫入任務,會依次執行,當文件很大時,效率很低,造成 ANR 就不奇怪了,尤其像字節跳動這種大規模的 App

在 8.0 之后 waitToFinish() 方法做了很大的優化,當生命周期切換的時候,會主動觸發任務的執行,而不是一直在等著,我們來看看 android-10.0.0_r14 源碼實現。

android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

private static final LinkedList<Runnable> sFinishers = new LinkedList<>(); public static void waitToFinish() {......try {processPendingWork(); // 主動觸發任務的執行} finally {StrictMode.setThreadPolicy(oldPolicy);}try {// 等待任務執行完畢while (true) {Runnable finisher;synchronized (sLock) {finisher = sFinishers.poll(); // 從 LinkedList 中取出任務}if (finisher == null) { // 當 LinkedList 中沒有任務時會跳出循環break;}finisher.run(); // 相當于調用 `mcr.writtenToDiskLatch.await()`}} ...... }

在 waitToFinish() 方法中會主動調用 processPendingWork() 方法觸發任務的執行,在 HandlerThread 中執行寫入任務。

另外還做了一個很重要的優化,當調用 apply() 方法的時候,執行磁盤寫入,都是全量寫入,在 8.0 之前,調用 N 次 apply() 方法,就會執行 N 次磁盤寫入,在 8.0 之后,apply() 方法調用了多次,只會執行最后一次寫入,通過版本號來控制的。

SharedPreferences 的另外一個缺點就是 apply() 方法無法獲取到操作成功或者失敗的結果,而 commit() 方法是可以接收 MemoryCommitResult 里面的一個 boolean 參數作為結果,來看一下它們的方法簽名。

public void apply() { ... }public boolean commit() { ... }

SP 不能用于跨進程通信

我們在創建 SP 實例的時候,需要傳入一個 mode,如下所示:

val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE)

Context 內部還有一個 mode 是 MODE_MULTI_PROCESS,我們來看一下這個 mode 做了什么

public SharedPreferences getSharedPreferences(File file, int mode) {if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {// 重新讀取 SP 文件內容sp.startReloadIfChangedUnexpectedly();}return sp; }

在這里就做了一件事,當遇到 MODE_MULTI_PROCESS 的時候,會重新讀取 SP 文件內容,并不能用 SP 來做跨進程通信。

到這里關于 SharedPreferences 部分分析完了,接下來分析一下 DataStore 為我們解決什么問題?

DataStore 解決了什么問題

Preferences DataStore 主要用來替換 SharedPreferences,Preferences DataStore 解決了 SharedPreferences 帶來的所有問題

Preferences DataStore 相比于 SharedPreferences 優點

  • DataStore 是基于 Flow 實現的,所以保證了在主線程的安全性
  • 以事務方式處理更新數據,事務有四大特性(原子性、一致性、 隔離性、持久性)
  • 沒有 apply() 和 commit() 等等數據持久的方法
  • 自動完成 SharedPreferences 遷移到 DataStore,保證數據一致性,不會造成數據損壞
  • 可以監聽到操作成功或者失敗結果

另外 Jetpack DataStore 提供了 Proto DataStore 方式,用于存儲類的對象(typed objects ),通過 protocol buffers 將對象序列化存儲在本地,protocol buffers 現在已經應用的非常廣泛,無論是微信還是阿里等等大廠都在使用,我們在部分場景中也使用了 protocol buffers,在后續的文章會詳細的分析。

注意:

Preferences DataStore 只支持 Int , Long , Boolean , Float , String 鍵值對數據,適合存儲簡單、小型的數據,并且不支持局部更新,如果修改了其中一個值,整個文件內容將會被重新序列化,可以運行 AndroidX-Jetpack-Practice/DataStoreSimple 體驗一下,如果需要局部更新,建議使用 Room。

在項目中使用 Preferences DataStore

Preferences DataStore 主要應用在 MVVM 當中的 Repository 層,在項目中使用 Preferences DataStore 非常簡單,只需要 4 步。

1. 需要添加 Preferences DataStore 依賴

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

2. 構建 DataStore

private val PREFERENCE_NAME = "DataStore" var dataStore: DataStore<Preferences> = context.createDataStore(name = PREFERENCE_NAME

3. 從 Preferences DataStore 中讀取數據

Preferences DataStore 以鍵值對的形式存儲在本地,所以首先我們應該定義一個 Key.

val KEY_BYTE_CODE = preferencesKey<Boolean>("ByteCode")

這里和我們之前使用 SharedPreferences 的有點不一樣,在 Preferences DataStore 中 Key 是一個 Preferences.Key<T> 類型,只支持 Int , Long , Boolean , Float , String,源碼如下所示:

inline fun <reified T : Any> preferencesKey(name: String): Preferences.Key<T> {return when (T::class) {Int::class -> {Preferences.Key<T>(name)}String::class -> {Preferences.Key<T>(name)}Boolean::class -> {Preferences.Key<T>(name)}Float::class -> {Preferences.Key<T>(name)}Long::class -> {Preferences.Key<T>(name)}...... // 如果是其他類型就會拋出異常} }

當我們定義好 Key 之后,就可以通過 dataStore.data 來獲取數據

override fun readData(key: Preferences.Key<Boolean>): Flow<Boolean> =dataStore.data.catch {// 當讀取數據遇到錯誤時,如果是 `IOException` 異常,發送一個 emptyPreferences 來重新使用// 但是如果是其他的異常,最好將它拋出去,不要隱藏問題if (it is IOException) {it.printStackTrace()emit(emptyPreferences())} else {throw it}}.map { preferences ->preferences[key] ?: false}
  • Preferences DataStore 是基于 Flow 實現的,所以通過 dataStore.data 會返回一個 Flow<T>,每當數據變化的時候都會重新發出
  • catch 用來捕獲異常,當讀取數據出現異常時會拋出一個異常,如果是 IOException 異常,會發送一個 emptyPreferences() 來重新使用,如果是其他異常,最好將它拋出去

4. 向 Preferences DataStore 中寫入數據

在 Preferences DataStore 中是通過 DataStore.edit() 寫入數據的,DataStore.edit() 是一個 suspend 函數,所以只能在協程體內使用,每當遇到 suspend 函數以掛起的方式運行,并不會阻塞主線程。

以掛起的方式運行,不會阻塞主線程 :也就是協程作用域被掛起, 當前線程中協程作用域之外的代碼不會阻塞。

首先我們需要創建一個 suspend 函數,然后調用 DataStore.edit() 寫入數據即可。

override suspend fun saveData(key: Preferences.Key<Boolean>) {dataStore.edit { mutablePreferences ->val value = mutablePreferences[key] ?: falsemutablePreferences[key] = !value} }

到這里關于 Preferences DataStore 讀取數據和寫入數據就已經分析完了,接下來分析一下如何遷移 SharedPreferences 到 DataStore。

遷移 SharedPreferences 到 DataStore

遷移 SharedPreferences 到 DataStore 只需要 2 步。

  • 在構建 DataStore 的時候,需要傳入一個 SharedPreferencesMigration
dataStore = context.createDataStore(name = PREFERENCE_NAME,migrations = listOf(SharedPreferencesMigration(context,SharedPreferencesRepository.PREFERENCE_NAME)) )
  • 當 DataStore 對象構建完了之后,需要執行一次讀取或者寫入操作,即可完成 SharedPreferences 遷移到 DataStore,當遷移成功之后,會自動刪除 SharedPreferences 使用的文件

注意: 只從 SharedPreferences 遷移一次,因此一旦遷移成功之后,應該停止使用 SharedPreferences。

相比于 MMKV 有什么不同之處

最后用一張表格來對比一下 MMKV、DataStore、SharedPreferences 的不同之處,如果發現錯誤,或者有其他不同之處,期待你來一起完善。

另外在附上一張 Google 分析的 SharedPreferences 和 DataStore 的區別

全文到這里就結束了,這篇文章主要分析了 SharedPreferences 和 DataStore 的優缺點,以及為什么需要引入 DataStore 和如何使用 DataStore,為了節省篇幅源碼分析部分會在后續的文章中分析。

關于 SharedPreferences 和 DataStore 相關的代碼,已經上傳到了 GitHub 歡迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple ,可以運行一下示例項目,體驗一下 SharedPreferences 和 DataStore 效果。

  • GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

參考文獻

  • Preferences DataStore codelab
  • Now in Android #25
  • Prefer Storing Data with Jetpack DataStore
  • 剖析 SharedPreference 引起的 ANR 問題
  • SharedPreferences 問題分析和解決

結語

我梳理了 LeetCode / 劍指 offer 及國內外大廠面試題解,截止到目前為止我已經在 LeetCode 上 AC 了 124+ 題,每題都會用 Java 和 kotlin 去實現,并且每題都有多種解法、解題思路、時間復雜度、空間復雜度分析,題庫逐漸完善中,歡迎前去查看。

  • 劍指 offer 及國內外大廠面試題解:在線閱讀
  • LeetCode 系列題解:在線閱讀


最后推薦我一直在更新維護的項目和網站:

  • 計劃建立一個最全、最新的 AndroidX Jetpack 相關組件的實戰項目 以及 相關組件原理分析文章,正在逐漸增加 Jetpack 新成員,倉庫持續更新,歡迎前去查看:
hi-dhl/AndroidX-Jetpack-Practice?github.com
  • LeetCode / 劍指 Offer / 國內外大廠面試題,涵蓋: 多線程、數組、棧、隊列、字符串、鏈表、樹,查找算法、搜索算法、位運算、排序等等,每道題目都會用 Java 和 kotlin 去實現,倉庫持續更新,歡迎前去查看
hi-dhl/Leetcode-Solutions-with-Java-And-Kotlin?github.com
    • 劍指 offer 及國內外大廠面試題解:在線閱讀
    • LeetCode 系列題解:在線閱讀
  • 最新 Android 10 源碼分析系列文章,了解系統源碼,不僅有助于分析問題,在面試過程中,對我們也是非常有幫助的,倉庫持續更新,歡迎前去查看 Android10-Source-Analysis
  • 整理和翻譯一系列精選國外的技術文章,每篇文章都會有譯者思考部分,對原文的更加深入的解讀,倉庫持續更新,歡迎前去查看
Technical-Article-Translation?github.com
  • 「為互聯網人而設計,國內國外名站導航」涵括新聞、體育、生活、娛樂、設計、產品、運營、前端開發、Android 開發等等網址,歡迎前去查看
Hi World | 為互聯網人而設計的國內國外名站導航?site.51git.cn

總結

以上是生活随笔為你收集整理的安卓 sharedpreferences可以被其它activity读取_Google|再见 SharedPreferences 拥抱 Jetpack DataStore...的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 又大又硬又爽免费视频 | 光棍影院一区二区 | 丰满少妇中文字幕 | 国产一区一一区高清不卡 | 国产精品视频999 | 黄色在线观看视频网站 | 911色| 三级黄色在线播放 | 国产日韩中文字幕 | www青青草| 国产夫妻性爱视频 | 黄色大片aaa | 日韩经典午夜福利发布 | 日本乱大交xxxx公交车 | 麻豆国产精品一区 | 风间由美av | 在线中文字幕亚洲 | 亚洲第一视频在线播放 | 一级在线播放 | 最近的中文字幕 | 欧美aaaa视频 | 青青青草视频在线观看 | 国产又大又黄的视频 | 伊人精品视频在线观看 | 亚洲av综合永久无码精品天堂 | 一级黄色片免费观看 | 超碰成人久久 | 又大又粗又爽18禁免费看 | 午夜九九九 | 亚欧成人 | 91视频免费入口 | 涩涩视频免费看 | 人妻巨大乳hd免费看 | 妖精视频一区二区 | 67194在线免费观看 | 日韩国产精品久久 | av资源吧首页| 99久久国产宗和精品1上映 | 肉丝美足丝袜一区二区三区四 | 非洲黑人毛片 | 国产吃瓜在线 | 免费毛片网站在线观看 | 天天干天天舔 | 国产精品视频网址 | 偷看洗澡一二三区美女 | 欧美日韩一区二区区别是什么 | 91精品在线播放 | 欧美一区二区三区爱爱 | 8050午夜一级毛片久久亚洲欧 | 日本成人在线看 | 亚洲美女综合网 | 一级片免费在线播放 | 国产精品久久婷婷六月丁香 | av永久免费在线观看 | 日韩高清影院 | 天天透天天操 | 激情开心网站 | av免费观看网站 | 特黄做受又粗又大又硬老头 | 粗大的内捧猛烈进出在线视频 | 亚洲欧美综合久久 | 国产精品一区二区久久 | 美女爆乳18禁www久久久久久 | 国产v亚洲 | av片国产 | 日本大胆人体视频 | 成人免费网站 | 国产爱搞| 亚洲欧洲日本精品 | 一区二区三区视频 | 亚洲三级电影网站 | 亚洲天堂网站在线 | 天堂影音| 九九精品免费 | 一二三区av | 岳乳丰满一区二区三区 | 男女福利视频 | 香蕉a视频 | 在线日韩精品视频 | 99国产精品| 欧洲亚洲成人 | 中文字幕国产一区二区 | 日本精品成人 | 日韩不卡一区二区三区 | 亚洲一区在线免费 | 亚洲狼人综合 | 中文字幕免费一区 | 一区二区三区四区日韩 | 91精品国自产在线偷拍蜜桃 | 尹人综合网 | 亚av在线| 国产亚洲欧美在线视频 | 成人黄色在线免费观看 | 天堂视频一区 | 亚洲精品久久久久久一区二区 | 日韩成人av网址 | 午夜影院福利社 | 免费看国产曰批40分钟粉红裤头 | 亚洲一区第一页 |