iOS开源库源码解析之SDWebImage

来自Leo的原创博客,转载请著名出处

我的stackoverflow

这个源码解析系列的文章


前言

SDWebImage是iOS开发中十分流行的库,大多数的开发者在下载图片或者加载网络图片并且本地缓存的时候,都会用这个框架。这个框架相对来说,源代码还是比较少的。本文会详细的讲解这些类的架构关系和原理。

本文会先介绍类的整体架构关系,先有一个宏观的认识。然后讲解sd_setImageWithURL的加载逻辑,因为这是SDWebImage最核心的,也是很多面试会问到的。接下来会介绍Image的解码,然后讲解缓存处理。最后再讲解API设计方式,以及其他我认为有用的。


整体架构关系

按照分组方式,可以分为几组

定义通用宏和方法

  • SDWebImageCompat, 宏定义和C语言的一些工具方法
  • SDWebImageOperation,定义通用的Operation协议,主要就是一个方法,cancel。从而在cancel的时候,可以面向协议编程。

下载

  • SDWebImageDownloader 实际的下载功能和配置提供者,使用了单例的设计模式
  • SDWebImageDownloaderOperation,继承自NSOperation,是一个异步的NSOperation,封装了NSURLConnection进行实际的下载任务

缓存处理

  • AutoPurgeCache,NSCache的子类,用于内存cache,会在收到内存警告的时候,自动清空
  • SDImageCache,实际处理内存cache和磁盘cache

功能类

  • SDWebImageManager,宏观的从整体上管理整个框架的类
  • SDWebImageDecoder,图片的解码类,后面会详细的讲解如何解码的
  • SDWebImagePrefetcher,图片的预加载管理

类别

  • 类别用来为UIView和UIImageView等”添加”属性来存储必要的信息,同时暴露出接口,进行实际的操作。

Tips:

  1. 用类别来提供接口往往是最方便的,因为用户只需要import这个文件,就可以像使用原生SDK那样去开发,不需要修改原有的什么代码
  2. 面向对象开发有一个原则是-单一功能原则,所以不管是在开发一个Lib或者开发App的时候,尽量保证各个模块之前功能单一,这样会降低耦合。

sd_setImageWithURL的加载逻辑

1. 取消当前正在加载的图片

  [self sd_cancelCurrentImageLoad];

这个方法的实际调用源代码如下,其中key是UIImageViewImageLoad

Tips:operationDictionary是通过Runtime为UIView”添加”的属性,不懂的同学可以看看我这篇文章

- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
     //用一个字典来存储当前的加载operation
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey:key];
    //两种类型,帧类型的的gif是多个operation,静态图是一个operaiton
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            //这里属于面向协议编程,不关心具体的类,只关心遵守某个协议
            [(id<SDWebImageOperation>) operations cancel];
        }
        //删除对应的key
        [operationDictionary removeObjectForKey:key];
    }
}


2. 如果有PlaceHolder,设置placeHolder

   if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }

这里的dispatch_main_async_safe是一个宏定义,会检查调用是否在主线程上,如果在主线程就直接调用,后台线程会用gcd切换到主线程

#define dispatch_main_async_safe(block)    if ([NSThread isMainThread]) {        block();    } else {        dispatch_async(dispatch_get_main_queue(), block);    }


3. 根据SDImageCache来查缓存,看看是否有图片

查看缓存的是这个方法

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {//异步返回查询的结果}

这块感觉代码优点难懂,其实这是执行了一个方法queryDiskCacheForKey:key,返回一个NSOperation,之所以这样,是因为从磁盘或者内存查询的过程是异步的,后面可能需要cancel,所以这样做。

我们再看看queryDiskCacheForKey:key这个方法是怎么实现的?

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    //输入检查,这里省略掉
    //先检查磁盘缓存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }
    //检查磁盘缓存
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{//切换到io队列上,进行磁盘操作
            //省略中间检查代码
            //回归到主线程行,进行doneBlock操作
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });
    return operation;
}


4. 创建下载任务

id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
 //这里是下载完成后的回调,没什么要讲解的,简单来说就是image下载成功,就wself.image = image;[wself setNeedsLayout];,下载失败仍然显示placeHolder。然后调用completion block回调。
        }];
//记录下来当前的下载,方便后面取消
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

接下来,我们来看看实际的下载operation是什么样子的

也就是这个方法

-(id)downloadImageWithURL:options:progress:completed:



3-1,由于有各种各样的block回调,例如下载进度的回调,完成的回调,所以需要一个数据结构来存储这些回调

所以,这个方法中,首先调用以下方法来存储回调

[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
//...
}

其中,用来存储回调的数据结构是一个NSMutableDictionary,其中key是图片的url,value是回调的数组

举个例子,存储后应该是这样的,

@{
        @"http://iamgeurl":[
                            @{
                                @"progress":progressBlock1,
                                @"completed":completedBlock1,
                            },
                            @{
                                @"progress":progressBlock2,
                                @"completed":completedBlock2,
                              },
                           ],
            //其他
}

Tips:注意,对于同一个URL,在第二次调用addProgressCallback:progressBlock用的时候,并不会执行createCallback,也就是说,保证一个URL在多次下载的时候,只进行多次回调,而不会进行多次网络请求

如果是我,可能更愿意用一个对象来存储这些block回调,觉得这个数据结构有点复杂,很难维护



3-2,对于同一个url,在第一次调用sd_setImage的时候进行,创建网络请求SDWebImageDownloaderOperation

创建的方法是这个

[[wself.operationClass alloc] initWithRequest:request
                                      options:options
                                     progress:^(NSInteger receivedSize, NSInteger expectedSize){//Progress 回调}
                                     completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished){//Completion回调}
                                     cancelled:^{//Cancel 回调}

在看看Progress回调

 //Block中强引用sself(weakself),保证在执行结束前不会被释放
 SDWebImageDownloader *sself = wself;
 //如果weakself已经为nil,此时已经释放了,所以直接放回
 if (!sself) return;
 //用__block来修饰callbacksForURL,保证在能在block中修改这个变量
 __block NSArray *callbacksForURL;
 //在队列`barrierQueue`里同步捕获callBack
 dispatch_sync(sself.barrierQueue, ^{
     callbacksForURL = [sself.URLCallbacks[url] copy];
 });
 for (NSDictionary *callbacks in callbacksForURL) {
//异步切换到主线程上进行回调
   dispatch_async(dispatch_get_main_queue(), ^{
         SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
         if (callback) callback(receivedSize, expectedSize);
     });
 }

completion回调和progress类似,不再赘述。

再看看cancel block的处理

SDWebImageDownloader *sself = wself;
if (!sself) return;
//阻碍barrierQueue,
dispatch_barrier_async(sself.barrierQueue, ^{
    [sself.URLCallbacks removeObjectForKey:url];
});

Tips:这里为什么要用dispatch_barrier_async呢?因为

_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

_barrierQueue是个并行队列,意味着队列上的任务可以并行执行。用dispatch_barrier_async来保证后续提交的block等待当前的dispatch_barrier_asyncblock执行完毕后再执行。

Tips:

  1. 用这么多GCD是为了保证线程安全

再简单提一下dispatch_barrier_async 的用法

Calls to this function always return immediately after the block has been submitted and never wait for the block to be invoked. When the barrier block reaches the front of a private concurrent queue, it is not executed immediately. Instead, the queue waits until its currently executing blocks finish executing. At that point, the barrier block executes by itself. Any blocks submitted after the barrier block are not executed until the barrier block completes.



4. 下载图片完成后,根据需要图片解码和处理图片格式,回调给Imageview

 UIImage *image = [UIImage sd_imageWithData:self.imageData];
            NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
            image = [self scaledImageForKey:key image:image];

            // Do not force decoding animated GIFs
            if (!image.images) {
                if (self.shouldDecompressImages) {
                    image = [UIImage decodedImageWithImage:image];
                }
            }

总结下整个调用过程

  1. 取消上一次调用
  2. 设置placeHolder
  3. 保存回调block
  4. cache查询是否已经下载过了,先检查内存,后检查磁盘
  5. 利用NSURLConnection来下载图片,根据需要解码,回调给imageview,存储到缓存

线程管理

整个SDWebImage一共有四个队列

  • Main queue,主队列,在这个队列上进行UIKit对象的更新,发送notification
  • barrierQueue,并行队列,在这个队列上统一处理3-1中的数据回调,为了保证线程安全,一致使用dispatch_barrier_sync
  • ioQueue,用在图片的磁盘操作
  • downloadQueue(NSOperationQueue),用来全局的管理下载的任务

图片解码

传统的UIImage进行解码都是在主线程上进行的,比如

UIImage * image = [UIImage imageNamed:@"123.jpg"]
self.imageView.image = image;

在这个时候,图片其实并没有解码。而是,当图片实际需要显示到屏幕上的时候,CPU才会进行解码,绘制成纹理什么的,交给GPU渲染。这其实是很占用主线程CPU时间的,而众所周知,主线程的时间真的很宝贵

现在,我们看看SDWebImage是如何在后台进行解码的

代码来自于这个原文件SDWebImageDecoder

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (image == nil) {
        return nil;
    }

    @autoreleasepool{
        //Gif不用解码,直接返回
        if (image.images != nil) {
            return image;
        }
        CGImageRef imageRef = image.CGImage
        ;
        CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
        BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                         alpha == kCGImageAlphaLast ||
                         alpha == kCGImageAlphaPremultipliedFirst ||
                         alpha == kCGImageAlphaPremultipliedLast);
        if (anyAlpha) {
        //有Alpha通道,直接返回
            return image;
        }
        //获得Color Space
        CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
        CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);

        BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                      imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                      imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                      imageColorSpaceModel == kCGColorSpaceModelIndexed);
        if (unsupportedColorSpace) {
            colorspaceRef = CGColorSpaceCreateDeviceRGB();
        }

        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        NSUInteger bytesPerPixel = 4;
        NSUInteger bytesPerRow = bytesPerPixel * width;
        NSUInteger bitsPerComponent = 8;
        //创建bitmapContext
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     bitsPerComponent,
                                                     bytesPerRow,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);

        // 绘制Image到Context中,强制解码
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];

        if (unsupportedColorSpace) {
            CGColorSpaceRelease(colorspaceRef);
        }

        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);

        return imageWithoutAlpha;
    }
}

缓存处理

整个缓存处理的类都在SDImageCache文件中,其中缓存又包括两个方面,

  • 内存缓存
  • 磁盘缓存

其中,内存缓存采用了NSCache的子类AutoPurgeCache

AutoPurgeCache

只是对NSCache添加了在收到内存警告通知UIApplicationDidReceiveMemoryWarningNotification的时候自动removeAllObjects

再看看磁盘缓存是如何做的?

磁盘缓存是基于文件系统的,也就是说图片是以普通文件的方式存储到沙盒里的。

缓存的目录是啥?

默认的缓存目录是

Lbirary/Caches/default/com.hackemist.SDWebImageCache.default/

缓存的文件名称是对缓存的key求md5

何时自动清除过期图片?

在App关闭的时候

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(cleanDisk)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];

清除的逻辑很简单,获取文件的modify时间,然后比较下过期时间,如果过期了就删除。当磁盘缓存超过阈值后,根据最后访问的时间排序,删除最老的访问图片。

存储成什么格式?

见SDImageCache中,

//获取Alpha信息
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                  alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;

//如果又imageData,并且有png的前8个字节,根据NSData前8个字节来检查是否是png
if ([imageData length] >= [kPNGSignatureData length]) {
    imageIsPng = ImageDataHasPNGPreffix(imageData);
}
//如果是Png,存储成png
if (imageIsPng) {
    data = UIImagePNGRepresentation(image);
}
else {
//否则存储称jpg
    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}

deprecated一个API

只需要在方法后面,添加__deprecated_msg例如

+ (NSString *)contentTypeForImageData:(NSData *)data __deprecated_msg("Use `sd_contentTypeForImageData:`");

条件编译

这个在之前AsyncDisplayKit解析的文章里也提到过,这里再提一次

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
//代码
#endif

又比如

#if TARGET_OS_IOS
//代码for iOS
#else
//代码for osx
#endif

就是条件编译,根据条件是否满足来让编译器编译这段代码。

Tips:根据条件编译,可以为不同的版本的iOS做一些适配


如何实现Gif动图?

本质上,使用这个iOS SDK提供的方法

//传入一个Image数组,和动画的时间
animatedImage = [UIImage animatedImageWithImages:images duration:duration];

那么,如何解析Gif图片呢?

原理也比较简单,源代码在UIImage+GIF.m中。利用CGImageSource的一系列方法依次提取每一帧的图片和每一帧的图片间隔,然后用上文提到的API来实现Gif

Tips:在ARC开启的时候,Foundation对象(CF开头)和CoreGraphics对象(CG开头)的一些对象仍然需要手动管理,例如

    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
//利用完毕
    CGImageRelease(image);

获取图片的格式

原文件NSData+ImageContentType.m ,代码不难,不做讲解了

+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return @"image/jpeg";
        case 0x89:
            return @"image/png";
        case 0x47:
            return @"image/gif";
        case 0x49:
        case 0x4D:
            return @"image/tiff";
        case 0x52:
            // R as RIFF for WEBP
            if ([data length] < 12) {
                return nil;
            }

            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return @"image/webp";
            }

            return nil;
    }
    return nil;
}

预下载

原文件SDWebImagePrefetcher.m

可以看到,由于类的功能划分非常清楚,所以SDWebImagePrefetcher 的实现文件很简单,本质上只是用单例的设计模式,并且这个类保存了SDWebImageManager对象来进行的实际下载操作


设计方式的一点理解

  • 整个框架的处理核心是SDWebImageManager类,而为了让使用者在使用的时候不必实例化这个类的一个对象,整个类采用了单利的设计模式。
  • 用block的方式,处理复杂的异步回调。用block的方式,在这里是要比代理来的简单直接的。如果用代理,那么上文讲解的sd_setImageWithURL的过程,将会有复杂的代理回调方法
  • 每个线程处理自己的独立任务。上文提到了,这个库一共有四个Queue
  • 面向协议编程。这个在SDWebImageOperation协议的体现上十分明显。
@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

在使用的时候,只需关注协议的本身就可以了

if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
     [(id<SDWebImageOperation>) operations cancel];
 }
  • 用Category的方式提供接口,例如UIImageView+WebCache等,这样能最大程度的降低使用者的使用难度。
  • 单一功能原则,这个在上文提到了,每个类or文件负责单一的功能,方便独立测试和维护

    最好的例子就是

UIImage+GIF.h
UIImage+MultiFormat
UIImageView+HighlightedWebCache.h
UIImageView+WebCache.h
  • 线程安全的保证。很明显,SDWebImage不能强求用户在某一个线程上调用,然后自己切换回主线程。所以你会看到类似这样的代码来保证线程安全
@synchronized (self) {}
    dispatch_barrier_sync(sself.barrierQueue, ^{
                                                                callbacksForURL = [sself.URLCallbacks[url] copy];
                                                                if (finished) {
                                                                    [sself.URLCallbacks removeObjectForKey:url];
                                                                }
                                                            });

总结

SDWebImage相对来说,源代码没有那么多,建议有时间的同学自己好好研究下源代码。对图片的基础知识巩固,各种线程的处理方式,类的架构和API设计等都很有帮助。

时间: 2024-10-22 05:59:57

iOS开源库源码解析之SDWebImage的相关文章

iOS开源库源码解析之Mantle

来自Leo的原创博客,转载请著名出处 我的StackOverflow 这个源码解析系列的文章 AsnycDispalyKit SDWebImage Mantle(本文) AFNetworking(3.0) MBProgressHud SwiftJSON MagicRecord Alamofire 前言 iOS开发中,不管是哪种设计模式,Model层都是不可或缺的.而Model层的第三方库常用的库有以下几个 JSONModel Mantle MJExtension JSON data到对象的转换原

iOS开源库源码解析之AsnycDispalyKit

来自Leo的原创博客,转载请著名出处 我的stackoverflow 前言 最近心血来潮,想研究下FaceBook的AsnycDispalyKit的源代码,学习一些界面优化的技术以及编码风格.这篇文章,会详细的记录下我认为对新手有用的部分.后面有空的时候,继续研究其他几个iOS开发很流行的库-AFNetworking,SDWebImage,MBProgressHud,Mantle等`.AsnycDisplayKit是一个非常庞大的库,所以我尽量捞干的讲. 关于AsyncDisplayKit 文档

Android 开源项目源码解析(第二期)

Android 开源项目源码解析(第二期) 阅读目录 android-Ultra-Pull-To-Refresh 源码解析 DynamicLoadApk 源码解析 NineOldAnimations 源码解析 SlidingMenu 源码解析 Cling 源码解析 BaseAdapterHelper 源码分析 Side Menu.Android 源码解析 DiscreteSeekBar 源码解析 CalendarListView 源码解析 PagerSlidingTabStrip 源码解析 公共

co函数库源码解析

一.co函数是什么 co 函数库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行.短小精悍只有短短200余行,就可以免去手动编写Generator 函数执行器的麻烦 二.co函数怎么用 举个栗子就能清楚的知道如何使用co函数 1 function* gen(){ 2 var f1 = yield func1; 3 var f2 = yield fnuc2; 4 //sth to do 5 }; 手动执行和co函数执行的写法如下

Android 常用开源框架源码解析 系列 (十)Rxjava 异步框架

一.Rxjava的产生背景 一.进行耗时任务 传统解决办法: 传统手动开启子线程,听过接口回调的方式获取结果 传统解决办法的缺陷: 随着项目的深入.扩展.代码量的增大会产生回调之中套回调的,耦合度高度增加的不利场景.对代码维护和扩展是很严重的问题. RxJava本质上是一个异步操作库 优点: 使用简单的逻辑,处理复杂 ,困难的异步操作事件库;在一定程度上替代handler.AsyncTask等等 二.传统的观察者模式 使用场景 1.一个方面的操作依赖于另一个方面的状态变化 2.如果在更改一个对象

Android 常用开源框架源码解析 系列 (九)dagger2 呆哥兔 依赖注入库

一.前言 依赖注入定义 目标类中所依赖的其他的类的初始化过程,不是通过手动编码的方式创建的. 是将其他的类已经初始化好的实例自动注入的目标类中. "依赖注入"也是面向对象编程的 设计模式 -----组合的配套使用 作用 :降低程序的耦合,耦合就是因为类之间的依赖关系所引起的 产生场景:在一个对象里去创建另一个对象的实例 问题:过多的类,对象之间的依赖会造成代码难以维护. 不符合开闭原则的对象的引用写法:错误示例: public class ClassA { classB b ; pub

Android常用库源码解析

图片加载框架比较 共同优点 都对多级缓存.线程池.缓存算法做了处理 自适应程度高,根据系统性能初始化缓存配置.系统信息变更后动态调整策略.比如根据 CPU 核数确定最大并发数,根据可用内存确定内存缓存大小,网络状态变化时调整最大并发数等. 支持多种数据源支持多种数据源,网络.本地.资源.Assets 等 不同点 Picasso所能实现的功能,Glide都能做,无非是所需的设置不同.但是Picasso体积比起Glide小太多. Glide 不仅是一个图片缓存,它支持 Gif.WebP.缩略图.Gl

Android 常用开源框架源码解析 系列 (十一)picasso 图片框架

一.前言 Picasso 强大的图片加载缓存框架 api加载方式和Glide 类似,均是通过链式调用的方式进行调用 1.1.作用 Picasso 管理整个图片加载.转换.缓存等策略 1.2.简单调用: Picasso .with(this 传入一个单例,上下文).load("url"/file文件/资源路径) .into() 1.2.1 .一些简单的链式调用参数 .placeholder(R.drawable.xx)  //网络未加载完成的时候显示的本地资源图片 .error(R.dr

Android 开源项目源码解析之DynamicLoadApk 源码解析

1. 功能介绍 1.1 简介 DynamicLoadApk 是一个开源的 Android 插件化框架. 插件化的优点包括:(1) 模块解耦,(2) 动态升级,(3) 高效并行开发(编译速度更快) (4) 按需加载,内存占用更低等等. DynamicLoadApk 提供了 3 种开发方式,让开发者在无需理解其工作原理的情况下快速的集成插件化功能. 宿主程序与插件完全独立 宿主程序开放部分接口供插件与之通信 宿主程序耦合插件的部分业务逻辑 三种开发模式都可以在 demo 中看到. 1.2 核心概念