青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

羅朝輝(飄飄白云)

關注嵌入式操作系統,移動平臺,圖形開發。-->加微博 ^_^

  C++博客 :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理 ::
  85 隨筆 :: 0 文章 :: 169 評論 :: 0 Trackbacks

 [深入淺出Cocoa]詳解鍵值觀察(KVO)及其實現機理

羅朝輝 (http://www.shnenglu.com/kesalin/)

本文遵循“署名-非商業用途-保持一致”創作公用協議

一,前言

Objective-C 中的鍵(key)-值(value)觀察(KVO)并不是什么新鮮事物,它來源于設計模式中的觀察者模式,其基本思想就是:

一個目標對象管理所有依賴于它的觀察者對象,并在它自身的狀態改變時主動通知觀察者對象。這個主動通知通常是通過調用各觀察者對象所提供的接口方法來實現的。觀察者模式較完美地將目標對象與觀察者對象解耦。

在 Objective-C 中有兩種使用鍵值觀察的方式:手動或自動,此外還支持注冊依賴鍵(即一個鍵依賴于其他鍵,其他鍵的變化也會作用到該鍵)。下面將一一講述這些,并會深入 Objective-C 內部一窺鍵值觀察是如何實現的。

本文源碼下載:點此下載

二,運用鍵值觀察

1,注冊與解除注冊

如果我們已經有了包含可供鍵值觀察屬性的類,那么就可以通過在該類的對象(被觀察對象)上調用名為 NSKeyValueObserverRegistration 的 category 方法將觀察者對象與被觀察者對象注冊與解除注冊:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

這兩個方法的定義在 Foundation/NSKeyValueObserving.h 中,NSObject,NSArray,NSSet均實現了以上方法,因此我們不僅可以觀察普通對象,還可以觀察數組或結合類對象。在該頭文件中,我們還可以看到 NSObject 還實現了 NSKeyValueObserverNotification 的 category 方法(更多類似方法,請查看該頭文件):

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

這兩個方法在手動實現鍵值觀察時會用到,暫且不提。

值得注意的是:不要忘記解除注冊,否則會導致資源泄露。

2,設置屬性

將觀察者與被觀察者注冊好之后,就可以對觀察者對象的屬性進行操作,這些變更操作就會被通知給觀察者對象。注意,只有遵循 KVO 方式來設置屬性,觀察者對象才會獲取通知,也就是說遵循使用屬性的 setter 方法,或通過 key-path 來設置:

[target setAge:30]; 
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];

3,處理變更通知

觀察者需要實現名為 NSKeyValueObserving 的 category 方法來處理收到的變更通知:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

在這里,change 這個字典保存了變更信息,具體是哪些信息取決于注冊時的 NSKeyValueObservingOptions。

4,下面來看看一個完整的使用示例:

觀察者類:

// Observer.h
@interface Observer : NSObject
@end

// Observer.m
#import "Observer.h"
#import <objc/runtime.h>
#import "Target.h"

@implementation Observer

- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object 
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

@end

注意:在實現處理變更通知方法 observeValueForKeyPath 時,要將不能處理的 key 轉發給 super 的 observeValueForKeyPath 來處理。

使用示例:

Observer * observer = [[[Observer alloc] init] autorelease];

Target * target = [[[Target alloc] init] autorelease];
[target addObserver:observer
         forKeyPath:@"age"
            options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
            context:[Target class]];

[target setAge:30];
//[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];

[target removeObserver:observer forKeyPath:@"age"];

在這里 observer 觀察 target 的 age 屬性變化,運行結果如下:

  >> class: Target, Age changed

  old age is 10

  new age is 30

 

三,手動實現鍵值觀察

上面的 Target 應該怎么實現呢?首先來看手動實現。

@interface Target : NSObject
{
    int age;
}

// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;

@end

@implementation Target

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        age = 10;
    }
    
    return self;
}

// for manual KVO - age
- (int) age
{
    return age;
}

- (void) setAge:(int)theAge
{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }

    return [super automaticallyNotifiesObserversForKey:key];
}

@end

首先,需要手動實現屬性的 setter 方法,并在設置操作的前后分別調用 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用于通知系統該 key 的屬性值即將和已經變更了;

其次,要實現類方法 automaticallyNotifiesObserversForKey,并在其中設置對該 key 不自動發送通知(返回 NO 即可)。這里要注意,對其它非手動實現的 key,要轉交給 super 來處理。

 

四,自動實現鍵值觀察

自動實現鍵值觀察就非常簡單了,只要使用了屬性即可。

@interface Target : NSObject// for automatic KVO - age
@property (nonatomic, readwrite) int age;
@end

@implementation Target
@synthesize age; // for automatic KVO - age

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        age = 10;
    }
    
    return self;
}

@end

 

五,鍵值觀察依賴鍵

有時候一個屬性的值依賴于另一對象中的一個或多個屬性,如果這些屬性中任一屬性的值發生變更,被依賴的屬性值也應當為其變更進行標記。因此,object 引入了依賴鍵。

1,觀察依賴鍵

觀察依賴鍵的方式與前面描述的一樣,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加處理變更通知的代碼:

#import "TargetWrapper.h"

- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object 
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else if ([keyPath isEqualToString:@"information"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Information changed", className);
        NSLog(@" old information is %@", [change objectForKey:@"old"]);
        NSLog(@" new information is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

2,實現依賴鍵

在這里,觀察的是 TargetWrapper 類的 information 屬性,該屬性是依賴于 Target 類的 age 和 grade 屬性。為此,我在 Target 中添加了 grade 屬性:

@interface Target : NSObject
@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;
@end

@implementation Target
@synthesize age; // for automatic KVO - age
@synthesize grade;
@end

下面來看看 TragetWrapper 中的依賴鍵屬性是如何實現的。

@class Target;

@interface TargetWrapper : NSObject
{
@private
    Target * _target;
}

@property(nonatomic, assign) NSString * information;
@property(nonatomic, retain) Target * target;

-(id) init:(Target *)aTarget;

@end

#import "TargetWrapper.h"
#import "Target.h"

@implementation TargetWrapper

@synthesize target = _target;

-(id) init:(Target *)aTarget
{
    self = [super init];
    if (nil != self) {
        _target = [aTarget retain];
    }
    
    return self;
}

-(void) dealloc
{
    self.target = nil;
    [super dealloc];
}

- (NSString *)information
{
    return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
}

- (void)setInformation:(NSString *)theInformation
{
    NSArray * array = [theInformation componentsSeparatedByString:@"#"];
    [_target setGrade:[[array objectAtIndex:0] intValue]];
    [_target setAge:[[array objectAtIndex:1] intValue]];
}

+ (NSSet *)keyPathsForValuesAffectingInformation
{
    NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
    return keyPaths;
}

//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
//{
//    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
//    NSArray * moreKeyPaths = nil;
//    
//    if ([key isEqualToString:@"information"])
//    {
//        moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
//    }
//    
//    if (moreKeyPaths)
//    {
//        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
//    }
//    
//    return keyPaths;
//}

@end

首先,要手動實現屬性 information 的 setter/getter 方法,在其中使用 Target 的屬性來完成其 setter 和 getter。

其次,要實現 keyPathsForValuesAffectingInformation  或 keyPathsForValuesAffectingValueForKey: 方法來告訴系統 information 屬性依賴于哪些其他屬性,這兩個方法都返回一個key-path 的集合。在這里要多說幾句,如果選擇實現 keyPathsForValuesAffectingValueForKey,要先獲取 super 返回的結果 set,然后判斷 key 是不是目標 key,如果是就將依賴屬性的 key-path 結合追加到 super 返回的結果 set 中,否則直接返回 super的結果。

在這里,information 屬性依賴于 target 的 age 和 grade 屬性,target 的 age/grade 屬性任一發生變化,information 的觀察者都會得到通知。

3,使用示例:

Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];

TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
[wrapper addObserver:observer
          forKeyPath:@"information"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:[TargetWrapper class]];

[target setAge:30];
[target setGrade:1];
[wrapper removeObserver:observer forKeyPath:@"information"];

輸出結果:

 >> class: TargetWrapper, Information changed

  old information is 0#10

  new information is 0#30

  >> class: TargetWrapper, Information changed

  old information is 0#30

  new information is 1#30

 

六,鍵值觀察是如何實現的

1,實現機理

鍵值觀察用處很多,Core Binding 背后的實現就有它的身影,那鍵值觀察背后的實現又如何呢?想一想在上面的自動實現方式中,我們并不需要在被觀察對象 Target 中添加額外的代碼,就能獲得鍵值觀察的功能,這很好很強大,這是怎么做到的呢?答案就是 Objective C 強大的 runtime 動態能力,下面我們一起來窺探下其內部實現過程。

當某個類的對象第一次被觀察時,系統就會在運行期動態地創建該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。

派生類在被重寫的 setter 方法實現真正的通知機制,就如前面手動實現鍵值觀察那樣。這么做是基于設置屬性會調用 setter 方法,而通過重寫就獲得了 KVO 需要的通知機制。當然前提是要通過遵循 KVO 的屬性設置方式來變更屬性值,如果僅是直接修改屬性對應的成員變量,是無法實現 KVO 的。

同時派生類還重寫了 class 方法以“欺騙”外部調用者它就是起初的那個類。然后系統將這個對象的 isa 指針指向這個新誕生的派生類,因此這個對象就成為該派生類的對象了,因而在該對象上對 setter 的調用就會調用重寫的 setter,從而激活鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。

如果你對類和對象的關系不太明白,請閱讀《深入淺出Cocoa之類與對象》;如果你對如何動態創建類不太明白,請閱讀《深入淺出Cocoa 之動態創建類》。

蘋果官方文檔說得很簡潔:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

2,代碼分析

由于派生類中被重寫的 class 對我們撒謊(它說它就是起初的基類),我們只有通過調用 runtime 函數才能揭開派生類的真面目。 下面來看  Mike Ash 的代碼:

首先是帶有 x, y, z 三個屬性的觀察目標 Foo:

@interface Foo : NSObject
{
    int x;
    int y;
    int z;
}

@property int x;
@property int y;
@property int z;

@end

@implementation Foo
@synthesize x, y, z;
@end

下面是檢驗代碼:

#import <objc/runtime.h>

static NSArray * ClassMethodNames(Class c)
{
    NSMutableArray * array = [NSMutableArray array];
    
    unsigned int methodCount = 0;
    Method * methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    }

    free(methodList);
    
    return array;
}

static void PrintDescription(NSString * name, id obj)
{
    NSString * str = [NSString stringWithFormat:
                      @"\n\t%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
                      name,
                      obj,
                      class_getName([obj class]),
                      class_getName(obj->isa),
                      [ClassMethodNames(obj->isa) componentsJoinedByString:@", "]];
    NSLog(@"%@", str);
}

int main (int argc, const char * argv[])
{
    @autoreleasepool {
        // Deep into KVO: kesalin@gmail.com
        
//
        Foo * anything = [[Foo alloc] init];
        Foo * x = [[Foo alloc] init];
        Foo * y = [[Foo alloc] init];
        Foo * xy = [[Foo alloc] init];
        Foo * control = [[Foo alloc] init];
        
        [x addObserver:anything forKeyPath:@"x" options:0 context:NULL];
        [y addObserver:anything forKeyPath:@"y" options:0 context:NULL];
        
        [xy addObserver:anything forKeyPath:@"x" options:0 context:NULL];
        [xy addObserver:anything forKeyPath:@"y" options:0 context:NULL];
        
        PrintDescription(@"control", control);
        PrintDescription(@"x", x);
        PrintDescription(@"y", y);
        PrintDescription(@"xy", xy);
        
        NSLog(@"\n\tUsing NSObject methods, normal setX: is %p, overridden setX: is %p\n",
               [control methodForSelector:@selector(setX:)],
               [x methodForSelector:@selector(setX:)]);
        NSLog(@"\n\tUsing libobjc functions, normal setX: is %p, overridden setX: is %p\n",
               method_getImplementation(class_getInstanceMethod(object_getClass(control),
                                                                @selector(setX:))),
               method_getImplementation(class_getInstanceMethod(object_getClass(x),
                                                                @selector(setX:))));
    }

    return 0;
}

在上面的代碼中,輔助函數 ClassMethodNames 使用 runtime 函數來獲取類的方法列表,PrintDescription 打印對象的信息,包括通過 -class 獲取的類名, isa 指針指向的類的名字以及其中方法列表。

 在這里,我創建了四個對象,x 對象的 x 屬性被觀察,y 對象的 y 屬性被觀察,xy 對象的 x 和 y 屬性均被觀察,參照對象 control 沒有屬性被觀察。在代碼的最后部分,分別通過兩種方式(對象方法和 runtime 方法)打印出參數對象 control 和被觀察對象 x 對象的 setX 方面的實現地址,來對比顯示正常情況下 setter 實現以及派生類中重寫的 setter 實現。 

編譯運行,輸出如下:

control: <Foo: 0x10010c980>

NSObject class Foo

libobjc class Foo

implements methods <x, setX:, y, setY:, z, setZ:>

x: <Foo: 0x10010c920>

NSObject class Foo

libobjc class NSKVONotifying_Foo

implements methods <setY:, setX:, class, dealloc, _isKVOA>

y: <Foo: 0x10010c940>

NSObject class Foo

libobjc class NSKVONotifying_Foo

implements methods <setY:, setX:, class, dealloc, _isKVOA>

xy: <Foo: 0x10010c960>

NSObject class Foo

libobjc class NSKVONotifying_Foo

implements methods <setY:, setX:, class, dealloc, _isKVOA>

Using NSObject methods, normal setX: is 0x100001df0, overridden setX: is 0x100001df0

Using libobjc functions, normal setX: is 0x100001df0, overridden setX: is 0x7fff8458e025

從上面的輸出可以看到,如果使用對象的 -class 方面輸出類名始終為:Foo,這是因為新誕生的派生類重寫了 -class 方法聲稱它就是起初的基類,只有使用 runtime 函數 object_getClass 才能一睹芳容:NSKVONotifying_Foo。注意看:x,y 以及 xy 三個被觀察對象真正的類型都是 NSKVONotifying_Foo,而且該類實現了:setY:, setX:, class, dealloc, _isKVOA 這些方法。其中 setX:, setY:, class 和 dealloc 前面已經講到過,私有方法 _isKVOA 估計是用來標示該類是一個 KVO 機制聲稱的類。在這里 Objective C 做了一些優化,它對所有被觀察對象只生成一個派生類,該派生類實現所有被觀察對象的 setter 方法,這樣就減少了派生類的數量,提供了效率。所有 NSKVONotifying_Foo 這個派生類重寫了 setX,setY方法(留意:沒有必要重寫 setZ 方法)。

接著來看最后兩行輸出,地址 0x100001df0 是 Foo 類中的實現,而地址是 0x7fff8458e025 是派生類 NSKVONotifying_Foo 類中的實現。那后面那個地址到底是什么呢?可以通過 GDB 的 info 命令加 symbol 參數來查看該地址的信息:

 (gdb) info symbol 0x7fff8458e025

_NSSetIntValueAndNotify in section LC_SEGMENT.__TEXT.__text of /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

看起來它是 Foundation 框架提供的私有函數:_NSSetIntValueAndNotify。更進一步,我們來看看 Foundation 到底提供了哪些用于 KVO 的輔助函數。打開 terminal,使用 nm -a 命令查看 Foundation 中的信息:

nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

其中查找到我們關注的函數:

00000000000233e7 t __NSSetDoubleValueAndNotify
00000000000f32ba t __NSSetFloatValueAndNotify
0000000000025025 t __NSSetIntValueAndNotify
000000000007fbb5 t __NSSetLongLongValueAndNotify
00000000000f33e8 t __NSSetLongValueAndNotify
000000000002d36c t __NSSetObjectValueAndNotify
0000000000024dc5 t __NSSetPointValueAndNotify
00000000000f39ba t __NSSetRangeValueAndNotify
00000000000f3aeb t __NSSetRectValueAndNotify
00000000000f3512 t __NSSetShortValueAndNotify
00000000000f3c2f t __NSSetSizeValueAndNotify
00000000000f363b t __NSSetUnsignedCharValueAndNotify
000000000006e91f t __NSSetUnsignedIntValueAndNotify
0000000000034b5b t __NSSetUnsignedLongLongValueAndNotify
00000000000f3766 t __NSSetUnsignedLongValueAndNotify
00000000000f3890 t __NSSetUnsignedShortValueAndNotify
00000000000f3060 t __NSSetValueAndNotifyForKeyInIvar
00000000000f30d7 t __NSSetValueAndNotifyForUndefinedKey

Foundation 提供了大部分基礎數據類型的輔助函數(Objective C中的 Boolean 只是 unsigned char 的 typedef,所以包括了,但沒有 C++中的 bool),此外還包括一些常見的 Cocoa 結構體如 Point, Range, Rect, Size,這表明這些結構體也可以用于自動鍵值觀察,但要注意除此之外的結構體就不能用于自動鍵值觀察了。對于所有 Objective C 對象對應的是 __NSSetObjectValueAndNotify 方法。

 

七,總結

KVO 并不是什么新事物,換湯不換藥,它只是觀察者模式在 Objective C 中的一種運用,這是 KVO 的指導思想所在。其他語言實現中也有“KVO”,如 WPF 中的 binding。而在 Objective C 中又是通過強大的 runtime 來實現自動鍵值觀察的。至此,對 KVO 的使用以及注意事項,內部實現都介紹完畢,對 KVO 的理解又深入一層了。Objective 中的 KVO 雖然可以用,但卻非完美,有興趣的了解朋友請查看《KVO 的缺陷》 以及改良實現 MAKVONotificationCenter 。

 

八,引用

Key-value observing:官方文檔

Key-Value Observing Done Right : 官方 KVO 實現的缺陷

MAKVONotificationCenter : 一個改良的 Notification 實現,托管在 GitHub 上

Friday Q&A 2009-01-23

深入淺出Cocoa之類與對象

深入淺出Cocoa 之動態創建類

posted on 2012-11-17 17:41 羅朝輝 閱讀(21960) 評論(4)  編輯 收藏 引用 所屬分類: Cocoa 開發

評論

# re: [深入淺出Cocoa]詳解鍵值觀察(KVO)及其實現機理 2013-11-11 09:50 hhb
你這個程序是 mac 的?  回復  更多評論
  

# re: [深入淺出Cocoa]詳解鍵值觀察(KVO)及其實現機理 2013-11-11 17:27 飄飄白云
@hhb

是的  回復  更多評論
  

# re: [深入淺出Cocoa]詳解鍵值觀察(KVO)及其實現機理 2013-12-03 13:36 Xing He
KVO does now support arbitrary types.

" It appears that KVO uses the NSInvocation support to package up the parameters being passed in. This allows it to support any type that the forwarding machinery can understand, which should be everything."

mentioned in http://www.mikeash.com/pyblog/friday-qa-2009-01-23.html  回復  更多評論
  

# re: [深入淺出Cocoa]詳解鍵值觀察(KVO)及其實現機理[未登錄] 2014-08-01 16:31 Mars
- 非常不錯,看了有了新的認識。  回復  更多評論
  

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            最新亚洲一区| 亚洲精品一区二区三| 欧美日韩在线高清| 另类欧美日韩国产在线| 国产精品日韩欧美大师| 亚洲日本视频| 亚洲国产高清aⅴ视频| 欧美一级日韩一级| 亚洲欧美日韩久久精品| 欧美日韩一区二区三区视频 | 在线亚洲国产精品网站| 你懂的国产精品永久在线| 久久久一区二区三区| 国产欧美日韩精品在线| 在线视频亚洲| 亚洲专区一区| 国产精品久久久久久久久久免费看| 亚洲精品国产精品国自产观看浪潮| 亚洲国产天堂久久综合| 久久亚洲春色中文字幕久久久 | 亚洲综合视频网| 欧美日韩另类综合| 日韩网站免费观看| 一区二区三区 在线观看视| 欧美黑人国产人伦爽爽爽| 欧美黄色视屏| 久久久久久久一区| 国产揄拍国内精品对白| 亚洲永久免费| 香港久久久电影| 国产午夜精品视频| 欧美一区亚洲一区| 牛夜精品久久久久久久99黑人| **欧美日韩vr在线| 欧美福利视频一区| 亚洲精品亚洲人成人网| 中文av一区特黄| 国产精品美女xx| 欧美一区91| 亚洲大片一区二区三区| 日韩小视频在线观看| 欧美性做爰毛片| 欧美一二三区精品| 欧美激情亚洲另类| 亚洲先锋成人| 国产有码一区二区| 欧美激情一区二区三级高清视频| 一区二区国产精品| 久久久亚洲一区| 日韩视频中午一区| 国产情侣一区| 欧美国产日韩视频| 亚洲一区免费网站| 欧美国产日韩精品| 亚洲制服av| 在线日韩av永久免费观看| 欧美日韩成人在线观看| 午夜视频在线观看一区| 亚洲国产精品久久91精品| 亚洲女ⅴideoshd黑人| 激情一区二区三区| 欧美亚州韩日在线看免费版国语版| 午夜精品美女久久久久av福利| 国产一区二区三区在线免费观看 | 一本色道综合亚洲| 国产亚洲欧美日韩精品| 欧美韩国一区| 欧美一激情一区二区三区| 91久久精品国产91性色tv| 久久av二区| 夜夜嗨av一区二区三区网站四季av| 国产欧美一区在线| 欧美日韩精品在线观看| 久久一二三区| 午夜亚洲伦理| 亚洲少妇最新在线视频| 欧美丰满高潮xxxx喷水动漫| 亚洲欧美中文另类| 一本色道久久99精品综合 | 亚洲一区二区高清视频| 亚洲高清视频的网址| 国产伦精品一区二区三区免费迷 | 亚洲直播在线一区| 亚洲激情电影在线| 国产欧美 在线欧美| 欧美日韩国产在线播放网站| 久久久精品国产一区二区三区| 中国av一区| 一本色道久久综合亚洲精品婷婷 | 亚洲在线播放电影| 一本色道久久综合狠狠躁篇的优点| 国产一区二区三区高清| 国产精品一区久久久久| 国产精品v亚洲精品v日韩精品| 欧美激情精品久久久久| 免费亚洲网站| 久久综合九色综合久99| 久久婷婷丁香| 久久久xxx| 久久久久免费| 久久天天躁狠狠躁夜夜爽蜜月 | 欧美激情欧美激情在线五月| 欧美一区二区精品在线| 夜夜嗨av一区二区三区| 亚洲日韩视频| 亚洲欧洲在线免费| 亚洲人成毛片在线播放| 亚洲黄色尤物视频| 亚洲人成网站在线观看播放| 亚洲激情在线播放| 亚洲精品一区中文| 99视频在线观看一区三区| 亚洲精品视频在线看| 夜夜精品视频一区二区| 在线亚洲伦理| 欧美一级网站| 久久久久久亚洲精品不卡4k岛国| 久久久久久久999| 美女网站在线免费欧美精品| 你懂的亚洲视频| 亚洲欧洲精品一区二区三区不卡| 91久久国产自产拍夜夜嗨| 日韩一级免费| 亚洲欧美日韩一区二区三区在线| 欧美综合国产| 你懂的视频一区二区| 欧美日韩一区精品| 国产区日韩欧美| 亚洲第一福利视频| 亚洲最新在线| 久久精品官网| 亚洲福利在线看| 一区二区三区高清在线| 欧美中日韩免费视频| 久久久久久久久久久成人| 亚洲天堂av图片| 欧美亚洲在线视频| 可以看av的网站久久看| 最新高清无码专区| 亚洲综合清纯丝袜自拍| 久久综合网络一区二区| 欧美日韩亚洲天堂| 黄色精品免费| 99re成人精品视频| 久久全球大尺度高清视频| 亚洲国产美女精品久久久久∴| 亚洲少妇中出一区| 免费视频一区| 国产精品一区二区三区四区五区| 影院欧美亚洲| 欧美一区午夜视频在线观看| 欧美阿v一级看视频| 亚洲一区二区三区高清| 免费在线播放第一区高清av| 国产乱码精品1区2区3区| 亚洲精品美女| 久久夜色精品一区| 亚洲一区二区三区影院| 欧美大尺度在线观看| 国产综合色在线| 在线观看视频一区| 日韩一区二区精品| 久久精品官网| 99精品久久久| 免费欧美在线视频| 精久久久久久| 欧美专区在线| 亚洲色图综合久久| 欧美激情综合网| 在线观看欧美激情| 久久国产精品一区二区| 宅男精品视频| 欧美日韩一区二区免费在线观看| 亚洲国产毛片完整版| 久久久91精品国产| 亚洲欧美日韩国产一区二区三区 | 亚洲图片欧美日产| 欧美激情综合色| 久久这里只有精品视频首页| 国产性做久久久久久| 欧美一区二区视频在线| 亚洲视频日本| 国产精品久久久免费| 亚洲一区二区三区777| 久久99伊人| 亚洲尤物影院| 国产精品一区在线观看你懂的| 亚洲一区3d动漫同人无遮挡| 亚洲人成在线影院| 欧美巨乳在线| 中文精品视频一区二区在线观看| 亚洲黄色影院| 欧美乱在线观看| av成人黄色| 亚洲最新合集| 国产美女精品免费电影| 久久福利影视| 久久米奇亚洲| 91久久综合| 亚洲毛片在线看|