WKWebView緩存和緩存刷新

  • 實現(xiàn):
    1、WKWebView加載過內(nèi)容需要做本地存儲。
    2、WKWebView加載的url本地有緩存時,在無網(wǎng)狀態(tài)下也能加載出來。
    3、WKWebView加載的url本地有緩存,但網(wǎng)頁內(nèi)容更改時,需要重新加載url(不取本地緩存,加載完成后更新本地緩存)。
    4、可設(shè)置緩存時間緩存最大容量。(參考“SD_WebImage”封裝的內(nèi)部緩存及清理緩存方式)。
    5、可清除緩存。

話不多說,直接上代碼(封裝類):


#import <WebKit/WebKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface LCWebViewCache : NSObject

/// 從池中獲取一個WKWebView
+ (WKWebView *)getWKWebViewFromPool;

/// 緩存路徑
+ (NSString *)getCacheDirectory;

/// 清除緩存
/// @param completionBlock 完成回調(diào)
+ (void)cleanDiskWithCompletionBlock:(void(^)(void))completionBlock;

@end

NS_ASSUME_NONNULL_END
#import "LCWebViewCache.h"
#import <CommonCrypto/CommonDigest.h>
#import "ReactiveObjC.h"
#import "NSDictionary+Lotus.h"

static NSTimeInterval const kLCWebViewCacheMaxCacheAge = -604800; //過期時間: 一周
static NSUInteger const kLCWebViewCacheMaxCacheSize = 524288000; //緩存大小: 500M
static NSString * const kLCWebViewCacheDirectory = @"LCWebViewCache"; //緩存文件夾


//MARK: - WKWebView (LCWebViewCache)
@implementation WKWebView (LCWebViewCache)
+ (BOOL)handlesURLScheme:(NSString *)urlScheme {
    return NO;
}
@end


//MARK: - LCWebViewCacheReourceItem
@interface LCWebViewCacheReourceItem : NSObject
@property (nonatomic,strong) NSURLResponse *response;
@property (nonatomic,strong) NSData *data;
@property (nonatomic,strong) NSError *error;
@end
@implementation LCWebViewCacheReourceItem
@end


//MARK: - LCWebViewURLHandler
@interface LCWebViewURLHandler : NSObject <WKURLSchemeHandler>
@property (nonatomic, strong) NSMutableDictionary *taskVaildDic;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@property (nonatomic, strong) NSOperationQueue *operationQueue;
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, copy) NSString *rootCachePath;
@end


@implementation LCWebViewURLHandler

- (instancetype)init {
    if (self = [super init]) {
        self.taskVaildDic = [NSMutableDictionary dictionary];
        self.serialQueue = dispatch_queue_create("lcweb_serial_queue", NULL);
        self.operationQueue = [[NSOperationQueue alloc] init];
        self.operationQueue.maxConcurrentOperationCount = 10;
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        self.session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:self.operationQueue];
        self.rootCachePath = [LCWebViewCache getCacheDirectory];
    }
    return self;
}

- (void)dealloc {
    [self.session invalidateAndCancel];
    self.session = nil;
}

- (NSString *)md5:(NSString *)string {
    const char* ptr = [string UTF8String];
    unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
    // CC_MD5 之前
    CC_SHA1(ptr, (int)strlen(ptr), md5Buffer);
    NSMutableString* output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
    for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
        [output appendFormat:@"%02x",md5Buffer[i]];
    }
    return output;
}

- (NSData *)dataForRequestId:(NSString *)requestId {
    //load from disk
    NSString *cacheFilePath = [self filePathWithType:1 sessionID:requestId];
    return [NSData dataWithContentsOfFile:cacheFilePath];
}

- (NSDictionary *)responseHeadersWithRequestID:(NSString *)requestId {
    //load from disk
    NSString *responsePath = [self filePathWithType:0 sessionID:requestId];
    return [NSDictionary dictionaryWithContentsOfFile:responsePath];
}

- (void)finishRequestForRequest:(NSURLRequest *)request
                       response:(NSURLResponse *)response
                         result:(NSData *)result {
    //load from cache
    NSString *responseId = [self md5:request.URL.absoluteString]; //存請求的url
    NSHTTPURLResponse *httpRes = (NSHTTPURLResponse *)response;
    NSDictionary *responseHeaders = httpRes.allHeaderFields;
    if (responseHeaders) {
        NSString *responsePath = [self filePathWithType:0 sessionID:responseId];
        [responseHeaders writeToFile:responsePath atomically:YES];
    }
    if (result) {
        NSString *dataPath = [self filePathWithType:1 sessionID:responseId];
        [result writeToFile:dataPath atomically:YES];
    }
}

/// 緩存請求
/// @param type 0:responseHeaders, 1:responseData
/// @param sessionID 唯一id
- (NSString *)filePathWithType:(NSInteger)type sessionID:(NSString *)sessionID {
    NSString *cacheFileName = [sessionID stringByAppendingPathExtension:[@(type) stringValue]];
    return [self.rootCachePath stringByAppendingPathComponent:cacheFileName];
}

- (LCWebViewCacheReourceItem *)loadResource:(NSURLRequest *)request {
    //load from cache
    NSString *requestId = [self md5:request.URL.absoluteString];
    NSDictionary *responseHeaders = [self responseHeadersWithRequestID:requestId];
    if (responseHeaders) {
        LCWebViewCacheReourceItem *item = [[LCWebViewCacheReourceItem alloc] init];
        NSHTTPURLResponse *resp = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:responseHeaders];
        item.response = resp;
        item.data = [self dataForRequestId:requestId];
        return item;
    } else {
        return nil;
    }
}

- (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL {
    NSArray *cookieArray = [self searchAppropriateCookies:URL];
    if (cookieArray != nil && cookieArray.count > 0) {
        NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieArray];
        if ([cookieDic objectForKey:@"Cookie"]) {
            return cookieDic[@"Cookie"];
        }
    }
    return nil;
}

- (NSArray *)searchAppropriateCookies:(NSURL *)URL {
    NSMutableArray *cookieArray = [NSMutableArray array];
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
        if ([URL.host containsString:cookie.domain]) {
            [cookieArray addObject:cookie];
        }
    }
    return cookieArray;
}

//MARK: WKURLSchemeHandler
- (void)webView:(WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
    dispatch_sync(self.serialQueue, ^{
        [self.taskVaildDic setValue:@(YES) forKey:urlSchemeTask.description];
    });
    
    //獲取Cookie
    NSURLRequest *request = [urlSchemeTask request];
    NSMutableURLRequest *mutaRequest = [request mutableCopy];
    [mutaRequest setValue:[self getRequestCookieHeaderForURL:request.URL] forHTTPHeaderField:@"Cookie"];
    request = [mutaRequest copy];
    //判斷是否緩存
    BOOL shouldCache = YES;
    if (request.HTTPMethod && ![request.HTTPMethod.uppercaseString isEqualToString:@"GET"]) {
        shouldCache = NO;
    }
    NSString *hasAjax = [request valueForHTTPHeaderField:@"X-Requested-With"];
    if (hasAjax != nil) {
        shouldCache = NO;
    }
    //獲取緩存item
    LCWebViewCacheReourceItem *item = [self loadResource:request];
    NSDictionary *responseHeaders = [(NSHTTPURLResponse *)item.response allHeaderFields];
    NSString *contentType = responseHeaders[@"Content-Type"];
    if ([contentType isEqualToString:@"video/mp4"]) {
        shouldCache = NO;
    }
    
    // 獲取網(wǎng)頁內(nèi)容有更改時關(guān)鍵字段
    NSDictionary *cachedHeaders = [[NSUserDefaults standardUserDefaults] objectForKey:mutaRequest.URL.absoluteString];
    //設(shè)置request headers (帶上上次的請求頭下面兩參數(shù)一種就可以,也可以兩個都帶上)
    if (cachedHeaders) {
        NSString *etag = [cachedHeaders objectForKey:@"Etag"];
        if (etag) {
            [mutaRequest setValue:etag forHTTPHeaderField:@"If-None-Match"];
        }
        NSString *lastModified = [cachedHeaders objectForKey:@"Last-Modified"];
        if (lastModified) {
            [mutaRequest setValue:lastModified forHTTPHeaderField:@"If-Modified-Since"];
        }
    }
    // 請求是否需要刷新網(wǎng)頁
    @weakify(self);
    NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        NSLog(@"httpResponse == %@", httpResponse);
        // 根據(jù)statusCode設(shè)置緩存策略
        if ((httpResponse.statusCode == 304 || httpResponse.statusCode == 0) && item && shouldCache) {
            [urlSchemeTask didReceiveResponse:item.response];
            if (item.data) {
                [urlSchemeTask didReceiveData:item.data];
            }
            [urlSchemeTask didFinish];
            [mutaRequest setCachePolicy:NSURLRequestReturnCacheDataElseLoad];
        } else {
            @strongify(self);
            if (![self.taskVaildDic boolValueForKey:urlSchemeTask.description default:NO] || !urlSchemeTask){
                return;
            }
            [urlSchemeTask didReceiveResponse:response];
            [urlSchemeTask didReceiveData:data];
            if (error) {
                [urlSchemeTask didFailWithError:error];
            } else {
                [urlSchemeTask didFinish];
                if (shouldCache) {
                    [self finishRequestForRequest:request response:response result:data];
                }
            }
            [mutaRequest setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
            // 保存當(dāng)前的NSHTTPURLResponse
            [[NSUserDefaults standardUserDefaults] setObject:httpResponse.allHeaderFields forKey:mutaRequest.URL.absoluteString];
        }
    }];
    [dataTask resume];
}

- (void)webView:(WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
    dispatch_sync(self.serialQueue, ^{
         [self.taskVaildDic setValue:@(NO) forKey:urlSchemeTask.description];
    });
}
@end


//MARK: - LCWebViewCache
@interface LCWebViewCache ()
@property (nonatomic, assign) NSUInteger initialViewsMaxCount;
@property (nonatomic, strong) NSMutableArray <WKWebView *>*preloadedViews;
@end

@implementation LCWebViewCache
+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    static LCWebViewCache *instance = nil;
    dispatch_once(&onceToken,^{
        instance = [[super allocWithZone:NULL] init];
    });
    return instance;
}

+ (id)allocWithZone:(struct _NSZone *)zone{
    return [self sharedInstance];
}
 
- (instancetype)init {
    if (self = [super init]) {
        self.initialViewsMaxCount = 10;
        self.preloadedViews = [NSMutableArray arrayWithCapacity:self.initialViewsMaxCount];
        [self prepareWithCount:self.initialViewsMaxCount];
    }
    return self;
}

+ (void)initialize {
    [self cleanDiskWithCompletionBlock:^{
        NSLog(@"cleanDisk");
    }];
}

//MARK: Public
/// 從池中獲取一個WKWebView
+ (WKWebView *)getWKWebViewFromPool {
    return [[self sharedInstance] getWKWebViewFromPool];
}

/// 緩存路徑
+ (NSString *)getCacheDirectory {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *path = [paths.firstObject stringByAppendingPathComponent:kLCWebViewCacheDirectory];
    
    BOOL isDir = YES;
    if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir]) {
        NSError *error = nil;
        [[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
        if (error) {
            return nil;
        }
    }
    return path;
}

/// 清除緩存
/// @param completionBlock 完成回調(diào)
+ (void)cleanDiskWithCompletionBlock:(void(^)(void))completionBlock {
    NSString *diskCachePath = [self getCacheDirectory];
    NSFileManager *fileManager = [NSFileManager defaultManager];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 這兩個變量主要是為了下面生成NSDirectoryEnumerator準(zhǔn)備的
        // 一個是記錄遍歷的文件目錄,一個是記錄遍歷需要預(yù)先獲取文件的哪些屬性
        NSURL *diskCacheURL = [NSURL fileURLWithPath:diskCachePath isDirectory:YES];
        NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
        
        // 遞歸地遍歷diskCachePath這個文件夾中的所有目錄,此處不是直接使用diskCachePath,而是使用其生成的NSURL
        // 此處使用includingPropertiesForKeys:resourceKeys,這樣每個file的resourceKeys對應(yīng)的屬性也會在遍歷時預(yù)先獲取到
        // NSDirectoryEnumerationSkipsHiddenFiles表示不遍歷隱藏文件
        NSDirectoryEnumerator *fileEnumerator = [fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        // 獲取文件的過期時間,SDWebImage中默認(rèn)是一個星期
        // 不過這里雖然稱*expirationDate為過期時間,但是實質(zhì)上并不是這樣。
        // 其實是這樣的,比如在2015/12/12/00:00:00最后一次修改文件,對應(yīng)的過期時間應(yīng)該是
        // 2015/12/19/00:00:00,不過現(xiàn)在時間是2015/12/27/00:00:00,我先將當(dāng)前時間減去1個星期,得到
        // 2015/12/20/00:00:00,這個時間才是我們函數(shù)中的expirationDate。
        // 用這個expirationDate和最后一次修改時間modificationDate比較看誰更晚就行。
        NSTimeInterval maxCacheAge = kLCWebViewCacheMaxCacheAge;
        NSUInteger maxCacheSize = kLCWebViewCacheMaxCacheSize;
        
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:maxCacheAge];
        // 用來存儲對應(yīng)文件的一些屬性,比如文件所需磁盤空間
        NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
        // 記錄當(dāng)前已經(jīng)使用的磁盤緩存大小
        NSUInteger currentCacheSize = 0;

        // 在緩存的目錄開始遍歷文件.  此次遍歷有兩個目的:
        //  1. 移除過期的文件
        //  2. 同時存儲每個文件的屬性(比如該file是否是文件夾、該file所需磁盤大小,修改時間)
        NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
            // 當(dāng)前掃描的是目錄,就跳過
            if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }
            // 移除過期文件
            // 這里判斷過期的方式:對比文件的最后一次修改日期和expirationDate誰更晚,如果expirationDate更晚,就認(rèn)為該文件已經(jīng)過期,具體解釋見上面
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            // 計算當(dāng)前已經(jīng)使用的cache大小,
            // 并將對應(yīng)file的屬性存到cacheFiles中
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
            [cacheFiles setObject:resourceValues forKey:fileURL];
        }
        
        for (NSURL *fileURL in urlsToDelete) {
            // 根據(jù)需要移除文件的url來移除對應(yīng)file
            [fileManager removeItemAtURL:fileURL error:nil];
        }
        // 如果我們當(dāng)前cache的大小已經(jīng)超過了允許配置的緩存大小,那就刪除已經(jīng)緩存的文件。
        // 刪除策略就是,首先刪除修改時間更早的緩存文件
        if (maxCacheSize > 0 && currentCacheSize > maxCacheSize) {
            // 直接將當(dāng)前cache大小降到允許最大的cache大小的一般
            const NSUInteger desiredCacheSize = maxCacheSize / 2;
            // 根據(jù)文件修改時間來給所有緩存文件排序,按照修改時間越早越在前的規(guī)則排序
            NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                            usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                            }];
            // 每次刪除file后,就計算此時的cache的大小
            // 如果此時的cache大小已經(jīng)降到期望的大小了,就停止刪除文件了
            for (NSURL *fileURL in sortedFiles) {
                if ([fileManager removeItemAtURL:fileURL error:nil]) {
                    // 獲取該文件對應(yīng)的屬性
                    NSDictionary *resourceValues = cacheFiles[fileURL];
                    // 根據(jù)resourceValues獲取該文件所需磁盤空間大小
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    // 計算當(dāng)前cache大小
                    currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        // 如果有completionBlock,就在主線程中調(diào)用
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}


//MARK: Private
/// 預(yù)初始化若干WKWebView
/// @param count 個數(shù)
- (void)prepareWithCount:(NSUInteger)count {
    // Actually does nothing, only initialization must be called.
    while (self.preloadedViews.count < MIN(count,self.initialViewsMaxCount)) {
        id preloadedView = [self createPreloadedView];
        if (preloadedView) {
            [self.preloadedViews addObject:preloadedView];
        } else {
            break;
        }
    }
}

/// 從池中獲取一個WKWebView
- (WKWebView *)getWKWebViewFromPool {
    if (!self.preloadedViews.count) {
        return [self createPreloadedView];
    } else {
        id preloadedView = self.preloadedViews.firstObject;
        [self.preloadedViews removeObject:preloadedView];
        return preloadedView;
    }
}

/// 創(chuàng)建一個WKWebView
- (WKWebView *)createPreloadedView {
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    configuration.preferences.javaScriptEnabled = YES;
    configuration.suppressesIncrementalRendering = YES; // 是否支持記憶讀取
    [configuration.preferences setValue:@YES forKey:@"allowFileAccessFromFileURLs"];//支持跨域
    [configuration setURLSchemeHandler:[LCWebViewURLHandler new] forURLScheme:@"https"];
    [configuration setURLSchemeHandler:[LCWebViewURLHandler new] forURLScheme:@"http"];
    
#ifndef POD_CONFIGURATION_RELEASE
    [self setupVConsoleEnabled:configuration]; //顯示vConsole
#endif
    
    WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
    wkWebView.allowsBackForwardNavigationGestures = YES; // 是否允許手勢左滑返回上一級, 類似導(dǎo)航控制的左滑返回
    wkWebView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    return wkWebView;
}

/// 用于進(jìn)行JavaScript注入
/// @param configuration 配置
- (void)setupVConsoleEnabled:(WKWebViewConfiguration *)configuration {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"LCWebModule" ofType:@"bundle"];
    NSString *vConsolePath = [path stringByAppendingPathComponent:@"vconsole.min.js"];
    NSString *vConsoleStr = [NSString stringWithContentsOfFile:vConsolePath encoding:NSUTF8StringEncoding error:nil];
    NSString *jsErrorPath = [path stringByAppendingPathComponent:@"jserror.js"];
    NSString *jsErrorStr = [NSString stringWithContentsOfFile:jsErrorPath encoding:NSUTF8StringEncoding error:nil];
    if (vConsoleStr && jsErrorStr) {
        NSString *jsStr = [vConsoleStr stringByAppendingString:jsErrorStr];
        WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jsStr injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
        [configuration.userContentController addUserScript:wkUScript];
    }
}
@end

使用方式如下:

#import "ViewController.h"
#import <WebKit/WKWebView.h>
#import "LCWebViewCache.h"


@interface ViewController ()<WKNavigationDelegate,WKUIDelegate>
@property (nonatomic,strong) WKWebView *webView;
@property (nonatomic,copy) NSString *url;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.url = @"http://www.itdecent.cn/u/3715407414e6";
    self.webView.frame = self.view.bounds;
    [self.view addSubview:self.webView];
    [self startLoadRequest];
}

- (void)startLoadRequest {
    self.url = [self.url stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    if ([self.url hasPrefix:@"http://"] || [self.url hasPrefix:@"https://"] || [self.url containsString:@"www"]) {
        NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:[self.url stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]]cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60];
        [self.webView loadRequest:request];
    } else {
        [self.webView loadHTMLString:self.url baseURL:nil];
    }
}

#pragma mark - Get
- (WKWebView *)webView {
    if (!_webView) {
        _webView = [LCWebViewCache getWKWebViewFromPool];
        _webView.navigationDelegate = self;
        _webView.UIDelegate = self;
    }
    return _webView;
}


@end

Demo在此

以上就是封裝和使用方式,因近期工作有需求,寫了下,不足之處還請各位大佬不吝賜教。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容