美文网首页ios经典第三方库汇总
深入理解SDWebImage-扩展

深入理解SDWebImage-扩展

作者: 文艺女青年的男人 | 来源:发表于2019-08-08 11:35 被阅读47次

    在应用SDWebImage过程中,遇到了一些技术问题和细节问题,现在总结一下,并
    进行了相关的技术扩展,SDWebImage确实是个值得研究的框架

    场景一:当我们在一个页面中加载特别多的九宫格图片,那么当我们滑动页面肯定会造成内存的暴涨,如何处理那?

    首先对内存进行监听

    //监听内存警告
     [[NSNotificationCenter defaultCenter]addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
            NSLog(@"内存暴涨");
            // 1.取消正在下载的操作
            [[SDWebImageManager sharedManager] cancelAll];
            // 2.清除内存缓存
            [[SDWebImageManager sharedManager].imageCache clearWithCacheType:SDImageCacheTypeAll completion:nil];
      }];
    

    加载图片的处理

    // SDWebImageLowPriority 当UIScrollView滑动减速时开始加载图片 SDWebImageAvoidDecodeImage由于过多的内存消耗,这个标志可以防止解码图像。
    [self.Pic sd_setImageWithURL:self.url placeholderImage:[UIImage imageNamed:@"logo"] options:SDWebImageLowPriority|SDWebImageAvoidDecodeImage];
    

    场景二:如何让下载的图片展示出网页那种从上到下显示的效果?

    直接设置SDWebImageProgressiveLoad

    [self.image sd_setImageWithURL:self.URL placeholderImage:nil options:SDWebImageProgressiveLoad];
    
    SDWebImageProgressiveLoad

    场景三:设置了SDWebImageRefreshCached,缓存图片如何更新?

    现在提供两种方法来解决这个问题 - 旧版本SDWebImage:
    首先我们需要探讨一下实现的原理:一种是NSURLCache(NSURL缓存的get请求),一种是SD中自己定义的SDImageCache进行缓存。在SDWebImage中我们可以查看SDWebImageRefreshCached是如何定义的 - 如果设置了该类型,缓存策略依据NSURLCache而不是SDImageCache,所以可以通过NSURLCache进行缓存了;
    但是图片的更新还需要的服务器的配合才能实现,服务器如何设置那?图片的更新与否取决于你服务器的cache-control设置,如果没有cache-control设置,那么客户端就享受不了自动更新的功能。首先了解一下cache-control,
    终端中输入命令: curl [url] --head


    cache-control

    发现有Cache-Control,说明是可以的。这其实就是请求照片的过程中,返回来的header信息,这其中还包括一个名为Last-Modified、数据是时间戳的键值对。
    首先为查看HTTP协议相关的资料,发现request header中有一个名为if-Modified-Since的key,value就是服务器返回的服务器最后被修改的时间;第一次请求过程中由于并没有携带该request header所以if-Modified-Since为空,第一次请求成功之后,将返回的Last-Modified值做为if-Modified-Since的值传回给服务器。这样后台就会对if-Modified-Since和Last-Modified进行比较,如果客户端图片已经过期,那么返回状态码200、Last-modified和图片内容,客户端重新将Last-modified存储到if-Modified-Since;如果客户端返回的是304 not Modified、则不会返回last-Modified、图片内容,说明图片没有更新,直接拿缓存中数据就行。
    回到SDWebImage上,通过查看老的SDWebImageDownloader版本代码发现,它开放了一个headersFilter的block,我们可以在这个block中追加额外的header,所以我们可以在例如AppDelegate didFinishLaunching的地方追加如下代码:

    SDWebImageDownloader *imgDownloader = SDWebImageManager.sharedManager.imageDownloader;
    imgDownloader.headersFilter  = ^NSDictionary *(NSURL *url, NSDictionary *headers) {
    NSFileManager *fm = [[NSFileManager alloc] init];
    NSString *imgKey = [SDWebImageManager.sharedManager cacheKeyForURL:url];
    NSString *imgPath = [SDWebImageManager.sharedManager.imageCache defaultCachePathForKey:imgKey];
    NSDictionary *fileAttr = [fm attributesOfItemAtPath:imgPath error:nil];
    
    NSMutableDictionary *mutableHeaders = [headers mutableCopy];
    
    NSDate *lastModifiedDate = nil;
    
    if (fileAttr.count > 0) {
        if (fileAttr.count > 0) {
            lastModifiedDate = (NSDate *)fileAttr[NSFileModificationDate];
        }
    
    }
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
    formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
    formatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss z";
    
    NSString *lastModifiedStr = [formatter stringFromDate:lastModifiedDate];
    lastModifiedStr = lastModifiedStr.length > 0 ? lastModifiedStr : @"";
    [mutableHeaders setValue:lastModifiedStr forKey:@"If-Modified-Since"];
    
    return mutableHeaders;
    复制代码
    };
    

    SDWebImage
    然后加载图片的地方之前怎么写就怎么写,但是option中一定要加上SDWebImageRefreshCached
    另外一种方法:
    在SDWebImageManager.m大约167行的地方加上
    // remove SDWebImageDownloaderUseNSURLCache flag downloaderOptions &= ~SDWebImageDownloaderUseNSURLCache;
    变成了

    if (cachedImage && options & SDWebImageRefreshCached) {
    
                // force progressive off if image already cached but forced refreshing
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // remove SDWebImageDownloaderUseNSURLCache flag
                downloaderOptions &= ~SDWebImageDownloaderUseNSURLCache;
                // ignore image read from NSURLCache if image if cached but force refreshing
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
    
    

    参考链接

    注意:最新版本中已经解决了这个问题
    在之前的版本中,如果服务端更新了图片,虽然设置了SDWebImageRefreshCached,还是拿到的老图片,在最新版5.1.0中已经解决了这个问题 - 应用的SDImageCache,通过每次图片的重新网络请求,和当前的缓存数据做比较,如果不同那么就将新请求到的image通过block返回。

    //判断是否更新了,包括SDWebImageDownloaderIgnoreCachedResponse/本地缓存对比
    if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
                       //如果和本地缓存相同,那么返回SDWebImageErrorCacheNotModified
                        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:nil];
                        // call completion block with not modified error
                        [self callCompletionBlocksWithError:self.responseError];
                        [self done];
    //如果没有更新,那么在子线程进图片处理
     } else {
                        // decode the image in coder queue
                        dispatch_async(self.coderQueue, ^{
                            @autoreleasepool {
    

    另外也可以应用Cache-control,但是最新版的并没有暴露headersFilter,而是暴露了

    /**
     * Set a value for a HTTP header to be appended to each download HTTP request.
     *
     * @param value The value for the header field. Use `nil` value to remove the header field.
     * @param field The name of the header field to set.
     */
    - (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;
    

    [[SDWebImageDownloader sharedDownloader] setValue:@"" forHTTPHeaderField:@""];
    通过这个方法,我们可以将if-Modified-Since 传入SDWebImageDownloader即可,同样还是需要在option中传入SDWebImageRefreshCached

    场景四:图片的解压缩问题?

    设置SDWebImageAvoidDecodeImage,这个option到底是如何实现在子线程解压缩图片的那?

    图片的加载工作流

    将一张图片从磁盘加载到内存然后渲染到屏幕上,这个过程的消耗其实非常的大,会明显降低界面的帧速率,当滚动的时候会加剧这一情况,因为内容变化的太快,需要更快的处理速度才能保持在60FPS的帧速率。
    首先考虑一下加载的工作流程:

    • [UIImage imageWithContentsOfFile:]使用Image I/O创建CGImageRef内存映射数据。此时,图像尚未解码。
    • 返回的数据被返回给UIImageView。
    • 隐式CATransaction捕获这些层树修改。
    • 在主运行循环的下一次迭代中,Core Animation提交隐式事物,这可能涉及创建已设置为层内容的任何图像的副本。根据图像,复制它涉及一下部分或全部步骤:
      • 缓冲区被分配用于管理文件和解压缩操作
      • 文件数据从磁盘读入内存
      • 压缩的图像数据被解压缩成其未压缩的位图形式,这通常是CPU密集型操作
      • 然后Core Animation使用未压缩的位图数据来渲染涂层

    扩展:Core Animation不仅能用来做动画,实际上是一个叫做Layer kit这么一个不怎么和动画相关的名字演变来的。Core Animation其实是一个复合引擎,它的指责是尽快的组合屏幕上不同的可视内容,这个内容是被分解成独立的涂层,存储在一个叫图层树的体系之中,这个树形成了UIKit以及在iOS程序中你能在屏幕上看到的一切的基础。
    时钟信号:垂直同步信号V-Sync/水平同步信号H-Sync,有这两个信号来按照信号时间,定时进行界面的相应展示
    CPU:计算视图frame,图片的解压缩
    GPU:纹理绘制,顶点变换,像素点的填充,渲染
    当图片过大那么CPU解压就会非常耗时,那么在当前的水平同步信号到来到结束这一段时间内,如果没有解压或者渲染完成,那么到下一个H-Sync信号到来时就会出现拖尾现象 - 卡顿。

    位图

    如果不进行解压缩,直接渲染是不行的,必须要解压成位图,那么什么是位图那?

    UIImage *image = [UIImage imageNamed:@"logo.png"];
    CFDataRef mapData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
    

    通过CGDataProviderCopyData获取到的mapData就是位图,可以尝试打印,
    发现位图其实就是一个像素数组,有一个获取图片解压后位图大小的公式
    图片像素宽 图片像素高4 = 位图大小
    事实上,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。值得一提的是,在苹果的 SDK 中专门提供了两个函数用来生成 PNG 和 JPEG 图片:

    // return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
    UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);
    
    // return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)                           
    UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);
    

    所以其实我们平时的PNG/JPEG,都是压缩之后的图片,需要对图片进行解压获取图片的位图进行渲染。

    解压缩API

    默认情况下SDWebImage获取到图片的压缩文件之后,需要用户在UIImageView赋值的同时进行解压缩,但是在SDWebImage中如果设置了SDWebImageAvoidDecodeImage,根本原理是在子线程解压成位图,并进行绘制。用到的主要API就是CGBitmapContextCreate:,

    CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
        size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
        CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
        CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
       
    

    这个函数就是绘制一个位图上下文。

    • data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;

    • width 和height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;

    • bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;

    • bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。当我们指定 0/NULL 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化

    • space :就是我们前面提到的颜色空间,一般使用 RGB 即可;

    • bitmapInfo :位图的布局信息,alpha/颜色分量是否是浮点数/像素格式的字节顺序。如果有alpha那么用kCGImageAlphaPremultipliedFirst,否则用kCGImageAlphaNoneSkipFirst。像素格式(大端小端/16或者32未)使用kCGBitmapByteOrder32Host(关于布局信息的更多信息)

    查看SDWebImage中的解压

    首先获得图片是否有alpha

    //判断是否有alpha
    + (BOOL)CGImageContainsAlpha:(CGImageRef)cgImage {
        if (!cgImage) {
            return NO;
        }
        //获取图片的alpha信息
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
        //kCGImageAlphaNone没有alpha
        //kCGImageAlphaNoneSkipFirst在RGB透明通道下,alpha没有在最高有效位
        //kCGImageAlphaNoneSkipLast在RGB透明通道下,alpha没有在最低有效位
        //这三者都得包括
        BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                          alphaInfo == kCGImageAlphaNoneSkipFirst ||
                          alphaInfo == kCGImageAlphaNoneSkipLast);
        return hasAlpha;
    }
    

    然后根据Bitmap构造上下文函数生成bitmap上下文,并对图片进行transform,获取图片上下文

    + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
        if (!cgImage) {
            return NULL;
        }
        //获取图片的像素宽高
        size_t width = CGImageGetWidth(cgImage);
        size_t height = CGImageGetHeight(cgImage);
        if (width == 0 || height == 0) return NULL;
        size_t newWidth;
        size_t newHeight;
        //查看当前图片的展示方式是否正确,对width/height进行调整
        switch (orientation) {
            case kCGImagePropertyOrientationLeft:
            case kCGImagePropertyOrientationLeftMirrored:
            case kCGImagePropertyOrientationRight:
            case kCGImagePropertyOrientationRightMirrored: {
                //kCGImagePropertyOrientationRightMirrored这种情况应该交换宽高
                newWidth = height;
                newHeight = width;
            }
                break;
            default: {
                //否则不需要处理
                newWidth = width;
                newHeight = height;
            }
                break;
        }
        //是否有alpha通道
        BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
        
        //像素格式中的字节顺序是系统提供的32位主机字节顺序
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        //将像素格式中用位域技术添加alpha信息
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        //获得位图的上下文
        //默认的颜色空间是CGColorSpaceCreateDeviceRGB()
        //bytesPerRow 每一行的位图大小设置为0,系统进行自动计算并且进行优化
        //每一个像素的颜色分量bit数是8
        CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
        if (!context) {
            return NULL;
        }
        
        //图片进行反转,保证展示出来的是没有transform的图片
        CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
        CGContextConcatCTM(context, transform);
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
        //获得当前上下文中的位图对应的图片
        CGImageRef newImageRef = CGBitmapContextCreateImage(context);
        CGContextRelease(context);
        
        return newImageRef;
    }
    

    这就是整个的图片在自线程利用CGBitmapContextCreate进行解压缩的过程

    雷纯锋的blog
    图片的解压缩和渲染过程

    场景五:SD中核心方法中context使用问题?

    /**
     * 通过URL加载图片,如果cache中存在就从cache中获取,否则开始下载
     *
     * @param url            传入的image的url
     * @param options        获取图片的方式
     * @param context        获取
     * @param progressBlock  获得图片的进度(注意是在子队列中)
     * @param completedBlock  完成获取之后的回掉block
     * @return  返回一个SDWebImageCombinedOperation对象,用于表示当前的图片获取任务,在这个对象中可以取消获取图片任务
     */
    - (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageOptions)options
                                                       context:(nullable SDWebImageContext *)context
                                                      progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                                     completed:(nonnull SDInternalCompletionBlock)completedBlock;
    

    其中context中对应了很多的业务场景,我们可以自己定义

    (1). SDWebImageContextImageTransformer - 处理加载出来的图片,比如翻转圆角等
    
    (2). SDWebImageContextCacheKeyFilter - 指定图片的缓存key
    
    (3). SDWebImageContextCacheSerializer - 转换需要缓存的图片格式
    
    

    (1)、SDWebImageContextImageTransformer 其对应的是遵守SDImageTransformer协议的类,查看系统方法可以找到具体的图片处理类型:

    
    @protocol SDImageTransformer <NSObject>
    
    @required
    /**
     @return 在原始缓存中最后添加的自定义cache key
     */
    @property (nonatomic, copy, readonly, nonnull) NSString *transformerKey;
    
    /**
     调用当前方法实现图片的处理
     @param image  处理之后的图片
     @param key 原始图片关联的cache key 
     @return 处理之后的图片
     */
    - (nullable UIImage *)transformedImageWithImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key;
    
    @end
    
    #pragma mark - Pipeline
    
    /**
     //可以传入一个NSArray<SDImageTransformer>数组,按顺序做转换
     */
    @interface SDImagePipelineTransformer : NSObject <SDImageTransformer>
    
    /**
     */
    @property (nonatomic, copy, readonly, nonnull) NSArray<id<SDImageTransformer>> *transformers;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithTransformers:(nonnull NSArray<id<SDImageTransformer>> *)transformers;
    
    @end
    
    /**
     添加圆角
     */
    @interface SDImageRoundCornerTransformer: NSObject <SDImageTransformer>
    
    @property (nonatomic, assign, readonly) CGFloat cornerRadius;
    
    @property (nonatomic, assign, readonly) SDRectCorner corners;
    
    @property (nonatomic, assign, readonly) CGFloat borderWidth;
    
    @property (nonatomic, strong, readonly, nullable) UIColor *borderColor;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithRadius:(CGFloat)cornerRadius corners:(SDRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(nullable UIColor *)borderColor;
    
    @end
    
    /**
     调整大小
     */
    @interface SDImageResizingTransformer : NSObject <SDImageTransformer>
    
    @property (nonatomic, assign, readonly) CGSize size;
    
    @property (nonatomic, assign, readonly) SDImageScaleMode scaleMode;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithSize:(CGSize)size scaleMode:(SDImageScaleMode)scaleMode;
    
    @end
    
    /**
     裁剪
     */
    @interface SDImageCroppingTransformer : NSObject <SDImageTransformer>
    
    @property (nonatomic, assign, readonly) CGRect rect;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithRect:(CGRect)rect;
    
    @end
    
    /**
     翻转
     */
    @interface SDImageFlippingTransformer : NSObject <SDImageTransformer>
    
    @property (nonatomic, assign, readonly) BOOL horizontal;
    
    @property (nonatomic, assign, readonly) BOOL vertical;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical;
    
    @end
    
    /**
     旋转
     */
    @interface SDImageRotationTransformer : NSObject <SDImageTransformer>
    
    @property (nonatomic, assign, readonly) CGFloat angle;
    
    @property (nonatomic, assign, readonly) BOOL fitSize;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithAngle:(CGFloat)angle fitSize:(BOOL)fitSize;
    
    @end
    
    #pragma mark - Image Blending
    
    /**
     添加色彩
     */
    @interface SDImageTintTransformer : NSObject <SDImageTransformer>
    
    @property (nonatomic, strong, readonly, nonnull) UIColor *tintColor;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor;
    
    @end
    
    #pragma mark - Image Effect
    
    /**
     添加模糊
     */
    @interface SDImageBlurTransformer : NSObject <SDImageTransformer>
    
    @property (nonatomic, assign, readonly) CGFloat blurRadius;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithRadius:(CGFloat)blurRadius;
    
    @end
    
    #if SD_UIKIT || SD_MAC
    /**
     添加滤镜
     */
    @interface SDImageFilterTransformer: NSObject <SDImageTransformer>
    
    @property (nonatomic, strong, readonly, nonnull) CIFilter *filter;
    
    - (nonnull instancetype)init NS_UNAVAILABLE;
    + (nonnull instancetype)transformerWithFilter:(nonnull CIFilter *)filter;
    
    @end
    

    通过这几个类可以对SDWebImageContextImageTransformer中value进行自定义,以达到我们的需要

    (2)、SDWebImageContextCacheKeyFilter - 图片的指定缓存key,达到自定义缓存的目的,生成SDWebImageCacheKeyFilter对象。
    一种是直接赋值给SDWebImageManager,另一种是放到context中处理。
    在UIImageView调用加载图片时,设置下面代码,自定义缓存key。
    注意block回调是在global queue中进行的。

    设置SDWebImageCacheKeyFilter,在SD内部根据URL缓存数据时,会进入block中,可以对url进行自定义
    SDWebImageManager.sharedManager.cacheKeyFilter =[SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:^NSString * _Nullable(NSURL * _Nonnull url) {
        url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
        return [url absoluteString];
     }];
    

    (3)、SDWebImageContextCacheSerializer - 缓存的图片格式 SDWebImageCacheSerializer对象
    一种是直接赋值给SDWebImageManager,另一种是放到context中处理。
    当webP格式的图片data数据从磁盘读取时,会比普通格式的图片读取更加费时,所以我们下载完webP格式的data数据,将其图片格式变为PNG/JPEG,然后将NSData数据放入磁盘,这样下次读取的时候速度会更快。
    在UIImageView调用加载图片时,设置下面代码,自定义缓存图片格式。
    注意block回调是在global queue中进行的。

     SDWebImageManager.sharedManager.cacheSerializer = [SDWebImageCacheSerializer cacheSerializerWithBlock:^NSData * _Nullable(UIImage * _Nonnull image, NSData * _Nullable data, NSURL * _Nullable imageURL) {
        SDImageFormat format = [NSData sd_imageFormatForImageData:data];
        switch (format) {
            case SDImageFormatWebP:
                return image.images ? data : nil;
            default:
                return data;
        }
    }];
    

    相关文章

      网友评论

        本文标题:深入理解SDWebImage-扩展

        本文链接:https://www.haomeiwen.com/subject/oartjctx.html