成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

在 Swift 中如何正確傳遞 Unsafe Pointers 參數

開發
在過去一個季度抖音規模化落地 Swift 組件的過程中,我負責的代碼在 CI 運行單測階段暴露了幾個問題,都與 Swift 中的 unsafe pointers 有關。

TL;DR

  • Swift 中對于類型大小為空的變量使用 & 取地址是未定義行為,編譯為目標碼之后的體現為一個根據之前代碼執行結果產生的任意數值。這是一個 feature。
  • Swift 中在多個線程中對同一個變量使用 & 將獲取「寫訪問」,會造成運行時崩潰。
  • Swift 中對 computed property 取地址會取到臨時變量的地址。如果 computed property 是一個鎖,將造成鎖被拷貝到多個線程的執行棧上,造成程序錯誤。

平平無奇但錯誤的代碼

在過去一個季度抖音規模化落地 Swift 組件的過程中,我負責的代碼在 CI 運行單測階段暴露了幾個問題,都與 Swift 中的 unsafe pointers 有關。

第一個是通過 Objective-C 中 associated object 技巧擴展出來的 property 在 release build 后再運行,set 之后只能 get 到 nil;debug build 下則正常:

圖片

范例代碼一

第二個是下列代碼在 release build 后,在多線程環境有可能崩潰在 swift_endAccess 函數中:

@_implementationOnly import Darwin


public class UnfairLock {


    var _lock: os_unfair_lock
    
    public func withLock<R>(perform action: () -> R) -> R {
        os_unfair_lock_lock(&_lock)
        defer {
            os_unfair_lock_unlock(&_lock)
        }
        return action()
    }
    
    public init() {
        _lock = os_unfair_lock()
    }
    
}

范例代碼二

圖片

是不是覺得很奇怪?以上兩段代碼既符合直覺,也沒有編譯錯誤。那么,為什么會產生上述問題呢?

歸因:ObjC 關聯對象訪存出錯

針對范例代碼一里面 ObjC associated object 訪存得到錯誤結果的問題,我們可以進入匯編模式,看看到底 objc_get(set)AssociatedObject 得到的參數是什么。首先打開 Xcode 的 Always Show Disassembly(看完文章后記得關閉哦),在 objc_getAssociatedObject  objc_setAssociatedObject 打下斷點。

圖片

運行后我們可以看到 objc_setAssociatedObject  key 這個參數(arm64 上的 x1 寄存器)的值是 0x04000001ed295c71,這個地址存儲的值是 0x00000001ed295c71。我們預期通過 dis -s 指令對這個地址進行反匯編可以獲得該地址對應的二進制鏡像名稱,然而這里卻提示「反匯編失敗」。這是為什么呢?

圖片

我們可以進一步檢查 x1 寄存器內容的來源。下圖紅線為 x1 寄存器在 MyObject.myProperty.setter (下稱 setter 函數)內的數據流。可以看到:

  1. setter 函數初始棧高為 0x20 (sub sp, sp, #0x20)
  2. x1 為 setter 函數執行棧基準地址 + 0x40 - 0x48 = setter 函數執行棧基準地址 - 0x8。所以對應 setter 函數執行棧上 0x18 偏移的棧變量

圖片

而 setter 函數開頭調用的 Optional<Any> 的拷貝初始化函數 outlined init with copy of Swift.Optional<Any> 的第二個參數 x1 為 setter 函數執行棧基準地址 + 0x40 - 0x60 = setter 函數執行棧基準地址 - 0x20。結合之前獲得的 setter 函數執行棧高 0x20 的信息,所以對應 setter 函數執行棧上 0x0 偏移的棧變量。

于是我們可以知道,setter 函數執行棧基準地址后的所有空間都有可能被 Optional<Any> 的拷貝初始化函數利用到。而實際上這個拷貝初始化函數的第二個參數就是拷貝操作的目標地址。加上上圖中藍線的原點在判別完 x21 的內容(即 objc_setAssociatedObject 接受到的 x1)之后進行了條件跳轉(cbz,即 conditional branching if zero 的縮寫),跳過的內容正是把 Any 橋接到 Objective-C(因為中途有調用 Swift._bridgeAnythingToObjectiveC),所以我們可以大膽猜測:

x21 —— 即 objc_setAssociatedObject 接受到的 x1,實際上是可以判斷 Optional<Any> 為空的信息——而這個并不是我們在源碼中給定的 key——這就是導致我們使用 objc_get(set)AssociatedObject 不正常工作的原因。而我們一開 dis -s 之所以會失敗,是因為我們在嘗試對函數執行棧進行反匯編。

深入:Void 全局變量編譯細節

為了了解更多代碼生成細節,知道為什么生成了這樣的代碼,我們可以使用 Swift 編譯器的 -emit-sil(gen)  -emit-ir(gen) 參數來考察 SIL(原始 SIL)和 IR(原始 IR)的生成結果,看到底是哪一步產生了意外。檢查的順序應該 -emit-silgen, -emit-sil, -emit-irgen, -emit-ir,確保先檢查原始 SIL(SILGen)和原始 IR(IRGen),再檢查優化后的 SIL 和 IR。檢查 SILGen 的命令如下:

// 這里我保存的文件叫 UnsafePointers.swift
// 我的 Xcode 放置的路徑是 /Applications/Xcode-15.0.app
// 大家可以根據自己的情況對以下命令進行修改:
xcrun swift-frontend -c UnsafePointers.swift \
    -enable-objc-interop \
    -target arm64-apple-macos14.0 \
    -sdk /Applications/Xcode-15.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
    -emit-silgen > UnsafePointers.silgen.sil

上文的 setter 函數在 SILGen 中生成了三個區塊:bb0 是函數入口,在 363 行進行 switch-case 之后跳轉至 bb1  bb2。但不論是 bb1 還是 bb2,最后都會跳轉至 bb3(如下圖藍線所示)。所以我們直接看 bb3 好了。

圖片

所以我們直接折疊 bb1  bb2,然后可以畫出 objc_setAssociatedObject 第二個參數 %32 的數據流。可以看到其最終來自于 %2,而 %2 會對一個全局 Swift 符號取地址。

圖片

而這個符號正是我們定義的 myKey

圖片

因為 global_addr 是 SIL 指令,已經是 SIL 這一層的「原語(最小不可分割語素)」了,所以我們應該進一步查看 IRGen 的結果。

xcrun swift-frontend -c UnsafePointers.swift \
    -enable-objc-interop \
    -target arm64-apple-macos14.0 \
    -sdk /Applications/Xcode-15.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
    -emit-irgen > UnsafePointers.irgen.ll

在 IRGen 結果中,我們可以直接搜索 MyObject.myProperty.setter 的 Swift 改編符號(如果你也使用 UnsafePointers.swift 這個文件名那么就是 $s14UnsafePointers8MyObjectC10myPropertyypSgvs)。我們可以看到,517 行給 objc_setAssociatedObject 的第二個參數已經變成了 undef

圖片

為了進一步探究 SIL 中的 global_addr 指令為何在 lower 到 IR 之后會得到 undef,我們可以動態調試一下 Swift 編譯器。這里我們使用簡化后的代碼以加速編譯器調試。因為 foo 是全局變量,所以第 4 行的 &foo 仍然會生成 global_addr 指令。

var foo: Void = Void()


func bar() -> UnsafeRawPointer {
    UnsafeRawPointer(&foo)
}

然后我們在 IRGenSILFunction::visitGlobalAddrInst 中打下斷點,編譯上面的源代碼,從 DEBUG CONSOLE 中的 i-> dump() 結果可以看到,此次 GlobalAddrInst 實例 i 的內容為被 & 引用的變量。最后代碼執行進入了 2950 行,這里可以看到針對該全局變量的 ti: TypeInfo&,如果其 isKnownEmpty 返回 true 就不會生成符號,因此地址是 undef。這是一個 feature。

圖片

圖片

而 Swift 編譯器中的 TypeInfo 類型負責記載類型信息對應信息。對于 TypeInfo::isKnownEmpty 而言,簡單來說如果可以在編譯器在編譯時可以確定大小的類型,并且大小為空,即可認為其會返回 true

歸因:多線程下取地址崩潰

要理解文章開頭范例代碼二中所出現的「多線程下對實例變量取地址」而導致的崩潰,更直接的方法是打開 Xcode 的 Thread Sanitizer 后運行程序。我們可以看到,Xcode 幫我們檢測出了「訪問競爭(access race)」。這是因為在 Swift 中對變量使用 & 即意味著需要獲取一個「寫訪問(write access)」,而目前的代碼有多個線程在訪問 UnfairLock.withLock,那么也就有多個線程在嘗試對 UnfairLock._lock 獲取「寫訪問」。這個在 Swift 中不符合運行時 exclusivity enforcement,所以會崩潰。

圖片

圖片

想要修復這個問題,將獲得指針的時機移動至多線程代碼外(即 x.testLock 外)即可。

但是 exclusivity 沖突并不是這個寫法的全部問題,這個寫法還有一個非常隱蔽的問題:& 可能取到的不是變量本身的地址,而是一個臨時分配的變量。要理解這個問題,需要知道 Swift 是如何實現變量取地址的。

深入:Swift 變量取地址實現

在 Swift 中 var 關鍵字定義的變量滿足以下抽象:

  • 一定包含一個 get accessor
  • 可選包含一個 set accessor
  • 可選包含一個存儲容器

然而,上面只是開發者在日常開發中能夠感知到的部分。上述抽象沒有解決的問題是:如果一個變量的存儲容器是可選的,那么我們應該如何獲得這個變量的地址呢?所以編譯器還會為我們自動生成:

  • 一定包含一個 _read accessor
  • 可選包含一個 _modify accessor

其中 _read accessor 是一個對變量產生「讀訪問」,并且拋出一個只讀地址的協程

 _modify accessor 是一個對變量產生「寫訪問」,并且拋出一個可寫地址的協程

而通過上述 _read  _modify accessor,我們就定義了獲取 var 關鍵字變量地址的手段。

「協程」可以理解為不保證棧平衡的函數(或稱「過程」)。協程本是過程的原始形態——過程引入棧平衡是為了實現本地變量,而這個特性在協程中無法實現。但是人們后來發現非平衡的棧可以讓后續執行的代碼沿用之前的棧內存內容,而不用重復在棧上傳參,又或者開辟堆空間傳參,所以人們又開始利用起了「協程」。Swift 引入協程的目的也是做性能優化。 需要注意的是 Swift Concurrency 并不是協程。

而在 Swift 中對變量使用 & 本質上就是獲取 _modify accessor 拋出的「變量可寫地址」。

我們可以從 SILGen 的結果中一窺究竟。

下面是 UnfairLock.withLock(perform:) 在未修改前的 SILGen 結果,紅色的線和方框代表了 &_lock 這句 Swift 源碼在 SIL 層面的數據流。

我們可以看到 &_lock 最初來自于 %6,而 %6 正是對 self (%2) 使用了 #UnfiarLock.lock!modify 這個協程產生的,同時產生的 %7 則是協程產生的非平衡棧的 resumption 函數,由 89 行的 endApply 調用,用以恢復棧平衡。

圖片

而當前實現的 UnfiarLock.lock._modify 則是通過 ref_element_addr 這條 SIL 指令直接拋出了 UnfairLock.lock 在當前 self 中的地址。

圖片

但是,當情況變得復雜一些的時候,這個 _modify accessor 拋出的將會變成一個臨時變量的地址——對,就是這個協程在非平衡棧里面分配的臨時變量。要造成這個結果很簡單:比如,把 os_unfair_lock 打包放入一個叫做 Data 的類型中,然后 var _lock: os_unfair_lock 改成 computed property,從 Data 中訪存 os_unfair_lock

private struct Data {
  var lock = os_unfair_lock()
}


public class UnfairLock {


  private var data = Data()


  internal var _lock: os_unfair_lock {
    get {
      data.lock
    }
    set {
      data.lock = newValue
    }
  }


  public func withLock<R>(perform action: () -> R) -> R {
    os_unfair_lock_lock(&_lock)
    defer {
      os_unfair_lock_unlock(&_lock)
    }
    return action()
  }


  public init() {
    
  }


}

我們可以看到,此時 UnfairLock._lock.modify 的 SILGen 結果中出現了棧分配(683 行),然后分配后的棧地址內又被 store(復制)了 UnfairLock._lock (687 行),隨后棧分配后的地址被 _modify 拋出,爾后在 _modify 的 resumption 函數(bb1  bb2)中,棧地址中的內容又被重新 set 回了 UnfairLock._lock(694 行及 703 行)。

圖片

上面這種行為在多線程場景就是災難性質的——因為每一個線程都有自己獨立的執行棧,而這種行為就是把一個鎖復制到了每一個在競爭這把鎖的線程的執行棧上再加解鎖——最后的結果一定是程序出錯。

深入:理解取地址中的臨時變量

要理解對 var 取地址時可能取到臨時變量的地址,還是需要回到 Swift 對 var 關鍵字定義的變量的抽象:

  • 一定包含一個 get accessor
  • 可選包含一個 set accessor
  • 可選包含一個存儲容器

而編譯器幫助合成 _read 或者 _modify 時,如果變量沒有實際存儲容器,那么也只能通過 get  set 實現:當出現 computed property 時,其 _modify 會先通過 get accessor 創建一個臨時變量,拋出臨時變量地址之后,在 resumption 時再使用 set accessor 寫回這種實現了。

所以,如果開發者書寫如下代碼:

struct Foo {
    var _bar: Int = 0
    var bar: Int {
        get {
            self._bar
        }
        set {
            self._bar = newValue
        }
    }
}


var foo = Foo()


withUnsafeMutablePointer(to: &foo, body: handleIntPtr)


func handleIntPtr(_ ptr: UnsafeMutablePointer<Int>) {
    // ...
}

那么實際上編譯器會幫助合成并生成如下代碼:

struct Foo {
    var bar: Int {
        // ...
        _read { // 編譯器合成代碼
            let tempFoo = 棧分配
            tempFoo = self.bar.getter
            拋出不可變棧地址 tempFoo
        }
        _read.resumption { // 編譯器合成代碼
            棧析構 tempFoo
        }
        _modify { // 編譯器合成代碼
            var tempFoo = 棧分配
            tempFoo = self.bar.getter
            拋出可變棧地址 tempFoo
        }
        _modify.resumption { // 編譯器合成代碼
            self.bar.setter = tempFoo
            棧析構 tempFoo
        }
    }
}


var foo = Foo()


// 棧分配 tempFoo 以及隱式地址到指針轉換
let ptrToTempFoo: UnsafeMutablePointer<Int> = Foo.bar._modify(foo)


// 應用 withUnsafeMutablePointer 的 body 閉包
handleIntPtr(ptrToTempFoo)


// 將臨時變量 set 回去,并完成 tempFoo 棧析構
Foo.bar._modify.resumption(foo)

知道這一點之后,我們也可以嘗試手寫 UnfairLock._lock._modify 的實現,直接拋出 data.lock 的地址,來消除臨時變量:

public class UnfairLock {


  private var data = Data()


  internal var _lock: os_unfair_lock {
    _read {
      yield data.lock
    }
    _modify {
      yield &data.lock
    }
  }
  
  // ...


}

此時我們可以通過 SILGen 的結果看到:UnfairLock._lock.modify 目前會委托到 UnfairLock.data.modify(84 行),然后利用 UnfairLock.data.modify 的結果,然后取出 Data.lock 的地址(85 行),最后拋出(86 行):

圖片

 UnfairLock.data.modify 則是直接拋出了 UnfairLock.data 在當前 self 下的地址(第 61 行)。

圖片

上述技巧繞過了「使用 var 關鍵字變量的 get 和 set」來實現 _modify,同時也產生了一個「副作用」。大家能想到是什么嗎?

最佳實踐及準入建設

在實際的日常開發活動中,我們并不想耗費如此多的心智在如何處理好給 unsafe pointers 傳參上,所以我們需要一套最佳實踐以及自動化準入機制來保證我們的日常開發的執行結果。

上述問題可以歸結為:

  1.  Void 取地址出現無意義數值
  2. 多線程使用 & 對變量取地址后崩潰
  3. 對 computed property 取地址得到的是臨時變量地址

下面我們分情況討論

ObjC 關聯對象訪存出錯

前文「ObjC 關聯對象訪存結果出錯」的本質是:Swift 中對于空大小類型的全局變量取地址編譯到 LLVM IR 后是 undef 的,編譯為目標碼之后的體現為一個根據之前代碼執行結果產生的任意數值。所以「對大小為空的類型的全局變量取地址」本身就是一個未定義行為。這里的最佳實踐就是不允許這樣做。在準入建設方面,我們可以設立靜態分析規則進行檢出。依據公司現有靜態分析設施,我們可以分析:1)標準庫以及2)文件內定義的空大小類型,并且3)有選擇地加入系統庫的類型定義。


多線程下取地址崩潰

前文「多線程下對實例變量取地址發生崩潰」的本質是:Swift 中,在多個線程中對同一個變量獲取「寫訪問」會引發「訪問競爭」,這是 Swift 運行時在開啟 runtime exclusivity enforcement 之后所不允許的。相關最佳實踐應該是將「寫訪問」提出多線程代碼:

比如下列代碼通過 DispatchQueue.concurrentPerform 這個并發執行的接口,在多個線程中通過 & 取地址對同一個變量 counter 獲取了「寫訪問」:

// ?
var counter = AtomicIntStorage() // zero init
DispatchQueue.concurrentPerform(iterations: 10) { _ in
  for _ in 0 ..< 1_000_000 {
    atomicFetchAddInt(&counter, 1)  // Exclusivity violation
  }
}
print(atomicLoadInt(&counter) // ???

我們可以將相關代碼提取出 DispatchQueue.concurrentPerform 的尾閉包:

// ?
var counter = AtomicIntStorage() // zero init
withUnsafeMutablePointer(to: &counter) { pointer in
  DispatchQueue.concurrentPerform(iterations: 10) { _ in
    for _ in 0 ..< 1_000_000 {
      atomicFetchAddInt(pointer, 1) // OK
    }
  }
  print(atomicLoadInt(pointer) // 10_000_000
}

但是上面是發生在極其局部的問題。泛化而言,面對運行時訪問競爭,蘋果使用 SIL 這種檢測能力很強的靜態檢測手段亦無法檢出,我們即可判斷:對于運行時的行為,我們需要運行時的設施進行檢測,所以我們需要研發流程準入同時進行調整:

  1. 研發流程中加入研判是否需要設計多線程測試用例的步驟;如果需要設計,則需要單獨在評審時提供多線程測試用例
  2. 準入調整為要求單元測試覆蓋率 100%
  3. 準入調整為在 CI 單測流水線在 Xcode test plan 中開啟 thread sanitizer,CI 消費 thread sanitizer 檢出結果

圖片

取地址得到臨時變量地址

前文「對變量取地址有可能取到臨時地址」的本質是:Swift 中對 computed property 的默認實現取地址過程依賴 get  set 來分配臨時變量,然后再通過 set 設置回去。其在多線程中產生的后果是:鎖在各個線程的執行過程中被 get 多份至各線程的執行棧上;在普遍的代碼中產生的后果是:取地址的結果不穩定。

所以這里最佳實踐也應該分情況討論:

  • 對于使用鎖的需求而言,蘋果的思路是提供封裝好的鎖,而不是鼓勵使用原始鎖(如 os_unfair_lock 或者 pthread_mutex)。但是蘋果在 iOS 16 才想起這件事,所以這里我們需要自行封裝好沒問題的鎖,并且鼓勵開發者只使用封裝好的鎖。
// 下列代碼是蘋果的封裝
enum MyState {
    case idle
    case loading
    case complete(MyAsset)
    case error(Error)
}
let protectedState = OSAllocatedUnfairLock(initialState: MyState.idle)
func myLoadMethod() {
    protectedState.withLock { state in
        state = .loading
    }
    var (resource, error) = loadMyResources()
    if resource != nil {
        protectedState.withLock { state in
            state = .complete(resource)
        }
    } else {
        protectedState.withLock { state in
            state = .error(error!)
        }
    }
}
  • 對于其他需求,這里最佳實踐應該是:
  • 禁用對 get/set 實現的 computed property 取地址。
  • 如果是對 stored property 取地址,也應該使用左值存儲指針,而不是直接使用右值送參。且 stored property 及存儲指針的左值的生命周期可以涵蓋使用指針代碼的生命周期——比如 stored property 及存儲指針的左值是一個全局變量。
// ?
private var myKey: Int8 = 0
objc_getAssociatedObject(self, &myKey) // &myKey 是一個右值


extension NSObject {
    var myProperty: Any? {
        get {
            objc_getAssociatedObject(self, &myKey)
        }
        set {
            objc_setAssociatedObject(
                self,
                &myKey, 
                newValue,
                .OBJC_ASSOCIATION_RETAIN_NONATOMIC
            )
        }
    }
}
// ?
// 全局變量
private var myKey: Int8 = 0
// 全局變量,myKeyPtr 是一個左值。
private let myKeyPtr = UnsafeRawPointer(withUnsafeMutablePointer(to: &myKey) {$0})
// 通常來說 withUnsafeMutablePointer 獲得的指針值只保證在尾閉包中有效
// 這里利用了 myKey 是全局變量,生命周期貫穿全 app 啟動關閉
// 所以即使 return 了 withUnsafeMutablePointer 中尾閉包的指針值也沒有問題


extension NSObject {
    var myProperty: Any? {
        get {
            objc_getAssociatedObject(self, myKeyPtr)
        }
        set {
            objc_setAssociatedObject(
                self,
                myKeyPtr, 
                newValue,
                .OBJC_ASSOCIATION_RETAIN_NONATOMIC
            )
        }
    }
}

準入方面,我們可以:

  1. 通過靜態檢測對「在 Swift 中使用原始鎖」這種行為進行攔截,并不再鼓勵直接使用原始鎖(如 os_unfair_lock 或者 pthread_mutex);對自己開發水平自信的開發者依然可以使用原始鎖,但是相關代碼需要單獨豁免。
  2. 通過靜態檢測對「Swift 中 & 取地址之后作為右值使用」這種行為進行攔截,攔截后建議修復為使用左值存儲指針值后再使用
  3. 通過靜態檢測對「向 computed property 取地址」這種行為進行攔截。

總結

上述問題的本質、后果總結如下

圖片

上述最佳實踐及處置手段總結如下:

圖片


作者介紹:

  • 李禹龍,2020 年加入字節跳動,來自抖音 iOS 基礎技術團隊,專注 Swift 語言及 UI DSL 框架。
責任編輯:龐桂玉 來源: 字節跳動技術團隊
相關推薦

2024-01-17 06:23:35

SwiftTypeScript定義函數

2022-06-07 08:31:44

JavaUnsafe

2025-02-12 10:51:51

2012-02-21 14:04:15

Java

2017-12-05 08:53:20

Golang參數傳遞

2021-04-13 09:20:21

JavaUnsafejava8

2010-01-05 14:49:03

JSON格式

2021-04-16 20:50:16

URL爬蟲參數

2014-07-04 09:47:24

SwiftSwift開發

2009-06-09 21:54:26

傳遞參數JavaScript

2022-06-27 09:00:55

SwiftGit Hooks

2014-07-22 09:01:53

SwiftJSON

2024-04-28 11:36:07

LambdaPython函數

2023-11-17 14:10:08

C++函數

2015-09-08 10:16:41

Java參數按值傳遞

2014-04-09 09:32:24

Go并發

2018-03-30 10:26:24

行間距行高iOS

2021-06-08 21:36:24

PyCharm爬蟲Scrapy

2023-03-29 23:23:00

MyBatis參數框架

2009-12-15 14:09:39

Ruby創建可參數化類
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲三级在线观看 | 日韩有码一区 | 欧美1区2区 | 欧美精品一区二区三区视频 | 99久久99| 国产精品国产三级国产aⅴ无密码 | 欧美11一13sex性hd | 欧美午夜视频 | 免费黄色录像片 | 国产在线精品区 | 久久久网| 91九色视频 | 亚洲精品一 | 一区福利视频 | 日韩色视频 | 国产日韩欧美一区二区 | 国产精品不卡视频 | 久久99深爱久久99精品 | 欧美乱码精品一区二区三区 | 国产精品无码专区在线观看 | 国产精品一区视频 | 精品国产乱码久久久久久闺蜜 | 91久久电影 | 国产成人免费视频网站高清观看视频 | 欧美亚洲视频 | 天堂久久天堂综合色 | 精品亚洲二区 | 欧美乱人伦视频 | 亚洲国产精品va在线看黑人 | 国产在线精品一区二区三区 | 日本不卡一区二区三区在线观看 | 日韩在线免费视频 | 91精品久久久久久久久久 | 色综合视频 | 国产美女永久免费无遮挡 | аⅴ资源新版在线天堂 | 人成在线视频 | 亚洲精品视频导航 | 欧美成人a∨高清免费观看 老司机午夜性大片 | 一区二区三区欧美在线 | 久久久精品久 |