德州安卓
IP:广东
0关注数
0粉丝数
0获得的赞
工作年
编辑资料
链接我:

创作·145

全部
问答
动态
项目
学习
专栏
德州安卓

iOS获取最顶层ViewController

1.获取当前屏幕显示的 Viewcontroller 案例源码 2.UIApplication 的简析 3.KeyWindow 的简析 4.rootViewController 的简析 5.PresentedViewController 的简析1 获取当前屏幕显示的 Viewcontroller//获取当前屏幕显示的viewcontroller - (UIViewController *)getCurrentVC { ///下文中有分析 UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; UIViewController *currentVC = [self getCurrentVCFrom:rootViewController]; return currentVC; } - (UIViewController *)getCurrentVCFrom:(UIViewController *)rootVC { UIViewController *currentVC; if ([rootVC presentedViewController]) { // 视图是被presented出来的 rootVC = [rootVC presentedViewController]; } if ([rootVC isKindOfClass:[UITabBarController class]]) { // 根视图为UITabBarController currentVC = [self getCurrentVCFrom:[(UITabBarController *)rootVC selectedViewController]]; } else if ([rootVC isKindOfClass:[UINavigationController class]]){ // 根视图为UINavigationController currentVC = [self getCurrentVCFrom:[(UINavigationController *)rootVC visibleViewController]]; } else { // 根视图为非导航类 currentVC = rootVC; } return currentVC; }2 分析2.1 UIApplication 的简析UIApplication的核心作用是提供了iOS程序运行期间的控制和协作工作,每一个程序在运行期必须有且仅有一个UIApplication(或则其子类)的一个实例,在程序启动运行时,会在 main 函数中创建一个 UIApplication的单例实例,在代码中可以通过调用[UIApplication sharedApplication]来得到这个单例实例的指针。2.2 KeyWindow 的简析在简析 KeyWindow 前我们先来看一看 UIWindow 的概念UIWindow 是 UIView 的子类,其在 UIView 添加了一些视图层级,管理视图,转发 UIEvent 对象的属性和 Method 等等在上述实例中,我们通过 [UIApplication sharedApplication] 来获取的 UIApplication 的单例实例对象,然后通过实例对象的 keyWindow再获取到当前活跃的window(或者说是当前显示的主窗口).KeyWindow 即指在IOS开发中活跃窗口,即能接到键盘和非触摸事件的一个窗口,一次只能有一个KeyWindow,在IOS 开发中,我们可以通过设置UIWindowLevel的数值来设置最前端的窗口为哪个,Level数值越高的窗口越靠前,如果两个窗口的Level等级相同,则我们可以通过makeKeyAndVisible来显示KeyWindow(void)makeKeyWindow;//让当前UIWindow变成keyWindow(主窗口) (void)makeKeyAndVisible;//让当前UIWindow变成keyWindow,并显示出来 [UIApplication sharedApplication].windows //获取当前应用的所有的UIWindow [UIApplication sharedApplication].keyWindow //获取当前应用的主窗口 view.window ///获得某个UIView所在的UIWindowmakeKeyAndVisible 与 makeKeyWindowmakeKeyWindow: 只做了一件事就是使当前窗口成为主要窗口。 当前窗口不一定显示出来makeKeyAndVisible : 做了两件事使当前窗口成为主要窗口并显示当前窗口。如果只想显示的话,我们还可以通过设置其属性hidden为 NO 即可。=becomeKeyWindow 与 resignKeyWindowbecomeKeyWindow: 程序自动调用,用来通知其他窗口 当前容器已被设置为主窗口。 我们不要主动调用此方法, 此方法是系统自动调用的来发通知的,此方法的默认实现不执行任何操作,但子类可以覆盖它并使用它来执行与成为关键窗口相关的任务。resignKeyWindow: 类似becomeKeyWindow, 其是调用以通知窗口它将要注销掉主键窗口的身份.同样的切勿直接调用此方法。2.3 rootViewController属性顾名思义:当前窗口的根视图 目前只有UIWindow有rootViewController这个属性,不要跟UINavigationController里面的根视图概念混淆。 UINavigationController其实并没有 rootViewController这个属性!也就没有自带的setter方法。要设置其根视图只能通过如下方法- (instancetype)initWithRootViewController:(UIViewController *)rootViewController; 获取 uiwindow的根视图方式一AppDelegate *app =(AppDelegate *) [UIApplication sharedApplication].delegate; UIViewController *rootViewController1 = appdelegate.window.rootViewController;方式二UIWindow *window = [UIApplication sharedApplication].keyWindow; UIViewController *rootViewController2 = window.rootViewController; 需要注意的是:在方式二中,UIAlertController、UIAlertView、UIActionSheet弹出后,上述这些View 出现生成了一个新的window,加在了界面上面,所以keyWindow就会变成UIAlertControllerShimPresenterWindow这个类2.4 PresentedViewController 简析在 ios 开发中,一般页面的组成有 NavigationController 或者 其他的 UiViewController、UITabViewController 等等,在有NavigationController导航栏的话,使用[self.navigationColler pushViewController:animated:] 进入到下一个视图 ,使用[self.navigationController popViewControllerAnimated:] 返回到上一视图。在没有NavigationController导航栏的时候,使用[self presentViewController:animated:completion:] 进入到下一个视图,使用 [self dismissViewControllerAnimated:completion:];返回到上一个视图中。presentedViewController 与 presentingViewController案例说明 A.presentedViewController A控制器跳转到B控制器;B.presentingViewController 就是返回到A控制器。
0
0
0
浏览量529
德州安卓

iOS 网络优化和网络基础

网络优化网络层的优化手段主要从以下三方面考虑:针对链接建立环节的优化针对链接传输数据量的优化针对链接复用的优化网络优化具体策略NSCache 缓存、Last-Modified、ETagDNS 优化请求策略优化(失败重发、缓存请求有网发送、节流等)弱网:2G、3G、4G、wifi下设置不同的超时时间资源优化(数据压缩,如protobuf、WebP)TCP对头阻塞:GOOGLE 提出 QUIC 协议,相当于在 UDP 协议之上再定义一套可靠传输协议一、缓存GET 网络请求缓存POST请求不能被缓存,只有 GET 请求能被缓存。因为从数学的角度来讲,GET 的结果是幂等的,就好像字典里的 key 与 value 就是幂等的,而 POST 不是幂等 。缓存的思路就是将查询的参数组成的值作为 key ,对应结果作为 value。设置缓存步骤:使用 GET 请求。GET 请求,iOS 系统 SDK 已经帮你做好了缓存。你仅仅需要设置内存缓存大小、磁盘缓存大小、以及缓存路径。甚至这两行代码不设置也是可以的,会有一个默认值。NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil]; [NSURLCache setSharedURLCache:urlCache];文件缓存:借助 ETag 或 Last-Modified 判断文件缓存是否有效控制缓存的有效性:指定超时时间。借助 ETag 或 Last-Modified 判断文件缓存是否有效。Last-ModifiedLast-Modified: 是资源最后修改的时间戳,往往与缓存时间进行对比来判断缓存是否过期。具体分析: 在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式类似这样:Last-Modified: Fri, 12 May 2006 18:53:33 GMT。客户端第二次请求此URL时,根据 HTTP 协议的规定,浏览器会向服务器传送 If-Modified-Since 报头,询问该时间之后文件是否有被修改过: If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT。如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。ETagHTTP 协议规格说明定义ETag为“被请求变量的实体值”。 另一种说法是,ETag是一个可以与Web资源关联的记号(token)。它是一个 hash 值,用作 Request 缓存请求头,每一个资源文件都对应一个唯一的 ETag 值。通过 ETag 和 If-None-Match 判断本地缓存数据是否发生变化。如果ETag没改变,则返回状态304不返回,和Last-Modified一样。ETag 是的功能与 Last-Modified 类似:服务端不会每次都会返回文件资源。客户端每次向服务端发送上次服务器返回的 ETag 值,服务器会根据客户端与服务端的 ETag 值是否相等,来决定是否返回 data,同时总是返回对应的 HTTP 状态码。客户端通过 HTTP 状态码来决定是否使用缓存。比如:服务端与客户端的 ETag 值相等,则 HTTP 状态码为 304,不返回 data。服务端文件一旦修改,服务端与客户端的 ETag 值不等,并且状态值会变为200,同时返回 data。因为修改资源文件后该值会立即变更。这也决定了 ETag 在断点下载时非常有用。Last-Modified 和 ETag 比较在官方给出的文档中提出 ETag 是首选的方式,优于 Last-Modified 方式。因为 ETag 是基于 hash ,hash 的规则可以自己设置,而且是基于一致性,是“强校验”。 Last-Modified 是基于时间,是弱校验,弱在哪里?比如说:如果服务端的资源回滚客户端的 Last-Modified 反而会比服务端还要新。虽然 ETag 优于 Last-Modified ,但并非所有服务端都会支持,而 Last-Modified 则一般都会有该字段。大多数情况下需要与服务端进行协调支持 ETag ,如果协商无果就只能退而求其次。其他值得注意的一点是: 如果借助了 Last-Modified 和 ETag,那么缓存策略则必须使用 NSURLRequestReloadIgnoringCacheData 策略,忽略缓存,每次都要向服务端进行校验。一些建议: 如果是 file 文件类型,用 Last-Modified 就够了。即使 ETag 是首选,但此时两者效果一致。九成以上的需求,效果都一致。如果是一般的数据类型--基于查询的 get 请求,比如返回值是 data 或 string 类型的 json 返回值。那么 Last-Modified 服务端支持起来就会困难一点。因为比如你做了一个博客浏览 app ,查询最近的10条博客, 基于此时的业务考虑 Last-Modified 指的是10条中任意一个博客的更改。那么服务端需要在你发出请求后,遍历下10条数据,得到“10条中是否至少一个被修改了”。而且要保证每一条博客表数据都有一个类似于记录 Last-Modified 的字段,这显然不太现实。如果更新频率较高,比如最近微博列表、最近新闻列表,这些请求就不适合,更多的处理方式是添加一个接口,客户端将本地缓存的最后一条数据的的时间戳或 id 传给服务端,然后服务端会将新增的数据条数返回,没有新增则返回 nil 或 304。二、DNS 优化DNS 主要解决三类问题LocalDNS劫持平均访问延迟下降用户连接失败率下降预防DNS劫持DNS劫持指的是改变DNS请求的返回结果,将目的ip指向另一个地址。一般有两种方式,一是通过病毒的方式改变本机配置的DNS服务器地址,而是通过攻击正常DNS服务器而改变其行为。不管是哪种方式,都会影响app本身的业务请求。如果遇到恶意的攻击还会衍生出各种安全问题。客户端自己做DNS与ip地址的映射就跨过了解析,让劫持者无从下手。运营商DNS劫持和故障实例图:很多三四级运营商会把运营解析指向他们的缓存服务器上,并把网页里面的广告替换成他们自己的,或者内嵌他们自己的广告。运营商DNS流量劫持,具体表现在你的H5网页莫名其妙的被加了广告(关于这个问题,也可以做域名白名单,非本域名资源禁止请求,或者H5方面做处理)DNS服务商解析出现故障造成的大批量用户无法正常使用App,按天计算。。DNS解析延迟过高造成的加载超时导致用户体验差 使用 HttpDNS 优化 DNS 解析和缓存  App内用域名发送请求都要经过DNS解析出ip,然后再根据ip去拿对应的资源,这个过程中,如果LocalDNS中存在这个域名对应的ip,就会直接返回这个ip,类似于App内做缓存。如果不存在,才会去权威DNS查询改访问哪个ip,然后查询到的ip会在LocalDNS中做缓存。HTTPDNS的实现,根据各自团队的情况可以选择自建或者第三方SDK的方案。根据目前DNS劫持和故障的严重程度,以及实现方案的成本对比。HTTPDNS集成整体简图: HttpDNS原理:A、客户端直接访问 HttpDNS 接口,获取业务在域名配置管理系统上配置的访问延迟最优的 IP。(基于容灾考虑,app内肯定是需要保留使用运营商 LocalDNS 解析域名方式的。)B、客户端获取到的 IP 后就直接往此IP发送业务协议请求。以 Http 请求为例,通过在 header 中指定 host 字段,向 HttpDNS 返回的 IP 发送标准的 Http 请求即可。总的来说,采用 HttpDNS 来解析域名,就绕过了三四级运营商解析域名会出现的问题,在 HttpDNS 返回了正确的 IP 之后,我们是直接采用 IP 去进行 Http 请求,只需要关注通信内容的安全即可。HttpDNS 实际使用接口层接口层主要为了对外提供简洁的接口,降低使用者的接入成本,提高开发效率,如接口层提供的部分接口如下:/// 开启HTTPDNS服务 - (void)startHTTPDNS; /// 白名单列表,如果设置了白名单,则只有在白名单内域名走httpdns服务 @property (nonatomic, copy) NSArray *whiteDomainList; /// 黑名单列表,如果设置了黑名单,黑名单内域名都不走httpdns,黑名单的优先级最高 @property (nonatomic, copy) NSArray *blackDomainList; /// 是否允许缓存ip,允许缓存的情况下,在通过第三方服务无法获取ip的情况下,允许使用上次解析成功的ip进行请求,默认YES @property (nonatomic, assign) BOOL enableCachedIP;策略层容灾策略:SDK内部优先使用HTTPDNS服务,当HTTPDNS服务不可用时,即无法获得有效ip时,服务自动降级为运营商的LocalDNS服务,确保不受HTTPDNS服务不可用时导致系统故障无法发出网络请求。注:目前阶段没有接入内置ip策略,后续会考虑。黑白名单策略:APP内的网络请求域名众多,目前并不是所有的网络请求都走HTTPDNS服务,设置了白名单或者黑名单后,会根据黑白名单中的域名去执行HTTPDNS,如果设置了白名单,则只有白名单内的域名走HTTPDNS服务;如果设置了黑名单,黑名单内的域名不走HTTPDNS服务,黑名单的优先级高于白名单。缓存策略:缓存策略除了基础服务层中腾讯云HTTPDNS SDK提供的基于TTL的缓存策略外,我们自己封装的接入层SDK中还存在一份内存缓存和本地化持久缓存,持久化缓存主要用来解决启动APP时无法获取HTTPDNS中的IP的问题,内存缓存主要为查询策略提供服务。当某个基于HTTPDNS的IP地址导致请求失败后,会清除当前域名和IP的缓存数据。同时外部可控制是否使用缓存。查询策略:查询策略主要是为了解决,短时间内同一个域名多次调用基础服务层的域名查询服务,当状态是正在查询中时,后来者不再调用查询服务,直接从缓存策略中的内存缓存中读取可用的IP,如果缓存内也无可用的IP,则直接降级为运营商的LocalDNS查询。查询策略可在确保服务可用的同时,有效减少和HTTPDNS服务器交互的次数。最佳实践小结因此,如果你对性能有这很高的要求,同时又需要处理SNI场景的问题,我建议不要直接主动使用HTTPDNS,而是在运营商LocalDNS获取的IP请求失败的情况下,可以在底层直接使用基于CFNetwork的网络请求进行重试,这样就能在请求DNS劫持和性能中间得到一个平衡,既能保证在运营商的LocalDNS解析出现问题时能够走HTTPDNS,保证成功率和可用性;同时又能够在运营商的LocalDNS可用时,使用基于NSURLSession的请求,享受系统实现的HTTP2.0特性带来的性能提升。备选LocalDNS原因如果之前访问api.weibo.cn的是联通用户,现在新用户使用电信来访问api.weibo.cn,由于localDNS缓存的存在,不会去查询新浪的权威DNS,这样返回的ip是联通这个运营商的ip,从而会使得用户出现访问变慢等状况。缓存还会导致一点就是,当权威DNS将域名与ip的映射发生改变之后,由于LocalDNS缓存没有及时改变,用户就会访问到错误的服务器,或者直接访问不到资源。基于HttpDNS扩展HttpDNS部署等有难度,替代方案:APP内置severs IP list,ping出最合适的Sever address。省去DNS查询时间。在App内维护一个Serve IP List。把每次App从HttpDNS取到的ip存储进入该数组,并设置权重,理论上来说从HttpDns解析下来的ip权重是最大的。这个List可以在App启动的时候,进行更新,同时取出本地缓存的Serve IP List的权重最大的ip进行数据的初始化操作(如果第一次启动,没有该List的话,就使用LocalDNS进行解析)。 Serve IP List里面的权重设置机制,很明显的一点就是从DNS解析出来的ip具有最大的权重,每次从List里面取ip应该要取权重最大的ip。列表中的ip也是需要可以动态更新配置的,根据连接或者服务的成功失败来进行动态调整,这样即使DNS解析失败,用户在一段时间后也会取到合适的ip进行访问。对ip进行数据统计。在所有app内统计每个ip进行请求所需平均时间、最长时间、最短时间、请求成功次数、失败次数,需要注意的是,要区分网络环境进行统计,Wifi、4G、3G,对在不同的网络环境下数据优秀的ip进行存储,下发到App里面使用起来。这样每次启动App时可以对收集起来的ip根据不同的网络环境进行测速,选择最好的ip进行请求。需要注意的是,在网络环境切换的时候,必须要重新进行速度测试。做到这一步,可以节约DNS解析时间,以及劫持的问题。将图片、音频等资源放到单独的服务器里面,与其他资源分开。 第一个是多个域名可以增加并行下载条数,因为客户端对同一个域的域名下载条数是有限制的,所以多个域就会增加并行下载条数,从而加快加载速度。当然二级域名也不能使用太多,因为太多要考虑到dns的解析花费的时间。 第二个是方便管理,一般来说,图片在站点的加载中是最占带宽的,可以用独立服务器方便后期管理;还可以使用异步加载的方式,增强用户体验。同时是图片多是静态内容,可以更好的使用CDN加速。 第三是如果使用了独立服务器的话,在安全设置上可以有差别的针对设置,很是方便。在防止劫持这一块,需要注意把资源的后缀名去掉,比如说.mp3.json这样的后缀,以免击中运营商的拦截。三、失败重发、缓存请求有网发送、节流等失败重发和后台确定一些错误码,根据错误码类型判断是否需要重发会比较合理。一般3次的重试基本可以排除网络抖动的情况。三次失败之后即可认为请求失败。百分百送达服务器对于百分百送达服务器的请求,第一步并不是直接发送,而是存入本地数据库中,这样即使杀掉进程、断电、重启等极端操作,请求数据也依旧存在,我们可以在App重启或者进入该业务界面时,还原请求数据到内存中,重新进行发送。节流使用一定的节流策略,如延迟请求、取消当前请求、取消所有请求等四、弱网优化www.52im.net/thread-2479…五、资源优化资源优化基本就是尽可能的缩小传输数据的大小,首先是图片大小的解决方案。方案: 在一定程度上使用webp来代替jpg、png图片 需要注意的是,webp的图片要通过解析才能成为可用的jpg图片,在iOS开发中,可以使用SDWebImage框架进行解析,webp->NSData->Image,app内解析是肯定需要花费一定时间和性能的。 在wifi条件下,超过300300的图片使用webp图片,解析时间+下载时间是比直接使用jpg\png图片要快的,并且在流量方面也是消耗很小。低于300300的可以直接下载使用jpg\png图片。在4G条件下,用户可能会对流量比较敏感,建议都走webp图片。网络基础一、HTTPSHTTPS 是在 HTTP 基础上增加了 SSL 层,传递加密的内容,比较安全。HTTPS 请求过程客户端发起HTTPS请求服务端的配置数字证书,这套证书其实就是一对公钥和私钥。服务器端返回数字证书的公钥。公钥包含了很多信息,如证书的颁发机构,过期时间等等。客户端解析证书。由客户端的TLS来完成的,首先会验证公钥是否有效,如颁发机构,过期时间等。如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值,然后用证书对该随机值进行加密。客户端发送加密信息。这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就通过这个随机值来进行加密解密。服务段解密信息。用私钥解密客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。服务器端向客户端传输加密后的信息。客户端解密信息。客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。对称加密: 采用单钥加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密。由于其速度快,对称性加密通常在消息发送方需要加密大量数据时使用。非对称加密算法需要两个密钥:公钥和私钥。如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。HTTPS 连接,从输入账号密码点击登陆按钮到服务器返回这个请求前,这期间经历了什么?客户端打包请求。其中包括URL、端口、账号和密码等。使用账号和密码登陆应该用的是POST方式,所以相关的用户信息会被加载到body中。这个请求应该包含3个方面:网络地址、协议和资源路径。注意:这里用的是HTTPS,即HTTP+SSL/TLS,在HTTP上又加了一层处理加密信息的模块(相当于加了一个锁)。这个过程相当于客户端请求钥匙。服务器端接受请求。一般客户端的请求会先被发送到DNS服务器中。DNS服务器负责将网络地址解析成IP地址,这个IP地址对应网上的一台计算机。这其中可能发生Hosts Hijack和ISP failure的问题。过了DNS这一关,信息就到服务器端,此时客户端和服务端的端口之间会建立一个socket连接。socket一般都是以file descriptor的方式解析请求的。这个过程相当于服务器端分析是否要想客户端发送钥匙模板。服务器端返回数字证书。服务器端会有一套数字证书(相当于一个钥匙模板),这个证书会先被发送个客户端。这个过程相当于服务端向客户端发送钥匙模板。客户端生成加密信息。根据收到的数字证书(钥匙模板),客户端就会生成钥匙,并把内容锁起来,此时信息已经被加密。这个过程相当于客户端生成钥匙并锁上请求。客户端发送加密信息。服务器端会收到由自己发送的数字证书加密的信息。这个时候生成的钥匙也一并被发送到服务端。这个过程相当于客户端发送请求。服务端解锁加密信息。服务端收到加密信息后,会根据得到的钥匙进行解密,并把要返回的数据进行对称加密。这个过程相当于服务器端解锁请求,生成、加锁回应信息。服务器端向客户端返回信息。客户端会收到相应的加密信息。这个过程相当于服务器端向客户端发送回应信息。客户端解锁返回信息。客户端会用刚刚生成的钥匙进行解密,将内容显示在浏览器上。HTTPS 和 HTTP 区别https协议需要到ca申请证书,一般免费证书很少,需要交费。http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。HTTPS 的产生 => HTTP的缺点传输过程中使用明文,内容可以被窃听。无法验证对方身份,可能遭遇到伪装。不知道报文的完整性,有可能被篡改。二、HTTP2.0 新特性浅析HTTP2.0 新特性二进制分帧 首部压缩 多路复用 服务器推送 请求优先级HTTP2.0 的优势HTTP/2 主要是 HTTP/1.x 在底层传输机制上的完全重构,相比 HTTP/1.x,HTTP/2 在底层传输做了很大的改动和优化:HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 的文本格式。二进制格式在协议的解析和优化扩展上带来更多的优势和可能。HTTP/2 对消息头采用 HPACK 进行压缩传输,能够节省消息头占用的网络的流量。而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。头压缩能够很好的解决该问题。多路复用,直白的说就是所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。Server Push:服务端能够更快的把资源推送给客户端。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 再发送这些请求。当客户端需要的时候,它已经在客户端了。三、HTTP、TCP、UDP、IP1. HTTPHTTP,超文本传输协议。是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式。由于其简捷、快速的方式,适用于分布式超媒体信息系统,是现今在WWW上应用最多的协议。HTTP 由来我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本三/IP做传输层协议将它发到网络上。HTTP 协议的作用HTTP 的全称是 Hypertext Transfer Protocol,超文本传输协议。规定客户端和服务器之间的数据传输格式。让客户端和服务器能有效地进行数据沟通。HTTP 请求的方法发送HTTP请求的方法在HTTP/1.1协议中,定义了8种发送http请求的方法 :GET、POST、PUT、DELETE、HEAD、OPTIONS、TRACE、CONNECT、PATCH。POST、GET 的区别GET 方法GET 方法提交数据不安全,数据置于请求行,客户端地址栏可见;GET 方法提交的数据大小有限,不过因为浏览器不同,一般限制在2~8K之间GET 方法不可以设置书签GET 请求能够被缓存POST 方法POST 方法提交数据安全,数据置于消息主体内,客户端不可见POST 方法提交的数据大小没有限制。靠服务器限制。POST 方法可以设置书签POST 请求不能够被缓存POST、PUT 的区别PUT文件大小无限制可以覆盖文件POST通常有限制2M新建文件,不能重名HTTP 请求头GET /home.html HTTP/1.1 Host: developer.mozilla.org User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: https://developer.mozilla.org/testpage.html Connection: keep-alive Upgrade-Insecure-Requests: 1 If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a" Cache-Control: max-age=0HTTP 相应头Access-Control-Allow-Origin: * Connection: Keep-Alive Content-Encoding: gzip Content-Type: text/html; charset=utf-8 Date: Mon, 18 Jul 2016 16:06:00 GMT Etag: "c561c68d0ba92bbeb8b0f612a9199f722e3a621a" Keep-Alive: timeout=5, max=997 Last-Modified: Mon, 18 Jul 2016 02:36:04 GMT Server: Apache Set-Cookie: mykey=myvalue; expires=Mon, 17-Jul-2017 16:06:00 GMT; Max-Age=31449600; Path=/; secure Transfer-Encoding: chunked Vary: Cookie, Accept-Encoding x-frame-options: DENY HTTP 协议的主要特点支持客户/服务器模式。简单快速:客户向服务器请求服务时,只需传送请求方法和路径。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。常见状态码200:请求成功。400:客户端请求语法的错误,服务器无法解析。404:服务器无法通过客户端的请求找到资源。500:服务器内部错误无法完成请求。2. TCP 连接的三次握手和四次挥手三次握手客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。三次握手目的是确认双方的接收与发送能力是否正常。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次挥手”。三次握手过程中可以携带数据吗?握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。但其实第三次握手的时候,是可以携带数据的,第一次、第二次握手不可以携带数据。假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。四次挥手1.客户端发送一个 FIN 报文(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。2.服务端收到 FIN,发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。3.如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态,即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。4.客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,关闭连接,处于 CLOSED 状态。3. TCP 和 UDP 的区别TCP:传输控制协议。是一种面向连接(连接导向)的、可靠的、基于字节流的运输层(Transport layer)通信协议。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。UDP:用户数据包协议。是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。面向连接:是指通信双方在通信时,要事先建立一条通信线路,其有三个过程:建立连接、使用连接和释放连接。电话系统是一个面向连接的模式,拨号、通话、挂机;TCP协议就是一种面向连接的协议。面向无连接:是指通信双方不需要事先建立一条通信线路,而是把每个带有目的地址的包(报文分组)送到线路上,由系统自主选定路线进行传输。邮政系统是一个无连接的模式,天罗地网式的选择路线,天女散花式的传播形式;IP、UDP协议就是一种无连接协议。4. HTTP、TCP、UDP 三者的关系都是通信协议,也就是通信时所遵守的规则,只有双方按照这个规则“说话”,对方才能理解或为之服务。HTTP 本身就是一个协议,是从Web服务器传输超文本到本地浏览器的传送协议。HTTP协议是基于TCP连接的。主要解决如何包装数据。TCP/IP 是个协议组,分为四个层次:网络接口层、网络层、传输层和应用层。主要解决数据如何在网络中传输。网络层:IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。传输层:TCP协议与UDP协议。应用层:FTP、HTTP、TELNET、SMTP、DNS等协议。四、Socket网页(网址以http://开头)都是 http 协议传输到你的浏览器的,而 http 是基于 socket 之上的。socket 是一套完成 tcp,udp 协议的接口。TCP和UDP协议属于传输层 。而http是个应用层的协议什么是socket?Socket 是对 TCP/IP 协议的封装,Socket 本身并不是协议,而是一个调用接口(API),通过 Socket,我们才能使用 TCP/IP 协议。Socket 的出现只是使得程序员更方便地使用 TCP/IP 协议栈而已,是对 TCP/IP 协议的抽象,从而形成了我们知道的一些最基本的函数接口。socket连接和http连接的区别?http 连接:http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断掉;socket连接:socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉;但是由于各种环境因素可能会断开,比如:服务器或客户端主机 down 了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该连接以释放网络资源。所以当一个 socket 连接中没有数据的传输,那么为了维持连接需要发送心跳消息,具体心跳消息格式是开发者自己定义的。利用Socket建立网络连接的步骤:建立Socket连接至少需要一对套接字,一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket。套接字之间的连接过程分为三个步骤:服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。五、URL什么是URL URL 的全称是 Uniform Resource Locator(统一资源定位符)。 通过1个URL,能找到互联网上唯一的1个资源。 URL 就是资源的地址、位置,互联网上的每个资源都有一个唯一的URL。URL 的基本格式 = 协议://主机地址/路径 协议:不同的协议,代表着不同的资源查找方式、资源传输方式 主机地址:存放资源的主机的IP地址(域名) 路径:资源在主机中的具体位置URL 中常见的协议 http 协议是在网络开发中最常用的协议 超文本传输协议,访问的是远程的网络资源,格式是 http:// file 访问的是本地计算机上的资源,格式是 file://(不用加主机地址) mailto 访问的是电子邮件地址,格式是 mailto: FTP 访问的是共享主机的文件资源,格式是 ftp://六、其他如果在网络数据处理过程中,发现一处比较卡,一般怎么解决?检查网络请求是否被放在主线程了看看异步请求的数量是否太多了(子线程的数量)数据量是否太大?如果太大,先清除一些不必要的对象(看不见的数据、图片)手机的CPU使用率和内存问题iOS 网络编程层次结构分为三层:Cocoa 层: NSURL,Bonjour,Game Kit,WebKi。这层是最上层的基于 Objective-C 的 API,比如 URL 访问,NSStream,Bonjour,GameKit等,这是大多数情况下我们常用的 API。Cocoa 层是基于 Core Foundation 实现的。(可触摸层)。Core Foundation 层: 基于 C 的 CFNetwork 和 CFNetServices。因为直接使用 socket 需要更多的编程工作,所以苹果对 OS 层的 socket 进行简单的封装以简化编程任务。该层提供了 CFNetwork 和 CFNetServices,其中 CFNetwork 又是基于 CFStream 和 CFSocket。(核心服务层)。OS 层: 基于 C 的 BSD socket。(核心操作系统层)数据安全网络数据加密加密对象:隐私数据,比如密码、银行信息 加密方案:1.提交隐私数据,必须用POST请求2.使用加密算法对隐私数据进行加密,比如MD53.加密增强:为了加大破解的难度 对明文进行2次MD5 : MD5(MD5(4.BASE 64:是网络传输中最常用的编码格式,用来将二进制的数据编码成字符串的编码方式。本地存储加密加密对象:重要的数据,比如游戏数据代码安全问题现在已经有工具和技术能反编译出源代码:逆向工程反编译出来的都是纯 C 语言的,可读性不高最起码能知道源代码里面用的是哪些框架解决方案:发布之前对代码进行混淆混淆之前@interface HMPerson :NSObject - (void)run; - (void)eat; @end混淆之后@interface A :NSObject - (void)a; - (void)b; @end文件解压缩技术方案第三方框架:SSZipArchive依赖的动态库:libz.dylib压缩方法第一个方法 zipFile :产生的zip文件的最终路径 directory : 需要进行的压缩的文件夹路径 [SSZipArchive createZipFileAtPath:zipFile withContentsOfDirectory:directory];第一个方法 zipFile :产生的zip文件的最终路径 files : 这是一个数组,数组里面存放的是需要压缩的文件的路径 files = @[@"/Users/apple/Destop/1.png", @"/Users/apple/Destop/3.txt"] [SSZipArchive createZipFileAtPath:zipFile withFilesAtPaths:files];解压缩zipFile :需要解压的zip文件的路径 dest : 解压到什么地方 [SSZipArchive unzipFileAtPath:zipFile toDestination:dest];
0
0
0
浏览量536
德州安卓

iOS 上的函数防抖与节流

前言函数防抖与节流不是新概念,在前端领域很常见,也是面试中的常客,搜索"前端 函数防抖"能看到很多文章。相反,在 iOS 上却看不到很多介绍。第一次知道 函数防抖与节流,是在 2018 年做交易所项目:当时的场景,是实时更新交易数据。交易订单数据变化很频繁,每次都去刷新,显然不是一个好方法。而且不能直接丢数据,常规的"第一次执行,后续丢弃"的限频策略,满足不了需求。当时思考,这个策略应满足的条件:一定时间内,合并多次触发为一次,并且触发时数据是最新的.因为代码实现问题,和大佬请教。说明完目的,他一听就说,这不是函数防抖和节流吗?在前端很常见..好嘛...原来人家前端早就有了?我都工作 2 年里才知道,又学会了新姿势,好饭不怕晚。而我发现这个概念,不仅是前端,后端也能应用,甚至 TCP 的流量控制策略,就是属于函数防抖。什么是函数防抖和节流前面解释了为什么要用到 函数防抖和节流,现在说说它们具体是什么。很多文章都提到一个演示的网址 debounce&throttle,里面模拟鼠标移动事件几种情况的调用,带颜色的竖线,代表一次函数执行。使用的大概效果是这样:regular 代表常规情况,不做限制时,函数直接调用的结果。deboundce 代表防抖,可以发现,如果函数一直调用,它不会立即执行,而是等到一段时间后,函数没有新调用,它才执行一次。throttle 代表节流,在一定时间内,只执行一次👾 防抖 (Debounce)防抖的情况,有点像一个极度珍惜 执行机会的人,只要时间段内,有任务来,就再等一会。等到最后一次,超过一定时间,确定没有新任务了,才去做执行。有人觉得它像黑车司机,有人形容它是上班时的电梯,但黑车或者电梯容量满了都会开走。而我认为,它就像一只耐心上好的怪兽,等到所有食物都来完了,确定没有新食物,再张开它的大嘴,一网打尽。🐯 节流 (Throttle)节流比较好理解,在一定时间段内,丢弃掉其它触发,就做一次执行。使用场景函数节流的使用场景:防止多次点击重复发多个网络请求等等..其实函数节流 最简单的实现方式,仅用时间戳对比,就可以办到,大家一般这么写:if((now-last)<time){ return; } last = now; //do something 很多人已经用过了,只是不知道名称。而特殊一点的节流需求:时间段内,只执行最后一次触发,丢掉之前的触发。碰到的应用场景是,消息队列在时间段内有数据变化,在最后一次进行批量处理传递。函数防抖,我看到的使用场景:列表刷新,为避免短时间内反复 reload,可以多次合并为一次TCP 流量控制直播房间的全屏游戏界面,点击 1 次出现控制工具,一定时间内,多次点击不隐藏工具。等时间过去后,执行自动隐藏现成的轮子 - MessageThrottle按照套路,该亮出自己的代码来实现了。然而 iOS 也早有人实现了轮子,不重复造轮子嘛,可以直接使用。发现 MessageThrottle 是比较完备的实现,而且在手 Q 中应用了,质量比较可靠。推荐一下。MessageThrottle 使用它的使用很简单:Stub *s = [Stub new]; MTRule *rule = [MTRule new]; rule.target = s; // You can also assign `Stub.class` or `mt_metaClass(Stub.class)` rule.selector = @selector(foo:); rule.durationThreshold = 0.01; [MTEngine.defaultEngine applyRule:rule]; // or use `[rule apply]` 主要就是对 MTRule的设置,决定我们将以哪种模式,多少的时间限制来控制方法调用。MessageThrottle 分析虽说不再造轮子,但要了解它是什么样的。当然如不感兴趣,看使用也够了。整个库就只有 MessageThrottle.h 和 MessageThrottle.m 两个文件。主要思路是:对进行节流和防抖的方法,进行 hook,然后再统一做处理。其实里面能学习的点不少,这里只大概介绍一下。主要设计思路引用作者自己说明主要类关系的图,虚线代表弱引用:NSMapTable 存储数据MTEngine 中通过 NSMapTable 来以target 作为key,selector数组作为 value,来存储管理数据。NSMapTable 的一个特性是支持任意指针作为 Key 且无需持有,NSMapTable 也会自动移除那些键或值为 nil 的数据。通过关联对象进行规则移除一个关键设计点在于,使用关联对象,将 MTDealloc 对象关联在 target 上:- (MTDealloc *)mt_deallocObject { MTDealloc *mtDealloc = objc_getAssociatedObject(self.target, self.selector); if (!mtDealloc) { mtDealloc = [MTDealloc new]; mtDealloc.rule = self; mtDealloc.cls = object_getClass(self.target); objc_setAssociatedObject(self.target, self.selector, mtDealloc, OBJC_ASSOCIATION_RETAIN); } return mtDealloc; }关联对象设计的好处是:在 target 释放时,关联对象也是会被清除的,所以 MTDealloc 对象也会释放,达到了 target 释放时自动移除 rule 的效果。在 MTDealloc 的 dealloc 方法进行discard操作:- (void)dealloc { SEL selector = NSSelectorFromString(@"discardRule:whenTargetDealloc:"); ((void (*)(id, SEL, MTRule *, MTDealloc *))[MTEngine.defaultEngine methodForSelector:selector])(MTEngine.defaultEngine, selector, self.rule, self); }里面调用写的有点骚...其实就是:[MTEngine.defaultEngine discardRule:self.rule whenTargetDealloc:self];消息转发中的核心处理逻辑整个库的核心处理在 mt_handleInvocation 中:/** 处理执行 NSInvocation @param invocation NSInvocation 对象 @param rule MTRule 对象 */ static void mt_handleInvocation(NSInvocation *invocation, MTRule *rule) { NSCParameterAssert(invocation); NSCParameterAssert(rule); if (!rule.isActive) {//规则非 active 状态的,直接 invoke [invocation invoke]; return; } if (rule.durationThreshold <= 0 || mt_invokeFilterBlock(rule, invocation)) {//时间小于等于0,设置aliasSelector(为原始方法IMP)后执行. invocation.selector = rule.aliasSelector; [invocation invoke]; return; } //时间戳处理,用 correctionForSystemTime 校正系统时间所需的差值。 NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; now += MTEngine.defaultEngine.correctionForSystemTime; switch (rule.mode) { //节流模式:执行第一次触发 case MTPerformModeFirstly: { //触发时,直接看现在的时间间隔是否比限制时间大,如果大于则直接执行,否则不响应 if (now - rule.lastTimeRequest > rule.durationThreshold) { invocation.selector = rule.aliasSelector; [invocation invoke]; //执行后,更新最近执行时间 rule.lastTimeRequest = now; dispatch_async(rule.messageQueue, ^{ // May switch from other modes, set nil just in case. rule.lastInvocation = nil; }); } break; } //节流模式:执行最后一次触发 case MTPerformModeLast: { invocation.selector = rule.aliasSelector; //invocation 提前持有参数,防止延迟执行时被释放掉 [invocation retainArguments]; dispatch_async(rule.messageQueue, ^{ //更新最近触发的 invocation rule.lastInvocation = invocation; //如间隔时间超出 rule 限定时间,则对方法做执行。保证为最后一次调用 if (now - rule.lastTimeRequest > rule.durationThreshold) { //更新执行时间 rule.lastTimeRequest = now; //按规则的间隔时间后执行 invoke dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{ if (!rule.isActive) { rule.lastInvocation.selector = rule.selector; } [rule.lastInvocation invoke]; //invoke 后将 lastInvocation 置 nil rule.lastInvocation = nil; }); } }); break; } //防抖模式:一段时间内不再有新触发,再执行 case MTPerformModeDebounce: { //设置 invocation 的 selector invocation.selector = rule.aliasSelector; //提前持有参数 [invocation retainArguments]; dispatch_async(rule.messageQueue, ^{ //更新 invocation rule.lastInvocation = invocation; //在限制时间段过后做执行 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(rule.durationThreshold * NSEC_PER_SEC)), rule.messageQueue, ^{ //假如还是rule.invocation 和 invocation一样,证明没有新的触发,达到执行条件 if (rule.lastInvocation == invocation) { if (!rule.isActive) { rule.lastInvocation.selector = rule.selector; } [rule.lastInvocation invoke]; rule.lastInvocation = nil; } }); }); break; } } }
0
0
0
浏览量527
德州安卓

iOS NSNotificationCenter 详解

通知是Foundation 提供了一种编程架构,用于传递有关事件发生的信息。 本文档描述了该架构的元素并解释了如何使用它们。 接下来从三部分剖析通知相关知识 notifications,notification centers, notification queues.Notifications概述Notification是封装了事件的信息,需要知道事件的对象(例如,需要知道其窗口何时关闭的文件)向通知中心注册,希望在该事件发生时得到通知。 当事件确实发生时,通知中心会发布通知,通知中心会立即将通知广播给所有已注册的对象。通知也可以在通知队列中排队,该队列在延迟指定通知后将通知发布到通知中心,并根据指定的某些指定条件合并相似的通知。Notifications是什么区别于标准的消息传递的形式(一个对象调用另一个对象的方法),通知使用了广播模型:一个对象发布一个通知,该通知通过一个 NSNotificationCenter 对象或简单的通知中心分派给适当的观察者.一个 NSNotification 对象(称为通知)包含一个名称、一个对象和一个可选字典。 名称是标识通知的标签。 对象是通知的发布者想要发送给该通知的观察者的任何对象——通常是发布通知本身的对象。 字典可能包含有关事件的附加信息。Notification 与代理有以下区别:任何数量的对象都可以收到通知,而不仅仅是委托对象。即一对多。任一个对象都可以从通知中心接收消息,而不仅仅是预定义的委托方法。发布通知的对象甚至不必知道观察者的存在。NotificationCenter刚才介绍了Notification是什么,那NotificationCenter就是用来管理Notification发送和接收的。 通过Notification Centers来发送通知到所有观察者。 通知的具体信息封装在 NSNotification 对象中。大致过程就是当事件发生时,对象会向通知中心发布适当的通知,通知中心向每个注册的观察者发送一条消息,将通知作为唯一参数传递,当然发布对象和观察对象可能相同。注:iOS中使用的NSNotificationCenter是单进程的通知中心类,区别于NSDistributedNotificationCenter(cocoa框架中的类,多进程管理的,暂不讨论)通知中心同步向观察者发送通知。同步的原因很简单,NSNotificationCenter是单线程的,发送消息会根据添加观察者的顺序发送。也就是说当发布通知时,控制权不会返回到发布者,直到所有观察者都收到并处理了通知。 如果想要异步发送通知,就需要使用通知队列,Notification Queues中对此进行了描述。Notification QueuesNSNotificationQueue,通知队列,充当通知中心(NSNotificationCenter)的缓冲区。 NSNotificationQueue 类为 通知机制贡献了两个重要特性:通知的合并和异步发布。Notification Queue 概述使用 NSNotificationCenter 的 postNotification: 方法及其变体,可以将通知发布到通知中心。 但是,该方法的调用是同步的,也就是说再观察者接收到通知执行完对应的回调后才会往下执行。如下示例:ClassB中添加了通知监听在vc中发送一个消息打印结果如下:notification send end是5s后打印的,也就是Classb中的回调结束后。所以,在发布对象恢复其执行线程之前,它必须等到通知中心将通知分派给所有观察者并返回,并且每个线程都有一个默认的通知队列,它与进程的默认通知中心相关联。当然也可以创建自己的通知队列,并且每个通知中心和线程有多个队列。且往下看。Posting Notifications Asynchronously(异步发布)使用 NSNotificationQueue 的 enqueueNotification:postingStyle: 和 enqueueNotification:postingStyle:coalesceMask:forModes: 方法,可以通过将通知放入队列中来将通知异步发布到当前线程。 这些方法在将通知放入队列后立即返回调用对象。注,如果在通知队列将通知发布到其通知中心之前,通知入队的线程终止,则不会发布通知。 上述方法中postingStyle、coalesceMask分别就对应了上文提到的比notificationcenter多的两个特性,发送时机与通知合并。postingStyle表示发送通知的时机NSPostWhenIdle空闲时发送仅当runloop处于等待状态(可以理解为休眠之前)时,才会发布此样式排队的通知。比如,下面的示例即使在ClassB中添加了observe也是不会收到通知的,而如果line26换成一个属性变量就可以了。注,即将退出的runloop未处于等待状态,因此不会发布通知。NSPostASAP尽快发送(As Soon As Possible) 官方说法有点抽象,可能是该时机比较复杂,简单理解就是事件结束后发送。如下,当ViewDidLoad执行完成后,RunLoop中事件执行完成,立即发送(日志中,先打印notification send end,再执行B - didclick)。NSPostNowPosting立即发送,无需多言,且注意是合并后(如果有合并)。coalesceMask表示合并策略在某些情况下,如果给定事件至少发生一次,可能希望发布通知,但即使事件发生多次,也希望发布不超过一个通知。此时可以将通知添加到指定适当的合并选项的 NSNotificationQueue 实例。合并是一个从队列中删除通知的过程,这些通知在某种程度上类似于之前排队的通知。可以通过在 enqueueNotification:postingStyle:coalesceMask:forModes: 方法的第三个参数中指定以下一个或多个常量来指示相似性标准。NSNotificationCoalescing 默认不合并NSNotificationCoalescingOnName 同名合并NSNotificationCoalescingOnsender 同object合并Notification 注册与移除调用通知中心方法 addObserver:selector:name:object: 注册一个对象以接收通知,指定观察者,通知中心应该发送给观察者的消息,它要接收的通知的名称,以及关于哪个对象。 一个观察者不再需要接收通知(例如,如果观察者正在被释放),使用方法 removeObserver: 或 removeObserver:name:object: 从通知中心的观察者列表中删除该观察者。说明:通知对应的是NSNotification对象,发送与接收通知其实是对该对象的操作,其发送方式无非是NSNotificationCenter与NSNotificationQueue。再啰嗦两句这两种发送方式:NSNotificationCenter,同步发送,我们可以理解为发送和接收消息需要一个过程(上文中已经使用延时模拟了),该过程结束之前后续的操作会同步进行,也就是最简单直接的发送与接收过程。NSNotificationQueue,此种方式呢是对NSNotificationCenter功能的丰富,即发送时机与合并,合并不多做赘述,发送时机呢就是相较于NSNotificationCenter的同步,做了异步发送通知的过程及对postingStyle该枚举的取值。线程与通知默认情况下,消息的传递是在消息post的线程中进行的.有时,可能需要在由指定线程上传递通知,比如,如果在后台线程中运行的对象正在侦听来自用户界面的通知,例如窗口关闭,您希望在后台线程而不是主线程中接收通知.在这些情况下,必须捕获在默认线程上传递的通知并将它们重定向到适当的线程。线程重定向的思路是使用自定义通知队列(不是 NSNotificationQueue 对象)来保存在错误线程上接收到的任何通知,然后在正确的线程上处理它们.该技术的工作原理如下,注册通知,当通知到达时,测试当前线程是否是应该处理通知的线程。如果是错误的线程,则将通知存储在队列中,然后向正确的线程发送信号,指示需要处理通知。另一个线程接收到信号,从队列中移除通知,并处理通知。观察者需要有以下值的实例变量:用于保存通知的可变数组;用于向正确线程发出信号的通信端口(Mach 端口);用于防止多线程与通知数组冲突的锁;以及一个标识正确线程的值(一个 NSThread 对象);#import <Foundation/Foundation.h> @interface MyThreadedClass : NSObject /* Threaded notification support. */ @property NSMutableArray *notifications; @property NSThread *notificationThread; @property NSLock *notificationLock; @property NSMachPort *notificationPort; @end实现相关#import "MyThreadedClass.h" @interface** MyThreadedClass ()<NSMachPortDelegate> @end @implementation** MyThreadedClass -(instancetype)init{ if (self = [super init]) { [self setUpThreadingSupport]; [[NSNotificationCenter defaultCenter]addObserver:self selector: @selector(processNotification:) name:@"NotificationName" object:nil];     } return self; } //初始化相关变量 - (void)setUpThreadingSupport {     if (self.notifications) { return;     }     self.notifications      = [[NSMutableArray alloc] init];     self.notificationLock   = [[NSLock alloc] init]; //     self.notificationThread = [NSThread currentThread];     self.notificationPort = [[NSMachPort alloc] init];     [self.notificationPort setDelegate:self]; //添加到runloop     [[NSRunLoop currentRunLoop] addPort:self.notificationPort             forMode:NSRunLoopCommonModes]; } //machport的回调 - (void)handleMachMessage:(void*)msg {     [self.notificationLock lock];     while ([self.notifications count]) {         NSNotification *notification = [self.notifications objectAtIndex:0];         [self.notifications removeObjectAtIndex:0];         [self.notificationLock unlock];         [self processNotification:notification];         [self.notificationLock lock];     };     [self.notificationLock unlock]; } //观察到通知的回调 - (void)processNotification:(NSNotification *)notification { //判断发送通知的线程是否是期望接收回调的线程,如果不是     if ([NSThread currentThread] != _notificationThread) {         // Forward the notification to the correct thread.         [self.notificationLock lock];         [self.notifications addObject:notification];         [self.notificationLock unlock];         [self.notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0];     }     else { //如果是 //dosomething         // Process the notification here;     } } @end
0
0
0
浏览量534
德州安卓

iOS加固可以,但是别用虚拟机......

一个关于iOS加固的小故事传说,有这么一家公司,他们使用了一种独特的iOS加固方法:在应用程序中添加一个虚拟机,以便在应用程序运行时保护其代码。咱也不知道这具体的实现方式,但是,不得不夸一句:人才!当然,人家苹果公司是不认的,苹果公司认为他们违反了应用程序开发规则,所以这家公司的应用程序最终被禁止在App Store上发布。多年前的故事了,我们今天只讲常规的几种加固方法。iOS加固的意义最近有很多人咨询iOS加固到底有什么用?app是否需要加固? 其实,真的因人而异,iOS主要作用是提高应用程序的安全性,防止黑客攻击和逆向工程。而“黑客攻击和逆向工程”会:获取未经授权的访问:黑客攻击的一个常见目的是获取未经授权的访问,例如入侵系统、窃取密码或身份验证凭据等。窃取机密信息:黑客攻击也可能是为了窃取敏感信息,例如信用卡号码、医疗记录或政府机密等。破坏或破解系统:黑客攻击可能是为了破坏或破解系统,例如通过恶意软件破坏计算机系统、妨碍网络连接或篡改数据等。获得商业优势:逆向工程的一个常见目的是为了获得商业优势。例如,逆向工程可以帮助竞争对手分析您的产品设计和工艺,从而提高他们的产品质量和性能。理解和修改软件:逆向工程可能是为了理解和修改软件,例如帮助诊断和解决软件缺陷,或者为了增强软件的性能和功能等。因此,对于个人来说,如果app没有太大的风险,基本可以不考虑加固问题。但是对于企业而言,尤其是银行、金融、车企以及电商、游戏等行业,应用程序的安全性需要更加重视。常见的iOS加固技术下面,我们将介绍几种常见的iOS加固技术,然后提供相应的代码演示。1. 防调试防调试是一种常见的iOS加固技术,它可以检测应用程序是否正在被调试,如果是,则会采取相应的措施,例如崩溃或退出应用程序。下面是一个使用ptrace()函数实现防调试的代码示例:#include <unistd.h> #include <sys/syscall.h> #include <dlfcn.h> #include <string.h> int anti_debug(void) { void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW); if (handle) { int (*ptrace_ptr)(int, pid_t, caddr_t, int) = dlsym(handle, "ptrace"); if (ptrace_ptr) { if (ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0) == -1) { dlclose(handle); return 1; } } dlclose(handle); } return 0; }解释一下:这段代码首先使用dlopen()函数打开一个指向应用程序的句柄,然后使用dlsym()函数获取ptrace()函数的地址,最后调用ptrace()函数将PT_DENY_ATTACH标志设置为防止调试。如果ptrace()函数返回-1,则说明应用程序正在被调试,这时可以采取相应的措施,例如退出应用程序。2. 混淆混淆是指对应用程序的代码和数据进行混淆,以使其难以被理解和破解。 下面是一个使用LLVM混淆器进行代码混淆的代码示例: int main(int argc, char *argv[]) { printf("Hello, world!\n"); return 0; }使用LLVM混淆器可以将上面的代码混淆成以下代码:#define _A _B + _C - _D + _E - _F #define _B _C + _D - _E + _F - _G #define _C _D + _E - _F + _G - _H #define _D _E + _F - _G + _H - _I #define _E _F + _G - _H + _I - _J #define _F _G + _H - _I + _J - _K #define _G _H + _I - _J + _K - _L #define _H _I + _J - _K + _L - _M #define _I _J + _K - _L + _M - _N #define _J _K + _L - _M + _N - _O #define _K _L + _M - _N + _O - _P #define _L _M + _N - _O + _P - _Q #define _M _N + _O - _P + _Q - _R #define _N _O + _P - _Q + _R - _S #define _O _P + _Q - _R + _S - _T #define _P _Q + _R - _S + _T - _U #define _Q _R + _S - _T + _U - _V #define _R _S + _T - _U + _V - _W #define _S _T + _U - _V + _W - _X #define _T _U + _V - _W + _X - _Y #define _U _V + _W - _X + _Y - _Z #define _V _W + _X - _Y + _Z - argc #define _W _X + _Y - _Z + argc - argv #define _X _Y + _Z - argc + argv - main #define _Y _Z + argc - argv + main - printf #define _Z argc - argv + printf - return #define _B0 _A1 #define _B1 _A0 #define _B2 _A3 #define _B3 _A2 #define _B4 _A5 #define _B5 _A4 #define _B6 _A7 #define _B7 _A6 #define _B8 _A9 #define _B9 _A8 #define _BA _AD #define _BB _AC #define _BC _AF #define _BD _AE #define _BE _B1 #define _BF _B0 #define a _BA #define b _BB #define c _BC #define d _BD #define e _BE #define f _BF int a(int b, char **c) { return d("H") - f + e("e") + e("l") - d("o") + e(",") + e(" ") + d("w") - e("o") + e("r") + e("l") + f + d("d") + e("!") + e("\n"); }可以看到,混淆后的代码与原始代码完全不同,这使得逆向工程变得困难,能达到我们防止逆向的目的。3. 加密加密是指对应用程序的代码和数据进行加密,以防止其被窃取和破解。下面是一个使用AES加密算法对字符串进行加密的代码示例:#include <string.h> #include <openssl/aes.h> #define KEY "0123456789012345" #define IV "0123456789012345" int main(int argc, char *argv[]) { char *plaintext = "Hello, world!"; unsigned char ciphertext[strlen(plaintext)]; memset(ciphertext, 0, sizeof(c加密过程的代码如下所示: AES_set_encrypt_key(KEY, 128, &aes_key); AES_cbc_encrypt(plaintext, ciphertext, strlen(plaintext), &aes_key, IV, AES_ENCRYPT); printf("Plaintext: %s\n", plaintext); printf("Ciphertext: "); for (int i = 0; i < strlen(plaintext); i++) { printf("%02x", ciphertext[i]); } printf("\n"); return 0; }解释一下:这段代码首先定义一个密钥和初始向量,然后使用AES_set_encrypt_key()函数将密钥设置为128位的AES密钥。接下来,使用AES_cbc_encrypt()函数将明文加密成密文,并将结果存储在ciphertext数组中。最后,输出明文和密文。不过,加密后的数据需要在应用程序中进行解密,否则无法正确地使用。所以,在应用程序中需要包含相应的解密代码。总结上面就是几种常见的iOS加固技术及相应的代码演示。虽然这些技术可以提高应用程序的安全性,但是并不能完全避免应用程序被破解和逆向工程。所以,为了保护应用程序的安全性,开发者最好还是需要采取其他措施,例如加强代码审查和安全测试,以及定期更新和修复漏洞。
0
0
0
浏览量522
德州安卓

iOS中为什么会有这么多锁呢?

其实iOS领域很多文章都谈到了关于锁的文章,但是我为什么要在这里重新写一篇文章呢?一是很多文章使用的观点依然是很老的观点,和我的测试结果不符合,二则是自己对这方面也比较生疏,所以就在最近重新梳理一下自己对着方面的调查,梳理一下这一块的知识点。首先是一波对比,我使用了10^7次遍历,使用的开发语言是Swift,在iOS15.5系统版本的iPhone13真机上跑出的数据:整体来说NSConditionLock的性能会略慢,但是其他的性能都类似,在这个量级的数据处理下,它们的表现都非常的接近。从图中可以看出性能最好的三个锁是os_unfair_lock、pthread_mutex以及DispatchSemaphore,前两者是互斥锁,后者是信号量。首先我想提出一个问题,那就是锁的目的是什么?在聊锁的目的之前,那首先我们来看一个概念,那就是**线程安全。**什么是线程安全?**我的定义是当多线程都需要操作某个共享数据时,并不会引起意料之外的情况,能保证该共享数据的正确性。**可是如何去实现一个线程安全类呢?通用的方式就是在一些数据的操作上加锁。而锁的目的就是确保多线程操作共享数据时,能保证数据的准确性和可预测性。os_unfair_lock我相信有很多人都阅读过ibireme关于锁的性能对比的知名文章《不再安全的 OSSpinLock》,其中提到了OSSpinLock不再安全的理由,但是由此却引发一个问题,那就是OSSpinLock主要的使用场景是哪里呢?我们都知道在Objective-C中定义一个属性的时候,有时属性会被声明为atomic,这就是说这个属性的set操作和get操作是原子性的,那么如何确保这些 操作的原子性呢?我想这个时候你已经猜到答案了,Apple使用的方案是OSSpinLock,这是一个自旋锁,但是这个锁有一个很严重的问题,那就是优先级反转问题会导致自旋锁发生死锁。iOS 系统中维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。苹果工程师 Greg Parker 提到,对于这个问题,一种解决方案是用 truly unbounded backoff 算法,这能避免 livelock 问题,但如果系统负载高时,它仍有可能将高优先级的线程阻塞数十秒之久;另一种方案是使用 handoff lock 算法,这也是 libobjc 目前正在使用的。锁的持有者会把线程 ID 保存到锁内部,锁的等待者会临时贡献出它的优先级来避免优先级反转的问题。理论上这种模式会在比较复杂的多锁条件下产生问题,但实践上目前还一切都好。而在iOS 10之后,Apple使用了os_unfair_lock来替代了OSSpinLock, 这是一个高性能的互斥锁,而不是自旋锁,如果是阻止两个线程可以同时访问临界区,那么这个锁无疑可以很好的完成工作,包括上述的pthread_mutex_lock 以及信号量都可以,但是如果我们需要锁具备某些特性,那么这个时候就需要其他多种类的锁了。// os_unfair_lock的使用 var unfairLock = os_unfair_lock() os_unfair_lock_lock(&unfairLock) os_unfair_lock_unlock(&unfairLock) // pthreadMutex的使用 var pthreadMutex = pthread_mutex_t() pthread_mutex_lock(&pthreadMutex) pthread_mutex_unlock(&pthreadMutex)这里再补充说明一下,Apple使用在保证原子性时实际会调用到的方法如下:static inline void reallySetProperty() { ... if (!atomic) { oldValue = *slot; *slot = newValue; } else { //PropertyLocks是一个StripedMap<spinlock_t>类型的全局变量 //而StripedMap是一个用数组来实现的hashmap,key是指针,value是类型是spinlock_t对象 //而spinlock_t则是mutex_tt<LOCKDEBUG>的类,而mutex_tt类内部是由os_unfair_lock mLock来实现 //所以,PropertyLocks[slot]目的就是获取os_unfair_lock对象 spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } ... }它通过地址从PropertyLocks数组中取出了spinlock_t锁,可是如何使用地址作为数组下标呢?它使用了一个很巧妙的hash算法,来实现指针到数组下标的转化:static unsigned int indexForPointer(const void *p) { uintptr_t addr = reinterpret_cast<uintptr_t>(p); // 这是一个哈希算法,可以将对象的地址转化为数组的下标 // 使得数组元素在0~StripeCount之间 return ((addr >> 4) ^ (addr >> 9)) % StripeCount; }当然这种方法也会偶尔导致哈希冲突,两个不同的地址会导致获取到同一个Lock,这样会造成资源闲置,没有充分利用CPU的资源,但是不妨碍这个哈希算法整体上是高效的。NSLock既然已经有了性能比较高的互斥锁,那为什么还需要有其它这些杂七杂八的锁呢?比如说接下来我们要提到的NSLock,这个锁也是一个互斥锁,而它是基于pthread_mutex_lock的封装,而在原有的基础上增加了一个特性那就是超时!没错这就是有其他各种锁的原因,给不同的锁不同的特性,以满足具体的开发场景,NSLock的API如下:open class NSLock : NSObject, NSLocking { open func `try`() -> Bool open func lock(before limit: Date) -> Bool open var name: String? }在某些时候,超时这个特性是非常有效的,因为在一些可能发生死锁的场景中,使用NSLock可以让我们有一个保险机制,即使发生了死锁,也可以在一定的时间之后走出加锁状态,恢复到正常的程序处理逻辑。但是和以上的互斥锁一样,它都无法应对递归的情况,那使用什么来处理递归锁呢?NSRecursiveLock!NSRecursiveLock使用NSRecursiveLock可以使得该锁被同一线程多次获取而不会导致线程死锁。但是每一次lock都对应一次unlock,这样unlock结束之后,锁才会释放。而顾名思义,这种类型的锁被用于一个递归方法内部来防止线程被阻塞。let rlock = NSRecursiveLock() class RThread : Thread { override func main(){ rlock.lock() print("Thread acquired lock") callMe() rlock.unlock() print("Exiting main") } func callMe(){ rlock.lock() print("Thread acquired lock") rlock.unlock() print("Exiting callMe") } } var tr = RThread() tr.start() // 多次申请锁,并不会导致崩溃,这就是递归锁的作用NSConditionLock条件锁满足NSLocking 协议,所以基本的NSLock类型锁的基本lock,unlock这种全局的锁方法它也是具备的,初次之外,它还具备自己的特性,通常情况下,当线程需要以某种特定的顺序执行任务时,比如一个线程生产数据,而另一个线程消耗数据时,可以使用NSConditionLock(比如常见的生产者消费者模型)。接下来我们来看一个实例:let NODATA = 1 let GOTDATA = 2 let clock = NSConditionLock(condition: NODATA) var shareInt = 0 class ProducerThread: Thread { override func main() { for _ in 0..<100 { clock.lock(whenCondition: NODATA) LockFile.ProducerThread.sleep(forTimeInterval: 0.5) sharedInt = sharedInt + 1 NSLog("生产者:\(sharedInt)") clock.unlock(withCondition: GOTDATA) } } } class ConsumerThread: Thread { override func main() { for _ in 0..<100 { clock.lock(whenCondition: GOTDATA) sharedInt = sharedInt - 1 NSLog("消费者:\(sharedInt)") clock.unlock(withCondition: NODATA) } } } let pt = ProducerThread.init() let ct = ConsumerThread.init() pt.start() ct.start()当创建一个条件锁的时候,需要指定一个特定Int类型的值。而lock(whenCondition:) 方法当条件满足时会获取这个锁,或者条件和另一个线程在使用unlock(withCondition:) 释放锁时设置的值满足时,NSConditionLock对象就会获取锁执行后续的代码片段,但是当lock(whenCondition:) 方法没有获取锁的时候(条件没满足时),这个方法会阻塞线程的执行,直到获得锁为止。NSConditionNSCondition和前者是很容易混淆的,但是这个锁解决了什么问题呢?当一个已获得锁的线程发现执行其工作所需的附加条件(它需要一些资源、另一个处于特定状态的对象等)暂时还没有得到满足时,它需要一种方法来暂停,并且一旦满足条件就继续工作的机制,可是如何实现呢?可以通过连续的检查(忙等待)来实现,但是这样做的话,线程持有的锁会发生什么?我们应该在等待时保留它们还是释放它们?还是在满足条件时再次获得它们?而NSCondition提供了一种简洁的方式来提供了这种问题的解决方案,一旦一个线程被放在该Condition的等待列表中,它可以通过另一个线程Signal来唤醒。以下是具体的案例:let cond = NSCondition.init() var available = false var sharedString = "" class WriterThread: Thread { override func main() { for _ in 0..<100 { cond.lock() sharedString = "🤣" available = true cond.signal() cond.unlock() } } } class PrinterThread: Thread { override func main() { for _ in 0..<100 { cond.lock() while (!available) { cond.wait() } sharedString = "" available = false cond.unlock() } } }当线程waits一个条件时,这个Condition对象会unlock当前锁并且阻塞线程。当Condition发出信号时,系统会唤醒线程,然后这个Condition对象会在wait()或者wait(until:)返回之前,这个Condition对象会重新获取到它的锁,因此,从线程的角度来看,它似乎一直持有者锁(虽然中途它会失去锁)。Dispatch Semaphore最后我们聊一聊信号量,简而言之,信号量是需要在不同的线程中进行锁定和解锁时使用的锁。因为它的wait方法会阻塞当前线程,所以需要其他线程发来signal信号来唤醒它。let semaphore = DispatchSemaphore.init(value: 0) DispatchQueue.global(qos: .userInitiated).async { // to do some thing semaphore.signal() } semaphore.wait() // will block thread如上述例子一样,信号量通常用于锁定一个线程,直到另外一个线程中事件的完成后发出signal信号。从上述的测试图标,以及其他诸多文章,信号量的速度是很快的。上述的生产者消费者模型也可以使用信号量来实现:let semaphore = DispatchSemaphore.init(value: 0) DispatchQueue.global(qos: .userInitiated).async { while true { sleep(1) sharedInt = sharedInt + 1 NSLog("生产了: \(sharedInt)") _ = semaphore.signal() } } DispatchQueue.global(qos: .userInitiated).async { while true { if sharedInt <= 0 { _ = semaphore.wait(timeout: .distantFuture) } else { sharedInt = sharedInt - 1 NSLog("消耗了: \(sharedInt)") } } }
0
0
0
浏览量1033
德州安卓

iOS常见三种定时器-NSTimer、CADisplayLink、GCD定时器

  在iOS开发过程当中,我们经常会直接或间接地使用到定时器,iOS系统中,带有延迟性操作的函数都是基于NSTimer,CADisplayLink或者GCD定时器来实现的。本文主要也是围绕这三种定时器展开,最后封装一个简单易用的定时器库。1、NSTimer定时器NSTimer是基于NSRunloop的实现定时器,在使用NSTimer过程当中,应该关注两个问题一、直接使用NSTimer定时器,可能存在循环应用问题。首先,NSTimer会强引用传入的target对象, 而此时,如果target又对NSTimer产生强引用,那么就会引发循环引用问题。 二、NSTimer回调的时间间隔可能会有存在误差。因为RunLoop每跑完一次圈再去检查当前累计时间是否已经达到定时器所设置的间隔时间,如果未达到,RunLoop将进入下一轮任务,待任务结束之后再去检查当前累计时间,而此时的累计时间可能已经超过了定时器的间隔时间,故可能会存在误差。针对循环引用问题,我们可以使用中间类来解决。原理大致如下: 中间类继承自NSProxy,基于消息转发实现的,目的是为了提高方法调用效率。 实现代码如下:中间类.h声明文件#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface XBWeakProxy : NSProxy /** weak target*/ @property (nonatomic, weak) id target; /** init proxy by target*/ + (instancetype)timerProxyWithTarget:(id)target; @end NS_ASSUME_NONNULL_END中间类.m声明文件#import "XBWeakProxy.h" @implementation XBWeakProxy + (instancetype)timerProxyWithTarget:(id)target{ if (!target) return nil; XBWeakProxy *proxy = [XBWeakProxy alloc]; proxy.target = target; return proxy; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation{ [invocation invokeWithTarget:self.target]; } @end为了方便调用NSTimer,我们可以给NSTimer新增一个分类,给分类扩展类方法,在扩展的方法中使用中间类来解决循环应用问题。 同时可以利用runtime关联技术,使用Block代替Selector回调。 代码大致如下://.h文件 #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef void (^XBTimerCallbackBlock)(NSTimer *timer); @interface NSTimer (XbTimer) /** 方法一,与系统同名方法一致, 需要手动添加到runloop中,自己控制启动*/ + (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; /** 方法二, 与系统同名方法一致,系统自动添加到runloop中,创建成功自动启动*/ + (NSTimer *)xb_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; /** 方法三,block回调, 不限制iOS最低版本, 需要手动添加到runloop中,自己控制启动*/ + (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(XBTimerCallbackBlock)block; /** 方法四,block回调, 不限制iOS最低版本, 系统自动添加到runloop中,创建成功自动启动*/ + (NSTimer *)xb_scheduledTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(XBTimerCallbackBlock)block; @end NS_ASSUME_NONNULL_END //.m文件 #import "NSTimer+XBTimer.h" #import "XBWeakProxy.h" #import <objc/runtime.h> @implementation NSTimer (XbTimer) #pragma mark - Public + (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo{ return [self timerWithTimeInterval:ti target:[XBWeakProxy timerProxyWithTarget:aTarget] selector:aSelector userInfo:userInfo repeats:yesOrNo]; } + (NSTimer *)xb_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo{ return [self scheduledTimerWithTimeInterval:ti target:[XBWeakProxy timerProxyWithTarget:aTarget] selector:aSelector userInfo:userInfo repeats:yesOrNo]; } + (NSTimer *)xb_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(nonnull XBTimerCallbackBlock)block{ if (!block) return nil; NSTimer *timer = [self timerWithTimeInterval:interval target:[XBWeakProxy timerProxyWithTarget:self] selector:@selector(_blockAction:) userInfo:nil repeats:repeats]; if (!timer) return timer; objc_setAssociatedObject(timer, @selector(_blockAction:), block, OBJC_ASSOCIATION_COPY); return timer; } + (NSTimer *)xb_scheduledTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(nonnull XBTimerCallbackBlock)block{ if (!block) return nil; NSTimer *timer = [self scheduledTimerWithTimeInterval:interval target:[XBWeakProxy timerProxyWithTarget:self] selector:@selector(_blockAction:) userInfo:nil repeats:repeats]; if (!timer) return timer; objc_setAssociatedObject(timer, @selector(_blockAction:), block, OBJC_ASSOCIATION_COPY); return timer; } #pragma mark - Privite + (void)_blockAction:(NSTimer *)timer{ XBTimerCallbackBlock block = objc_getAssociatedObject(timer, _cmd); !block?:block(timer); } @end3.关于NSTimer时间误差问题,可以使用GCD定时来代替NSTimer定时器,后面讲GCD定时器部分会讲到。2、CADisplayLink定时器  CADisplayLink 依托于设备屏幕刷新频率触发事件,所以其触发时间比NSTimer较准确,也是最适合做UI不断刷新的事件,过渡相对流畅,无卡顿感。 而CADisplayLink定时器也是依赖于NSRunLoop, 所以,CADisplayLink定时器也一样会存在NSTimer的两个问题。针对解决循环引用问题,直接上代码了://.h文件 #import <QuartzCore/QuartzCore.h> NS_ASSUME_NONNULL_BEGIN typedef void (^XBDisplayLinkCallbackBlock)(CADisplayLink *link); @interface CADisplayLink (XBDisplayLink) /** 同系统方法,仅解决循环引用问题*/ + (CADisplayLink *)xb_displayLinkWithTarget:(id)target selector:(SEL)sel; /** 同系统方法,自动添加到当前runloop中,Mode: NSRunLoopCommonModes*/ + (CADisplayLink *)xb_scheduledDisplayLinkWithTarget:(id)target selector:(SEL)sel; /** Block callback,auto run, runloop mode: NSRunLoopCommonModes*/ + (CADisplayLink *)xb_scheduledDisplayLinkWithBlock:(XBDisplayLinkCallbackBlock)block; @end NS_ASSUME_NONNULL_END //.m文件 #import "CADisplayLink+XBDisplayLink.h" #import "XBWeakProxy.h" #import <objc/runtime.h> @implementation CADisplayLink (XBDisplayLink) #pragma mark - Public + (CADisplayLink *)xb_displayLinkWithTarget:(id)target selector:(SEL)sel{ return [self displayLinkWithTarget:[XBWeakProxy timerProxyWithTarget:target] selector:sel]; } + (CADisplayLink *)xb_scheduledDisplayLinkWithTarget:(id)target selector:(SEL)sel{ CADisplayLink *link = [self displayLinkWithTarget:[XBWeakProxy timerProxyWithTarget:target] selector:sel]; [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; return link; } + (CADisplayLink *)xb_scheduledDisplayLinkWithBlock:(XBDisplayLinkCallbackBlock)block{ if (!block) return nil; CADisplayLink *link = [self xb_displayLinkWithTarget:self selector:@selector(displayLinkAction:)]; objc_setAssociatedObject(link, @selector(displayLinkAction:), block, OBJC_ASSOCIATION_COPY); [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; return link; } #pragma mark - Privite + (void)displayLinkAction:(CADisplayLink *)link{ XBDisplayLinkCallbackBlock block = objc_getAssociatedObject(link, _cmd); !block?:block(link); } @end3、GCD定时器  GCD定时器是这三种定时器中,时间最为准确的。因为GCD定时器不依赖与NSRunLoop, GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理,通过系统级调用,更加精准。以下是对GCD定时器的封装,支持block和selector两种回调方式//.h #import <Foundation/Foundation.h> @class XBGCDTimer; typedef void (^XBGCDTimerCallbackBlock)(XBGCDTimer *timer); @interface XBGCDTimer : NSObject /// Create GCDTimer, but not fire(定时器创建但未启动) /// @param start The number of seconds between timer first times callback since fire /// @param interval The number of seconds between firings of the timer /// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated /// @param queue Queue for timer run and callback, default is in main queue /// @param block Timer callback handler + (XBGCDTimer *)xb_GCDTimerWithSartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats block:(XBGCDTimerCallbackBlock)block; /// Create GCDTimer and fire immdiately (定时器创建后马上启动) /// @param start The number of seconds between timer first times callback since fire /// @param interval The number of seconds between firings of the timer /// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated /// @param queue Queue for timer run and callback, default is in main queue /// @param block Timer callback handler + (XBGCDTimer *)xb_scheduledGCDTimerWithSartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats block:(XBGCDTimerCallbackBlock)block; /// Create GCDTimer, but not fire(定时器创建但未启动) /// @param target target description /// @param selector selector description /// @param start The number of seconds between firings of the timer /// @param interval The number of seconds between firings of the timer /// @param queue Queue for timer run and callback, default is in main queue /// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated + (XBGCDTimer *)xb_GCDTimerWithTarget:(id)target selector:(SEL)selector SartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats; /// Create GCDTimer and fire immdiately (定时器创建后马上启动) /// @param target target description /// @param selector selector description /// @param start The number of seconds between timer first times callback since fire /// @param interval The number of seconds between firings of the timer /// @param repeats If YES, the timer will repeatedly reschedule itself until invalidated /// @param queue Queue for timer run and callback, default is in main queue + (XBGCDTimer *)xb_scheduledGCDTimerWithTarget:(id)target selector:(SEL)selector SartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats; /** start*/ - (void)fire; /** stop*/ - (void)invalidate; @end //.m #import "XBGCDTimer.h" #import "XBWeakProxy.h" #import <objc/runtime.h> @implementation XBGCDTimer #pragma mark - Public + (XBGCDTimer *)xb_GCDTimerWithSartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats block:(XBGCDTimerCallbackBlock)block{ if (!block || start < 0 || (interval <= 0 && repeats)) return nil; XBGCDTimer *gcdTimer = [[XBGCDTimer alloc] init]; // queue dispatch_queue_t queue_t = queue ?: dispatch_get_main_queue(); // create dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue_t); // set time dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0); objc_setAssociatedObject(gcdTimer, @selector(fire), timer, OBJC_ASSOCIATION_RETAIN); // callback dispatch_source_set_event_handler(timer, ^{ block(gcdTimer); if (!repeats) { // no repeats [gcdTimer invalidate]; } }); return gcdTimer; } + (XBGCDTimer *)xb_scheduledGCDTimerWithSartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats block:(XBGCDTimerCallbackBlock)block{ XBGCDTimer *gcdTimer = [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:block]; [gcdTimer fire]; return gcdTimer; } + (XBGCDTimer *)xb_GCDTimerWithTarget:(id)target selector:(SEL)selector SartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats{ XBWeakProxy *proxy = [XBWeakProxy timerProxyWithTarget:target]; return [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:^(XBGCDTimer * _Nonnull timer) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [proxy performSelector:selector]; #pragma clang diagnostic pop }]; } + (XBGCDTimer *)xb_scheduledGCDTimerWithTarget:(id)target selector:(SEL)selector SartTime:(NSTimeInterval)start interval:(NSTimeInterval)interval queue:(dispatch_queue_t)queue repeats:(BOOL)repeats{ XBWeakProxy *proxy = [XBWeakProxy timerProxyWithTarget:target]; XBGCDTimer * gcdTimer = [self xb_GCDTimerWithSartTime:start interval:interval queue:queue repeats:repeats block:^(XBGCDTimer * _Nonnull timer) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [proxy performSelector:selector]; #pragma clang diagnostic pop }]; [gcdTimer fire]; return gcdTimer; } /** start*/ - (void)fire{ dispatch_source_t timer = objc_getAssociatedObject(self, _cmd); if (timer) dispatch_resume(timer); } /** stop*/ - (void)invalidate{ dispatch_source_t timer = objc_getAssociatedObject(self, @selector(fire)); if (timer) dispatch_source_cancel(timer); objc_removeAssociatedObjects(self); } @end4、总结NSTimer和CADisplayLink依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时,相比之下GCD的定时器会更加准时,因为GCD不是依赖RunLoop,而是由内核决定CADisplayLink和NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用上面所有代码已经封装成一个定时器库XBSTimer,欢迎下载体验。 GitHub地址
0
0
0
浏览量1027
德州安卓

iOS 应用加载流程

好好学习,不急不躁,每天进步一点点;前言本文将从底层原理出发,讲解iOS 应用加载流程;程序加载框架源文件通过预编译,将代码词法和语法进行分析,然后交给编译器;编译之后生成一些汇编文件,链接装载进应用内,最终变成可执行文件;动态库/静态库静态库: 链接时,会被完整的复制到可执行文件内,会被系统多次使用,拷贝多份;静态库形式:.a 和 .framework形式动态库: 链接时不复制,程序运行时由系统动态加载进内存,系统只加载一次,多个程序共用,节省内存空间;动态库形式:.so、.tbd(之前叫.dylib) 和 .framework静态库与动态库的区别,主要在链接时的区别,一个是静态链接,一个是动态链接;静态库/动态库生成可执行文件后,接下来验证可执行文件 (提示::尽量使用MacO工程验证,如果使用iOS工程,在终端运行可执行文件的时候,会将模拟器或者是真机运行起来,这时候会需要一些访问权限,处理起来比较麻烦;此处我使用源码工程验证)运行源码工程,生成可执行文件:在 Finder 中,将可执行文件,拽入终端运行;使用静态库或者动态库的优势:1、减少包体积大小;2、动态库热更新(目前苹果已杜绝次方法)3、提升程序运行效率了解了库的部分原理后,那么在应用中,库是如何加载到内存中呢???dylddyld 链接器库是通过链接器:也就是dyld动态链接器,加载到内存中的;App启动,会加载程序需要的库(如:libSystem库),进入runtime 运行时,注册回调函数(_dyld_objc_notify_register函数),然后加载新image(image是库,是镜像文件,库加载的过程,就是一种映射的过程,将库映射一份到内存中) ,image加载完毕,执行map_images、Load_images函数,这两个函数执行完毕后,才是main()函数执行; 这就是库的加载流程;dyld原理接下来,我们着重查看,调用main()函数之前,dyld的链接过程;运行工程,在main函数之前,先执行start流程;通过全局断点,看看start流程是怎样的?start断点没有执行,但是我们发现,在调用main()函数之前,日志区打印load方法log,这就表示,load方法在main()函数之前就已经调用。既然load函数在main()之前,那么在load函数内断点查看底层执行流程,断点在load函数内,在log区通过bt打印栈队列:通过栈流程,可以看到,最先执行的是_dyld_start,并且是在dyld源码内;下载dyld源码,可以通过苹果开源网下载,目前官网最新的是dyld-852.2版本,今天也就用这个版本解说;dyld源码分析在dyld源码中,搜索_dyld_start函数,我们发现_dyld_start函数,是采用汇编的形式,并且,根据不同的架构,采用不同的流程;i386 架构x86 架构arm 架构汇编的源码,阅读起来不是很方便,但从注释中,我们阅读到_dyld_start函数,将会执行dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)函数,所以我们可以直接定位到这个函数;由于这是C函数命名规范,可以解读为:在dyldbootstrap文件内,执行start函数; 如此,我们先查询dyldbootstrap文件,再查询start函数;start函数内,最终返回的是dyld::_main()函数,而dyld::_main函数源码复杂,难已阅读,需要开启上帝视角,我们直接查看它的返回值,依据返回值,由下往上逆推;dyld::_main函数返回的result,result的赋值方式,更多的是来自于sMainExecutable函数;sMainExecutable初始化:// instantiate ImageLoader for main executable sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath); //instantiateFromLoadedImage:镜像文件加载器加载镜像文件,赋值给sMainExecutable;loadInsertedDylib获取并插入动态库:// load any inserted libraries **if** ( sEnv.DYLD_INSERT_LIBRARIES != **NULL** ) { **for** (**const** **char*** **const*** lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != **NULL**; ++lib)  loadInsertedDylib(*lib); } // record count of inserted libraries so that a flat search will look at  // inserted libraries, then main, then others. //获取动态库镜像文件数量 sInsertedDylibCount = sAllImages.size()-1;获取镜像文件后,接下来开始链接;link链接镜像文件://开始链接镜像文件 link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, **true**, ImageLoader::RPathChain(**NULL**, **NULL**), -1); sMainExecutable->setNeverUnloadRecursive(); if ( sMainExecutable->forceFlat() ) { gLinkContext.bindFlat = **true**; gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding; } //判断动态库镜像文件是否存在 if ( sInsertedDylibCount > 0 ) { for(unsigned int i=0; i < sInsertedDylibCount; ++i) { ImageLoader* image = sAllImages[i+1]; //link 动态库镜像文件 link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1); image->setNeverUnloadRecursive(); } ......镜像文件链接结束,开始绑定程序;weakBind弱引用绑定程序:sMainExecutable->weakBind(gLinkContext); gLinkContext.linkingMainExecutable = false; ......当link,weakBind结束,主程序开始运行;initializeMainExecutable:运行主程序initializeMainExecutable(); 最后通知dyld,进入main()函数notifyMonitoringDyldMain:通知dyld,可以进入主程序notifyMonitoringDyldMain();这就是dyld的大体流程,内部有很多细节,大家可以自行查看,在此就不一一展开讲述;initializeMainExecutable 程序运行initializeMainExecutable源码:通过流程可以看到,初始化主程序系统会做一些准备,那么是什么准备呢,接下来我们看processInitializers()这个函数;processInitializers()函数:void ImageLoader::processInitializers(const LinkContext& context, mach_port_t thisThread, InitializerTimingList& timingInfo, ImageLoader::UninitedUpwards& images) { uint32_t maxImageCount = context.imageCount()+2; ImageLoader::UninitedUpwards upsBuffer[maxImageCount]; ImageLoader::UninitedUpwards& ups = upsBuffer[0]; ups.count = 0; // Calling recursive init on all images in images list, building a new list of // uninitialized upward dependencies. //在当前线程,对images list 中的所有 image 调用递归init,建立一个新的images list for (uintptr_t i=0; i < images.count; ++i) { images.imagesAndPaths[i].first->recursiveInitialization(context, thisThread, images.imagesAndPaths[i].second, timingInfo, ups); } // If any upward dependencies remain, init them. //递归流程 if ( ups.count > 0 ) processInitializers(context, thisThread, timingInfo, ups); }processInitializers()函数是一个递归的过程,并且init所有的image,那么调用recursiveInitialization函数,又是什么情况呢?这个流程真的是,越看越迷糊继续看吧recursiveInitialization函数先加载依赖文件,再加载当前文件;这是因为依赖文件没有加载的话,当前文件是无法加载的;举例: ViewA内有一个子ViewB,如果子ViewB没有加载完成的话,那么ViewA就没有办法引用ViewB,就导致ViewA无法完成;在recursiveInitialization函数中,加载文件后,系统都执行了notifySingle函数,接下来我们分析这个函数;notifySingle函数sNotifyObjCInitinitializeMainExecutable流程图:
0
0
0
浏览量1023
德州安卓

iOS 高效开发指南

本专栏将深入介绍 iOS 网络优化和基础、函数防抖与节流、常见三种定时器(NSTimer、CADisplayLink、GCD定时器)、获取最顶层ViewController、为什么会有那么多锁、加固技巧、iOS 应用加载流程、NSNotificationCenter 详解等主题。通过学习本专栏,读者将能够更好地应用 iOS 开发技巧和最佳实践,提高自己的工作和生活水平。
0
0
0
浏览量1269
德州安卓

你需要懂的Kotlin开发技巧之七

1.kotlin中的for循环第一种 ..for (i in 0..5) { println(i) }正序输出 :0 1 2 3 4 5,这是一个左闭右闭区间第二种 untilfor (i in 0 until 5) { println(i) }正序输出:0 1 2 3 4,这是一个左闭右开区间第三种 downTofor (i in 5 downTo 0) { println(i) }倒序输出:5 4 3 2 1 0,这是一个左闭右闭区间第四种 step 这种是带步长的,比如:for (i in 0 until 5 step 2) { println(i) }输出:0 2 4,可以看到步长设置为2就隔2输出第五中 zipfor((i, j) in (1..5).zip(5..10)) { println("$i -- $j") }zip支持两个参数,看下输出:2.use扩展函数平常从文件输出输入流中进行读写,需要在读写完毕之后调用close方法关闭输出或输入流,防止产生异常,但是要求是要求,程序开发的时候还有有概率漏写的。所以kotlin提供了一个user扩展方法帮助我们关闭流,先看下使用:File("").outputStream().use { }使用use后就不需要程序手动关闭流了,看下use源码:同理,读写数据库中的cursor也支持使用use。3.密封类sealed class密封类的定义如下:sealed class Kit { data class H(val name: String = "") data class O(val age: Int = 0) data class L(val show: Boolean = false) }其实密封类和抽象类、枚举非常像,但是有两个非常显著区别于抽象类、枚举的优点:类结构层次化(相比较抽象类)抽象类的具体实现类可以定义在包内的各个位置,非常不方便管理,而密封类具有层次化结构,即密封类的子类只能和密封类定义在同一个类文件中。当然,kotlin1.4之后的版本不会限制这么严格子类对象支持动态创建(相比较枚举)java实现单例最安全的方式就是利用枚举实现。枚举声明的每个元素内容都是固定的,无法通过外部传入参数。而密封类的子类可以声明为object,这个效果就和枚举元素一样,也可以声明为普通的class,可以外部进行传参创建类的对象
0
0
0
浏览量1107
德州安卓

你需要懂的Kotlin开发技巧之三

1.嵌套函数业务开发中,我们可能会遇到这样一个场景:一个函数只会被某一处多次调用,且不想让这个函数在该类的其他地方调用,这个时候就需要对这个函数的访问性进行进一步限制。private是无法满足的,这个时候我们就可以使用嵌套函数提供更好的封装:fun test1() { //被限制访问行的函数 fun test2(content: String) { println(content) } test2("hahaha") test2("babababa") test2("uuuuuuu") }这时候,只有test1()能够被访问,test2()是无法被除了test1()外的其他地方进行访问的不过这样test2()方法体过大会导致test1()方法太长,所以嵌套函数要根据具体场景选择性使用2.@JvmOverloads快捷实现函数重载Android自定义View时,一般需要定义三个构造方法:class CustomView : View { constructor(context: Context) : super(context) constructor(context: Context, attributes: AttributeSet? = null) : super(context, attributes) constructor(context: Context, attributes: AttributeSet? = null, defStyleAttr: Int) : super( context, attributes, defStyleAttr ) }每次自定义View都这样写过于麻烦,这个时候就可以借用@JvmOverloads实现运算符重载:class CustomView @JvmOverloads constructor( context: Context, attributes: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attributes, defStyleAttr) { }反编译成java代码看下:可以看到,kotlin编辑器会自动帮助我们生成三个重载构造方法。PS:当@JvmOverloads使用在基于已有的WebView、EditText等组件自定义View时,一定要注意第三个参数defStyleAttr默认值时不一定为0的,需要根据继承的父类组件填充正确的参数3.延迟初始化lateinit var日常开发中,我们会使用lateinit var实现属性的延迟初始化:lateinit var mContent: String mContent = "test test"使用延迟初始化的属性,当我们不确定这个属性有没有被初始化,可以通过下面方式判断://判断mContent有没有被初始化 if (this::mContent.isLateinit) { Log.i("CustomView", "test: ") }4.@JvmField减少属性set和get方法的生成定义一个变量:var mData: String = ""反编写成java代码:可以看到编译器会自动帮助我们生成mData的set、get方法,如果不想要编译帮助我们生成属性的set、get方法,可以添加@JvmField注解:@JvmField var mData: String = ""反编译成java代码:这样系统就不会帮助我们生成get、set方法了,因为编译器将mData的访问修饰符改成了public
0
0
0
浏览量1117
德州安卓

写一篇好懂的常见Kotlin集合函数讲解(中)

any{}: 查找集合只要存在指定条件的元素就返回true我们经常会遇见一种场景:遍历集合,只要某个元素符合指定条件就立刻返回为true:fun any(): Boolean { val list = listOf(3, 4, 6, 5, 2) for (i in list) { if (i == 5) { return true } } return false }这样写太太麻烦了,使用any改写为:return list.any { it == 5 }该函数还有其他相似函数:all{}:查找集合只有集合中所有元素都符合指定条件才返回truefold(){}:带有初始值的叠加器,如果集合为空返回的将是传入的初始值我们实现一个n!(n的阶乘):fun fold(n: Int): Int = (1..n).fold(1) { acc, next -> acc * next }第一次叠加的过程中,acc就是初始值1,next就是集合的第一个元素,计算出的acc * next就是下一次叠加的acc,下一次的next也就是集合的第二个元素,依次类推...其他相似函数:reduce:不带初始值的叠加器,其中第一次叠加的acc就是集合的第一个元素,next就是集合的第二个元素,依次类推...foldIndexed{}/reduceIndexed{}:和上面的区别就只是带了索引joinToString:将集合按照一定格式转化为字符串比如将集合转换成字符串并使用","作为集合元素间的分隔符:fun join() { val list = listOf("aa", "bf", "gd", "et") val result = list.joinToString(",") //输出:aa,bf,gd,et }还可以给集合转换后的字符串分别增加前缀和后缀:list.joinToString(",", "pre", "post") //输出:preaa,bf,gd,etpost集合转换成字符串的过程中,可以对集合的每个元素映射成的字符串内容进行处理:list.joinToString(",") { "$it haha" } //输出:aa haha,bf haha,gd haha,et hahabinarySearch:二分查找指定条件的元素相比较于find系列的操作符,查找的效率更高,时间复杂度为lognlogn(以2为底) ,如果查找不到就返回-1。举个例子,二分查找集合中是否存在某个整数(前提是要集合有序)fun binarySearch() { val list = listOf(3, 5, 7, 9, 33, 66, 88, 99) println(list.binarySearch { when { it == 66 -> 0 it < 66 -> -1 it > 66 -> 1 else -> 0 } }) } //输出:5asReversed:集合倒序val list = listOf(3, 5, 7, 9, 33, 66, 88, 99) println(list.asReversed()) //输出:[99, 88, 66, 33, 9, 7, 5, 3]elementAt(index):根据索引获取集合元素,和get[index]一样,index小于0或者越界抛出异常其他相似集合:elementAtOrElse(){}:根据索引获取元素,当传入的索引小于0或者超过集合长度时,采取传入的函数类型生成结果值elementAtOrNull():根据索引获取元素,当传入的索引小于0或者超过集合长度时,返回为nullslice(IntRange/Iterable<Int>):返回指定索引对应集合元素相比较传统的subList获取的是一段索引连续的元素元素,而slice则是能灵活指定任意数量的具体索引(可以非连续)并返回对应的元素集合:val list = listOf(3, 5, 7, 9, 33, 66, 88, 99) println(list.slice(listOf(3, 5, 6))) //输出:[9, 66, 88]
0
0
0
浏览量1839
德州安卓

Kotlin invoke约定,让Kotlin代码更简洁

前言最近看到DSL这个东西,不由的觉得里面可以利用Kotlin的一些特性能简化代码,所以具体来看看它是如何实现的。正文首先一上来就说原理或许对于不熟悉Kotlin的来说会感觉有点突兀,所以我准备从头梳理一下。约定Kotlin的约定我们在平时开发中肯定用到过,不过我们没有仔细去注意这个名词而已。约定的概念就是:使用与常规方法调用语法不同的、更简洁的符号,调用着有着特殊命名的函数。这里提取2个关键点,一个是更简洁的符号调用,一个是特殊命名的函数。说白了就是让函数调用更加简洁。比如我们最熟悉的集和调用 [index] 来 替代 get(index),我们自己也来定义个类,来实现一下这个约定:data class TestBean(val name: String,val age: Int){ //定义非常简单 使用operator重载运算符get方法 operator fun get(index : Int): Any{ return when(index) { 0 -> name 1 -> age else -> name } } }然后我们在使用时://这里就可以使用 [] 来替换 get来简化调用方法了 val testBean = TestBean("zyh",20) testBean.get(0) testBean[0]invoke约定和上面的get约定一样,[] 就是调用 get 方法的更简洁的方式,这里有个invoke约定,它的作用就是让对象像函数一样调用方法,下面直接来个例子: data class TestBean(val name: String,val age: Int){ //重载定义invoke方法 operator fun invoke() : String{ return "$name - $age" } }定义完上面代码后,我们来进行使用:val testBean = TestBean("zyh",20) //正常调用 testBean.invoke() //约定后的简化调用 testBean()这里会发现testBean对象可以调用invoke方法是正常调用,但是也可以testBean()直接来调用invoke方法,这就是invoke约定的作用,让调用invoke方法更简单。invoke约定和函数式类型既然了解了invoke约定,我们来和lambda结合起来。对于lambda有点疑惑的可以查看文章:# Kotlin lambda,有你想了解的一切我们知道函数类型其实就是实现了FunctionN接口的类,然后当函数类型是函数类型时,这时传递给它一个lambda,lambda就会被编译成FunctionN的匿名内部类(当然是非内联的),然后调用lambda就变成了一次FunctionN接口的invoke调用。还是看个例子代码://定义代码 class TestInvoke { //高阶函数类型变量 private var mSingleListener: ((Int) -> Unit)? = null //设置变量 public fun setSingleListener(listener:((Int) -> Unit)?){ this.mSingleListener = listener } // fun testRun() { //调用invoke函数 mSingleListener?.invoke(100) //使用invoke约定,省去invoke if (mSingleListener != null){ mSingleListener!!(100) } } }定义完上面回调变量后,我们来使用这个回调,由于我们知道高阶函数其实是实现了FunctionN接口的类,也就是实现了://注意,这里接口的方法就是invoke public interface Function1<in P1, out R> : Function<R> { /** Invokes the function with the specified argument. */ public operator fun invoke(p1: P1): R }那我也就可以直接使用下面代码来传递参数:val function1 = object: Function1<Int,Unit> { override fun invoke(p1: Int) { Logger.d("$p1") } } testInvoke.setSingleListener(function1)这里看起来合情合理,因为在testRun函数中我们调用了invoke函数,把100当做参数,然后这个100会被回调到function1中,但是我们传递lambda时呢:val testInvoke = TestInvoke() testInvoke.setSingleListener { returnInt -> Logger.d("$returnInt") }上面代码传递lambda和传递一个类的实例效果是一样的,只不过这里只是一段代码块,没有显示的调用invoke啥的,所以这就是一个特性,当lambda被用作参数被函数调用时,也就可以看成是一次invoke的自动调用。invoke在DSL中的实践:Gradle依赖这里我们为什么要说这个invoke依赖呢,很大的原因就是它在一些DSL中有很好的用法,这里我们就来看个Gradle依赖的使用。我们很常见下面代码:dependencies { implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.3.1' //... }这里我们都很习以为常,感觉这里很像配置项,而不像是代码,其实这个也是一段代码,只不过是这种风格。那这种风格如何实现呢,我们来简单实现一下:class DependencyHandler{ //编译库 fun compile(libString: String){ Logger.d("add $libString") } //定义invoke方法 operator fun invoke(body: DependencyHandler.() -> Unit){ body() } }上面代码写完后,我们便可以有下面3种调用方式:val dependency = DependencyHandler() //调用invoke dependency.invoke { compile("androidx.core:core-ktx:1.6.0") } //直接调用 dependency.compile("androidx.core:core-ktx:1.6.0") //带接受者lambda方式 dependency{ compile("androidx.core:core-ktx:1.6.0") }由此可见,上面代码第三种方式便是我们在Gradle配置文件中常见的一种,这里其实就2个关键点,一个是定义invoke函数,一个是定义带接受者的lambda,调用时省去this即可。总结其实关于invoke约定和带接受者lambda的写法现在越来越流行了,比如之前的anko库,现在的compose库都是这种声明式的写法,看完原理后,就会发现其实还是很方便的。后续开始研究compose的时候,再来补充一波。
0
0
0
浏览量1753
德州安卓

Kotlin Flow? 真香!

前言由于项目是使用的是MVVM架构,使用DataStore等组件时会发现返回的是Flow喂!我LiveData都还没有用熟呢,咋就开始用Flow了,既然官方这样推广,当然是道理所在,那本章就说一说Flow。为了看到第一手准确资料,我是硬着头皮看完了Google官方英语介绍的视频,虽然很多都没有听懂,不过Google的大佬做的图还是很容易理解,本章很多图都是截取视频。背景在日常开发中,尤其是APP端经常有同时发起多个请求然后进行展示内容的需求,比如我先拿到标题(因子)先展示出来,再根据因子去加载数据,拿到数据后再进行分类、处理后再进行展示,这种复杂的业务,如果使用Java原来的回调方式,那就会一层套一层,还要不断地切换线程那当然不用,前面文章也说了使用Kotlin协程可以方便地切换线程以及使用阻塞式地方式写出非阻塞式的代码,那这个问题解决了。还有一个问题就是数据处理问题,这时你当然会说LiveData也有数据处理的API,比如map等等,说的很有道理,确实也是这个道理,那为什么Google官方又要整这么一出呢?等我后面细细说来。LiveData有什么不足?什么是LiveData,它就是一个基于观察者模式的可观察的数据存储器类,同时和普通的观察者又不太一样,它结合了Android生命周期,能感知Android界面的生命周期变化,这种能力可以确保LiveData仅更新处于活跃生命周期状态的应用组件观察者。所以,LiveData是一个简单易用的组件,同时也就是因为它的简单易用,和之前同是观察者模式的老大哥RxJava比功能就欠缺了不少,所以处理比较复杂的场景会比较麻烦。由于LiveData设计比较简单,所以有以下不足:LiveData只能在主线程更新数据。LiveData操作符不够多,处理复杂数据时不够用。虽然我也是刚用Jetpack,我咋记得可以子线程中更新数据呢,不少有个postValue方法吗?是的,但是postValue方法也需要切换到主线程,再进行更新数据,所以在处理复杂业务时,我从子线程获取的数据,我就想在子线程中先展示一下,然后接着再处理再请求,不想老是切换线程,这时就可以使用Flow了。注:由于本人没有真正了解、使用过RxJava,所以关于Flow和RxJava的比较就不细说了,主要也就是RxJava入门门槛高,在Android中需要自己处理生命周期,Flow也支持线程切换、有协程Scope兜底不会出现性能浪费等。Flow简介因为关于Flow的文章挺少,我就直接看了官方的视频介绍,先不谈什么API和原理,先按照官方文档来普及一波先。问题现在有3个耗时任务,分别是A、B、C,在协程中进行,那么使用不同的返回值来接收3个任务的结果会是什么区别呢?使用List如图所示,调用foo时会切换线程,很显然这里的缺点就是必须要等3个任务都执行完,然后把结果放入到一个List中,返回给调用者,如果某个任务执行很久,则需要等待。示例代码:runBlocking{ val list = foo() for (x in list) Log.i(TAG, "initData: $x") }suspend fun foo(): List<String> = buildList { Log.i(TAG, "foo: 开始发送数据") delay(1000) add("A") delay(1000) add("B") delay(1000) add("C") Log.i(TAG, "foo: 结束发送数据") }再看一下打印结果:`2021-08-25 14:34:45.307 15141-15141/: foo: 开始发送数据2021-08-25 14:34:48.309 15141-15141/: foo: 结束发送数据2021-08-25 14:34:48.311 15141-15141/: initData: A2021-08-25 14:34:48.312 15141-15141/: initData: B2021-08-25 14:34:48.312 15141-15141/: initData: C`这里耗时任务执行了3S多,然后数据全部返回,调用者再进行处理。使用Channel接着看一下Channel,什么是Channel的具体定义以及使用我之前也没有使用过,不过从代码注释可以知道它实际就是一个队列,而且是并发安全的,可以用来连接协程,实现不同协程的通信,还是看官方的截图:从这个图更可以看出Channel就是一个队列,不过这个队列相当于管道一样,一边生产数据,一边消费数据,最主要是它还可以在跨协程工作。那同样写个小例子://先定义一个变量,也就是这个Channel val channel = Channel<String>()//开启协程,不断地receive也就是消费管道里地数据 lifecycleScope.launch(Dispatchers.IO) { while (true){ Log.i(TAG, "initData: receive: ${channel.receive()}") } }//开启协程发送数据 lifecycleScope.launch { foo() }//模拟耗时操作 suspend fun foo(){ Log.i(TAG, "foo: 开始发送数据") delay(1000) channel.send("A") delay(1000) channel.send("B") delay(1000) channel.send("C") Log.i(TAG, "foo: 结束发送数据") }结果可想而知是什么样子的:`2021-08-25 14:58:42.236 16024-16024/: foo: 开始发送数据2021-08-25 14:58:43.240 16024-16062/: initData: receive: A2021-08-25 14:58:44.247 16024-16061/: initData: receive: B2021-08-25 14:58:45.252 16024-16024/: foo: 结束发送数据2021-08-25 14:58:45.254 16024-16062/: initData: receive: C`可以看出在第一个耗时任务结束发送时,消费者已经开始工作了,不用等待所有任务都结束。使用Flow关于Flow可以叫做流,这个和管道Channel设计的很像,也是生产者、消费者模型,一边生产数据,一边消耗数据。不过官方视频里说Flow是冷流,在有订阅对象时才开始产生数据也就是emit事件,所以在没有collect之前,Flow内部没有任何协程被激活,不会造成资源泄漏。直接看一下官方截图:示例代码://返回一个Flow实例 suspend fun foo(): Flow<String> = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) emit("A") delay(1000) emit("B") delay(1000) emit("C") Log.i(TAG, "foo: 结束发送数据") }//进行collect lifecycleScope.launch(Dispatchers.IO) { val flow = foo() flow.collect { Log.i(TAG, "initData: $it") } } //发送数据 lifecycleScope.launch { foo() }可以预见,这个结果肯定和使用Channel是一样的,打印:2021-08-25 15:15:08.507 16419-16458/: foo: 开始发送数据2021-08-25 15:15:09.515 16419-16457/: initData: A2021-08-25 15:15:10.522 16419-16458/: initData: B2021-08-25 15:15:11.529 16419-16457/: initData: C2021-08-25 15:15:11.529 16419-16457/: foo: 结束发送数据到这里会发现和使用Channel一模一样的,区别就是Flow这里先拿到了Flow引用,只有去collect的时候,上游才去emit数据。为了加深印象和区别,总结一下:Flow介绍1.Flow是序列形式的,这个是区分List的,下面这个图可以很容易的看出区别。2.Flow是冷流,什么是冷流?Flow默认是冷流,如果要使用热流,可以使用SharedFlow,关于2者的区别,可以看下面图:(1)冷流冷流可以保证不必要的内存浪费,因为只有去collect时才会触发发射端的协程代码运行,如果有2个Collector,那另一个Collector也只有在collect时才会触发发射端协程运行,且会都跑一遍,如图:看一下代码://foo是冷流Flow var foo: Flow<String>? = null//在init函数里 开启线程,去emit数据 lifecycleScope.launch { foo() } //开启2个协程 都拿到foo,然后进行collect lifecycleScope.launch(Dispatchers.IO) { foo?.collect { Log.i(TAG, "initData: A开始收集 $it") } } //这里的协程,先延迟了2s 再去进行collect lifecycleScope.launch { delay(2000) foo?.collect { Log.i(TAG, "initData: B开始收集 $it") } }foo()函数如下://这里会每隔1s发送一个数据 suspend fun foo(){ foo = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) Log.i(TAG, "foo: 开始发送A") emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") } }然后看一下打印:2021-08-30 11:49:13.862 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 11:49:14.868 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 11:49:14.870 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 A 2021-08-30 11:49:14.870 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 11:49:15.868 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 11:49:15.874 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 11:49:15.875 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 B 2021-08-30 11:49:15.875 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 11:49:16.870 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 11:49:16.871 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 A 2021-08-30 11:49:16.871 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送数据 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 B 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 11:49:18.876 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送数据从上面打印我们不难看出:A收集者开始调用collect时,发射协程开始工作,在13.862秒开始等待2s后,B收集者开始调用collect时,发射协程又开始工作一遍,在15.868,2者相隔2s多虽然A收集者已经开始收集了,B收集者开始时,依然又跑一遍(2)热流热流是一对多的关系,当有多个collector时,这时发射端发射一个数据,每个collector都能接收到,这个很像那个LiveData观察者模型,数据能得到共享,所以也是被称为SharedFlow。同时和冷流只有在collect时才去跑发射端的协程代码不同,热流会在对象创建出时便开始执行。同样也是看一下示例代码:val _events = MutableSharedFlow<String>()//先开启协程,创建出SharedFlow lifecycleScope.launch { foo1() } //立马进行收集 lifecycleScope.launch(Dispatchers.IO) { _events.collect { Log.i(TAG, "initData: A开始收集 $it") } } //延迟2秒再进行收集 lifecycleScope.launch { delay(2000) _events.collect { Log.i(TAG, "initData: B开始收集 $it") } }//一开始就发射A,后面每延迟1s发射一次 suspend fun foo1(){ Log.i(TAG, "foo: 开始发送数据") Log.i(TAG, "foo: 开始发送A") _events.emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") _events.emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") _events.emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") }打印数据:2021-08-30 14:04:53.404 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 14:04:53.404 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 14:04:53.405 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 14:04:54.406 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 14:04:54.407 8383-8424/com.wayeal.yunapp I/zyh: initData: A开始收集 B 2021-08-30 14:04:54.407 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 14:04:55.415 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 14:04:55.421 8383-8426/com.wayeal.yunapp I/zyh: initData: A开始收集 C 2021-08-30 14:04:55.423 8383-8383/com.wayeal.yunapp I/zyh: initData: B开始收集 C 2021-08-30 14:04:55.425 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 14:04:55.426 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送数据从上面打印不难看出:热流在创建时,便开始发射数据由于A收集器在发射器发射完一个数据才开始collect,所以A收集器也收集不到A2s后B收集器开始收集,这时它只能被迫接受C,因为A和B数据都错过了,也不会重新再跑一遍发射代码Flow的详细解析从前面的代码,我们肯定了解了什么是Flow,以及冷流和热流的概念,那现在我们根据来官方的源码,来介绍一下Flow是如何使用和常见API。上面图中可以大概说出Flow的特性和使用了,另外补充几点:1、改善的Flow操作符前面说了Flow就是一个数据流,在中间可以对它进行各种操作,所以Flow的API为了更好的使用链式调用,把一些API进行了改善,比如在每个数据发送时进行延迟、开始进行延迟等等,这里就使用官方的一张图来表示:2、背压在生产者、消费者模型中都避免不了背压,在RxJava中使用了很多策略来应对,不过Flow在设计之处就解决了这个问题,其一是Flow发射和收集数据都是异步的,其二就是在发射或者收集添加延迟,来达到缓解背压的情况:说道背压,这个也是和Channel有很大的区别,Channel的策略是缓冲区,但是Flow自带承压机制,因为是Cold事件源,如果没有消费者,事件源不会主动emit事件。collect方法以及flow的构造方法均为suspend,所以均可以延迟,这样如果某端没有准备好,也可以通过延迟来承压,在上图也可以看出,简而言之就是没做事情处理背压。3、buffer操作符前面的总结图里说了,Flow是顺序队列,也就是开始collect,发射A,处理A,然后发射B,处理B,这里如果收集端有耗时操作,整个耗时就非常长了,如图代码例子://这里消费者,先collect,但是我消费也需要时间,所以延迟1s lifecycleScope.launch(Dispatchers.IO) { val flow = foo() flow.collect { Log.i(TAG, "initData: 消费 $it") delay(1000) } } lifecycleScope.launch { foo() }//为了更好的看出执行顺序,每个发送都加了打印 suspend fun foo(): Flow<String> = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) Log.i(TAG, "foo: 开始发送A") emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") }可以看出最后的执行打印:2021-08-25 15:37:44.640 16997-17035/: foo: 开始发送A 2021-08-25 15:37:44.640 16997-17035/: initData: 消费 A 2021-08-25 15:37:45.641 16997-17036/: foo: 结束发送A 2021-08-25 15:37:46.643 16997-17035/: foo: 开始发送B 2021-08-25 15:37:46.643 16997-17035/: initData: 消费 B 2021-08-25 15:37:47.643 16997-17036/: foo: 结束发送B 2021-08-25 15:37:48.645 16997-17035/: foo: 开始发送C 2021-08-25 15:37:48.645 16997-17035/: initData: 消费 C 2021-08-25 15:37:49.648 16997-17036/: foo: 结束发送C 2021-08-25 15:37:49.649 16997-17036/: foo: 结束发送数据可以看出这里的操作一共花了6s,如何进行改善呢,就是使用buffer:SharedFlow的详细解析说完了Flow是冷流,冷流固然很好,但是用的多的还是SharedFlow,也就是热流,也可以叫成共享流。比如在Android的MVVM架构中,需要使用数据驱动来完成,这时就可以替换LiveData为SharedFlow(当然是后面要说的StateFlow),对于一个flow会有多个订阅者,这时就可以进行观察,从而达到多个订阅者都可以根据数据变化而做出变化。还是简单看一下总结图:下面我们来具体分析一波。(1)、构造函数public fun <T> MutableSharedFlow( replay: Int = 0, extraBufferCapacity: Int = 0, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND ): MutableSharedFlow<T>replay:表示当有新的订阅者collect时,发送几个已经发送过的数据给它,默认是0,即默认新订阅者不会获取到订阅之前的数据。 -extraBufferCapacity:表示减去replay,这个Flow还可以缓存多少个数据,默认是0; -onBufferOverflow:表示缓存策略,即缓冲区满了后,如何处理,默认是挂起。(2)、ShareIn函数前面说了,如果使用SharedFlow最好使用MutableSharedFlow然后使用emit来发射数据,但是经常有需求是返回一个Flow,这时就需要使用ShareIn函数把Flow转成SharedFlow,ShareIn函数是Flow的扩展函数,看一下参数:public fun <T> Flow<T>.shareIn( scope: CoroutineScope, started: SharingStarted, replay: Int = 0 ): SharedFlow<T>scope,这个很好理解,协程范围,表示共享开始时所在的协程作用域范围,之前说过SharedFlow很像LiveData,这里的收集者就是观察者,所以为了控制范围,需要传递一个观察者执行操作的协程范围。replay,这个就是当有一个新的观察者订阅时,需要重新传递给这个新的观察者的数量。zstarted,这个控制共享的开始和结束的策略。这里有3种策略,分别说一下:(3)、ShareIn函数上游flow操作符在前面我们说了一些操作符,这里就可以给应用上,比如我在上游结束、异常等情况onCompletionval flow = loginRepository.getLoginName() flow.onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }retry可以对一些指定的异常进行处理,比如IO异常时进行重连等。val flow = loginRepository.getLoginName() flow.onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.retry(5000) { val shallRetry = it is IOException if (shallRetry) delay(1000) shallRetry }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }onStart 可以在上游数据开始之前,做一些操作。val flow = loginRepository.getLoginName() flow.onStart { emit("start") } .onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.retry(5000) { val shallRetry = it is IOException if (shallRetry) delay(1000) shallRetry }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }StateFlow的详细解析终于到了喜闻乐见的StateFlow了,官方就是希望用这个来替代LiveData,那么它到底是如何来替代LiveData的呢?(1)、特性StateFlow是SharedFlow的子类,根据前面说的那么它是个热流,可以被多个观察者观察,同时可以设置它消失的scope以及条件。StateFlow只更新最新的数据,也就是它是一个replay为0的SharedFlow。StateFlow里面和LiveData很像,都有个value来保存其值,也可以通过这个属性来获取或者设置它的值。(2)、使用使用MutableStateFlow就和LiveData一样,不过它需要一个默认值。也可以使用stateIn函数把一个Flow转成StateFlow,直接看这个函数:val result = userId.mapLatest { newUserId -> repository.observeItem(newUserId) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.Loading )从这个最基本的使用,我们可以看出以下信息:它的范围是viewModelScope,当viewModelScope结束时,流会停止。当观察者都被移除后,超过5s,为了不浪费性能,流会停止。会有个默认值,也就是这个initivalValue。(3)、观察从上面来看,确实很像LiveData,不过LiveData有个很重要的功能就是具有生命周期感知性能力,在UI处于活跃状态时才会更新数据,那这个StateFlow在收集时并没有传递lifecycleOwner,那如何达到一样的效果呢?首先是观察的操作执行在协程中,这个协程的范围是lifecycleScope不错,但是是直接launch还是launchWhenStarted呢,看下面这个图:从这个图我们发现如果我们直接使用launch的话,可能会在onStop执行时这时来个数据更新,我的View根本没法更新,所以会造成错误,这里要达到和LiveData一样的效果在界面活跃时进行更新,所以这里启动协程就需要使用launchWhenStarted。这个看起来没啥问题,不过我们前面不是有个WhileSubscribed这个优化项呢,表示没有观察者时5s后停止上游数据发射,那现在这个则无法做到,这时就需要使用repeatOnLifecycle,直接看图:这个repeatOnLifecycle的作用就是当满足特定状态时启动协程,并且在生命周期退出这个状态时停止协程。那这就可以比如我APP退到后台,这时如果超过5S,则不会再订阅,协程终止,这时上游发射逻辑也会停止,提高性能。当APP再切换到前台时,会执行onStart,这时又会有观察者,这时上游逻辑又会开始,所以最佳的写法如下:onCreateView(...) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) { myViewModel.myUiState.collect { ... } } } }总结看完了关于Flow的介绍,我们会发现它不仅有类似RxJava的强大操作符,也可以和JetPack一起使用,在处理复杂数据上会比LiveData方便很多。
0
0
0
浏览量2017
德州安卓

你需要懂的Kotlin开发技巧之六

1.尾递归优化平常我们可能面临编写递归函数的场景,比如:fun test5(n: Int): Int = if (n == 1) { 1 } else test5(n - 1)每个方法都对应一个栈帧,方法的递归调用会导致方法栈深度过深,存在StackOvewFlow的风险。Kotlin中提供了尾递归特性进行优化:tailrec fun test5(n: Int): Int = if (n == 1) { 1 } else test5(n - 1)可以看到尾递归优化就是给方法增加tailrec,并且递归方法的调用要处于方法的尾部,反编译成java代码看下效果:可以看到编译器对方法的递归调用进行了优化。2.中缀方法infix的实战中缀方法的修饰是给方法增加infix声明,并且方法参数只能声明一个,比如:infix fun String.a(b: String) = "$this - $b"下面我们通过一个案例讲解下:我们平常肯定有一个这样的需求:传入文件路径String返回File类型fun makeFile(parent: String, child: String): File { return File(parent, child) }这样写起来没什么问题,但是结合中缀函数我们可以实现更优雅的封装,如下:infix fun String.div(child: String): File = File(this, child)使用如下:fun makeFile(parent: String, child: String): File = parent div child其中parent div child就等价于File(this, child)的实现。这样写起来还不是特别优雅,比如每次都得div来创建File对象,div写起来太麻烦了,而且可读性太差。为了解决上面的两个问题,我们可以结合运算符重载operator进行更进一步优化,看下常见的运算符重载函数及对应运算符的映射关系:其中有个div函数重载/运算符的,借助于这个我们对上面String转File的函数改造下:infix operator fun String.div(child: String): File = File(this, child)然后就可以这样使用:fun makeFile(parent: String, child: String): File = parent / child如上所见,直接使用parent / child就可以完成File的创建,而且/的可读性更高。
0
0
0
浏览量1468
德州安卓

这些flow常见API的使用,你一定需要掌握!(四)

combine()传递两个流合并后的结果public fun <T1, T2, R> Flow<T1>.combine(flow: Flow<T2>, transform: suspend (a: T1, b: T2) -> R): Flow<R> = flow { combineInternal(arrayOf(this@combine, flow), nullArrayFactory(), { emit(transform(it[0] as T1, it[1] as T2)) }) }这个方法有些类似于rxjava中的zip()方法,将两个流的结果通过某种方式合并并发送给下游流,看下使用:fun flat() { GlobalScope.launch { flow { emit(10) }.combine(flow { emit(20) }) { i, j -> "$i-$j" }.collect { println("flat collect: $it") } } }执行结果:debounce()限流public fun <T> Flow<T>.debounce(timeoutMillis: Long): Flow<T> { require(timeoutMillis >= 0L) { "Debounce timeout should not be negative" } if (timeoutMillis == 0L) return this return debounceInternal { timeoutMillis } }这个方法可是十分的有用,就是用来限流的,这里举个例子阐述:调用debounce(200)后,当上游发送一次数据后,只有在200ms之内不再发生上游流到下游流的数据传递,之前发送的数据才能传递到下游流进行消费,否则之前发送的数据都会丢弃并重新开始即使200内是否发送数据传递,直到没发生后下游流才能收到消费。fun flat() { GlobalScope.launch { flow { for (i in 0..7) { emit(i) } }.debounce(200) .collect { println("flat collect: $it") } } }如上,在for循环中循环发送数据,数据发送的间隔肯定每次都是小于200ms的,所以最终只有发送最后一条数据7才能最终能达到下游流进行消费。输出结果也是如此:下面我们改造下上面的代码,每次发送数据后增加个休眠210ms的操作,关键代码:flow { for (i in 0..7) { emit(i) delay(210) } }输出:可以看到,从0到7数据都能顺利被下游流接受,就是因为每次休眠210ms是大于ddebounce(200)方法指定的200ms。sample()按时间间隔采样这个方法和上面的debounce()方法的区别是:比如sample(200)是每隔200ms取最后一次上游流发送的数据,举个例子:做一个最近聊天消息列表,每个消息item会展示最后的一条聊天消息摘要,聊天大群中聊天消息频繁发送接受就会触发频繁的聊天消息摘要的刷新,这个时候就非常适合sample()发挥,比如每隔200ms取该聊天大群的最后一条聊天消息作为摘要,这样就降低了列表的刷新频率,又能及时显示最新的聊天消息摘要,所谓一举两得。举个例子:fun flat() { GlobalScope.launch { flow { for (i in 0..7) { emit(i) delay(110) } }.sample(200) .collect { println("flat collect: $it") } } }执行结果如下:每隔200ms取一次上游流数据,第一次上游发送0休眠110ms没到200ms,那就发送1再休眠你110ms,此时肯定超过了200ms,所以取最后的发送数据1发送给下游流消费,之后的依次如此。
0
0
0
浏览量1705
德州安卓

Kotlin项目开发实践二

1.单例实现的三种方式object 先看代码:object Pro { }就这么简单实现了Pro的单例,我们可以反编译成java代码看下具体的实现原理:public final class Pro { @NotNull public static final Pro INSTANCE; private Pro() { } static { Pro var0 = new Pro(); INSTANCE = var0; } }首先Pro类的构造方法声明为private,其次可以看到这就是通过静态代码块实现的单例,利用类静态代码块只会执行一次的特性,属于线程安全且饿汉式的;java中通过Pro.INSTANCE获取这个单例,而kotlin直接通过Pro获取单例lazy 先上代码: class Pro private constructor() { companion object { val INSTANCE by lazy { Pro() } } }主要是利用了伴生对象声明的属性为静态变量,且lazy默认的实现模式是加锁线程安全的,这是个线程安全且懒汉式单例实现,关于lazy想要了解更多可以参考Kotlin开发实践之一双重检查锁 上代码:@Volatile var singleton: Pro? = null fun getInstance(): Pro { if (singleton == null) { synchronized(Pro::class.java) { if (singleton == null) { singleton = Pro() } } } return singleton!! }上面就是java双重检查锁的kotlin实现形式,其中:@Volatile保证代码指令有序性getInstance方法内外层singleton判空保证singleton已经初始化完成了,线程不要额外再去竞争锁getInstance方法内内层singleton判空保证如果之前线程已经初始化singleton完成了,后续的线程不要再重复初始化了可以看到,这是个线程安全且懒汉式方式实现的单例2.typealias给复杂类型取个别名这个typealias关键字主要是用于给类型取个别名,下面介绍下两种使用的场景:函数类型取别名 日常开发中,函数类型应该是使用很普遍的,比如//拼接Int和String类型并返回String类型 val block: ((Int, String) -> String)? = null这个函数类型(Int, String) -> String)写起来很麻烦且可读性很差,这个时候就到了typealias上传的时候了:typealias Concat = (Int, String) -> String val block: Concat? = null将(Int, String) -> String)取别名为Concat,不仅使用起来很方便,还容易看出这个函数类型的使用场景:拼接简化泛型传递 使用ViewModel时,我们可能经常会对接口的返回进行如下封装:class MainViewModel: ViewModel() { val data: MutableLiveData<Response<String>> = MutableLiveData() fun test() { data.value = Response("haha") } data class Response<T>(val data: T? = null) }使用Response对服务器返回进行封装,泛型T表示响应数据可以反序列化成的实体类。可以看到上面,每定义一个MutableLiveData都得在其泛型中声明Response<T>,由于我们这个Response是对所有接口响应的统一封装,是一个确定的类型,而Response<T>中的T才是每次创建MutableLiveData需动态化指定的类型。那Response<T>中的Response可不可以省略呢,这个时候就到了typealias上传的时候了:typealias ExternalLiveData<T> = MutableLiveData<MainViewModel.Response<T>>这样每次再创建MutableLiveData就可以这样写:val data1: ExternalLiveData<String> = MutableLiveData()
0
0
0
浏览量2017
德州安卓

写一篇好懂的常见Kotlin集合函数讲解(前)

map{}:实现集合中元素的类型转换举个例子,现在有一个Model元素的集合:List<Model>data class Model(val name: String = "", val id: Int = 0)我们现在想从这个集合中获取到每个Model元素的id属性并组成一个新的集合,通常的做法如下:fun map() { val list = mutableListOf(Model(id = 4), Model(id = 2), Model(id = 9)) val result = mutableListOf<Int>() list.forEach { result.add(it.id) } }麻烦,现在有了map就可以这么写:val result2 = list.map { it.id }当然也可以使用方法引用:val result2 = list.map(Model::id)mapTo(){}:比map多了可以指定原始集合元素类型转换后写入的目标集合之前的map调用了会返回一个新的集合类型,有时候我们想把这个新的集合添加到已有的集合中,比如:fun mapTo(source: MutableList<Int>) { val list = mutableListOf(Model(id = 4), Model(id = 2), Model(id = 9)) //将转换后的集合写入到source中 source.addAll(list.map { it.id }) }这样写起来也挺简单,但是我们还可以有更简单的写法://将转换后的集合写入到source中 list.mapTo(source) { it.id }mapIndexed{}:支持带索引的元素类型转换如果元素类型转换的时候还想知道该元素在集合中的索引,mapIndexed轻松搞定://index代表元素在集合中的索引 list.mapIndexed { index, model -> }filter{}:过滤符合指定条件的元素开发中应该会经常遇到:从某个集合中筛选处符合特定条件的元素并重新组成一个新的集合,可能大家会立马写出下面代码:fun filter() { val list = listOf(3, 4, 6, 5, 2) //从集合中筛选出不小于4的元素 val result = mutableListOf<Int>() list.forEach { if (it >= 4) { result.add(it) } } }这样写起来还是比较繁琐,直接使用filter函数实现:val result2 = list.filter { it >= 4 }该函数还有和map一样类似的函数:filterIndexed{}:带下表索引的过滤函数filterTo(){}: 类似于mapTo,可以将过滤后的元素写入到某个指定的集合中indexOfFirst{}:正向查找集合中满足指定条件的元素的索引下标,不存在返回-1val result4 = list.indexOfFirst { it.id == 5 }该函数还有下面的类似函数:indexOfLast{}: 反向查找集合中满足指定条件的元素的索引下标,不存在返回-1indexOf():正向查找指定的元素在集合中的位置,不存在返回-1lastIndexOf(): 反向查找指定的元素在集合中的位置,不存在返回-1take(n):获取集合中前n个元素当n大于等于集合的长度时,即代表获取整个集合元素;当n小于0,则会抛出异常;还有其他类似的集合函数:first()/first{}:获取集合第一个元素/符合指定条件的第一个元素last()/last{}:获取集合最后一个元素/符合指定条件的最后一个元素takeLast(n): 获取集合中最后的n个元素
0
0
0
浏览量1884
德州安卓

Kotlin反射全解析1 -- 基础概念

前言其实关于反射这块内容在我们平时至少作为Android开发还是用的蛮少的,不过在阅读源码时或者开发一些SDK时就遇到了很多,尤其是看到什么 KProperty 类型类似的参数的时候,总是不熟悉,所以我准备好好梳理这一部分的知识点。其中反射有Java的反射和Kotlin的反射,它俩其实差不多,不过API不同,我们后续都会逐一介绍。正文既然准备全面梳理,所以还是从反射概念说起,慢慢道来。啥是反射网上有很多文章都是直接上来就告诉你如何使用,但是这样你不能知其所以然,先说反射的概念:一种在运行时动态地访问对象属性和方法的方式,而不需要事先确定这些属性是什么。什么是运行时熟悉Java运行的开发者都知道,我们平时写的.java代码,会被编译成.class文件,然后被JVM加载,这时我们的代码就处于运行时了,假如我们定义了一个类People的对象people,到运行时就能获取People这个类的一些信息了。而保存这些信息的类就是我们非常熟悉的Class类了,后面我们细说。为什么会有这种需求既然我定义了一个类People,那我在编译期不就知道它里面的属性了吗 那为什么还要反射呢这里举个很常见的例子,那就是序列化库,假如你开发了一个序列化库XXJSON,这时你开发时你肯定不知道别人想把json转成啥类型,所以只有在运行时拿到别人想转换的类型A,然后获取类型A中有哪些属性,才可以进行转换,这时就可以使用反射来实现。这个序列化库的思想在很多Java框架中都有,就是灵活配置的思想,你只需要传入特定的类型或者在XML中定义特定的类型,在运行时再由这个库对它进行处理。反射可以获取哪些信息既然了解了啥是反射,那通过反射可以获取一个类的哪些信息呢,这些信息包括类型信息、父类信息、方法、属性,包括方法的修饰符、参数等等,反正就是很强大的功能,我们到后面细说。Kotlin和Java反射作为熟悉Kotlin的同学都知道Kotlin也是编译成字节码在JVM中运行,那Kotlin有又优化或者开发了反射内容吗答案是有的,当你使用Kotlin语言时,你完全可以使用Java的反射以及API,但是Kotlin语言又加了库,专门来新增了一些API,这些API实现的功能完全包含了Java反射API实现的功能,但是也有一些独特的Kotlin功能,比如可空类型、是否是数据类这种。不仅如此,Kotlin的反射API更符合人们使用,当然这也是我个人觉得,等后面我们解析其方法时再细说。假如你想使用Kotlin的反射库,Kotlin默认是不带的,原因是减小内存使用,可以通过添加下面依赖来完成://kotlin版本和使用的版本一致 implementation "org.jetbrains.kotlin:kotlin-reflect:1.5.21"具体使用,我们后面文章继续。总结这里只是一个引子,来说明了什么反射和反射的意义,以及让Android开发者明白如果使用Kotlin的话,可以使用Kotlin库中的反射API。后面我们会逐步介绍和对比Java和Kotlin的反射实现以及反射的具体使用例子。
0
0
0
浏览量1692
德州安卓

你需要懂的Kotlin开发技巧之九

给属性的get、set方法加锁一般我们给类的某个属性的set或get方法加锁的方式如下:class DelegateImpl2 { private var name: String = "" @Synchronized fun setName(name: String) { } @Synchronized fun getName(): String { } }这种方式效率太低,直接利用注解@set/get: Synchronized即可:@set: Synchronized @get: Synchronized var name: String = ""val的属性需要加锁时只需要使用@get: Synchronized重命名kotlin文件名便于java调用我们定义一个顶层函数:看下在java中如何调用:也就是说,我们每次创建一个kotlin文件xxx.kt,kotlin编辑器默认都会帮助我们生成一个xxxKt的类,而定义的顶层函数就是这个类的静态方法,所以在java中才会这样调用。kotlin中可以直接test()调用本质上也是编译器帮助我们实现了DelegateImpl2Kt.test()调用,大家可以自行反编译看下。我们可以通过注解@file: JvmName("DelegateImplTest")重命名kotlin文件xxx.kt所默认生成的xxxKt类名:然后在java中这样使用:整体去除kotlin文件中黄色警告上面Test()方法会由于方法名大写、未使用警告,test属性也会报未使用警告,我们可以在类或者方法上使用@Suppress注解:但是如果一个kotlin文件中的类非常多,每个类都会有方法名大写、未使用或其他警告,难道我们要一个个类上增加@Suppress注解吗?太麻烦了!!可以直接在kotlin文件开头增加@file: Suppress("")即可:限制kotlin方法禁止java访问有时我们定义的Kotlin扩展方法专门是为kotlin提供的,不想让java调用以至于太复杂,比如下面方法:fun ext(block: Demo1.(String) -> Unit) { val demo = Demo1() demo.block("") }kotlin直接可以这样调用:ext {},在java中使用起来就很复杂了:DelegateImpl2Kt.ext(new Function2<Demo1, String, Unit>() { @Override public Unit invoke(Demo1 demo1, String s) { return null; } });想要限制java调用kotlin方法直接使用@JvmSynthetic注解修饰调用的kotlin方法ext即可,这样在java中调用ext顶层方法就会报错:
0
0
0
浏览量1278
德州安卓

这些flow常见API的使用,你一定需要掌握!(五)

stateIn()转变为热流StateFlowpublic fun <T> Flow<T>.stateIn( scope: CoroutineScope, started: SharingStarted, initialValue: T ): StateFlow<T> { val config = configureSharing(1) val state = MutableStateFlow(initialValue) val job = scope.launchSharing(config.context, config.upstream, state, started, initialValue) return ReadonlyStateFlow(state, job) }冷流flow: 只有调用collect{}方法才会触发冷流执行热流:热流的执行不依赖是否添加观察者stateIn()将冷流转换为热流StateFlow(),这个流有几个特点:需要给予一个初始值是一个粘性流,类似于LiveData,会重放最后一个更新数据过滤重复数据,也就是说,发送重复数据会进行丢弃提供value属性获取内部的值一般可以用作替代LiveData,直接使用热流作为ViewModel中可观察的数据源,LiveData能实现的它都能实现,不能实现的它也都能实现。stateIn()转变为热流SharedFlowpublic fun <T> Flow<T>.shareIn( scope: CoroutineScope, started: SharingStarted, replay: Int = 0 ): SharedFlow<T> { val config = configureSharing(replay) val shared = MutableSharedFlow<T>( replay = replay, extraBufferCapacity = config.extraBufferCapacity, onBufferOverflow = config.onBufferOverflow ) @Suppress("UNCHECKED_CAST") val job = scope.launchSharing(config.context, config.upstream, shared, started, NO_VALUE as T) return ReadonlySharedFlow(shared, job) }这个就是将冷流flow转换为SharedFlow,上面的热流StateFlow实现了SharedFlow,它主要有以下几个特点:无法通过.value的方式访问内部值通过replay参数自定义你需要的粘性或非粘性的热流两种冷流都需要传递一个SharingStarted类型的参数,这个参数有三种类型:Eagerly、Lazily、WhileSubscribed决定热流的启动模式,这里主要介绍WhileSubscribed: public fun WhileSubscribed( stopTimeoutMillis: Long = 0, replayExpirationMillis: Long = Long.MAX_VALUE ): SharingStarted = StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis)stopTimeoutMillis:这个参数指定一个在最后一个订阅者取消和停止流执行的时间间隔,意思就是当最后一个个订阅者取消后,隔stopTimeoutMillisms之后再停止流的执行。replayExpirationMillis:这个参数指定一个再停止流执行和清除流缓存的时间间隔,也就是当停止流执行后,间隔replayExpirationMillisms去清楚流的缓存。举个应用场景,当应用横竖屏切换时,订阅者就会被取消,但是没必要去停止流执行或者清理缓存,因为横竖屏过后很快就会重建重新显示,这样能更快的刷新界面数据。retryWhen{}public fun <T> Flow<T>.retryWhen(predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean): Flow<T> = flow { var attempt = 0L var shallRetry: Boolean do { shallRetry = false val cause = catchImpl(this) if (cause != null) { if (predicate(cause, attempt)) { shallRetry = true attempt++ } else { throw cause } } } while (shallRetry) }这个方法也很有用,出现异常时进行重试,并决定是否重试还是弹出提示信息,比如当我们进行网络请求时,请求失败就可以使用这个方法,一方面在retryWhen{}方法中记录错误信息并通知下游流,一方面选择是否进行网络重试。比如下面这个例子:fun test2() { GlobalScope.launch { flow { emit("${10 / 随机数}") } .retryWhen { cause, attempt -> if (cause is ArithmeticException && attempt < 3) { emit("retry") true } else { false } }.collect { println("jja: $it") } } }当上面的随机数出现0是就会触发ArithmeticException异常,这样retryWhen{}就能捕获并可以尝试重试,随机一个非0且能被整除的数,并且限制了重试次数为3次以为。总结关于flow常见api系列文章陆陆续续写了五篇了,暂时就告一段落,基本上常用的都介绍了一遍,希望能够给大家带来帮助。
0
0
0
浏览量746
德州安卓

Kotlin反射全解析2 -- 超级好用的KClass

前言我们继续来聊反射,或许对于Java开发者来说,Class很熟悉,不过不熟悉也没事,我们来看Kotlin中的反射类:KClass。至于什么是反射可以查看前面一篇文章:# Kotlin反射全解析1 -- 基础概念正文KClass其实就是Class的Kotlin版本,不过我个人觉得它的API设计的更好。KClass的概述和获取这个KClass就是Kotlin反射的主要类,我们还是先看一下源码注释://KClass的注释 Represents a class and provides introspection capabilities. Instances of this class are obtainable by the ::class syntax. See the Kotlin language documentation for more information. Params: T - the type of the class.表示一个类具有内省功能,然后该类的实例可以通过 ::class 语法获取。我觉得这里的内省说的非常好,也就是通过KClass能获取这个类的信息。我们直接来看个例子://直接定义一个类 class MainActivity : AppCompatActivity() { //私有成员变量 private val privateInt = 10 //成员变量 val norInt = 10 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) testFunction() } fun testFunction() { //获取类的KClass val kClass = MainActivity::class //获取成员信息进行打印 kClass.memberProperties.forEach { println("memberProperties $it") } } }然后会发现这个打印:2022-01-22 15:17:47.127 7832-7832/com.wayeal.testgit I/System.out: memberProperties val com.wayeal.testgit.MainActivity.norInt: kotlin.Int 2022-01-22 15:17:47.128 7832-7832/com.wayeal.testgit I/System.out: memberProperties val com.wayeal.testgit.MainActivity.privateInt: kotlin.Int会发现能打印出MainActivity类的2个属性。既然这里说到了memeberProperties方法,我们来看一下这是个啥:val <T : Any> KClass<T>.memberProperties: Collection<KProperty1<T, *>> get() = (this as KClassImpl<T>).data().allNonStaticMembers.filter { it.isNotExtension && it is KProperty1<*, *> } as Collection<KProperty1<T, *>>从这里会发现它是KClass的扩展函数,同时进行了过滤,当成员不是扩展的且是KProperty1类型的才可以,看到这里你或许又对了这个KProperty有点疑惑,不慌,后面我们都进仔细说说。KClass结构既然了解了反射就是获取类的信息,而Kotlin又把这些信息保存到KClass,所以必须要熟悉KClass结构,直接看一下其源码://一共实现了3个接口,接口后面分析 public actual interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier { //简单名字 public actual val simpleName: String? //类的全名 public actual val qualifiedName: String? //类以及父类定义的所有属性和方法 //其中类型是KCallable 后面分析 override val members: Collection<KCallable<*>> //所有构造函数 //类型KFunction 后面分析 public val constructors: Collection<KFunction<T>> //内部定义的所有类,包括内部类和静态嵌套类 public val nestedClasses: Collection<KClass<*>> //该类的类型参数,即泛型类的类型参数 public val typeParameters: List<KTypeParameter> //该类直接的父类类型列表 public val supertypes: List<KType> //假如这个类是密封类,获取其所有子类 public val sealedSubclasses: List<KClass<out T>> //该类的可见修饰符,也就是PUBLIC PROTECT等4种情况 public val visibility: KVisibility? //是否是final,Kotlin的类默认就是final,无法继承 public val isFinal: Boolean //是否是Open,和isFinal反过来 public val isOpen: Boolean //是否是抽象的类 public val isAbstract: Boolean //是否是密封类 @SinceKotlin("1.1") public val isSealed: Boolean //是否是数据类 public val isData: Boolean //是否是内部类 public val isInner: Boolean //是否是伴生对象 public val isCompanion: Boolean //类是否是一个Kotlin函数接口 public val isFun: Boolean //是否是value class,这个是1.5才推出的新内容 public val isValue: Boolean }这里的KClass接口加了很多Kotlin才有的方法,比如是否是某种class这种接口。其实这些方法大可不必去死记硬背,你可以想想你平时定义一个类的话,它包含哪些内容,我个人分析可以分为下面几个方面:类名,这个对应有对应的API去获取简易类名和全类名。可见修饰符,不外乎就是public、protected、internal和private。是否是open修饰即是否可以被继承,Kotlin默认是final的。如果是泛型类的话,它的类型参数。它继承或者实现的父类。它的内部类或者嵌套类。除了上面一些,还有以下重点:这个类的属性和方法,Kotlin这里把属性和方法都看成是KCallable的子类,所以用members统一返回,关于这个类,我们后面细说。构造方法,这里单独把构造方法给提出来,因为它是比较特殊的方法。KClass扩展函数其实KClass的API设计的还是非常好理解的,但是可能还是不太方便,举个很简单的例子,上面的KClass中members会返回父类和当前类的所有方法和属性,这就不太好,假如我只想获取当前类的方法或者属性呢 熟悉Java的同学可能知道,在Java中有不一样的API来区分这2种情况,在Kotlin中,直接就是又设计了一些扩展函数来区分,我们也来梳理一下,加深个印象,以防止平时开发自己又去实现一遍。//KClass的扩展函数 //返回类的主构造函数,没有主构造函数返回null val <T : Any> KClass<T>.primaryConstructor: KFunction<T>? get() = (this as KClassImpl<T>).constructors.firstOrNull { ((it as KFunctionImpl).descriptor as ConstructorDescriptor).isPrimary } //返回伴生对象实例,没有的话返回null val KClass<*>.companionObject: KClass<*>? get() = nestedClasses.firstOrNull { (it as KClassImpl<*>).descriptor.isCompanionObject } //返回伴生对象实例,否则为null val KClass<*>.companionObjectInstance: Any? get() = companionObject?.objectInstance //返回该类定义的属性和方法,父类中的不计入 val KClass<*>.declaredMembers: Collection<KCallable<*>> get() = (this as KClassImpl).data().declaredMembers //返回该类以及父类的所有函数,包括静态函数 val KClass<*>.functions: Collection<KFunction<*>> get() = members.filterIsInstance<KFunction<*>>() //返回该类中的静态函数 val KClass<*>.staticFunctions: Collection<KFunction<*>> get() = (this as KClassImpl).data().allStaticMembers.filterIsInstance<KFunction<*>>() //返回该类和父类的所有成员函数,即非扩展、非静态的函数 val KClass<*>.memberFunctions: Collection<KFunction<*>> get() = (this as KClassImpl).data().allNonStaticMembers.filter { it.isNotExtension && it is KFunction<*> } as Collection<KFunction<*>> //返回该类和父类所有的扩展函数 val KClass<*>.memberExtensionFunctions: Collection<KFunction<*>> get() = (this as KClassImpl).data().allNonStaticMembers.filter { it.isExtension && it is KFunction<*> } as Collection<KFunction<*>> //返回该类的所有函数 val KClass<*>.declaredFunctions: Collection<KFunction<*>> get() = (this as KClassImpl).data().declaredMembers.filterIsInstance<KFunction<*>>() //返回该类中的非静态、非扩展函数 val KClass<*>.declaredMemberFunctions: Collection<KFunction<*>> get() = (this as KClassImpl).data().declaredNonStaticMembers.filter { it.isNotExtension && it is KFunction<*> } as Collection<KFunction<*>> //返回该类的扩展函数 val KClass<*>.declaredMemberExtensionFunctions: Collection<KFunction<*>> get() = (this as KClassImpl).data().declaredNonStaticMembers.filter { it.isExtension && it is KFunction<*> } as Collection<KFunction<*>> //返回该类的静态属性 val KClass<*>.staticProperties: Collection<KProperty0<*>> get() = (this as KClassImpl).data().allStaticMembers.filter { it.isNotExtension && it is KProperty0<*> } as Collection<KProperty0<*>> //返回该类和父类的所有非扩展属性 val <T : Any> KClass<T>.memberProperties: Collection<KProperty1<T, *>> get() = (this as KClassImpl<T>).data().allNonStaticMembers.filter { it.isNotExtension && it is KProperty1<*, *> } as Collection<KProperty1<T, *>> //返回该类和父类的扩展属性 val <T : Any> KClass<T>.memberExtensionProperties: Collection<KProperty2<T, *, *>> get() = (this as KClassImpl<T>).data().allNonStaticMembers.filter { it.isExtension && it is KProperty2<*, *, *> } as Collection<KProperty2<T, *, *>> //返回该类中的非扩展属性 val <T : Any> KClass<T>.declaredMemberProperties: Collection<KProperty1<T, *>> get() = (this as KClassImpl<T>).data().declaredNonStaticMembers.filter { it.isNotExtension && it is KProperty1<*, *> } as Collection<KProperty1<T, *>> //返回该类的扩展属性 val <T : Any> KClass<T>.declaredMemberExtensionProperties: Collection<KProperty2<T, *, *>> get() = (this as KClassImpl<T>).data().declaredNonStaticMembers.filter { it.isExtension && it is KProperty2<*, *, *> } as Collection<KProperty2<T, *, *>> //创建实例,通过空参数构造函数或者全参构造函数 @SinceKotlin("1.1") fun <T : Any> KClass<T>.createInstance(): T { // TODO: throw a meaningful exception val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) } ?: throw IllegalArgumentException("Class should have a single no-arg constructor: $this") return noArgsConstructor.callBy(emptyMap()) }上面这些方法我个人觉得比Java中的API设计的更好一点,其实也是非常好理解和记忆的,比如方法分为是该类的还是包含其父类的,仅该类的可以用declared来命名其函数名,比如方法是否是扩展的,是扩展的话用extensions来命名其函数名,还有就是函数用Function,而属性用Properties来进行区分。总结其实看了这个KClass后是不是有一种豁然开朗的感觉,我们平时写的类,其信息都可以在这个KClass来获取,当然这里只是个简单概况,比如我想获取其中的属性或者方法,那就是KFunction和KProperty的使用了,我们下篇文章继续。
0
0
0
浏览量1698
德州安卓

写一篇好懂的常见Kotlin集合函数讲解(后)

zip(Iterable):集合合并举个例子,有很多玩具,一个集合记录着玩具的id,一个集合记录着玩具的名称,两个集合中相同索引的元素是指同一个玩具,那现在我们觉得两个集合存储麻烦,想要合并成一个集合存储玩具信息,就可以这样实现:fun zip() { val ids = listOf(4, 6, 7) val names = listOf("trains", "toe", "airport") println(ids.zip(names)) }输出:[(4, trains), (6, toe), (7, airport)]associate:将集合转换成Map类型比如将集合的每个元素作为key,value就是元素的长度转换成mapval names = listOf("trains", "toe", "airport") println(names.associate { it to it.length }) //输出:{trains=6, toe=3, airport=7}其他相似函数:associateWith(value):就等价于associate { 集合元素 to value }associateBy(value):就等价于associate { value to 集合元素 }associateWithTo/associateByTo:相比较于上面的操作函数,就是额外提供了可以将转换结果写入到指定的map对象中chunked(n):将集合分割成一个个长度为n的集合,如果剩余的元素小于n,也单独放到一个集合中val names = listOf("trains", "toe", "airport", "trains", "toe", "airport")println(names.chunked(2))//输出:[[trains, toe], [airport, trains], [toe, airport]]minOf:获取集合中元素权重最小的值,集合为空抛出异常比如我们获取字符串集合中最短字符串的长度:val names = listOf("trains", "toe", "airport", "trains", "toe", "airport")println(names.minOf { it.length })//输出:3其他相似函数:minOfOrNull:如果集合长度为0,返回null,否则和minOf相同maxOf:获取集合中元素权重最大的值,集合为空抛出异常比如我们获取字符串集合中最长字符串的长度:val names = listOf("trains", "toe", "airport", "trains", "toe", "airport")println(names.maxOf { it.length })//输出:7其他相似函数:maxOfOrNull:如果集合长度为0,返回null,否则和maxOf相同sortedWith:集合排序排序很简单,我们只需要传入比较器即可,但jdk1.7以后对于排序的规则有所变化,要求排序的规则必须满足:自反性:元素a和元素b的比较的结果r1和元素b和元素a的比较的结果r2要满足:r1 == -r2举个例子我们平常排序很容易写出下面错误的排序规则:names.sortedWith(Comparator { a, b -> if (a > b) 1 else -1 })如果元素a和元素b相等,那么a和b比较结果为-1,b和a比较结果也为-1,此时就不满足自反性的规则,直接崩溃传递性:元素a大于元素b,元素b大于元素c,那么元素a也应该大于元素c对称性:元素a等于元素b,那么元素a和元素c比较的结果值应该等于元素b和元素c比较的结果值其他类似排序函数:sortedBy:为了避免程序写出的比较器不符合上面规则,直接传入元素要比较的值即可sortedByDescending:和sortedBy相比,降序排序
0
0
0
浏览量900
德州安卓

这些flow常见API的使用,你一定需要掌握!(三)

flatMapLatest{}展平并丢弃上个未执行完毕的流public inline fun <T, R> Flow<T>.flatMapLatest(@BuilderInference crossinline transform: suspend (value: T) -> Flow<R>): Flow<R> = transformLatest { emitAll(transform(it)) }这个扩展函数非常好用,当收到下一个流时,会取消上一次未执行完毕的流。举个在Android中的应用例子:当通过搜索框输入字符实时搜索时,上一次搜索还没处理完毕前收到了下一次搜索请求,此时就自动会把上一次未处理完毕的搜索请求取消掉。接下来看个例子:fun flat() { GlobalScope.launch { flowOf( flow { println("flow1") delay(1000) emit(1) }, flow { println("flow2") delay(2000) emit(2) }).flatMapLatest { flow { it.collect { emit("haha".repeat(it)) } } }.collect { println("flat collect: $it") } } }输出结果如下:可以看到flow2代码块中休眠了2s而flow1只休眠了1s,所以flow1会执行较快,所以当flow1到达处理时而flow2未执行完毕就会被取消掉,最终的结果也只输出了flow1对应的执行结果。distinctUntilChanged()去重处理public fun <T> Flow<T>.distinctUntilChanged(): Flow<T> = when (this) { is StateFlow<*> -> this // state flows are always distinct else -> distinctUntilChangedBy(keySelector = defaultKeySelector, areEquivalent = defaultAreEquivalent) }最关键的是调用了distinctUntilChangedBy函数,该方法接受两个参数,用来定义去重的规则,我们也可以自定义去重规则。fun flat() { GlobalScope.launch { flow { println("flow2") for (i in 0..10) { emit(10) } } .distinctUntilChanged() .collect { println("flat collect: $it") } } }输出:takeWhile{}是否中止流public fun <T> Flow<T>.takeWhile(predicate: suspend (T) -> Boolean): Flow<T> = flow { return@flow collectWhile { value -> if (predicate(value)) { emit(value) true } else { false } } }这个方法也非常好用,接受一个返回值为布尔类型的函数类型,其中返回true则继续执行流的执行传递,返回false将直接中止后续流的执行。这里也用一个Android的例子说明使用场景:加载某个列表数据需要同时从本地数据库和网路进行请求,如果网络请求响应快于本地数据库读取,此时当网络响应的结果收到时,直接中断后续流的执行(本地数据库的)。fun flat() { GlobalScope.launch { flow { for (i in 0..10) { emit(i) } } .takeWhile { it in 0..5 } .collect { println("flat collect: $it") } } }看下执行结果:可以看到当打印到5时就不再输出了,也就说后续的流被中止了。take(n)取前n条数据public fun <T> Flow<T>.take(count: Int): Flow<T> { return flow { var consumed = 0 try { collect { value -> if (++consumed < count) { return@collect emit(value) } else { return@collect emitAbort(value) } } } catch (e: AbortFlowException) { e.checkOwnership(owner = this) } } }这个方法和takeWhile{}有点像,不过就是取前n条数据后就中止流的执行,这个场景也挺常见,就不再举例。使用takeWhilt{}的例子调用take(4)的执行效果如下:
0
0
0
浏览量1765
德州安卓

Kotlin泛型,有你想了解的一切

前言在Java中就有泛型的概念,但是一直没有做个统一的梳理,里面的很多知识点也补了解其细节,所以本章内容就准备好好梳理一下。为了更好的理解,我直接准备从最简单的泛型使用到泛型擦除、Kotlin中的实化类型参数、协变、逆变、点变型等等都介绍一遍。正文话不多说,直接开整。泛型基本概念我们平时总是泛型,那泛型是什么呢?泛型就是可以定义带类型参数的类型,当这种类型的实例被创建出来时,类型形参会被替换为类型实参的具体类型。所以这里重点是类型参数,关于参数我们很熟悉,比如方法的参数在定义方法时就是形参,调用时就是传递的实参给方法,那类型参数就是我们平时在类或者方法中经常看见的T,这个T就是类型形参。//这里E就是类型形参 public interface MutableList<E> : List<E>//这里的String就类型实参 val list: MutableList<String> = mutableListOf()这里就和我们平时调用方法一样需要传递参数,只不过这里传递的参数是类型而已。Kotlin要求类型实参被显示说明这里就会和Java不一样的地方了,既然我创建实例需要传参,按理说这个类型实参是必须要传递的,但是Java的泛型是1.5之后才有的,所以可以在创建泛型类实例时不进行传递实参。//这里Java代码中,tempList的类型就是List List tempList = new ArrayList();不过这种写法在Kotlin中是不允许的,因为Kotlin的创建之初就有泛型的概念,要不显示的指明泛型参数的类型,要不通过自动推导。//已经指明类型参数就是String val list: MutableList<String> = mutableListOf() //能自动推导出类型参数是String val list1 = mutableListOf("a","b")但是下面代码是无法通过IDE编译://这里无法知道类型参数,会直接报错 val errorList = listOf()对于Kotlin这样必须提供类型参数的实参,也是极大的能减少平时代码错误。声明泛型函数看一下如何声明泛型函数,在我们的集合类中,有很多泛型函数,我们来看一个。//这里的 <T> 就是定义了一个类型形参 //接收者和返回者都用了这个形参 public fun <T> List<T>.slice(indices: IntRange): List<T> { if (indices.isEmpty()) return listOf() return this.subList(indices.start, indices.endInclusive + 1).toList() }这里没啥说的,就是通过在fun关键字后和方法名前定义类型形参即可。那如何使用呢,把一个类型实参传递给这个函数,在之前的mutableList()函数其实我们就知道了,直接放在函数名后即可://类型自动推导,List的类型实参是String val list1 = mutableListOf("a","b") //显示的给slice函数传递一个类型实参 val newList = list1.slice<String>(0 .. 1)所以泛型函数可以是在接口或者类的函数,顶层函数,扩展函数,没有什么限制。声明泛型属性泛型属性就和泛型函数有点区别,它只能定义为扩展属性,不能定义为非扩展属性。//定义一个扩展属性last val <T> List<T>.last : T get() = this.last()比如上面代码给List定义了一个扩展属性last,但是你不能定义非扩展属性://类中定义这个 编译不过 val <E> e: E上面代码肯定无法编译,因为不能在一个类的属性中存储多个不同类型的值。声明泛型类声明泛型类也非常简单,和接口一样,把需要定义的类型参数通过<>放在类名后面即可,//直接在类名后面加上<>定义类型形参 public interface List<out E> : Collection<E> { //方法内部可以使用这个形参 public operator fun get(index: Int): E这里没啥可说的。类型参数约束类型参数约束可以约束泛型类或者泛型方法的类型实参的类型。这里也非常容易理解,也就是约束这个类型参数的范围,在Java中使用extends来表达,在Kotlin中直接使用冒号,<T:Number>就说明这个类型参数必须是Number或者Number的子类型。//定义类型参数约束条件,上界为Number fun <T : Number> List<T>.sum(): Number{ //... }//可以编译 val list0 = arrayListOf(1,2,3,4,5,6,7,8) list0.sum() //无法编译 val list1 = arrayListOf("a","b") list1.sum()这里也可以添加多个上界约束条件,比如同时定义类型T有2个上界A、B,那传入的类型实参必须是A、B的子类型。让类型形参非空前面说了可以添加类型参数的约束,如果不加约束,那类型T的默认上界是Any?,注意这里是可空的。//这里没有约束,T的上界默认是Any? class TestKT<T> { fun progress(t: T){ //可以为空 t?.hashCode() } }//所以调用的时候可以传入null val stringNull = TestKT<String?>() stringNull.progress(null)由于Java是没有可空类型的概念,所以这里我想让类型非空,给指定一个上界 Any 即可。//这里类型参数就不能传入可空类型实参 class TestKT<T : Any> { fun progress(t: T){ t.hashCode() } }类型擦除Java/Kotlin的泛型可以说是伪泛型,因为在运行时这个类型参数会被擦除,具体为什么要这么设计,很大原因是占用内存会更少。这里就要明白基础类型的概念,比如 List< Int > 这种类型它的基础类型就是List,当代码运行时,只知道它是List,这里有个很经典的例子,我们来看一下://定义一个保存Int类型的List val listInt = arrayListOf(1,2,3) //定义一个保存String类型的List val listString = arrayListOf("hello","java") //在运行时,他们的class是相同的,都是List if (listInt.javaClass == listString.javaClass){ Logger.d("类型相同") } //通过反射可以往保存Int类型的List中添加String listInt.javaClass.getMethod("add",Object::class.java).invoke(listInt,"aa") //打印结果发现居然还加成功了,没有报错 Logger.d("listInt size = ${listInt.size}")由上面这几行经典的代码我们能明白一个道理,就是泛型类实例在运行时是不会携带类型实参的,在运行时对于List< Int >和List< String >都是List,不知道它应该是保存何种类型的List。类型检查伴随了类型擦除这个特性,泛型类的类型就有了一些约束,比如类型检查。Kotlin中的类型检查是通过 is 函数,但是当类型参数被擦除时,你无法直接使用以下代码://定义一个ArrayList<String> val listInt = arrayListOf(1,2,3) //判断是否是ArrayList<String> 这代码无法编译 if (listInt is ArrayList<String>){ }上面代码是无法编译的,会直接报错,因为在运行时根本携带不了类型实参,这里你只能判断它是不是List,如下代码:val listInt = arrayListOf(1,2,3) //可以判断这个变量是不是一个HashSet if (listInt is HashSet<*>){ }除了 is 函数受到限制外,还有就是as函数。类型转换同样伴随着类型擦除,类型转换 as 函数也受到限制,直接看个代码://接收Collection fun printSum(c: Collection<*>){ //这里当类型转换不成功时会主动抛出异常 val intList = c as? List<Int> ?: throw IllegalArgumentException("期望是list") Logger.d("${intList.sum()}") }定义完函数,我们来进行调用:val intSet = hashSetOf(1,2,3) printSum(intSet)这个代码会直接抛出异常,提示期望是list。那我们就给他传递个list呢,但不是Int类型:val stringList = arrayListOf("a","b") printSum(stringList)这行代码执行会抛出不一样的错误:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number其实想一下泛型的类型擦除就完全明白了,第一个调用肯定会失败,因为即使是擦除泛型,基类类型也不对。第二个调用代码能编译过,但是会有个警告:因为编译器知道这个泛型参数会被擦除,这个操作是危险操作,所以后面还是会报出类型转换错误。上面2个函数也是由编译器就直接在你编码阶段会提醒你的危险操作,防止出现由于泛型类型擦除导致的程序异常。Kotlin泛型上面介绍了泛型的基本概念,下面来介绍一些关于Kotlin泛型的知识。声明带实化类型参数的函数不同于Java,Kotlin可以实化类型参数,啥意思呢 也就是这个类型参数在运行时不会被擦除。比如下面代码是编译不过的:fun <T> isAny(value:Any): Boolean{ //由于类型擦除,在执行时这个T的值是不携带的 return value is T }但是Kotlin可以让上面的代码编译且执行成功,如下://函数声明为inline函数,且泛型参数使用reified来修饰 inline fun <reified T> isAny(value:Any): Boolean{ //能执行成功 return value is T }上面能保留类型实参的关键不仅仅是把类型实参使用reified修饰,也必须在内联函数中,因为编译器把实现内联函数的字节码插入到每一次调用发生的地方,所以每次调用带实化类型参数的函数时,编译器都知道调用中作为类型实参的确切类型,会直接把类型实参的值替换到字节码中,所以不会被擦除。关于普通的内联函数,Java是可以调用的,但是无法进行内联。对于带reified类型参数的内联函数,Java是不能调用的,因为要实现实化功能,必须要内联,且做一些处理,Java编译器是无法内联的。实化类型参数代替类引用关于实化类型参数还有一个场景就是替代类引用,当一个函数参数接接收java.lang.Class类型时,可以用实化类型参数来优化,这个是啥意思呢 举个简单的例子,比如我们都是知道startActivity需要Intent,而Intent知道跳转类的class类型:public Intent(Context packageContext, Class<?> cls) { mComponent = new ComponentName(packageContext, cls); }这里的Class就是类型的意思,既然我可以直接类型实化,那就可以改写一下://直接类型实化 inline fun <reified T: Activity> Context.StartActivity(){ //类型实化,T就可以代表具体类型 val intent = Intent(this, T::class.java) startActivity(intent) }调用也方便一点:StartActivity<AboutUsActivity>()实化类型参数的限制既然Kotlin的实化类型参数很好用,但是它也有诸多限制,我们简单总结一下,可以做的操作:用在类型检查和类型转换中 is和as使用Kotlin反射API获取相应的java.lang.Class作为调用其他函数的类型实参 不可以的操作有:不能创建指定为类型参数的类的实例调用类型参数类的伴生对象的方法在非内联函数中使用变型继续来聊泛型,这里引入了一个叫做变型的概念,什么是变型呢 这里要了解清楚,首先从字面意思来理解是变异/变化的泛型参数,用来描述拥有相同基础类型和不同类型实参的类型之间是如何关联的。这里比较绕,就比如List< T >的2个类型List< String >与List< Any >之间是如何关联的,这2个类型实参String是Any的子类型,那List< String >与List< Any >是什么关系呢,变型就是用来描述这个关系的。为什么存在变型既然知道了变型的概念,那这个在什么地方能用到这个变型呢 就是给参数传递实参的时候,可以直接看个例子://函数参数期望是List<Any>类型 fun printContent(list: List<Any>){ Logger.d(list.joinToString()) }定义完上面函数,我直接传入一个String类型集和会如何://这里能正常运行 val strings = arrayListOf("a","bc") printContent(strings)这里我们知道String是Any的子类型,所以这里期望是List< Any >,但是我传入的是List< String >没有问题也可以正常运行,那就说明这种关系一定是正常的吗 这可不一定,再看一个例子://传递进来一个可变list,然后加一个元素 fun addContent(list: MutableList<Any>){ list.add(42) }同样定义完上面函数,把string类型集和传递:会发现编译器直接报错,说期望的类型不一致,说明这时MutableList< String >和MutableList< Any >和上面的List< T >有很大区别,这就是为什么要讨论变型的原因。其实上面2段代码我们都是把一个String类型实参的列表传递给期望是Any类型实参的列表,但是当函数中需要添加或者替换列表中的值就是不安全的,当仅仅是获取列表的值就是安全的,当然上面这个结论是我们很熟悉List和MutableList接口可以得出结论,所以我们要把这个问题推广到任何泛型类,而不仅仅是List,这就是为什么需要存在变型的原因。类、类型、子类型在继续说变型前,我们先理一下上面3个东西。在我们平时使用中总是认为类就是类型,这当然是不对的。对于非泛型类来说,比如 Int 是类,但是有2个类型,分别是 Int 和 Int? 类型,其中 Int? 类型不仅仅可以保存Int类的数据还可以保存null。对于泛型类来说,一个类就对应着无数个类型了,比如List是类,它有List< Int >、List< String >等等无数个类型。子类型定义非常重要,具体定义是:在任何时候如果需要类型A的值,都能够使用类型B的值来当做类型A的值,这样类型B就是类型A的子类型。比如我定义一个Any 类型的变量a,这时用一个String 类型的值来当做a,这当然是可以的,这里就会有个问题,String是Any的子类,那类型也是这种关系吗对于非泛型类来说,子类和子类型还真是差不多的东西,但是Kotlin有可空类型的存在,比如Int类型是Int?类型的子类型,但是它俩是一个类。对于泛型类,我们前面就举过例子当我需要List< Any >的时候我们可以传递List< String >给它,所以List< String >就是List< Any >的子类型,但是当期望是MutableList< Any >的时候却不能使用MutableList< String >类型值来给它,说明这俩不是子类型关系。总结一下术语:对于List这种类来说,A是B的子类型,List< A >是List< B >的子类型,就说这个类是协变的;对于MutableList这种类来说,对于任意2种类型A和B,MutableList< A >即不是MutableList< B >的子类型也不是超类型,它就被称为在该类型参数上是不变型的。协变:保留子类型化关系理解协变这种泛型参数的变型,必须要理解上面说的子类型化的概念,这样才好理解为什么放在in和out位置。假如我有一个泛型类,就叫做List< T >,这时String类型是Any类型的子类型,那么List< String >就是List< Any >的子类型,这个类就是在T这个类型参数上是协变的,也就需要加个out来修饰T。最常见的协变泛型类就是Kotlin中的List,代码如下:public interface List<out E> : Collection<E> { override val size: Int override fun isEmpty(): Boolean }不过这个例子大家可能都看烦了,其实List也是最好的一个协变例子,我们自己来写个类试试://定义动物类 open class Animal{ fun feed(){ Logger.d("zyh animal feed") } }//定义畜群,假设一个Herd就包含10只动物 class Herd<T : Animal>{ //上界是Animal,所以底层实现也就是保存的是Animal类型 val animal = Animal() fun getVa():T{ return animal as T } //一个畜群有10个动物 val size = 10 }//给畜群投喂食物 fun feedAll(animals: Herd<Animal>){ for (i in 0 until animals.size){ //遍历取出Animal,然后投喂食物 animals.getVa().feed() } }//定义猫,继承至动物 class Cat : Animal(){ //需要清理 fun clean(){ Logger.d("zyh cat clean") } }上面函数看起来都没啥问题,我主要就是有个10只动物的畜群类,想给它们投喂食物,然后我现在想给猫群投喂食物://方法形参是猫群 fun feedCatHerd(cats: Herd<Cat>){ for (i in 0 until cats.size){ cats.getVa().clean() } //猫群属于畜群,所以我一次性投喂 feedAll(cats) }这里看起来合情合理,不过代码却无法编译:这里提示类型不匹配,所以这里就有问题了,我们再来梳理一下。首先Herd是一个类,Cat类型是Animal类型的子类型,然后Herd泛型类变型是啥呢 就是Herd< Cat >和Animal< Animal >这2个类,我们也想Herd< Cat >是Herd< Animal >的子类型,这样我就可以处理上面的问题了,所以这种变型的关系就叫做协变,只需要在Herd的泛型参数前加个out,就说明该类在该参数是协变的。//类型参数T 就是协变的 class Herd<out T : Animal>{ val animal = Animal() fun getVa():T{ return animal as T } val size = 10 }in 位置 和 out 位置上面我们熟悉了协变,就是给类型参数加上out关键字,那能不能把所有类型参数都定义为out呢 当然不可以,定义为out的类型参数在泛型类中只能被用在out位置,其实这里的out位置也就是返回值的位置,或者叫做生产者的位置,也就是生成类型为 T 的值。//比如这里的例子 interface Transformer<T>{ //方法参数的位置就是 in位置 fun transform(t: T): T //方法返回值的位置就是 out位置 }所以在类型参数T上加上out关键字有2层含义:子类型化会被保留T 只能用在out位置所以当类型参数定义为了out,这个类型参数在泛型类中就不能出现在in位置,编译器会报错。逆变:反转子类型化关系我们清楚了协变的概念,就很容易理解逆变,逆变就是协变的反子类型化。直接定义:一个在类型参数上逆变的类是这样一个泛型类(我们以Consumer< T >为例),对于这种类来说,如果B是A的子类型,那么Consumer< A >就是Consumer< B >的子类型。关于逆变,我们也举个例子://这是迭代集和的排序函数,这里要传递进来一个Comparator,它的类型是in T public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> { if (this is Collection) { if (size <= 1) return this.toList() @Suppress("UNCHECKED_CAST") return (toTypedArray<Any?>() as Array<T>).apply { sortWith(comparator) }.asList() } return toMutableList().apply { sortWith(comparator) } }然后我们有以下代码://泛型实参为Any val anyComparator = Comparator<Any> { o1, o2 -> o1.hashCode() - o2.hashCode() } //泛型实参为Number val numberComparator = Comparator<Number> { o1, o2 -> o1.toInt() - o2.toInt() } //泛型实参为Int val intComparator = Comparator<Int> { o1, o2 -> o1.toInt() - o2.toInt() } //创建了Number类型的集和,按照接口,它期望的参数是Comparator<Number> val numbers: List<Number> = arrayListOf(1,11,111) //可以编译 numbers.sortedWith(numberComparator) //可以编译 numbers.sortedWith(anyComparator) //不可以编译 numbers.sortedWith(intComparator)上面我们举了个例子,其中Number是Int的超类型,是Any的子类型,但是在传递参数却无法传递Comparator< Int >,其实我们可以想想这是为什么因为标记为in的逆变类型参数,是需要入参的,也就是需要使用这个类的实例,比如这里期望是Number,我调用Number中的方法,但是来了一个它的子类型Int,假如Int有个getInt的方法,这时在泛型类中类型参数还是Number,它是没有getInt方法的,所以不允许。但是来了一个它的超类型Any,Any中的操作我Number都可以操作,所以是安全的。到这里,我们把协变和逆变都说完了,其实不用死记硬背,要理解库设计的思想和原理,这个就很容易理解。点变型: 在类型出现的地方指定变型在开始我们就说了,变型是描述泛型类类型之间的关系,根据是否保留子类型化可以分为协变、逆变和不变。除了这些,我们还要掌握点变型的概念。其实上面说的协变和逆变我们都是在定义泛型类或者泛型函数时使用,这样还是不太方便,比如MutableList这个类的泛型参数是不变的,但是有一些需求我们想改变一下这个定义,直接还是看个例子。我们想编写一个复制 MutableList的函数,直接如下代码://参数都是mutableList fun <T> copyData(source: MutableList<T> , target: MutableList<T>){ for (item in source) { target.add(item) } }然后我们在使用时:val strings = arrayListOf("zyh","wy") var targets: ArrayList<String> = ArrayList() //都是String类型,当然可以 copyData(strings,targets)但是仔细一想这不太符合逻辑,我可以定义一个String类型的集和,把它复制到Any类型的集和,这个逻辑肯定是符合逻辑的:不出意外,这里肯定不行,因为MutableList< String >不是Mutable< Any >的子类型,按照之前的做法我们只需要把source的泛型类给改成协变类,那不免有些麻烦,我们换个方式,用下面代码://直接定义2个类型参数,来限制这2个类的关系 fun <T : R,R> copyData1(source: MutableList<T> , target: MutableList<R>){ for (item in source) { target.add(item) } }然后我们在使用时:val strings = arrayListOf("zyh","wy") var targets: ArrayList<Any> = ArrayList() copyData1(strings,targets)这样不会有任何问题,不过这太麻烦了,Kotlin和Java都有更优雅的处理方式,也就是点变型,啥是点变型呢 就比如这里我想在souce这个参数类型上它是协变的,这个在Java很常见,因为Java的所有泛型在定义时都是不变的,只有在使用时指定其变型,我们来看一下Java代码://这里使用<? extent T>就说明可以使用T的子类型 public static <T> void copyDataJava(ArrayList<? extends T> source , ArrayList<T> destination){ for (int i = 0; i < source.size(); i++) { destination.add(source.get(i)); } } private void testJava(){ ArrayList<String> strings = new ArrayList<>(); strings.add("zyh"); strings.add("wy"); //这里虽然类型是Object,但是不会报错 ArrayList<Object> objects = new ArrayList<>(); copyDataJava(strings,objects); }这里代码如果用Kotlin的话就更简单了://加了一个out变型修饰符 fun <T> copyData2(source: MutableList<out T> , target: MutableList<T>){ for (item in source) { target.add(item) } }直接指定source的类型类是协变的,这样就可以传递其子类型了。这样一看就能看出来点变型是非常重要了,不仅仅可以在方法参数,也可以在局部变量、返回类型等都可以指定其点变型。投影看到点变型如此方便,我就有了个疑惑,MutableList这个类在定义的时候是不变型的啊,在使用时又给它设置为了协变,那是不是MutableList< T >在T上就是协变了呢 当然不是,要是这样那岂不是乱套了。比如代码:fun <T> copyData2(source: MutableList<out T> , target: MutableList<T>){ for (item in source) { target.add(item) } }这里的source其实不是一个常规的MutableList,它是受到限制的,这个叫做类型投影,即它不是真正的类型,只是一个投影。其实投影还挺好理解的,它就是受限制的类型,不是真正的类型,因为它的有些功能是受限的,是假类型。比如上面代码,标记为了协变,那MutableList种在in位置使用的方法将无法调用,这里add方法在MutableList中是泛型参数在in的位置,所以这里直接无法使用,功能受限,也验证了为什么叫做投影。这个其实也非常好理解,一旦使用点变型让一个类型协变,将无法使用类型参数在in位置的方法,然后这个新的"假"类型就叫做投影。星号投影在理解星号投影时,还要回顾一下啥是投影,前面说了投影是受限的类型,比如在in和out都可以用的类型参数,在点变形为out时,生成的投影类型将不能使用实参在in位置的方法。星号投影也是一种投影,它的限制是相当于out,即只能使用类型参数在out位置的方法。直接理解完定义后,我们需要思考为什么要有这个星号投影,当类型参数不重要时可以使用星号投影,啥叫做不重要呢 就是当不调用带有类型参数的方法或者只读取数据而不关心其具体类型时。比如我想判断一个集和是不是ArrayList时:val strings = arrayListOf("zyh","wy") //这里只能用星号投影 if (strings is ArrayList<*>){ Logger.d("ArrayList") }因为泛型在运行时会被擦除,所以类型参数并不重要,所以这里使用星号投影。星号投影和Any?关于星号投影还容易理解出错,比如MutableList< * >和MutableList< Any? >这2个是不一样的东西,其中MutableList< Any? >这种列表可以包含任何类型,但是MutableList< * >是包含某种类型的列表,这个类型你不知道而已,只有当被赋值时才知道。直接看个例子:在被赋值前,不知道unknowns的类型,但是赋值后,可以通过get获取它的值,但是add方法无法调用,这里受到了限制,是不是有点熟悉,这个限制就是 out 点变型的协变。所以这里星号投影被转换成了 < out Any? >,只能调用其类型参数在out位置的方法,不能调用类型参数在in位置的方法。总结泛型的知识还是很多的,这一篇文章也可能说不全,下面做个简单的总结:Java/Kotlin的泛型都是伪泛型,在使用时可以用在函数、扩展属性和类上,同时泛型参数在运行时会被擦除。由于泛型参数在运行时会被擦除,所以对类型检查和类型转换函数在运行时可能会不起效果,这时IDE会给出警告。Kotlin通过内联函数和reified关键字可以实化类型参数,即在运行时还可以保留该类型参数,常常可以用于替代类引用。加强认知一点就是类、类型和子类型的关系,对于非泛型类来说,类和类型差不多;对于泛型类来说,一个类,有多类型;而任何时候要求A类型参数时可以用B类型实例代替,B就是A的子类型。变型就是主要讨论相同基础类型的不同类型的关系,比如Int是Number的子类型,那List< Int >类型是List< Number >的子类型,这就是协变; 同理还有逆变和不变。
0
0
0
浏览量1827
德州安卓

你需要懂的Kotlin开发技巧之五泛型实化

1.泛型实化先看一个函数,用来判断参数name是否为某种指定泛型类型并执行相对应逻辑:fun <T> test(name: Any, block: () -> Unit) { if (name is T) { block() } }上面的name is T是会直接报错的,或者调用T::class.java也是会报错的,编译器是不支持这种写法的。为了通过编译器的支持,我们可能会做如下繁琐的封装:fun test2(name: Any, block: () -> Unit) { when(name) { is String -> block() is Int -> block() is Double -> block() is GCCheck -> block() else -> block() } }写起来十分麻烦,这个时候泛型实化要登场了:泛型T加上reified声明并且方法也要加上inline修饰然后我们就可以优雅写出下面代码:inline fun <reified T> test(name: Any, block: () -> Unit) { if (name is T) { block() } }可以看到,test方法写起来更加简单,请注意reified一定要搭配inline使用。2.泛型实化应用案例讲解利用泛型实化更加简单启动Activity 看下平常代码中是怎么启动Activity的:startActivity(Intent(this, ZygoteActivity::class.java))又或者我们还可能进行如下封装:fun Intent.startActivity(activity: AppCompatActivity) { activity.startActivity(this) } //使用 Intent(this, ZygoteActivity::class.java).startActivity(this)但是上面写起来都太复杂了,其实上面封装关键都是要拿到目标Activity的Activity::class.java类型,这恰好到了泛型实化发挥的场景了:定义如下封装:inline fun <reified T> Activity.startActivity() { startActivity(Intent(this, T::class.java)) }看下优雅的使用:startActivity<ZygoteActivity>()有时候我们在进行Activity跳转到还要通过Intent传参,下面我们对这个startActivity扩展函数进一步封装:inline fun <reified T> Activity.startActivity(block: Intent.() -> Unit = {}) { startActivity(Intent(this, T::class.java).apply(block)) }我们增加一个带接收者的函数类型参数block: Intent.() -> Unit,并给与默认值,看下使用效果:startActivity<ZygoteActivity> { putExtra("key", "value") putExtra("key2", 100) }使用起来就是这么简单丝滑。搭配Gson实现快捷反序列化这里就不再细说了,可以参考我之前写的文章:Gson序列化的TypeToken写起来太麻烦?优化它,最终的封装效果如下:使用起来也是特别的简单丝滑:
0
0
0
浏览量1655
德州安卓

Kotlin反射全解析3 -- 大展身手的KProperty

前言前面我们说了Kotlin的反射就是把类的信息保存到了KClass,通过KClass可以获取类的一些信息可以查看文章:# Kotlin反射全解析2 -- 超级好用的KClass但是对于类最关键的属性和方法信息,它是保存在://KClass中保存类属性和方法的变量 override val members: Collection<KCallable<*>>所以我们来看看类中的属性和方法在Kotlin的反射中是如何保存的。正文上面我们发现Kotlin把类的属性和方法都定义成了KCallable接口的子类,我们就从这个KCallable开始看起。KCallableKCallable是函数和属性的超接口,理解这个还是蛮关键的,我们直接还是看源码://Kcallable表示一个可调用的实体,比如函数或者属性 public actual interface KCallable<out R> : KAnnotatedElement { //这个可调用对象在代码中声明的名称 public actual val name: String //调用这个可调用对象所需要的参数,假如调用这个对象需要this实例或者扩展接收器参数,把这些 //参数类型放在列表首位 public val parameters: List<KParameter> //这个可调用对象的返回类型 public val returnType: KType //类型参数列表,也就是使用泛型时会用到 @SinceKotlin("1.1") public val typeParameters: List<KTypeParameter> //使用指定的参数列表调用这个可调用对象并且返回结果 public fun call(vararg args: Any?): R //访问权限可见性,即public、protected等 @SinceKotlin("1.1") public val visibility: KVisibility? //是否是final @SinceKotlin("1.1") public val isFinal: Boolean //是否是open @SinceKotlin("1.1") public val isOpen: Boolean //是否是抽象的 @SinceKotlin("1.1") public val isAbstract: Boolean //是否是挂起函数 @SinceKotlin("1.3") public val isSuspend: Boolean }这里其实就把一个函数和属性它的公共点给抽离出来,包括属性/方法的可见性、参数、返回值等等,我们这里主要关注的是这个call方法。call方法call方法说的是调用这个可调用对象,把参数传递给它,然后便可以调用该实例,返回结果。其实当Callable是属性的话,就是调用属性的getter函数,当Callable是函数时,也就是调用函数它本身。我们还是先看个例子://定义一个类叫做ChildClass class ChildClass : ParentClass(){ //有一个属性 val childClassValue: Int = 20 //有一个方法 fun childClassFunction() { println("childClassFunction") } }根据前面一篇文章我们介绍了KClass,假如我想获取这个childClassValue的话那需要先获取memebers,再进行筛选,其实大可不必,在前面文章我们介绍过 :: 这个叫做成员引用的东西,它的原理其实就是利用反射,以这个例子我们看下面代码://能获取ChildClass的childValue这个属性的Callable val kcallable = ChildClass::childClassValue //需要一个接收者 val child = ChildClass() //进行调用 val value = kcallable.call(child) println("ChildClass value = $value")这里的 :: 符号就比较神奇了,它可以直接获取该类的属性,而且返回的是一个Callable对象。这里要注意一点就是调用call的参数,这里kcallable是一个属性,它虽然没有参数,但是它需要一个接收者,具体看IDE的提示:会发现它的类型叫做KProperty1类型,关于这个我们后面细说。call方法调用函数前面我们以ChildClass为例,调用了其属性,也就是调用了其属性的getter方法,那调用函数呢 我们继续看一下代码://定义一个测试类 class ChildClass : ParentClass(){ val childClassValue: Int = 20 //它包含一个方法,只有一个参数 fun childClassFunction(intValue: Int) { println("childClassFunction $intValue") } }同样我们这里使用 :: 符号来获取这个函数的实例引用,它也就是一个Callable类的实例://获取该类该方法的引用 val kcallable = ChildClass::childClassFunction val child = ChildClass() //调用 kcallable.call(child,20)这里也是可以调用成功的,通过call方法可以调用它自己,假如有接收者的话需要传入一个接收者,还是来看一下IDE的提示:这是一个KFunction2类型的函数。call方法调用顶层属性基本说完了定义在类中的属性和方法你需要如何去调用,假如我有个属性它就是顶层属性,比如下面例子://定义的顶层属性 var topLevelValue = 10然后我同样可以通过 :: 成员引用来获取它的Callable实例:val kcallable = ::topLevelValue println("value = ${kcallable.call()}")然后我们看一下这个kcallable的类型,因为调用这个方法不需要接受者:会发现它是KProperty0类型的。KProperty通过上面几个例子我们发现当KCallable是一个属性时,想调用它需要可能需要传递参数,也可能不需要,而且我想调用其setter方法呢,该如何是好,所以Kotlin反射库的重要角色KProperty就登场了。KProperty就是在反射中的属性,这个属性包含类中的成员属性、顶层属性和扩展属性,我们直接来看一下源码://Kotlin反射表示属性,继承至KCallable public actual interface KProperty<out V> : KCallable<V> { //该属性是否是延迟初始化 @SinceKotlin("1.1") public val isLateinit: Boolean //该属性是否是const @SinceKotlin("1.1") public val isConst: Boolean //get函数 public val getter: Getter<V> }表示一个属性,比如在代码中用val或者var来声明的属性,可以用 :: 操作符来获取它。由于Kotlin中定义属性有val和var之分,所以这个KProperty也有个可修改的版本://对于var定义的属性 public actual interface KMutableProperty<V> : KProperty<V> { public val setter: Setter<V> public interface Setter<V> : KProperty.Accessor<V>, KFunction<Unit> }既然有了更好用KProperty就不用再使用KCallable来表示属性了,就不用再调用call方法了,比如下面例子://获取顶层属性的引用 val kcallable = ::topLevelValue //调用set方法设置属性值 kcallable.set(100) //调用get方法来获取值 println("value = ${kcallable.get()}")上面的用法就比直接使用Callable要方便很多。由于属性定义的地方不一样,当调用其反射对象时需要传递的参数也不一样,在Kotlin中可以分为下面3种。KProperty0无接收的属性,这种情况是定义在顶层函数中的属性或者属性定义时它自带get方法,源码如下:public actual interface KProperty0<out V> : KProperty<V>, () -> V { //不用传递参数即可获取属性的值 public actual fun get(): V }public actual interface KMutableProperty0<V> : KProperty0<V>, KMutableProperty<V> { //直接传递属性值即可赋值 public actual fun set(value: V) }KProperty1一个接收者的属性,这种也是最常见的,比如定义在类中的非扩展属性,定义的扩展属性,这种属性获取其值必须要传入其接收者,该类源码如下:public actual interface KProperty1<T, out V> : KProperty<V>, (T) -> V { //必须传递一个接收者,才可以获取属性值 public actual fun get(receiver: T): V } public actual interface KMutableProperty1<T, V> : KProperty1<T, V>, KMutableProperty<V> { //必须传递一个接收者,也就是给哪个对象的属性设置值 public actual fun set(receiver: T, value: V) }KProperty22个接收者的属性,这种比较少见,只有定义在类中的扩展属性才可以,当然要获取这种属性的值或者给它赋值需要传入2个接收者。public actual interface KProperty2<D, E, out V> : KProperty<V>, (D, E) -> V { public actual fun get(receiver1: D, receiver2: E): V } public actual interface KMutableProperty2<D, E, V> : KProperty2<D, E, V>, KMutableProperty<V> { public actual fun set(receiver1: D, receiver2: E, value: V) }总结本篇文章主要说了当使用反射时,其类的属性是如何表示的,其实也非常简单,主要要了解其属性在哪里定义,分为几类即可。在接下来文章,我们再分析一些其他常用类,以及利用反射来实现一个序列化库。
0
0
0
浏览量984
德州安卓

Kotlin的inline、noinline、crossinline全面分析1

前言说起内联函数,熟悉Kotlin的Android开发肯定使用过,比如我们常见的apply函数:public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }我们也大概知道这个inlie的作用是啥,不过一般都没有仔细思考过为什么,以及它还有2个好兄弟oninline和crossinline的作用,这篇文章我就准备从最简单的地方,挨个举例来说清楚这些。正文首先就是必须了解Kotlin的lambda以及函数类型,可以看我之前的文章:# Kotlin lambda,有你想了解的一切这里有说了一个重要概念,就是Kotlin的高阶函数其实就是实现FunctionN的一个实例,还有就是Kotlin的lambda作为实参传递给参数时,也会创建匿名内部类以及调用invoke方法,我们这里来逐步深入。Android Studio反编译Kotlin代码这个是AS的很好用的一个功能,当我们写了一个Kotlin文件,但是我们想看它编译成Java是什么样子的,可以直接使用AS来进行。比如下面代码:如下显示Kotlin字节码:然后点击这个反编译按钮:然后便生成了对应的Java文件:比如这里我们就可以看出Kotlin代码中的lambda这里创建了一个OnClickListener的实例对象,所以当不熟悉的Kotlin代码,把它反编译成对应的Java代码将会好容易理解很多。lambda作为实参传入Kotlin高阶函数这里还是复习一下Kotlin高阶函数中把lambda作为实参会发生什么,比如下面代码://在Kt文件中定义一个高阶函数 fun lambdaFun(action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") action() Log.i("zyh", "testLambdaFun: 调用后") }调用上面高阶函数:fun testHello1(){ lambdaFun { Log.i("zyh", "testLambdaFun: 正在调用") } }给转换成Java代码:public final class TestFun { public final void testHello1() { //把lambda转换成了Function0的实例 TestInlieKt.lambdaFun((Function0)null.INSTANCE); } }会发现这里没啥问题,把lambda转换成匿名内部类实现合情合理,但是当有很多的地方都这样写://调用多次lambda fun testHello1(){ for (i in 0 .. 10){ lambdaFun { Log.i("zyh", "testLambdaFun: 正在调用") } } }反编译结果:public final class TestFun { public final void testHello1() { int var1 = 0; //创建了多个匿名内部类实例 for(byte var2 = 10; var1 <= var2; ++var1) { TestInlieKt.lambdaFun((Function0)null.INSTANCE); } } }这里就会发现有问题了,当匿名内部类过多时,会导致内存增加,这里也就是inline关键字出现的原因。inline关键字inline关键字可以修饰函数,然后函数称之为内联函数,当函数是内联函数时,函数内部的函数体以及函数类型参数都会被“内联”到调用地方。这里比较难理解,要理解2点,一个是函数内部的函数体的内联,一个传递进来的lambda表达式的内联,这个十分重要,在后面细说。为了理解,我们还是举个简单例子://定义一个非高阶函数 fun normalFun(){ Log.i("zyh", "testLambdaFun: 调用前") Log.i("zyh", "testLambdaFun: 调用后") }调用地方,然后进行反编译:fun testHello(){ normalFun() }//反编译 public final class TestFun { public final void testHello() { TestInlieKt.normalFun(); } }这里就是正常调用,当把normalFun定义成inline函数://普通函数添加inline inline fun normalFun(){ Log.i("zyh", "testLambdaFun: 调用前") Log.i("zyh", "testLambdaFun: 调用后") }然后进行调用,然后反编译://反编译代码 public final class TestFun { public final void testHello() { int $i$f$normalFun = false; Log.i("zyh", "testLambdaFun: 调用前"); Log.i("zyh", "testLambdaFun: 调用后"); } }看到这里是不是有一种豁然开朗的感觉,这里直接把normalFun函数内的逻辑直接复制到调用地方,很nice。注意这里的操作是由Kotlin编译器干的事,所以我们可以不用探讨。假如这里只有这个对普通函数的效果,那未免没有什么意思,顶多也就让调用站少了一层,但是让编译器干了这么多事,肯定不划算,真正的亮点在于高阶函数。inline修饰高阶函数inline不仅可以“铺平内联”函数内的代码,还可以“铺平内联”函数类型的参数,这才是关键,比如下面代码://高阶函数添加inline inline fun lambdaFun(action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") action() Log.i("zyh", "testLambdaFun: 调用后") }然后进行调用,进行反编译://反编译代码 public final class TestFun { public final void testHello() { int $i$f$lambdaFun = false; Log.i("zyh", "testLambdaFun: 调用前"); int var2 = false; Log.i("zyh", "testLambdaFun: 调用中"); Log.i("zyh", "testLambdaFun: 调用后"); } }会惊喜地发现,这里反编译的代码没有匿名内部类的影子,这也就达到了我们的目的,即使这里调用多个lambda,假如被调用的函数被声明为了inline,那也不会创建出多个无用的类,可以大大减小内存使用。总结其实内联函数很关键,它解决了使用lambda方便的同时创建过多的匿名内部类的弊端,这里我更喜欢把被调用的inline函数的变化称为复制铺平,也就是把函数体的代码和函数类型参数给赋值铺平到调用地方,当然这种叫法只是个人理解。既然赋值铺平很好用,那是不是所有inline函数的函数类型参数都可以复制铺平呢 我们下篇文章再说,也就是noinlien和crossinline的使用。# Kotlin的inline、noinline、crossinline全面分析2
0
0
0
浏览量304
德州安卓

这些flow常见API的使用,你一定需要掌握!(二)

flowOn()指定调度线程public fun <T> Flow<T>.flowOn(context: CoroutineContext): Flow<T> { checkFlowContext(context) return when { context == EmptyCoroutineContext -> this this is FusibleFlow -> fuse(context = context) else -> ChannelFlowOperatorImpl(this, context = context) } }传入的参数类型为一个协程上下文元素之一的分发器,指定flowOn上面的代码块执行所在的线程环境:即指定上面红框中代码块执行的线程环境,不包括下面,看下日志打印:flattenConcat串行展平流public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T> = flow { collect { value -> emitAll(value) } }展平流,也就是将Flow<Flow<T>>类型的嵌套流展平成Flow<T>类型的流,请注意这个flattenConcat()方法属于串行的展平流,也就是说,只有执行完上一个流之后,才回去执行下一个流中的flow代码块逻辑。fun flat() { GlobalScope.launch { flowOf( flow { println("flow1") delay(1000) emit(10) }, flow { println("flow2") emit(20) } ).flattenConcat().collect { println("flat collect: $it") } } }输出结果:可以看到,即使第一个flow中挂起了1s,也不会执行第二个flow中的逻辑,只有第一个flow执行完毕才会轮到第二个flow开始执行。还有个flatMapConcat()方法,展平的时候可以变换发送的数据类型,这里不做过多介绍,就是map()和flattenConcat的结合。flattenMerge并行展平流public fun <T> Flow<Flow<T>>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY): Flow<T> { require(concurrency > 0) { "Expected positive concurrency level, but had $concurrency" } return if (concurrency == 1) flattenConcat() else ChannelFlowMerge(this, concurrency) }和flattenConcat()的区别就是,要展平的流之前是并行执行的:fun flat() { GlobalScope.launch { flowOf( flow { println("flow1") delay(1000) emit(10) }, flow { println("flow2") delay(2000) emit(20) }, flow { println("flow3") emit(30) } ).flattenMerge().collect { println("flat collect: $it") } } }看下输出结果:当flow1和flow2调用delay挂起就去执行flow3,几个流之间相互都是并行执行的。不过flattenMerge方法有个参数concurrency执行并行执行的流的个数,默认16个:如果我们将上面的flattenMerge(2)传入2看下执行效果:可以看到,有限flow1和flow2代码块中的逻辑处理完再执行flow3的代码块逻辑。还有个flatMapMerge()方法,展平的时候可以变换发送的数据类型,这里不做过多介绍,就是map()和flattenMerge的结合。buffer()背压支持public fun <T> Flow<T>.buffer(capacity: Int = BUFFERED, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND): Flow<T> { // create a flow return when (this) { is FusibleFlow -> fuse(capacity = capacity, onBufferOverflow = onBufferOverflow) else -> ChannelFlowOperatorImpl(this, capacity = capacity, onBufferOverflow = onBufferOverflow) } }这个扩展函数主要是解决上游流的发送速度超过下游流的处理速度的场景,有点类似与rxjava中的背压。并且提供了三种策略:挂起、丢弃最新、丢弃最久:fun flat() { GlobalScope.launch { flow { println("flow2") for (i in 0..100) { emit(i) } }.buffer(1, BufferOverflow.DROP_OLDEST).collect { println("flat collect: $it") } } }指的BufferOverflow.DROP_OLDEST丢弃最旧模式的运行结果:
0
0
0
浏览量404
德州安卓

你需要懂的Kotlin开发技巧之十

string.xml资源id快速转换String定义如下扩展属性:val Int.resToString: String get() = MainApp.mApplication.getString() 使用起来就很方便了:R.string.xxx.resToStringView隐现设置平常设置View显隐的方式this.visibility = View.xxx有点麻烦,封装下:fun View.hide() { this.visibility = View.GONE } fun View.show() { this.visibility = View.VISIBLE }使用起来:view.hide()、view.show()。我们项目中还有根据判断条件设置View显隐的逻辑,针对这个再封装下:fun View.showIf(isShow: Boolean) { if (isShow) { show() } else { hide() } }设置View背景颜色先直接封装下面两个扩展属性:将String类型的颜色值转换成Int值val String.color: Int get() = Color.parseColor(this)2.快捷设置View背景inline var View.bgColor: String get() = "" set(value) { setBackgroundColor(value.color) }最后就可以这样使用了:mBinding.root.bgColor = "#00ff00"::成员引用之前写的一篇文章你需要懂的Kotlin开发技巧之三有提到怎么判断一个lateinit var的属性有没有初始化使用的是:this::age.isInitialized,这个::是什么呢?这个其实和java中的成员引用差不多。data class Student(@JvmField val name1: String = "") { fun getName() = name1 }引用属性 比如Student::name1这种就是引用属性,我们看下Student::name1是个什么东东:是一个KProperty1类型的属性,泛型中的第一个类型代表当前属性所在的类,第二个类型代表属性值的类型,有点类似于属性反射,需要传入Student才能拿到name1的内容:我们看下KProperty1这个类:KProperty1提供了get方法帮助我们获取到值,再看下它的父类对象:继续看下父类的父类:KCallable:这里面的属性是不是很熟悉,都是大家常用到的判断一个属性是否为抽象、是否可被子类重写、是否是延迟初始化属性等等如果是在类内部引用类的属性,比如this::xxx,返回的类型将是一个Kproperty0类型:我们熟悉的isInitialized判断属性是否初始化的就是Kproperty0的一个扩展方法:引用方法 方法引用这个使用起来也很普遍,我在之前文章基于BRVAH和RecycleView封装适配器时就使用到了,直接将其转换成函数类型作为构造参数进行传递。我们看下Student::getName通过类引用方法的是个什么东西:可以看到这就是一个KFunction1对象,如果引用的方法中带有一个参数呢:就变成了KFunction2了:也就是说,随着方法的参数增加,::方法引用的类型值也在不断变化KFunction数字,但是不管类型怎么变化,都会实现同一个接口KFunction:是不是很熟悉,我们可以通过这个来判断方法是否为内联方法、中缀方法、运算符重载方法、协程挂起方法等等。而且KFunction也实现了KCallable,我们也可以用来判断方法是否抽象、open的等等总结关于kotlin相关的知识系列陆陆续续已经写了十篇文章了,基本上总结了Kotlin使用的过程中一些注意点以及使用技巧,这里就不粘贴其他的文章链接了,大家有兴趣可以关注下下面的专栏:Kotlin演义。后续就计划准备研究下Sequence及其内部机制、Kotlin反射元编程等相关知识了。
0
0
0
浏览量716
德州安卓

你需要懂的Kotlin开发技巧之八

字符串模板:日志打印日常在java中打印日志会这样写:Log.i("ChapterActivity", "onCreate: result = " + result + ", param = " + param);但是通过+拼接字符串的行为比较低效,会间接创建不少的中间String对象。一般如果是使用很多个+拼接的字符串的行为,是建议使用StringBuild代替。 到了Kotlin这里更是提供了字符串模板简化操作:Log.i("ChapterActivity", "onCreate: result = $result, param = $param")当然本质上还是通过+拼接的字符串。而且kotlin的字符串模板中可以执行其他的逻辑操作,比如return:fun test2(name: String?) { Log.i("ChapterActivity", "test2: ${name ?: return}") }虽然可以这样操作,但是在日志中执行非打印信息的行为在我看来是不可取的,主要是有两个原因:可能创建无用对象 反编译下上面的代码:首先创建了一个StringBuild对象,当name不为null打印日志。但是当name为null的时,这个创建的StringBuild对象就没被用到,反而增加了内存开销不利于维护日志中存在控制代码逻辑的执行会增加维护成本,开发者后续维护时可能会忽略日志,导致后续可能发生异常。运算符重载for in遍历集合是一个非常普遍的行为:fun test2() { val list = listOf("tom", "lily", "meg", "joe") for (item in list) { } }反编译成java代码:可以看到for in的实现就是个迭代器Iterator,并实现其hasNext和next方法。所以我们也可以自定义个迭代器实现for in行为:比如打印当前类及其父类的所有public方法名称输出:非常的简单方便,自定义迭代器Iterator的行为在kotlin集合的扩展函数和sequence中非常普遍,大家有兴趣可以自定看下源码。vararg可变参数这个就类似于java中的XXX...,比如String...fun test5(vararg names: String) { println(names.size) } test5("和", "的", "吧", "啊")如果把vararg转换成list集合,需要添加个*:val list = listOf(*names)我们可以看下vararg的Java源码:可以看到vararg names: String就等价于于java的String...,使用的时候就是把vararg转换成了一个数组而已。
0
0
0
浏览量1101
德州安卓

Kotlin开发实践之一

1.正确使用lazy委托val vm: String by lazy { "" }这种使用默认是加锁实现的,并发环境下使用没什么问题, 不过在非并发环境中比如Android主线程中未免会造成性能开销。看下lazy的实现源码:public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) }lazy懒加载支持三种模式:LazyThreadSafetyMode.SYNCHRONIZED:添加同步锁,使lazy延迟初始化线程安全LazyThreadSafetyMode.PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值LazyThreadSafetyMode.NONE:非加锁实现,非线程安全所以我们可以对lazy二次封装下,暴露出线程安全的lazy和不安全的lazy,根据具体的使用场景选择对应的方法调用 //线程安全 fun <T> safeLazy(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.SYNCHRONIZED, initializer) //非线程安全 fun <T> unSafeLazy(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE, initializer)2.View.postDelay()优化使用日常开发中,我们一般会使用View.postDelay()获取view的宽高信息,更可以在onResume中粗略估算界面绘制耗时:mBinding.root.postDelayed({ println("size: ${mBinding.root.measuredHeight}") }, 1000) 但是这个原生的postDelayed写起来特别别扭,因为它的Runnable是放在第一个参数中的,不能完全利用kotlin编写lamdba的优势。我们可以在此基础上定义个View的postDelayed的重载扩展函数,将Runnable放到方法参数的最后一个位置: fun View.postDelayed(delayMillis: Long, runnable: Runnable) { postDelayed(runnable, delayMillis) }然后就可以这样使用:mBinding.root.postDelayed(1000L) { println("size: ${mBinding.root.measuredHeight}") }3.dp与px之间快捷转换平常我们会定义工具类方法实现dp和px之间的转换,这里我们以dp转px举例:fun dp2px(dpValue: Float): Float { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, MainApp.mApplication.resources.displayMetrics) } //使用 val value = dp2px(10f) 这样使用起来还是不便捷,我们可以利用kotlin属性的getter进一步封装:val Float.dp: Float get() = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, this, MainApp.mApplication.resources.displayMetrics )然后直接调用10f.dp就完成了10dp到px的转换4.arrayOf、intArrayOf的区别使用fun main() { val test1 = arrayOf(2, 4, 5) val tet2 = intArrayOf(4, 6, 7) }反编译成java代码:public static final void main() { Integer[] var10000 = new Integer[]{2, 4, 5}; int[] var2 = new int[]{4, 6, 7}; }使用arrayOf的数组元素都使用的基本类型的包装类型,使用intArrayOf的数组元素则是使用的基本数据类型,减少了包装类带来的性能开销。其他类型诸如float、byte等都和这个相同,所以大家平常编码构造基本数据类型数组时避免使用arrayOf,应选用对应的intArrayOf、byteArrayOf等等
0
0
0
浏览量2022
德州安卓

这些flow常见API的使用,你一定需要掌握!(一)

collect通知flow执行public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit = collect(object : FlowCollector<T> { override suspend fun emit(value: T) = action(value) })flow是冷流,只有调用collect{}方法时才能触发flow代码块的执行。还有一点要注意,collect{}方法是个suspend声明的方法,需要在协程作用域的范围能调用。除此之外,collect{}方法的参数是一个被crossinline修饰的函数类型,旨在加强内联,禁止在该函数类型中直接使用return关键字(return@标签除外)。 fun main() { GlobalScope.launch { flow { emit("haha") }.collect { } } }launchIn()指定协程作用域通知flow执行public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch { collect() // tail-call }这个方法允许我们直接传入一个协程作用域的参数,不需要直接在外部开启一个协程执行。本质上就是使用我们传入的协程作用域手动开启一个协程代码块调用collect{}通知协程执行。这里看官方的源码有个tail-call的注释,也就是尾调用的意思,猜测这里可能官方会在这里进行了优化,减少了栈中方法调用的层级,降低栈溢出的风险。fun main() { flow { emit("haha") }.launchIn(GlobalScope) } catch{}捕捉异常public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> = flow { val exception = catchImpl(this) if (exception != null) action(exception) }这个就是用来捕捉异常的,不过注意,只能捕捉catch()之前的异常,下面来个图阐述下:即,只能捕捉第一个红框中的异常,而不能捕捉第二个红框中的异常。merge()合流public fun <T> merge(vararg flows: Flow<T>): Flow<T> = flows.asIterable().merge()最终的实现类如下:请注意,这个合流的每个流可以理解为是并行执行的,而不是后一个流等待前一个流中的flow代码块中的逻辑执行完毕再执行,这样做的目的可以提供合流的每个流的执行效果。测试代码如下:fun main() { GlobalScope.launch { merge(flow { delay(1000) emit(4) }, flow { println("flow2") delay(2000) emit(20) }).collect { println("collect value: $it") } } }输出日志如下:map{}变换发送的数据类型public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value -> return@transform emit(transform(value)) }这个api没什么可将的,很多的地方比如集合、livedata中都有它的影子,它的作用就是将当前数据类型变换成另一种数据类型(可以相同)。fun main() { GlobalScope.launch { flow { emit(5) }.map { "ha".repeat(it) }.collect { println("collect value: $it") } } }总结本篇文章介绍了flow常见的api,接下来还会有一些列文章用来介绍flow的其他api,感谢阅读。
0
0
0
浏览量2017
德州安卓

Kotlin的inline、noinline、crossinline全面分析2

前言前面我们说了inline关键字,不多说它很好用,# Kotlin的inline、noinline、crossinline全面分析1它还有2个好兄弟noinline和crossinline,我们继续来分析。正文noinline作为inline的反义,字面意思就是不内联,那和普通函数不就一样了? 当然不是,inline是用来修饰函数的,而这个noinline只能修饰函数的参数,也就是函数是内联的,但是这个函数类型参数不是内联的。既然知道了这个定义,我们来看看它有什么用。noinline话不多说,直接看个例子://函数是内联的,但是参数action不是内联的 inline fun lambdaFun(noinline action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") action() Log.i("zyh", "testLambdaFun: 调用后") }然后我们调用://调用 fun testHello(){ lambdaFun { Log.i("zyh", "testLambdaFun: 调用中") } }直接反编译://反编译代码 public final class TestFun { public final void testHello() { //创建了匿名内部类实例 Function0 action$iv = (Function0)null.INSTANCE; int $i$f$lambdaFun = false; Log.i("zyh", "testLambdaFun: 调用前"); action$iv.invoke(); Log.i("zyh", "testLambdaFun: 调用后"); } }这里我们清晰的看出lambdaFun内部的代码进行了复制铺平到调用地方,但是对于action却没有复制铺平,原因也非常简单,因为它是noinline修饰的,那它有什么用呢 我们接着分析。使用noinline的原因我们前面说了inline会让函数类型参数进行复制和铺平,那这个参数也就不再是函数类型参数了,毕竟它变成了几行代码,所以这就是局限性,当我们还要把它作为函数类型参数或者返回时,就要使用noinline了。还是直接看例子://定义高阶函数,非内联 fun lambdaFun1(action: () -> Unit){ action() }然后我们定义一个内联函数://内联函数 inline fun lambdaFun(action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") action() //调用高阶函数 lambdaFun1(action) Log.i("zyh", "testLambdaFun: 调用后") }这种使用很常见,但是我们会发现这段代码无法编译:原因也非常简单,想把action传递给lambdaFun1,那这个action必须函数类型参数,但是在被inline修饰的函数,其参数也会被铺平,也就不会再是函数类型了,所以这里的action要使用noinline来修饰://使用noinline修饰参数 inline fun lambdaFun(noinline action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") action() lambdaFun1(action) Log.i("zyh", "testLambdaFun: 调用后") }高阶函数除了把函数类型参数当做其他高阶函数的参数外,还可以作为返回值,同样这时也不能把函数类型参数给铺平为lambda表达式,我们看例子:这里我想返回这个action,遗憾的是,和前面一样,这个action会被铺平,将不再是函数类型参数了,所以必须把action用noinline修饰://因为返回值要使用action,要保留action为函数类型 inline fun lambdaFun(noinline action: (() -> Unit)):() -> Unit{ Log.i("zyh", "testLambdaFun: 调用前") action() Log.i("zyh", "testLambdaFun: 调用后") return action }上面的代码进行反编译,也能想象出,肯定函数内部会被内联,action会被编译成匿名内部类:public final void testHello() { Function0 action$iv = (Function0)null.INSTANCE; int $i$f$lambdaFun = false; Log.i("zyh", "testLambdaFun: 调用前"); //这里action不会被内联 action$iv.invoke(); Log.i("zyh", "testLambdaFun: 调用后"); }return难题inline和noinline说完,我们来说一个return问题,为啥return会有不一样呢 我们来慢慢细说。非内联高阶函数先定义一个高阶函数:fun lambdaFun(action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") action() Log.i("zyh", "testLambdaFun: 调用后") }然后进行调用,在lambda中想使用return语句:这里直接使用return语句无法使用,提醒使用return@lambdaFun,这当然可以理解,那我只想使用return呢 有没有办法,当然可以,把lambdaFun定义为内联函数。内联函数改成内联函数:inline fun lambdaFun(action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") action() Log.i("zyh", "testLambdaFun: 调用后") }然后再进行调用return语句:这里居然不报错了,由于内联函数会把函数体和lambda给复制铺平到调用地方,所以这里的return必然是返回testHello函数了,而不是lambdaFun函数。return和inline之间的约定所以这里为了解决lambda表达式中的return语句问题,Kotlin直接规定,在非inline函数中,return无法使用(必须return@xxx指明返回的函数),只有在inline函数中可以使用return语句,这样就不会有异议,根据inline的特性,这个return必然是返回调用者函数。crossinline既然我们了解了return问题,以及它return和inline之间的约定,也就是为了不产生歧义,那我们继续看问题,引出crossinline这个修饰词了。直接看例子,再定义一个高阶函数://新的函数,参数是函数类型 fun lambdaFun1(postAction: () -> Unit){ postAction() }然后在前面定义的函数中,调用这个函数://调用lambdaFun1,并且使用lambda inline fun lambdaFun(action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") lambdaFun1 { action() } Log.i("zyh", "testLambdaFun: 调用后") }千万注意,这里和前面说noinline的例子是不一样的,这里是使用lambda表达式,在表达式内调用action()也就是其invoke函数,而不是把action这个函数类型参数进行传递,和下面是不一样的:inline fun lambdaFun(action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") //这里期望把action还是当做类型参数,而不是铺平 lambdaFun1(action) Log.i("zyh", "testLambdaFun: 调用后") }上面要区分开,现在我们是讨论第一种情况,这个代码能编译吗,我们看一下:注意这里的报错信息:无法内联action参数,原因是它可能包含return语句。哦?为什么呢?原因非常简单,这里的lambdaFun是内联函数,但是lambdaFun1是非内联的,根据前面return和内联函数之间的约定,只有内联函数可以使用return,否则不行,那这就尴尬了,不可能我每个内联函数都必须调用内联函数吧,这显然不符合逻辑。突破限制,加强inline功能因为上面的代码,永远不知道action会不会包含return语句,所以inline受限严重,这里只有突破这个限制才可以,这里采用的方式是把action加个crossinline修饰符。然后会发现:这里居然不报错了,看似问题解决了,它内部又做了什么优化吗 这种又内联又不内联的,怕是要把编译器整疯了,当然不是,它只是解决提出问题的人。crossinline的作用crossinline的作用仅仅是当有被这个修饰的参数会告诉IDE来检查你写的代码中有没有包含return,假如有的话会编译不过,就是这么简单暴力。直接看例子对比一下,首先这里不加crossinline:inline fun lambdaFun(action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") lambdaFun1 { action() } Log.i("zyh", "testLambdaFun: 调用后") }调用的时候:可以正常写return语句,当我们对这个参数加了crossinlie时:inline fun lambdaFun(crossinline action: (() -> Unit)){ Log.i("zyh", "testLambdaFun: 调用前") lambdaFun1 { action() } Log.i("zyh", "testLambdaFun: 调用后") }再进行调用:直接就不让写了,哈哈,真的简单暴力。小结一下其实前面我们已经说完了crossinline出现的原因以及解决的问题,以及最直接暴力解决问题的办法,但是还是总结一下,这里什么情况会出现这种问题。也就是当在内联函数中,想把参数的执行放入其他函数中,这时这个其他函数和最外层调用这个内联函数的函数就分割开了,这时你就要注意了,这个参数是不是要用crossinline修饰了。当然这个我们不必刻意,当你代码逻辑有问题时,这个修饰符IDE会自动提醒。总结最后还是总结一下子,这3个关键字涉及的东西还真不少。为了解决每次调用高阶函数时给它传递lambda时都会创建匿名内部类的问题引入了inline。inline修饰的函数,不仅会把函数体的内容进行复制铺平,还会把函数类型参数的内容复制铺平。当在内联函数中想把某个函数类型的参数进行传递或者返回,这时就不能把这个参数给铺平,所以使用noinline修饰这个参数。在lambda中使用return语句会造成歧义,不知道返回哪一层,所以Kotlin直接定义非inline函数时不能使用return,当是inline函数时,return是返回调用着那一层函数。在内联函数中,调用非内联的高阶函数,把lambda传递到非内联函数中,将和上面造成冲突,无法判断这个参数的lambda中有没有return,为了解决这个问题,引入了crossinline。crossinline的目的是告诉IDE,这个参数的lambda不能写return语句。
0
0
0
浏览量331
德州安卓

Kotlin lambda,有你想了解的一切

前言在平时工作中,我们对于lambda表达式再熟系不过了,不过一直没有对lambda有个很明确的、系统的了解,那这篇文章就来仔细梳理一下。正文我们使用Kotlin时,比如给函数传递参数使用lambda,或者对集和处理时传入筛选条件也是lambda,可能你会认为这些代码再正常不过了,不过平时也没有仔细分析过,本篇文章就来系统介绍一下。Java8为什么要引入lambda在我们使用Java 8之前,是没有lambda这个概念的,比如我们想写个按钮监听回调只能实现下面这种:button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } });这种写起来太麻烦了,当使用Java8时,就可以简化为下面代码:button.setOnClickListener(v -> { });你或许觉得这种理所当然,但是Java8引入lambda却是很多人期望的。其实Java8引入lambda就是为了解决这种单方法的接口,让写匿名内部类的繁琐代码给优化、简洁一些。lambda的定义lambda表达式简称为lambda,首先它是一个表达式,何为表达式,和语句最明显的区别就是表达式是有值的,而语句没有。其次它就是一段代码,有固定的格式,这个很重要,格式如下:注意这里的语法,必须由花括号给括起来,参数没有小括号,注意和函数类型给区分开。Kotlin调用Java函数前面我们看了Java8对于单方法接口做的优化,但是我们很多代码都是Kotlin编写,而库的代码是Java的,所以Kotlin能调用上面代码很关键,比如下面例子://单方法的回调 使用Java定义一个单方法接口 interface SingleFunListener { public int singleFunction(int k); }//接口的实例 private SingleFunListener singleFunctionListener;//设置监听方法 public void setSingleFunctionListener(SingleFunListener singleFunctionListener){ this.singleFunctionListener = singleFunctionListener; }上面代码在Java中我们最熟悉了,现在我们来使用Kotlin调用上面代码://使用匿名内部类 val testJava = TestJava() testJava.setSingleFunctionListener(object: SingleFunListener{ override fun singleFunction(k: Int): Int { return 100 } })由于Kotlin可以使用高阶函数,可以把函数当做值来对待,所以我们可以定义一个函数,来当做参数://使用函数式编程 val listener:(Int) -> Int = { 100 } testJava.setSingleFunctionListener(listener)上面代码还可以进一步简化://使用lambda表达式 testJava.setSingleFunctionListener { k: Int -> 100 }注意这里使用高阶函数和lambda很像,但是是有区别的,后面再细说。Java函数式接口前面我们举例了lambda很好用,不过为什么Java定义的接口,kotlin或者Java 8可以使用lambda来替代匿名内部类的写法呢当定义的接口只有一个抽象方法时,这种接口叫做函数时接口,或者叫做SAM接口,SAM代表抽象方法。比如Java中的Runnable、Callable接口等。Kotlin允许在调用函数式接口作为参数时使用lambda,来保证代码的整洁。注意,kotlin是拥有完全的函数类型,所以需要接送lambda作为参数的kotlin函数应该使用函数类型而不是函数式接口类型。所以kotlin不支持把lambda自动转换成实现kotlin接口的对象,所以用kotlin写的SAM接口,无法使用lambda。函数式接口 VS 函数类型这里很有意思,对于SAM接口,Kotlin可以把lambda转换成对应的SAM接口实例,从而简化代码,这其实就是一个约定,但是当Kotlin代码定义SAM接口,将无法转换,因为Kotlin自己就有函数类型。我们直接看个例子://KT文件单方法接口 interface OneMethodListener{ fun onMethod(a:Int) } //接口实例 private var listener:OneMethodListener? = null //设置监听 fun setOnMethodListener(listener:OneMethodListener){ this.listener = listener }按理说,我这里调用setOnMethodListener时会提醒我们转换成lambda,我们看看:IDE并没有提醒我们可以改成lambda,这就是不支持约定,因为在Kotlin中,这种SAM接口可以使用高阶函数来实现,比如下面代码://高阶函数 private var onMethodListener:((Int) -> Unit)? = null fun setMethodListener(listener:((Int) -> Unit)?){ this.onMethodListener = listener }对于这种一样的功能的代码,我们调用时:这里就可以把lambda传递给函数类型的变量,所以这个关键点要记住,使用Kotlin的话,对于SAM接口,就可以不用定义了,可以简化代码。Kotlin为了让你这样做,也不让你直接把lambda转成Kotlin定义的SAM接口实例。高阶函数既然上面我们看到了定义的函数类型变量可以传递lambda,就必须要说一下啥是高阶函数。高阶函数其实很简单,就是参数是其他函数或者返回值是其他函数的函数,那这里如何来传递一个函数呢 就是定义函数类型的变量。比如上面的代码:private var onMethodListener:((Int) -> Unit)? = null这里的类型就是函数类型,但是这里最最关键的是Kotlin中,高阶函数可以用lambda来表示,这也是约定,所以期望传递的是函数类型的参数,可以用lambda来代替。在作用域中访问变量在Java中,我们在方法中使用匿名内部类或者lambda是无法直接使用方法的局部变量,比如下面代码:这里不能访问和修改的原因很简单,这里lambda其实原理也就是匿名内部类,根据Java的生命周期我们知道引用类型对象的回收是通过GC来控制,但是方法的生命周期在当方法执行完便会介绍,所以如果在lambda表达式或者匿名内部类中调用方法的局部变量是不允许的。但是在Kotlin中,Kotlin允许在lambda内部访问方法的非final变量甚至修改它们,比如下面代码://这里在lambda中访问tempString可以正常访问且修改 val testJava = TestJava() var tempString = "zyh" testJava.setSingleFunctionListener { tempString = "wy" 100 }看到这里是不是感觉很奇怪,Kotlin是如何做到的呢。捕捉不管是Java还是Kotlin,如果从lambda中访问外部变量,我们就称为这些变量被lambda捕捉,比如上面的tempString变量。为什么要捕捉呢 原因也就是变量的生命周期问题,如果变量被lambda捕捉了,使用这个变量的代码可以被存储并稍后再执行。其实也就是这个变量的保存地方改变了,函数生命周期正常运行。当捕捉final变量时,它的值和使用这个值的lambda代码一起存储,而对于非final变量来说,它的值可以在lambda中进行修改,这时需要把它的值封装在一个特色的包容器中,这样就可以改变这个值,而对于包容器的引用会和lambda代码一起存储。其实捕捉的原理非常容易,对于不可修改的变量,就直接把它提出来到和lambda一起保存,对于可修改的变量进行封装,这样引用的回收就是由GC处理,和匿名内部类一样了,就解决问题了。Kotlin捕捉既然了解了捕捉的原理,那Kotlin的捕捉只不过把Java中的一些实现细节给优化了,比如捕捉val变量时,它的值会被拷贝下来,当捕捉var变量时,它的值会被作为Ref类的一个实例被保存下来。传递lambda参数给Java的SAM类型参数的函数的原理为什么要说这个呢 前面我们说了kotlin是有函数类型的,所以对于Java的SAM接口作为函数时,Kotlin可以传递给lambda给他,但是原理是啥,其实很简单Kotlin把lambda转换为了对应SAM接口的实例。不过这里和一般的匿名内部类写法有点区别,它不是每次都创建一个实例,类似于在Java中创建一个匿名内部类的变量,然后反复使用这个变量。//这里Kotlin调用Java SAM参数函数使用lambda val testJava = TestJava() testJava.setSingleFunctionListener { 100 }上面代码等效于下面代码:val singleListener : SingleFunListener = object: SingleFunListener{ override fun singleFunction(k: Int): Int { return 100 } } testJava.setSingleFunctionListener(singleListener)可以看出,当没有捕捉变量时,lambda可以少创建几个接口实例变量。但是当捕捉了变量,这个就发生了变化,就变成了新实例,原因非常简单,它需要把捕捉的变量给放到自动生成的类中。这里的原理由于是编译器做的,前面也说了Kotlin捕捉的原理,它类似如下://这里捕捉了tempString变量 val testJava = TestJava() var tempString = "zyh" testJava.setSingleFunctionListener { tempString = "wy" 100 }由于lambda捕捉的变量在不同函数中是不一样的,所以编译器自动生成的实例也不一样,上面类似于下面://这里捕捉了tempString val singleListener : SingleFunListener$1 = object: SingleFunListener$1(var tempString: String){ override fun singleFunction(k: Int): Int { tempString = "wy" return 100 } } testJava.setSingleFunctionListener(singleListener)如果在另一个函数中捕捉了其他变量,那这个生成的接口就会不一样,所以会有多个实例。注意上面说的lambda转换成SAM接口,仅仅适用于kotlin调用Java的函数式方法时的处理,对于其他的,我们后面再说。SAM构造方法前面都是说的SAM接口作为函数参数,Kotlin为了方便可以传递lambda给他,但是当返回值是SAM接口时,这时需要通过lambda创建出一个SAM接口的实例,这时可以使用SAM构造方法。这里直接看个例子://单方法的回调,这里返回Runnable类型的实例 interface SingleFunListener { public Runnable singleFunction(int k); } //设置方法 public void setSingleFunctionListener(SingleFunListener singleFunctionListener){ this.singleFunctionListener = singleFunctionListener; }这里的代码我们使用匿名内部类很好解决,就创建一个类实现Runnable接口即可:testJava.setSingleFunctionListener(object: SingleFunListener{ override fun singleFunction(k: Int): Runnable { return Runnable { Logger.d("zyh") } } })但是优雅的lambda怎么可能还会让你写这么多代码呢,可以简化为下面://直接把lambda传递给SAM接口 testJava.setSingleFunctionListener { Runnable { Logger.d("zyh") } }这里也是一个约定了,Runnable是个接口,它肯定没有构造方法之说。Java使用函数类型其实前面我们说了函数类型和lambda的区别,还说了Kotlin调用Java的SAM接口时会把lambda转成对于的SAM接口的实例,那对于Kotlin中定义的函数类型参数在Java中如何调用呢//Kotlin代码定义的高阶函数 private var onMethodListener:((Int) -> Unit)? = null fun setMethodListener(listener:((Int) -> Unit)?){ this.onMethodListener = listener }然后我们在Java代码中调用这个函数:public void runTestJava(){ TestLambda testLambda = new TestLambda(); //调用函数类型参数的方法 testLambda.setMethodListener(new Function1<Integer, Unit>() { @Override public Unit invoke(Integer integer) { return null; } }); }会发现这里把高阶函数使用Function1这个接口实例来替换,其实到这里也就明了了,Kotlin的高阶函数其实就是FunctionN的接口的实现,每个接口都有一个invoke方法,调用invoke方法可以调用函数自己。 /** A function that takes 0 arguments. */ public interface Function0<out R> : Function<R> { /** Invokes the function. */ public operator fun invoke(): R } /** A function that takes 1 argument. */ public interface Function1<in P1, out R> : Function<R> { /** Invokes the function with the specified argument. */ public operator fun invoke(p1: P1): R } //.... /** A function that takes 22 arguments. */ public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> { /** Invokes the function with the specified arguments. */ public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R } 会发现这里最多只能传入22个参数,哈哈,不过一般也不会传递这么多,这就是高阶函数的原理,其实也非常简单,对把你定义的高阶函数都变成FunctionN接口的一个实现。集合的函数式API前面我们说了Kotlin高阶函数的实现原理,那现在来说一个我们平时代码最常用的功能,就是集和的函数式API。方法很多,这里就以filter为例://使用代码 val ints = arrayListOf(1, 2, 3, 4, 5) val newInts = ints.filter { it > 2 }//源码 public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> { return filterTo(ArrayList<T>(), predicate) }我们查看源码会发现,首先它是一个扩展函数,然后参数predicate是函数类型,所以可以传递lambda,而且返回值必须是Boolean类型,这个由我们之前的知识都了解,注意这里会多个inline,来关注一下这个inline。内联函数被inline修饰的函数叫做内联函数,上面的filter为什么要设置为内联呢前面我们可知函数类型其实就是FunctionN接口的一个实例,所以filter传递的lambda会被编译成匿名内部类,看起来没啥问题,我们再看一下filter的实现://这里遍历集和,每个元素都调用一次函数类型参数 public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C { for (element in this) if (predicate(element)) destination.add(element) return destination }这里就有问题了,当集和有100个元素时,它会创建100个匿名内部类,这就有很严重的性能问题了,所以这里引入了内联函数的概念。一句话说完,内联函数会把函数的实现给拷贝到调用的地方,这样就不会创建额外的匿名内部类实例了。带接收者的lambda说到这里,我们来想一个问题,就是在lambda中使用this,这个this会指向什么东西 在编译器来看,lambda是一个代码块,所以this会指向是包围它的类,这里就介绍一个kotlin的lambda独特的功能,带接收者的lambda,在这个lambda中我们的this和it就指向了其他类实例。比如代码:val ints = arrayListOf(1, 2, 3, 4, 5) ints.apply { this.add(6) }这里就是apply和with函数,对于这2个函数我们再熟系不过了,不过为什么在这个lambda内部可以使用this呢,或许你没有注意过,我们看一下源码:public inline fun <T> T.apply(block: T.() -> Unit): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } block() return this }会发现这里定义的函数很普通,但是有一点区别,就是block的类型,它的类型是 T.() -> Unit,而不是普通的() -> Unit,这里就是Kotlin的独特地方,这里就定义了函数类型的参数它有了一个接受者,可以在lambda中使用this来访问这个接受者。比如上面的apply源码中,类型参数T就是接受者,而且在lambda中可以访问该接受者的属性和方法。总结其实lambda的知识还是蛮多的,很多细节都需要注意,他们是Kotlin能写出简介代码的关键。我们来简单回顾一些细节:Java 8的lambda引入是为了解决Java单方法接口作为参数写匿名内部类的繁琐步骤。单方法的接口被叫做SAM接口或者函数式接口。Kotlin调用Java的SAM接口参数传递lambda会被转换成对应的SAM接口的实例。Kotlin可以直接使用SAM接口的构造函数来返回一个SAM接口类型的实例。Java的匿名内部类或者lambda只能捕捉final变量,Kotlin都可以捕捉且修改。Kotlin的高阶函数可以用lambda来表示,它是FunctionN接口的一个实现。Kotlin拥有完整的函数类型,所以SAM接口在Kotlin中不用定义。lambda在集和中使用非常多,为了减少lambda编译成匿名内部类情况造车的性能损耗,引入了内联函数的概念。lambda就是一个代码块,默认其内部的this是指外面包括它的类,通过定义带接受者的lambda可以在lambda中访问其他对象的方法。
0
0
0
浏览量1186
德州安卓

你需要懂的Kotlin开发技巧之四

1.@JvmName修改方法名直接看例子: @JvmName("testCopy") fun test(name: String, age: Int) { }直接反编译成java代码看下:最终生成的方法名称就是testCopy而不是test2.@get:JvmName、@set:JvmName修改属性名@get: JvmName("getSource") @set: JvmName("setSource") var mData: String = ""直接反编译成java代码看下:对于val属性则只能使用@get: JvmName3.String判空使用isNullOrEmpty,避免TextUtils.isEmpty()我们分别对比下这两种写法:可以看到,使用isNullOrEmpty判空的String,在调用name.length不会报错,使用TextUtils.isEmpty()的则会报错,需要强制声明name!!不为空`为什么第一种不会报错呢,看下isNullOrEmpty的源码:@kotlin.internal.InlineOnly public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 }最核心的就是isNullOrEmpty方法体中有个contract,这个会帮助编译器告知String是否为空,以至于当调用name.length时编译器能推断出String不是空的,就不需要程序强制声明name!!非空4.忽略大小写比较equals一般的大小写比较如下:val res = "aa".toLowerCase(Locale.ROOT) == "AA".toLowerCase(Locale.ROOT)反编译成java代码看下:可以看到,"aa".toLowerCase(Locale.ROOT)赋值给一个局部变量var6,"AA".toLowerCase(Locale.ROOT)赋值给另一个局部变量var7,然后再进行比较。这种方式的比较会额外创建两个局部String变量,所以建议使用equals替换,其中第二个参数可以指定忽略大小写比较val res = "aa".equals("AA", ignoreCase = true)反编译:可以看到,实现非常的简单,不会创建额外的局部变量5.运算符重载get、setgetclass Pro { operator fun get(content: String): String { return content.repeat(10) } }然后就可以这样使用:println(Pro()["blue"])setoperator fun set(key: String, value: String) { }set运算符重载至少需要传入两个参数,使用如下:Pro()["key"] = "value"这两个运算符使用场景非常多,比如针对于Android中SharedPreference读写封装,具体详情可以参考文章:三.委托与SharedPreference的结合还有很多其他的运算符重载函数,比如plus对应"+"、contains对应"in"等等,都是日常开发中比较常用的,大家可以根据具体场景灵活运用
0
0
0
浏览量1982
德州安卓

Kotlin常用的by lazy你真的了解吗

前言在使用Kotlin语言进行开发时,我相信很多开发者都信手拈来地使用by或者by lazy来简化你的属性初始化,但是by lazy涉及的知识点真的了解吗 假如让你实现这个功能,你会如何设计。正文话不多说,我们从简单的属性委托by来说起。委托属性什么是委托属性呢,比较官方的说法就是假如你想实现一个比较复杂的属性,它们处理起来比把值保存在支持字段中更复杂,但是却不想在每个访问器都重复这样的逻辑,于是把获取这个属性实例的工作交给了一个辅助对象,这个辅助对象就是委托。比如可以把这个属性的值保存在数据库中,一个Map中等,而不是直接调用其访问器。看完这个委托属性的定义,假如你不熟悉Kotlin也可以理解,就是我这个类的实例由另一个辅助类对象来提供,但是这时你可能会疑惑,上面定义中说的支持字段和访问器是什么呢,这里顺便给不熟悉Kotlin的同学普及一波。Java的属性当你定义一个Java类时,在定义字段时并不是所有字段都是属性,比如下面代码://Java类 public class Phone { //3个字段 private String name; private int price; private int color; //name字段访问器 private String getName() { return name; } private void setName(String name){ this.name = name; } //price字段访问器 private int getPrice() { return price; } private void setPrice(int price){ this.price = price; } }上面我在Phone类中定义了3个字段,但是只有name和price是Phone的属性,因为这2个字段有对应的get和set,也只有符合有getter和setter的字段才叫做属性。这也能看出Java类的属性值是保存在字段中的,当然你也可以定义setXX函数和getXX函数,既然XX属性没有地方保存,XX也是类的属性。Kotlin的属性而对于Kotlin的类来说,属性定义就非常简单了,比如下面类:class People(){ val name: String? = null var age: Int? = null }在Kotlin的类中只要使用val/var定义的字段,它就是类的属性,然后会自带getter和setter方法(val属性相当于Java的final变量,是没有set方法的),比如下面:val people = People() //调用name属性的getter方法 people.name //调用age属性的setter方法 people.age = 12这时就有了疑问,为什么上面代码定义name时我在后面给他赋值了即null值,和Java一样不赋值可以吗 还有个疑问就是在Java中是把属性的值保存在字段中,那Kotlin呢,比如name这个属性的值就保存给它自己吗带着问题,我们继续分析。Kotlin属性访问器前面我们可知Java中的属性是保存在字段中,或者不要字段,其实Kotlin也可以,这个就是给属性定义自定义setter方法和getter方法,如下代码:class People(){ val name: String? = null var age: Int = 0 //定义了isAbove18这个属性 var isAbove18: Boolean = false get() = age > 18 }比如这里自定义了get访问器,当再访问这个属性时,便会调用其get方法,然后进行返回值。Kotlin属性支持字段field这时一想那Kotlin的属性值保存在哪里呢,Kotlin会使用一个field的支持字段来保存属性。如下代码:class People{ val name: String? = null var age: Int = 0 //返回field的值 get() = field //设置field的值 set(value){ Log.i("People", "旧值是$field 新值是$value ") field = value } var isAbove18: Boolean = false get() = age > 18 }可以发现每个属性都会有个支持字段field来保存属性的值。好了,为了介绍为什么Kotlin要有委托属性这个机制,假如我在一个类中,需要定义一个属性,这时获取属性的值如果使用get方法来获取,会在多个类都要写一遍,十分不符合代码设计,所以委托属性至关重要。委托属性的实现在前面说委托属性的概念时就说了,这个属性的值需要由一个新类来代理处理,这就是委托属性,那我们也可以大概猜出委托属性的底层逻辑,大致如下面代码:class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //email属性进行委托,把它委托给ProduceEmail类 var email: String by ProduceEmail() }假如People的email属性需要委托,上面代码编译器会编译成如下:class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //委托类的实例 private val productEmail = ProduceEmail() //委托属性 var email: String //访问器从委托类实例获取值 get() = productEmail.getValue() //设置值把值设置进委托类实例 set(value) = productEmail.setValue(value) }当然上面代码是编译不过的,只是说一下委托的实现大致原理。那假如想使ProduceEmail类真的具有这个功能,需要如何实现呢。by约定其实我们经常使用 by 关键字它是一种约定,是对啥的约定呢 是对委托类的方法的约定,关于啥是约定,一句话说明白就是简化函数调用,具体可以查看我之前的文章:# Kotlin invoke约定,让Kotlin代码更简洁那这里的by约定简化了啥函数调用呢 其实也就是属性的get方法和set方法,当然委托类需要定义相应的函数,也就是下面这2个函数://by约定能正常使用的方法 class ProduceEmail(){ private val emails = arrayListOf("111@qq.com") //对应于被委托属性的get函数 operator fun getValue(people: People, property: KProperty<*>): String { Log.i("zyh", "getValue: 操作的属性名是 ${property.name}") return emails.last() } //对于被委托属性的get函数 operator fun setValue(people: People, property: KProperty<*>, s: String) { emails.add(s) } }定义完上面委托类,便可以进行委托属性了:class People{ val name: String? = null var age: Int = 0 val isAbove18: Boolean = false //委托属性 var email: String by ProduceEmail() }然后看一下调用地方:val people = People() Log.i("zyh", "onCreate: ${people.email}") people.email = "222@qq.com" Log.i("zyh", "onCreate: ${people.email}")打印如下:会发现每次调用email属性的访问器方法时,都会调用委托类的方法。关于委托类中的方法,当你使用by关键字时,IDE会自动提醒,提醒如下:比如getValue方法中的参数,第一个就是接收者了,你这个要委托的属性是哪个类的,第二个就是属性了,关于KProperty不熟悉的同学可以查看文章:# Kotlin反射全解析3 -- 大展身手的KProperty它就代表这属性,可以调用其中的一些方法来获取属性的信息。而且方法必须使用operator关键字修饰,这是重载操作符的必须步骤,想使用约定,就必须这样干。by lazy的实现由前面明白了by的原理,我们接着来看一下我们经常使用的by lazy是个啥,直接看代码://这里使用by lazy惰性初始化一个实例 val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { DataStoreManager(store) }比如上面代码,使用惰性初始化初始了一个实例,我们来看一下这个by的实现://by代码 @kotlin.internal.InlineOnly public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value哦,会发现它是Lazy类的一个扩展函数,按照前面我们对by的理解,它就是把被委托的属性的get函数和getValue进行配对,所以可以想象在Lazy< T >类中,这个value便是返回的值,我们来看一下://惰性初始化类 public interface Lazy<out T> { //懒加载的值,一旦被赋值,将不会被改变 public val value: T //表示是否已经初始化 public fun isInitialized(): Boolean }到这里我们注意一下 by lazy的lazy,这个就是一个高阶函数,来创建Lazy实例的,lazy源码://lazy源码 public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) }这里会发现第一个参数便是线程同步的模式,第二个参数是初始化器,我们就直接看一下最常见的SYNCHRONIZED的模式代码://线程安全模式下的单例 private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable { private var initializer: (() -> T)? = initializer //用来保存值,当已经被初始化时则不是默认值 @Volatile private var _value: Any? = UNINITIALIZED_VALUE //锁 private val lock = lock ?: this override val value: T //见分析1 get() { //第一次判空,当实例存在则直接返回 val _v1 = _value if (_v1 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") return _v1 as T } //使用锁进行同步 return synchronized(lock) { //第二次判空 val _v2 = _value if (_v2 !== UNINITIALIZED_VALUE) { @Suppress("UNCHECKED_CAST") (_v2 as T) } else { //真正初始化 val typedValue = initializer!!() _value = typedValue initializer = null typedValue } } } //是否已经完成 override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." private fun writeReplace(): Any = InitializedLazyImpl(value) }分析1:这个单例实现是不是有点眼熟,没错它就是双重校验锁实现的单例,假如你对双重校验锁的实现单例方式还不是很明白可以查看文章:# Java双重校验锁单例原理 赶快看进来这里实现懒加载单例的模式就是双重校验锁,2次判空以及volatile关键字都是有作用的,这里不再赘述。总结先搞明白by的原理,再理解by lazy就非常好理解了,虽然这些关键字我们经常使用,不过看一下其源码实现还是很舒爽的,尤其是Kotlin的高阶函数的一些SDK写法还是很值的学习。
0
0
0
浏览量1217
德州安卓

Kotlin开发技巧与实践精粹

涵盖Kotlin的实际项目应用和高级开发技巧,为开发者提供从基础到高级的全面指导,助力提升在实际项目中的Kotlin应用效率和质量
0
0
0
浏览量2121
德州安卓

Kotlin泛型全解析

本专栏不仅涵盖泛型的基础知识,还深入探讨了泛型在实际编程中的应用,如类型约束、型变与逆变、泛型函数与类等。此外,我们还会通过案例分析,让读者理解泛型如何优化代码结构,提高代码的复用性和安全性。
0
0
0
浏览量2110
德州安卓

Kotlin演义

专注于Kotlin常见开发必备知识
0
0
0
浏览量2064
德州安卓

Kotlin集合函数深度解析

本专栏详细讲解Kotlin中常用的集合函数,通过清晰的示例和易懂的解释,帮助读者掌握集合操作的核心技巧,提升数据处理能力
0
0
0
浏览量2043
德州安卓

协程(18) | 扒一扒挂起函数的扩展函数:startCoroutine{}

前言在前面文章中,我们重点分析了挂起函数的原理,包括知识点有:挂起函数内部其实就是CPS加状态机的模型,Continuation类似于Callback,即可以用于实现挂起函数向挂起函数外传递数据,也可以使用匿名内部类方式接收挂起函数返回值,最后就是创建挂起函数的最底层函数suspendCoroutineUninterceptedOrReturn方法其实就是为了实现状态机逻辑,同时消除suspend关键字。上面知识点务必要清晰,如果不明白的可以查看本系列文章专栏,本篇文章就开始协程原理部分。正文这里我们又回到了Continuation.kt这个文件,因为这是协程框架的基础元素,上一篇文章我们介绍了创建挂起函数的俩个高阶函数就是这个类中的基础层API,除此之外,在这个类,还有启动协程的基础API。协程启动的基础API什么是基础API呢?其实我们前面所说的启动协程的方法,比如launch、async都是属于上层或者中间层API,它们都是调用了基础API。既然这么重要,我们就来看看://创建协程 public fun <R, T> (suspend R.() -> T).createCoroutine( receiver: R, completion: Continuation<T> ): Continuation<Unit> = SafeContinuation(createCoroutineUnintercepted(receiver, completion).intercepted(), COROUTINE_SUSPENDED) //启动协程 public fun <T> (suspend () -> T).startCoroutine( completion: Continuation<T> ) { createCoroutineUnintercepted(completion).intercepted().resume(Unit) } 这里可以发现createCoroutine{}和startCoroutine{}都是扩展函数,而且扩展的接收者类型是(suspend () -> T)。或许我们经常给一些常用的类型添加扩展函数,但是几乎没有干过给函数类型添加扩展函数。既然Kotlin中,函数作为一等公民,我们给它添加扩展函数也是可以的。那我们如何调用上面扩展函数呢?测试代码如下:fun main(){ Thread.sleep(2000L) testStartCoroutine() } /** * 这里的block类型是"suspend () -> String" * * 这里我们秉承 单方法接口 <--> 高阶函数 <--> lambda这种关系 * */ val block = suspend { println("Hello") delay(1000L) println("Kotlin") "Result" } /** * 这里调用了[startCoroutine]扩展函数,这个扩展函数是 suspend () -> T 的 * 扩展函数。 * * [Continuation]有2个作用,一个是实现挂起函数时用来向外传递数据;一个是以匿名 * 内部类的方式来接收一个挂起函数的值。 * */ private fun testStartCoroutine(){ val continuation = object : Continuation<String>{ override val context: CoroutineContext get() = EmptyCoroutineContext override fun resumeWith(result: Result<String>) { println("Result is ${result.getOrNull()}") } } block.startCoroutine(continuation) } 这里定义了变量名为block的lambda表达式,它的类型是suspend () -> String,因为lambda表达式最后一行是该lambda的返回值;同时在Kotlin中,高阶函数、单接口方法、lambda可以看成是一样的。然后定义了一个continuation变量,根据前一篇文章我们知道Continuation有2个作用:一种是在实现挂起函数的时候,用于传递挂起函数的执行结果;另一种是在调用挂起函数的时候,以匿名内部类的方式,接收挂起函数的执行结果。而上面代码的作用就是第二种,用来接收block的执行结果。这里的这种使用方法,就感觉像是给一个挂起函数设置了Continuation参数一样,根据前面CPS原理,我们知道每个挂起函数都需要一个Continuation参数追加到参数列表后,那这里真是这样吗?我们可以通过分析源码来解读一下。startCoroutine{}原理解析这里我们直接把上面代码进行反编译,可以得到如下Java代码:public final class KtCreateCoroutineKt { @NotNull private static final Function1 block; //注释1 主函数调用 public static final void main() { Thread.sleep(2000L); testStartCoroutine(); } // $FF: synthetic method public static void main(String[] var0) { main(); } @NotNull public static final Function1 getBlock() { return block; } //注释2 这里创建了一个Continuation对象,但是类型无法解析 //这是因为它是一个匿名内部类 private static final void testStartCoroutine() { <undefinedtype> continuation = new Continuation() { @NotNull public CoroutineContext getContext() { return (CoroutineContext)EmptyCoroutineContext.INSTANCE; } public void resumeWith(@NotNull Object result) { String var2 = "Result is " + (String)(Result.isFailure-impl(result) ? null : result); System.out.println(var2); } }; //注释3 扩展函数变成了Java静态方法调用,参数为block和continuation ContinuationKt.startCoroutine(block, (Continuation)continuation); } static { //注释4,lambda原本是一个无参高阶函数,这里默认会添加一个Continuation //同样的,这里是匿名内部类的原因,无法具体解析出var0的类型 Function1 var0 = (Function1)(new Function1((Continuation)null) { int label; //CPS后的状态机逻辑,当调用continuaiton的resume方法,会回调如此。 //这里的0分支中,调用delay后,会挂起,进入delay方法中,并且参数this也就是var0自己 //调用完delay后,进入1分支,同时打印Kotlin,返回Result字段 @Nullable public final Object invokeSuspend(@NotNull Object $result) { Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED(); String var2; switch(this.label) { case 0: ResultKt.throwOnFailure($result); var2 = "Hello"; System.out.println(var2); this.label = 1; if (DelayKt.delay(1000L, this) == var3) { return var3; } break; case 1: ResultKt.throwOnFailure($result); break; default: throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine"); } var2 = "Kotlin"; System.out.println(var2); return "Result"; } //注释5 根据一个Continuation对象,创建一个新的Continuation对象,其实这个类型就是 //状态机中的Continuation类型,即block实现类的类型 @NotNull public final Continuation create(@NotNull Continuation completion) { Intrinsics.checkNotNullParameter(completion, "completion"); Function1 var2 = new <anonymous constructor>(completion); return var2; } public final Object invoke(Object var1) { return ((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE); } }); block = var0; } } 这里反编译的代码,如果看过文章 # 协程(15) | 挂起函数原理解析 中的CPS后的状态机原理,就不难理解,代码中关键地方,都进行了注释标注。我们还是来简单说明一下:注释1、2、3是testStartCoroutine()方法的调用,这里使用匿名内部类的方式,把Continuation对象传递给startCoroutine函数。注释4就是典型的状态机逻辑,就是把原来suspend () -> String类型的block转换为var0,在这其中注释4的逻辑就是CPS后的状态机逻辑,里面有2个分支,因为在这里面我们调用了delay挂起函数。不同于普通的匿名内部类实现,在这里多了注释5的方法,这说明var0所实现的接口中有create()方法,在该方法中,会根据一个Continuation参数创建var0。 这个var0其实就是block这一段lambda在经过编译器处理后的对象,其类型我们目前只知道是Continuation的子类。我们接着来看一下startCoroutine{}的源码实现:public fun <T> (suspend () -> T).startCoroutine( completion: Continuation<T> ) { createCoroutineUnintercepted(completion).intercepted().resume(Unit) } 这里调用了createCoroutineUnintercepted()方法:public expect fun <T> (suspend () -> T).createCoroutineUnintercepted( completion: Continuation<T> ): Continuation<Unit> 会发现这里是用expect修饰的,即是一种声明,我们需要到协程源代码的JVM实现部分中找到对应的实现:public actual fun <T> (suspend () -> T).createCoroutineUnintercepted( completion: Continuation<T> ): Continuation<Unit> { val probeCompletion = probeCoroutineCreated(completion) //注释1 return if (this is BaseContinuationImpl) create(probeCompletion) else createCoroutineFromSuspendFunction(probeCompletion) { (this as Function1<Continuation<T>, Any?>).invoke(it) } } 可以发现这也是(suspend () -> T)的扩展函数,所以this其实就是前面代码中的block。这里需要注意了,前面我们说的反编译中block的实现类类型是继承至ContinuationImpl的,这个十分重要,因为反编译代码无法完整显示出,所以注释2的第一个if就能返回ture,而这里就是调用create(probeCompletion)函数。而这个create()方法就是前面反编译中block实现类的create()方法:@NotNull public final Continuation create(@NotNull Continuation completion) { Intrinsics.checkNotNullParameter(completion, "completion"); Function1 var2 = new <anonymous constructor>(completion); return var2; } 在这个create方法中,会把我们传入的continuation对象进行包裹,再次返回一个Continuation对象,根据前面文章挂起函数原理可知,这个其实就相当于第一次进入状态机,我们新建一个Continuation对象,而这个对象类型就是var0的实现类类型。注意了,这里返回值是Continuation类型对象,即调用完create()方法,其实就对应着协程被创建了,和挂起函数一样,类型是Continuation类型。所以这里就好办了,根据前面的知识,这时调用resume,便会触发协程体的状态机入口,所以:public fun <T> (suspend () -> T).startCoroutine( completion: Continuation<T> ) { createCoroutineUnintercepted(completion).intercepted().resume(Unit) } 这里的最后调用就是resume(Unit),调用完resume就会调用continuation的invokeSuspend方法,从而开启协程的执行。注意上面在resume()方法调用之前,还调用了intercepted()方法,我们简单看一下:public expect fun <T> Continuation<T>.intercepted(): Continuation<T> 这个方法在Continuation.kt类中,是基础元素,同时也是用expect修饰的,所以我们要去Kotlin源码中找到JVM平台的实现:public actual fun <T> Continuation<T>.intercepted(): Continuation<T> = (this as? ContinuationImpl)?.intercepted() ?: this 这里逻辑非常简单,就是将Continuation强转为ContinuationImpl,然后调用它的intercpeted()方法,而前面我们说过block实现类就是这个类的子类,所以强转一定能成功,而这个方法如下:internal abstract class ContinuationImpl( completion: Continuation<Any?>?, private val _context: CoroutineContext? ) : BaseContinuationImpl(completion) { @Transient private var intercepted: Continuation<Any?>? = null public fun intercepted(): Continuation<Any?> = intercepted ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this) .also { intercepted = it } } 这里的逻辑其实就是通过ContinuationInterceptor类来对Continuation进行拦截和处理,而这里的处理其实就是将协程派发到线程上,这部分知识点等我们说Dispatchers时再细说。所以到这里我们就大致说明白了底层启动协程API的原理,其中block就是一个协程,它的类型必须是suspend类型的,然后本质就是一个内部类实例,父类是Function1和ContinuationImpl,创建完协程就是返回一个内部类实例,即状态机。然后调用resume(Unit)方法来触发状态机的invokeSuspend方法,从而开始其状态机逻辑。createCoroutine{}原理分析和startCoroutine{}对应的还有一个创建协程的基础API,方法如下:public fun <T> (suspend () -> T).createCoroutine( completion: Continuation<T> ): Continuation<Unit> = SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED) 从这里我们发现,它是一样调用了createCoroutineUnintercrepted方法,但是没有调用resume(Unit),即没有进入状态机。所以上面测试代码,和下面写法是一样的:private fun testCreateCoroutine(){ val continuation = object : Continuation<String>{ override val context: CoroutineContext get() = EmptyCoroutineContext override fun resumeWith(result: Result<String>) { println("Result is ${result.getOrNull()}") } } //这里手动调用resume(Unit)方法 val c = block.createCoroutine(continuation) c.resume(Unit) } 关于原理,我们就不分析了,和前面是一样的。总结本篇文章我们见识到了创建协程的底层API,即:startCoroutine{}和createCoroutine{},这个方法是suspend () -> T挂起函数的扩展函数,根据挂起函数CPS后的原理,它需要传入一个Continuation,而该方式下,挂起函数的实现类,会继承ContinuationImpl类,该类中有create()方法,从而产生一个Continuation类型的状态机对象。最后调用resume方法来开启状态机。学习完本篇文章,我们就知道,其实协程就是对挂起函数的进一步处理,下篇文章我们就来仔细看看启动协程的launch函数的原理。
0
0
0
浏览量803
德州安卓

协程(14) | 让你的代码支持Flow

前言协程到现在,我们已经差不多学完了所有基础知识,包括协程启动方式、挂起函数、结构化并发、异常处理、Channel以及Flow等,而关于Flow的进阶使用以及协程更多进阶使用,在后面还需要继续探索。在之前有一篇文章,我们简单实现了一个Retrofit,并且使用协程的API实现了挂起函数,让我们可以用同步的方式写异步代码。文章地址:# 协程(09) | 实现一个简易Retrofit。那这还不够过瘾,因为我们之前学习Flow的时候,知道Flow就像是一条河流,那假如我们从网络获取的数据,就像是河流一样流淌下来,我们使用各种中间操作符进行处理,最后再展示出来,使用链式调用,不仅大大简化代码编写,还让逻辑更加清晰。本章内容我们就来实现一个简易的支持Flow返回类型的Retrofit。和支持挂起函数一样,我们分为2个方向:第一个方向是不改动原来SDK代码,把Callback类型改成支持Flow,这种适合我们没有第三方库源码的情况;第二个方向是直接有权限修改源码,在源码阶段支持Flow。正文代码实现还是继续第9篇中的简易Retrofit代码,所以这里简易先看之前的文章。和实现挂起函数一样,我们先来改造Callback。Callback转Flow和实现挂起函数一样,我们给KtCall类型再加一个扩展函数asFlow:/** * 把原来[CallBack]形式的代码,改成[Flow]样式的,即消除回调。其实和扩展挂起函数一样,大致有如下步骤: * * 调用一个高阶函数,对于成功数据进行返回,即[trySendBlocking]方法 * * 对于失败的数据进行返回异常,即[close]方法 * * 同时要可以响应取消,即[awaitClose]方法 * */ fun <T: Any> KtCall<T>.asFlow(): Flow<T> = callbackFlow { //开始网络请求 val c = call(object : CallBack<T>{ override fun onSuccess(data: T) { //返回正确的数据,但是要调用close() trySendBlocking(data) .onSuccess { close() } .onFailure { close(it) } } override fun onFail(throwable: Throwable) { //返回异常信息 close(throwable) } }) awaitClose { //响应外部取消请求 c.cancel() } } 这里的代码比较简单,但是有许多细节知识点,我们来简单分析一下:通过callbackFlow高阶函数实现功能,返回Flow类型的数据,该函数定义:public fun <T> callbackFlow(@BuilderInference block: suspend ProducerScope<T>.() -> Unit): Flow<T> = CallbackFlowBuilder(block) 该方法通过ProducerScope,向block代码块中提供SendChannel实例,通过SendChannel实例,我们可以向其中发射元素,从而创建出一个冷的Flow。这个函数的定义,在之前文章中我们反复强调过,block是高阶函数类型,它的接收者是ProducerScope:public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> { public val channel: SendChannel<E> } 通过该接口中的默认属性channel,我们可以发送数据到Channel中,比如上面代码块中的trySendBlocking和close方法,这也就说明该方法实现是使用了Channel。由于该方法返回的Flow是冷流,只有当一个终端操作符被调用时,该block才会执行。该构造者builder能确保线程安全和上下文保存,因此提供的ProducerScope可以在任何上下文中使用,比如基于Callback的API中,也就是本例测试代码中。结果Flow会在block代码块执行完立即结束,所以应该调用awaitClose挂起函数来保证flow在运行,否则channel会在block执行完成立即close,这也就是为什么在上面代码中,写完业务代码,还要调用awaitClose挂起函数的原因。awaitClose是一个高阶函数,它的参数block会在Flow的消费者手动取消Flow的收集,或者基于Callback的API中调用SendChannel的close方法时被执行。所以awaitClose可以用来做一些block完成后的收尾工作,比如上面代码中我们用来取消OkHttp的请求,或者在反注册一些Callback。同时awaitClose是必须要调用的,可以防止当flow被取消时发生内存泄漏,否则代码会一直执行,即使flow的收集已经完成了。为了杜绝上面情况,我们在Callback中,如果业务代码执行完成,不论是成功还是失败,都需要调用close,就比如上面代码中返回成功和返回失败都要调用close,并且在失败时,还需要传递参数。写完上面代码,我们也做了一个简单分析,主要是一些规则要执行,现在我们就来在代码中使用一下:findViewById<TextView>(R.id.flowCall).setOnClickListener { val dataFlow = KtHttp.create(ApiService::class.java).reposAsync(language = "Kotlin", since = "weekly").asFlow() dataFlow .onStart { Toast.makeText(this@MainActivity, "开始请求", Toast.LENGTH_SHORT).show() } .onCompletion { Toast.makeText(this@MainActivity, "请求完成", Toast.LENGTH_SHORT).show() } .onEach { findViewById<TextView>(R.id.result).text = it.toString() } .catch { Log.i("Flow", "catch exception: $it") } .launchIn(lifecycleScope) } 现在我们的网络请求返回值就变成了Flow类型,我们就可以使用Flow的API进行链式调用,在编码和逻辑上都更加方便。直接支持Flow上面代码使用Callback转为Flow适用于一些第三方库,我们无权修改源码,但是大多数情况下,我们还是可以修改源码的。就比如本章所说的简易Retrofit,没看过之前的代码实现还是建议看一下,这里我们根据之前实现异步效果一样,来定义一个直接返回Flow类型的方法:/** * [reposFlow]用于异步调用,同时返回类型是[Flow] * */ @GET("/repo") fun reposFlow( @Field("lang") language: String, @Field("since") since: String ): Flow<RepoList> 然后还是判断方法的返回值,类似于之前判断返回值类型是否是KtCall一样,我们判断返回值是否是Flow类型:/** * 判断方法返回值类型是否是[Flow]类型 * */ private fun isFlowReturn(method: Method) = getRawType(method.genericReturnType) == Flow::class.java 然后在具体调用的invoke方法中进行处理:/** * 调用[OkHttp]功能进行网络请求,这里根据方法的返回值类型选择不同的策略。 * @param path 这个是HTTP请求的url * @param method 定义在[ApiService]中的方法,在里面实现中,假如方法的返回值类型是[KtCall]带 * 泛型参数的类型,则认为需要进行异步调用,进行封装,让调用者传入[CallBack]。假如返回类型是普通的 * 类型,则直接进行同步调用。 * @param args 方法的参数。 * */ private fun <T: Any> invoke(path: String, method: Method, args: Array<Any>): Any?{ if (method.parameterAnnotations.size != args.size) return null ... //泛型判断 return when{ isKtCallReturn(method) -> { val genericReturnType = getTypeArgument(method) KtCall<T>(call, gson, genericReturnType) } isFlowReturn(method) -> { logX("Start Out") flow<T> { logX("Start In") val genericReturnType = getTypeArgument(method) val response = okHttpClient.newCall(request).execute() val json = response.body?.string() val result = gson.fromJson<T>(json, genericReturnType) // 传出结果 logX("Start Emit") emit(result) logX("End Emit") } } else -> { val response = okHttpClient.newCall(request).execute() val genericReturnType = method.genericReturnType val json = response.body?.string() Log.i("zyh", "invoke: json = $json") //这里这个调用,必须要传入泛型参数 gson.fromJson<Any?>(json, genericReturnType) } } } 在isFlowReturn分支中,我们首先加了一些可以打印协程信息的log,方便我们看线程切换效果。然后就是我们非常熟悉的flow{}高阶函数,它是Flow的上游操作符,在创建Flow的同时,使用emit发送数据,这部分知识点在Flow的文章中,我们已经非常熟悉了。最后我们来进行调用:findViewById<TextView>(R.id.flowReturnCall).setOnClickListener { KtHttp.create(ApiService::class.java).reposFlow(language = "Kotlin", since = "weekly") .flowOn(Dispatchers.IO) .onStart { Toast.makeText(this@MainActivity, "开始请求", Toast.LENGTH_SHORT).show() } .onCompletion { Toast.makeText(this@MainActivity, "请求完成", Toast.LENGTH_SHORT).show() } .catch { Log.i("Flow", "catch exception: $it") } .onEach { logX("Display UI") findViewById<TextView>(R.id.result).text = it.toString() } .launchIn(lifecycleScope) } 同样的,我们使用flowOn来切换该操作符之前的操作的线程,然后使用launchIn在收集数据的同时指定Scope。打印如下:在红框中,代码执行在主线程,网络请求部分执行在工作线程,这样就完成了异步请求,也不会造成Android的UI卡顿了。总结本篇文章从2个方面来介绍了Flow的使用,当我们使用第三方库时,可以使用第一种方法来支持Flow;当是新代码时,我们就可以直接让其支持Flow。本篇文章涉及的代码:github.com/horizon1234…
0
0
0
浏览量1244
德州安卓

协程(21) | 结构化并发原理解析

前言在之前文章介绍协程时,我们说过协程有个特性就是结构化并发,这是因为协程是具有父子关系的,取消父协程,会让所有子协程都取消,这可以有效防止内存泄漏。那本篇文章就来看看结构化并发的原理。正文在协程框架的中层概念中,CoroutineScope就是实现结构化并发的关键,其实从字面意思也非常好理解,协程作用域,也就是规定了一个作用域,可以批量管理一个作用域内的所有协程。为什么有CoroutineScope其实越是到后面,越容易串起来整个协程框架的知识,让知识形成体系。我们这里回顾一下启动协程的2个API:launch和async:public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { ... } public fun <T> CoroutineScope.async( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T ): Deferred<T> { ... } 这里发现它们都是CoroutineScope的扩展函数,这里为什么要设计为CoroutineScope的扩展函数呢?其实不然,在早期的协程API,这2个函数还真不是CoroutineScope的扩展函数,假如是使用早期API,伪代码如下:// 使用协程最初的API,只是伪代码 private fun testScopeJob() { val job = Job() launch(job){ launch { delay(1000000L) logX("Inner") } logX("Hello!") delay(1000000L) logX("World!") // 不会执行 } launch(job){ launch { delay(1000000L) logX("Inner!!!") } logX("Hello!!!") delay(1000000L) logX("World1!!!") // 不会执行 } Thread.sleep(500L) job.cancel() } 这里想实现结构化并发,我们不得不创建一个Job对象,然后传入launch中当做参数,但是开发者可能会忘记传输这个参数,所以就会打破结构化关系。所以后面发展就专门设计出CoroutineScope来管理协程批量处理,而且把launch和async都作为该类的扩展函数,这样就不会有前面所说的忘记传递参数从而导致的非结构关系。原理分析从前面协程API的迭代就可以看出,其实起作用的还是Job,而CoroutineScope中包含了协程上下文,协程上下文又包含了Job,所以我们还是以launch{}启动协程为例,来分析其结构化并发的原理。创建父子关系这里我们写出下面示例代码:private fun testScope() { //新建一个CoroutineScope val scope = CoroutineScope(Job()) //由于launch是CoroutineScope的扩展函数 scope.launch{ //block函数类型参数的接收者是CoroutineScope launch { delay(1000000L) logX("Inner") // 不会执行 } logX("Hello!") delay(1000000L) logX("World!") // 不会执行 } Thread.sleep(500L) // 2 scope.cancel() } 在上面代码中,有个值得注意的地方,就是launch方法不仅仅是CoroutineScope的扩展函数,它的block类型是:suspend CoroutineScope.() -> Unit,所以在协程体中,我们依旧可以调用launch方法。这里我们创建了一个CoroutineScope,这里我们来看一下这个方法源码:public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job()) internal class ContextScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)" } 可以发现CoroutineScope()是一个顶层函数,同理函数体内部的Job()也是一个顶层函数,这里还有一个小知识点:当顶层函数当做"构造函数"来使用时,这个函数的命名可以不使用驼峰命名法,而是以大写开始。这里返回的是CoroutineScope,在前面文章我们知道它是对CoroutineContext的封装:public interface CoroutineScope public val coroutineContext: CoroutineContext } 在CoroutineScope()方法中,通过context[Job]就可以取出保存在context中的Job对象,假如没有Job对象的话,就创建一个Job对象传入到context中,这说明一件事,每一个CoroutineScope对象,它的context当中必然存在一个Job对象。同时也说明在调用CroutineScope()方法时,也可以不传Job对象。接着我们继续看launch的源码:public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { //注释1 继承父协程的上下文 val newContext = newCoroutineContext(context) //注释2 创建协程对象 val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) //注释3 开始协程 coroutine.start(start, coroutine, block) return coroutine } 这里的注释1和3,分别在前面文章分析launch启动以及线程调度都分析过了,现在轮到了注释2://Standalone翻译就是独立,即独立运行的协程 private open class StandaloneCoroutine( parentContext: CoroutineContext, active: Boolean //继承协程抽象类,泛型为Unit,initParentJob为true ) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) { override fun handleJobException(exception: Throwable): Boolean { handleCoroutineException(context, exception) return true } } //懒惰的协程 private class LazyStandaloneCoroutine( parentContext: CoroutineContext, block: suspend CoroutineScope.() -> Unit //继承至上面的类,initParentJob还是为true ) : StandaloneCoroutine(parentContext, active = false) { private val continuation = block.createCoroutineUnintercepted(this, this) override fun onStart() { continuation.startCoroutineCancellable(this) } } 可以发现StandaloneCoroutine是AbstractCoroutine的子类,在前面文章中我们说过这个可以看成是代表协程的抽象类,在调用其构造函数时,第二个参数initParentJob参数,一直为true,其实就是代表了协程创建以后,需要初始化协程的父子关系。AbstractCoroutine构造函数如下:public abstract class AbstractCoroutine<in T>( parentContext: CoroutineContext, initParentJob: Boolean, active: Boolean ) : JobSupport(active), Job, Continuation<T>, CoroutineScope { init { if (initParentJob) initParentJob(parentContext[Job]) } } 该类的继承关系,在上一篇文章中我们重点分析了继承Continuation分支的,主要是用来调用intercepted()来拦截其线程调度器,本篇文章重点就是其JobSupport类。这里initParentJob参数我们从前面可知,这里必为true,即需要初始化父子关系,其中initParentJob()函数定义在JobSupport类中:public open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob, SelectClause0 JobSupport内容比较多,可以把它看成一个具体化的Job实现,这是因为关于Job的各种操作,都是在该类中完成的。下面是initParentJob方法://parent就是父协程的Job protected fun initParentJob(parent: Job?) { assert { parentHandle == null } //当没有父协程时,不需要创建和父协程的关系 if (parent == null) { parentHandle = NonDisposableHandle return } //确保父协程已经启动了 parent.start() //把当前Job添加到父Job中 val handle = parent.attachChild(this) parentHandle = handle // now check our state _after_ registering (see tryFinalizeSimpleState order of actions) if (isCompleted) { handle.dispose() parentHandle = NonDisposableHandle // release it just in case, to aid GC } } 上面代码比较简单,看注释即可,所以我们可以把协程看成一颗N叉树,每一个协程都对应一个Job对象,而每一个Job可以有一个父Job和多个多个子Job。结构化取消既然Job的关系如上图中的N叉树,所以结构化取消原理其实也就是事件传递了。当某个Job收到取消事件时,需要通知其上下级。这个设计思路,就和我们公司架构一样,当某个人需要通知重要事情时,可以先告诉其下属,再告诉其领导,通过循环迭代从而可以让整个公司都知道。我们可以想象出其取消协程的代码应该如下:fun Job.cancelJob() { //通知子Job取消 children.forEach { cancelJob() } //通知父Job取消 notifyParentCancel() } 当然这是只是简化的伪代码,真实代码复杂很多,但是原理差不多。我们先来看一下CoroutineScope的cancel函数的代码:public fun CoroutineScope.cancel(cause: CancellationException? = null) { val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this") job.cancel(cause) } 正常情况下,我们调用scope.cancel()时,一般都不会传递参数,假如要传递额外说明参数,这里必须是CancellationException类型的。在方法实现中,我们会发现真实是调用Job的cancel()方法,该方法的实现就是在前面所说的JobSupport类中://外部带原因的取消,内部不能隐式调用 public override fun cancel(cause: CancellationException?) { cancelInternal(cause ?: defaultCancellationException()) } 这个方法是供外部来调用的,这里注意当cause为空时,这里在调用cancelInternal时会传入一个默认的CancellationException实现:public open fun cancelInternal(cause: Throwable) { cancelImpl(cause) } 该方法由方法名可以看出是内部调用,这种设计思路,我们在平时也可以使用,方法的访问权限要严格分开,该方法的参数类型是Throwable类型,会调用下面方法:internal fun cancelImpl(cause: Any?): Boolean { var finalState: Any? = COMPLETING_ALREADY if (onCancelComplete) { //1 finalState = cancelMakeCompleting(cause) if (finalState === COMPLETING_WAITING_CHILDREN) return true } if (finalState === COMPLETING_ALREADY) { //2 finalState = makeCancelling(cause) } return when { finalState === COMPLETING_ALREADY -> true finalState === COMPLETING_WAITING_CHILDREN -> true finalState === TOO_LATE_TO_CANCEL -> false else -> { afterCompletion(finalState) true } } } 在该方法中,cause的类型是Any?,其实从源码注释我们可以知道该参数可能是Throwable,也可能是一个ParentJob,第二种情况只会在cancelChild方法被调用时传入。而且该方法返回true则表示异常被处理,否则表示没有被处理。那么为什么该类中有这么多状态判断呢?原因非常简单,因为Job的状态变化是一个持续过程,只有子Job都取消完成后,该Job才能算完成了。所以这里会调用注释1的cancelMakeComplting方法:private fun cancelMakeCompleting(cause: Any?): Any? { loopOnState { state -> // 省略部分 val finalState = tryMakeCompleting(state, proposedUpdate) if (finalState !== COMPLETING_RETRY) return finalState } } 从方法名中的Completing为完成、完整的意思就可以看出,这个过程是一个持续的过程,这里有一个循环方法loopOnState,我们可以在日常项目中借鉴一下:private inline fun loopOnState(block: (Any?) -> Unit): Nothing { while (true) { block(state) } } 这里的核心还是调用tryMakeCompleting方法:private fun tryMakeCompleting(state: Any?, proposedUpdate: Any?): Any? { if (state !is Incomplete) return COMPLETING_ALREADY ... // 省略部分 return COMPLETING_RETRY } ... return tryMakeCompletingSlowPath(state, proposedUpdate) } 通过源码注释,我们可知该方法会返回状态,而且是已完成的状态,比如这里的COMPLETING_ALREADY、COMPLETING_RETRY等,同时在该方法中分出了2个分支。一个是快速返回分支,当该Job没有子Job,可以立即返回。当有子Job时,才会调用tryMakeCompletingSlowPath方法,这也是简化函数逻辑的一种常见手段,方法如下:private fun tryMakeCompletingSlowPath(state: Incomplete, proposedUpdate: Any?): Any? { // 省略部分 notifyRootCause?.let { notifyCancelling(list, it) } return finalizeFinishingState(finishing, proposedUpdate) } 这里代码调用比较复杂,我们可以不用关注,最终会调用notifyCancelling方法,这个才是最关键的代码。前面为什么调用一个取消要附带这么多状态判断,也是因为Job需要管理协程的状态,即只有子Job都完成时,父Job才算完成,所以这是一个持续过程。我们看一下这个核心方法:private fun notifyCancelling(list: NodeList, cause: Throwable) { onCancelling(cause) // 1,通知子Job notifyHandlers<JobCancellingNode>(list, cause) // 2,通知父Job cancelParent(cause) } 这个方法和我们前面所说的伪代码逻辑基本一致了,我们分别来看看其中的逻辑://通知子Job进行取消 private inline fun <reified T: JobNode> notifyHandlers(list: NodeList, cause: Throwable?) { var exception: Throwable? = null list.forEach<T> { node -> try { //调用每个子Job的invoke方法 node.invoke(cause) } catch (ex: Throwable) { exception?.apply { addSuppressedThrowable(ex) } ?: run { exception = CompletionHandlerException("Exception in completion handler $node for $this", ex) } } } exception?.let { handleOnCompletionException(it) } } 这里就是遍历当前Job的子Job,并且将取消的case传递过去,这里的invoke()最终会调用ChildHandleNode的invoke()方法://这里是Node类型,也侧面说明了Job是树结构 internal class ChildHandleNode( @JvmField val childJob: ChildJob ) : JobCancellingNode(), ChildHandle { override val parent: Job get() = job //调用parentCancelled方法 override fun invoke(cause: Throwable?) = childJob.parentCancelled(job) override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause) } //JobSupport中实现 public final override fun parentCancelled(parentJob: ParentJob) { cancelImpl(parentJob) } 而这里代码最终会调用cancelImpl()方法,即对应了前面所说的该方法参数可能是一个Job,同时也说明这是一个递归调用,一直会调用到没有子Job的Job。我们接着看一下如何通知父Job:private fun cancelParent(cause: Throwable): Boolean { if (isScopedCoroutine) return true //是否是CancellationException异常 val isCancellation = cause is CancellationException val parent = parentHandle if (parent === null || parent === NonDisposableHandle) { return isCancellation } // 1 return parent.childCancelled(cause) || isCancellation } 注意注释1的返回值,这个返回值是有意义的,返回true代表父协程处理了异常,而返回false,代表父协程没有处理异常。该方法代码如下:public open fun childCancelled(cause: Throwable): Boolean { //特殊处理取消异常 if (cause is CancellationException) return true return cancelImpl(cause) && handlesException } 这里我们发现当异常是CancellationException的时候,协程是会进行特殊处理的。一般来说,父协程会忽略子协程的取消异常,当是其他异常时,那么父协程就会响应子协程的取消了。这时又会调用cancelImpl(),来继续递归调用。这里我们再结合前面文章所说的协程异常处理,我们就说过对于CancellationException异常要特殊处理,一般都是要抛出去,这里我们就可以看到原因了,原来协程的结构化取消,是需要依赖这个异常的。这也就说明一件事,当出现CancellationException异常时,只会向下传播,来达到结构化取消的效果;但是当是其他异常时,则会双向传递,如下图:SupervisorJob原理在之前文章,我们说过一个特殊的Job,就是SupervisorJob,它可以防止子协程异常的蔓延,这时我们就可以知道其实现原理了://顶层函数当构造函数使用 public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent) //这里一直返回false private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) { override fun childCancelled(cause: Throwable): Boolean = false } 根据上面源码分析,这个childCancelled方法是用来对上报告的,这里直接返回false且不处理,也就是不论是什么异常都不会蔓延到其他兄弟Job。总结本篇文章涉及的代码跳转较多,我们做个总结:每次创建CoroutineScope的时候,它的内部会确保CoroutineContext当中一定有Job元素,而CoroutineScope就是通过这个Job对象来管理协程的。在我们通过launch、async创建协程的时候,会同时创建AbstractCoroutine的子类,在它的initParentJob()方法中,会建立父子关系。每个协程都会对应一个Job,而每个Job都会有一个父Job,多个子Job。最终他们会形成一个N叉树的结构。由于协程是一个N叉树的结构,因此协程的取消事件以及异常传播,也会按照这个结构进行传递。每个Job取消的时候,都会通知自己的子Job和父Job,最终以递归的形式传递给每一个子协程。协程向上取消父Job的时候,还利用了责任链模式,确保取消事件可以一步步传递到顶层的协程。这里还有一个细节就是,默认情况下,父协程会忽略子协程的CancellationException。对于其他异常,父协程不仅会响应,还会造成其他兄弟Job出现异常,所以这里可以使用SupervisorJob来阻断异常的向上传递。
0
0
0
浏览量507
德州安卓

协程(10) | Flow

前言前面文章我们介绍了Channel,它可以用于协程间的通信,我们可以把它看成是一个密闭的管道,管道俩头可以发送和接收数据。在使用协程处理异步任务时,在简单的场景中,我们可以使用launch、async、挂起函数、Channel等来实现,但是对于复杂的逻辑操作,比如一个数据源需要从IO线程中获取,然后进行过滤和处理,再在UI线程展示,再在IO线程中进行保存,这种就需要使用更强大的组件,即Flow。Flow及其强大,及其灵活,可以这样说,在Flow出现之前,Kotlin的挂起函数、结构化并发可能不足以形成协程的核心竞争力,但是类似RxJava的Flow出现后,Kotlin的协程就被大家认可了。学会使用Flow,不仅仅可以让我们在切换线程、处理数据上更加方便,Flow的链式调用风格的API可以让代码更加容易阅读。理解Flow的原理,以及挂起API的实现,更有益于让我们写出Flow风格的代码。正文把Channel比喻为管道,因为它只发送接这2个操作,数据在封闭的管道中进行传递。但是Flow就不一样了,这个是"流"的概念,即可以把Flow比喻为一条河流,河流中流淌的是数据,这个数据从河流的发源地开始出发,可以在中间被各种处理厂进行处理,最后流入大海。所以Flow就是一个数据流,它也有上下游的概念,比如下图:数据从发源地开始,可以经过多个中转站进行处理。上游操作符既然Flow是数据流,那流中的数据是如何产生的呢?这就需要上游操作符,我们直接看代码:fun main() = runBlocking { //上游,发源地 flow { //挂起函数,emit是挂起函数 emit(1) emit(2) emit(3) emit(4) emit(5) }.filter { it > 2 } // 中转站1 .map { it * 2 } // 中转站2 .take(2) // 中转站3 .collect{ // 下游 println(it) } } 上面代码中就创建了一个Flow,然后往Flow中发送了5个数据,再通过中间3个中转站对数据进行了处理,最后通过collect高阶函数进行收集,上面代码的运行结果如下:这里完全符合预期,而且这种链式调用非常符合阅读习惯。flow{}创建Flow我们这里先重点看一下这里的flow{}高阶函数,它的作用是创建一个新的Flow,同时在它的lambda中我们使用emit()挂起函数往这个Flow中发送数据,它是一个上游操作符。所以上游操作符的作用是创建一个Flow,然后负责往Flow中发送数据;类比于现实中,河流的水也是从上游产生的一样,所以上游操作符不仅要创建Flow,还负责发送数据。我们可以简单看一下flow{}函数定义:public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block) 首先它是一个顶层函数,然后block是函数类型,而且它的接收者是FlowCollector类型,根据简写约定,block就相当于是FlowCollector的成员函数,所以它可以调用FlowCollector类型中的方法、变量,该接口:public fun interface FlowCollector<in T> { public suspend fun emit(value: T) } 这里我们可以发现我们可以在block中调用emit也就知道原因了。这里还有一个疑惑,flow方法不是扩展函数,但是block中可以调用emit方法,这个emit方法是哪个对象的呢?我们简单来看一下其实现SafeFlow:private class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : AbstractFlow<T>() { override suspend fun collectSafely(collector: FlowCollector<T>) { collector.block() } } flow{}函数会返回一个SafeFlow对象,然后在其实现方法collectSafely中会有一个FlowCollector类型的对象,然后可以利用该对象调用block,即collector.block(),这里也进一步说明了前面的约定观点。那这个collector对象是什么赋值的呢?具体原理研究,等后面原理篇文章再细说。flowOf和asFlow创建Flow除了上面方法外,还有类似于集和API的arrayListOf等的flowOf函数,以及asFlow把集和转换为Flow,代码如下:fun main() = runBlocking { listOf(1,2,3,4,5).asFlow() .filter { it > 2 } // 中转站1 .map { it * 2 } // 中转站2 .take(2) // 中转站3 .collect{ // 下游 println(it) } flowOf(1,2,3,4,5) .filter { it > 2 } // 中转站1 .map { it * 2 } // 中转站2 .take(2) // 中转站3 .collect{ // 下游 println(it) } } 这2种方法也可以创建Flow并且往里发送数据。我们来简单看一下asFlow和flowOf:public fun <T> Iterable<T>.asFlow(): Flow<T> = flow { forEach { value -> emit(value) } } public fun <T> flowOf(vararg elements: T): Flow<T> = flow { for (element in elements) { emit(element) } } 可以发现这2个方法都是调用flow{}高阶函数函数来实现的,并且都是遍历调用emit方法来完成发送数据,所以核心还是这个emit方法,后面文章再说其原理。所以这里上游操作符的创建Flow的方式有3种:Flow创建方式使用场景用法flow{}未知的数据集,用emit发送数据flow { emit(1)}flowOf()已知数据,类似于集合的arrayOfflowOf(1,2,3)asFlow()已知数据集合list.asFlow()我们在日常项目中需要根据具体情况来使用。中间操作符当数据被发送到Flow中,就可以使用中间操作符来对流中的数据进行处理,由上面例子代码中我们可以 发现比如filter、map这些类似的API其实就是从集和那边抄来的,这些操作符我相信看名字就能够识别和使用。所以这里重点说一些和集和无关的API。Flow生命周期回调函数在Flow的中间操作符中有2个比较特殊的操作符,甚至他们有点不像是中间操作符,就是onStart和onCompletion,其实是Flow开始和完成的回调函数,我们来看个例子:fun main() = runBlocking { flow{ println("发射 1") emit(1) println("发射 2") emit(2) println("发射 3") emit(3) println("发射 4") emit(4) println("发射 5") emit(5) } .filter { println("filter $it") it > 2 } .map { println("map $it") it * 2 } .take(2) .onStart { println("start") } .onCompletion { println("onCompletion") } .collect{ println("collect $it") } } 这里逻辑就不说了,其中onStart就是Flow开始的回调,而onCompletion就是Flow结束的回调,通过这个我们可以看出Flow的运行状态,同时这2个操作符和位置是无关的,虽然onStart已经放在了发射和几个操作数据的操作符后面,但是在执行时他却是最先执行的,下面是运行结果:而其他操作数据的操作符是和所在的位置有关的,这个也非常容易理解,比如我们把take(2)放到.filter前,打印结果如下:这里结果就会发生变化,也非常好理解,因为Flow是数据流,中间的操作会影响后的。所以这也是为什么感觉onStart和onCompletion不太像中间操作符就是这个原因。而这里onCompletion回调触发的情况有下面3种情况:Flow正常执行完毕;Flow当中出现异常;Flow被取消。其实这些都挺好理解的,在介绍协程的句柄Job那一章我们说过类似的逻辑。这里我们还是简单看看onStart方法:public fun <T> Flow<T>.onStart( action: suspend FlowCollector<T>.() -> Unit ): Flow<T> = unsafeFlow { val safeCollector = SafeCollector<T>(this, currentCoroutineContext()) try { safeCollector.action() } finally { safeCollector.releaseIntercepted() } collect(this) // directly delegate } 首先,调用onStart方法会返回一个新的Flow,其次action函数类型接收者是FlowCollector,所以可以在onStart的lambda中调用emit方法,并且会在上游发送数据前先执行,比如如下代码flowOf("a", "b", "c") .onStart { emit("Begin") } .collect { println(it) } 这里会打印Begin,a,b,c,也会说明onStart方法的执行优先级非常高,和位置无关。这里衍生一点,就是SharedFlow,因为Flow是冷的,而SharedFlow是热的,所以当onStart和SharedFlow一起使用时,就无法保证onStart一定会在发送数据之前执行,这时可以使用onSubscription其他API来完成,关于这点,后面说SharedFlow再细说。接着来看看onCompletion:public fun <T> Flow<T>.onCompletion( action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit ): Flow<T> = unsafeFlow { try { collect(this) } catch (e: Throwable) { ThrowingCollector(e).invokeSafely(action, e) throw e } // Normal completion val sc = SafeCollector(this, currentCoroutineContext()) try { sc.action(null) } finally { sc.releaseIntercepted() } } 该方法会在flow正常完成、被取消后执行,并且运行给定的action。这里我们需要注意一点,就是action的函数类型是(Throwable?) -> Unit,即当程序发生异常时,或者调用cancel()会产生一个CancellationException时,这个异常信息会被传递到onCompletion的action中。和下面要说的catch不同,这个操作符会抛出上游和下游的异常,和位置关系是不固定的,如果Flow在执行过程中没有异常,则cause会是null。还有一个神奇操作,就是action的接收者类型是FlowCollector,所以在action中依旧可以调用emit发送数据,由上面源码可知,在onCompletion中emit的数据,会在最后被收集。异常处理函数在前面我们知道Flow就是数据流,它有3个部分组成:上游、中间操作和下游,那么当Flow中发生异常时,也可以根据这个标准来进行分类,也就是异常发生的位置。catch捕获异常当Flow的异常发生在上游或者中间时,可以使用catch进行捕获异常,注意catch这个操作符就是和我们平时使用的try-catch意义用来捕获异常,只不过这个是用在Flow中的。同时catch操作符的作用和它的位置是强相关的,比如下面代码:fun main() = runBlocking { flow{ emit(1) emit(2) throw IllegalStateException() emit(3) }.map { it * 2 } .catch { println("catch exception: $it") } .collect{ println("collect : $it") } } 这里在上游操作符中抛出一个异常,这里就会被捕获到,同时会终止Flow的继续发射和执行,所以打印如下:注意这里catch只能捕获发生在它上游的异常,当异常发生时不再继续发生数据和执行数据。try-catch捕获下游异常这里说了catch是一个中间操作符,可以捕获在它之前的异常,那对于下游操作符的异常呢 比如在collect高级函数中发生异常,这里最简单的办法就是直接使用try-catch即可,比如下面代码:fun main() = runBlocking { flow{ emit(1) emit(2) throw IllegalStateException() emit(3) }.map { it * 2 } .catch { println("catch exception: $it") } .collect{ try { println("collect : $it") throw IllegalStateException() }catch (e: Exception){ println("Catch : $e") } } } 这里直接就在下游操作符中使用普通的try-catch进行捕获异常。关于异常捕获,我们后面有文章单独介绍,这里我们只需要知道Flow提供的catch中间操作符是和位置有关的,只能捕获它上游的异常,想捕获终止操作符中的异常,还是得老老实实使用try-catch。切换Context前面说了Flow在复杂业务上可以取代RxJava,而复杂的业务经常需要频繁地切换工作的线程,对于耗时任务,我们需要在线程池中执行,对于UI任务,我们需要在主线程上执行,而在Flow当中,我们可以借助flowOn这个中间操作符便可以完成需求。flowOn切换上游的上下文还是看个示例代码,如下:fun main() = runBlocking { flow { logX("Start") emit(1) logX("Emit 1") emit(2) logX("Emit 2") emit(3) logX("Emit 3") }.filter { logX("Filter: $it") it > 2 }.flowOn(Dispatchers.IO) .collect{ logX("Collect $it") } } 和前面的catch操作符的作用域一样,它的作用域只对它的上游有作用,上面代码的运行结果如下:================================ Start Thread:DefaultDispatcher-worker-1 @coroutine#2 ================================ ================================ Filter: 1 Thread:DefaultDispatcher-worker-1 @coroutine#2 ================================ ================================ Emit 1 Thread:DefaultDispatcher-worker-1 @coroutine#2 ================================ ================================ Filter: 2 Thread:DefaultDispatcher-worker-1 @coroutine#2 ================================ ================================ Emit 2 Thread:DefaultDispatcher-worker-1 @coroutine#2 ================================ ================================ Filter: 3 Thread:DefaultDispatcher-worker-1 @coroutine#2 ================================ ================================ Emit 3 Thread:DefaultDispatcher-worker-1 @coroutine#2 ================================ ================================ Collect 3 Thread:main @coroutine#1 ================================ Process finished with exit code 0 可以发现在Flow中,emit和filter所在的协程2运行在子线程中,而collect运行在主线程中,这也就印证了前面所说的作用域问题。launchIn指定CoroutineScope前面和catch操作符一样的问题,它的作用域范围只是它前面的部分,同时假如多次调用flowOn则当前flowOn的范围是到上一个flowOn,这个也非常好理解。那下面就来解决如何指定下游操作符的运行线程,或者更直接的就是flowOn后面的中间操作符和终止操作符所运行的线程,Kotlin这里提供了一个叫做launchIn的操作符,它可以把一部分操作指定Scope,示例代码如下://新建一个Dispatcher val mySingleDispatcher = Executors.newSingleThreadExecutor { Thread(it, "MySingleThread").apply { isDaemon = true } }.asCoroutineDispatcher() fun main() = runBlocking { //创建一个CoroutineScope val scope = CoroutineScope(mySingleDispatcher) flow { logX("Start") emit(1) logX("Emit 1") emit(2) logX("Emit 2") emit(3) logX("Emit 3") }.flowOn(Dispatchers.IO) .filter { logX("Filter: $it") it > 2 }.onEach { logX("onEach $it") } //指定运行在哪个协程范围内 .launchIn(scope) delay(1000) } 这里的代码比较特殊,我们可以肯定地是发射数据地代码肯定是在IO线程中,而filter和onEach中的代码运行在什么地方呢?通过运行我们会发现这个俩部分代码会运行在我们自定义的线程池中:================================ Start Thread:DefaultDispatcher-worker-1 @coroutine#3 ================================ ================================ Emit 1 Thread:DefaultDispatcher-worker-1 @coroutine#3 ================================ ================================ Emit 2 Thread:DefaultDispatcher-worker-1 @coroutine#3 ================================ ================================ Emit 3 Thread:DefaultDispatcher-worker-1 @coroutine#3 ================================ ================================ Filter: 1 Thread:MySingleThread @coroutine#2 ================================ ================================ Filter: 2 Thread:MySingleThread @coroutine#2 ================================ ================================ Filter: 3 Thread:MySingleThread @coroutine#2 ================================ ================================ onEach 3 Thread:MySingleThread @coroutine#2 ================================ Process finished with exit code 0 这里的疑点我们来慢慢解答:onEach操作符是什么东西,就是返回一个新的Flow,但是把每个上游的值都在其高级函数中执行一遍,比如这里就是println一下,然后再emit到新的Flow中,源码如下:public fun <T> Flow<T>.onEach(action: suspend (T) -> Unit): Flow<T> = transform { value -> action(value) return@transform emit(value) } 可以看出,这里出现了一个新的Flow。launchIn操作又是啥呢 为什么没有终止操作符,这里的代码依旧可以运行,我们来看一下源码:public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch { collect() // tail-call } 会发现launchIn是Flow的扩展函数,而且直接在scope调用launch启动了一个新协程,而协程中调用了collect(),这也就说明了为什么这里没有终止操作符的原因。所以严格意义上说launchIn算是一个终止操作符,把它上游的代码都分发到指定的线程当中。同时这个launchIn操作符在源码中有特殊的使用说明,代码注释如下:This operator is usually used with onEach, onCompletion and catch operators to process all emitted values handle an exception that might occur in the upstream flow or during processing, for example: flow .onEach { value -> updateUi(value) } .onCompletion { cause -> updateUi(if (cause == null) "Done" else "Failed") } .catch { cause -> LOG.error("Exception: $cause") } .launchIn(uiScope) 即这个操作符经常和onEach、onCompletion、catch一起使用,处理所有发出的值,处理可能在上游或者处理过程中发生的异常。终止操作符最后,我们到了Flow的下游部分,这里可以使用终止操作符来终止整个Flow,当使用了终止操作符,我们再也无法使用map这种中间操作符了。最常用的就是collect终止操作符,代表数据流的终止。同时还有一些从集合中抄过来的操作符,比如first()、single()、fold、reduce等等。另外把Flow转换为集合的时候,本身也意味着Flow数据流的终止,比如toList,也算是终止操作符。Flow是冷的上一篇文章我们说了Channel即管道这个是热的,特点就像是热情的服务员,不管你想不想喝水,都会给你端茶递水,那这里的Flow其实也非常好理解,我们看个代码:fun main() = runBlocking { // 冷数据流 val flow = flow { (1..3).forEach { println("Before send $it") emit(it) println("Send $it") } } // 热数据流 val channel = produce<Int>(capacity = 0) { (1..3).forEach { println("Before send $it") send(it) println("Send $it") } } println("end") } 上面代码的打印:可以发现:Channel之所以认为是热的,是因为不管有没有接收方,发送方都会工作;而Flow冷的原因是,只有调用终止操作符以后,Flow才会工作。Flow还是懒的默认情况下,Flow不仅是冷的,还是懒的,比如下面代码:fun main() = runBlocking { flow { println("emit: 3") emit(3) println("emit: 4") emit(4) println("emit: 5") emit(5) }.filter { println("filter: $it") it > 2 }.map { println("map: $it") it * 2 }.collect { println("collect: $it") } } 这里的执行结果如下:会发现它一次只处理一个数据,结合上一篇文章的服务员端茶倒水的例子,就是Flow不仅是一个冷淡的服务员,还是一个懒惰的服务员,明明饭桌上有3个人需要喝水,但服务员偏偏不一次递上3杯水,而是每个人都叫服务员一次,服务员才一杯一杯地把水递过来。总结这篇文章内容很多,也是Kotlin的一个重点,Flow在Android中有很多地方使用,等后面说一下Flow的原理,再来讨论在Android中如何使用Flow。总体来说,Flow和Channel一样,都是用来协程间的通信的,可以让我们更方便的处理复杂逻辑。整个Flow的API设计可以分为上游、中间操作符和下游操作符:上游:主要负责创建Flow,同时产生数据,主要有3个API:flow{}、flowOf()和asFlow();中间操作符:可以分为4类,第一类是集合抄过来的操作符;第二类是生命周期回调,比如onStart和onCompletion;第三类是捕获异常catch操作符;第四类是切换context,比如flowOn和launchIn。下游终止操作符:可以分为3类,第一个就是collect;第二类就是集合抄过来的;第三类就是Flow转集合的API,比如flow.toList()。最后就是和Channel对比,说Flow为什么是冷的,以及优势和劣势。
0
0
0
浏览量937
德州安卓

协程(20) | 线程调度原理解析

前言在前面2篇文章中,我们以启动协程基础API:startCoroutine{}入手,它是一个挂起函数类型的扩展函数,最终会让我们状态机Continuation对象多一个create()方法,从而创建状态机对象。然后我们以launch{}入手,会发现其最终还是会调用createCoroutineUnintercepted来创建状态机Continuation对象,但是在这其中我们加入了很多关于协程的概念,比如Job、CoroutineScope等,这里能实现的原因非常简单,在协程构建器中,我们传入给挂起函数的Continuation对象,实现了Job等多个接口。那本篇文章还是以launch{}入手,分析协程是如何进行线程调度的。正文协程中关于线程调度的类就是Dispatchers了,我们可以使用withContext(Dispatchers.IO)或者其他方法来指定协程block所执行的线程,所以我们先来看看Dispatchers相关的内容。Dispatchers调度器相关首先就是Dispatchers,它是一个单例类:public actual object Dispatchers { public actual val Default: CoroutineDispatcher = DefaultScheduler public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined public val IO: CoroutineDispatcher = DefaultIoScheduler } 这里可以发现我们平时用的几种调度器线程池:Default、Main、Unconfined和IO都是该单例的属性成员,同时他们的类型都是CoroutineDispatcher,即协程调度器。这个协程调度器CoroutineDispatcher的继承关系如下:public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {} public interface ContinuationInterceptor : CoroutineContext.Element {} public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element 这里的继承关系乍一看挺复杂的,我们来一一分析:首先就是协程调度器CoroutineDispatcher类,它是继承至抽象协程上下文Element类AbstractCoroutineContextElement,从命名来看,它就是Element的抽象类,提供了一些实现的公共方法。其次就是它实现了ContinuationInterceptor接口,这个接口也是Element的子接口,所以在代码第一行传递给AbstractCoroutineContextElement中的Key就是ContinuationInterceptor中的Key。这里我们一直无法找到Continuation合适的翻译,但是根据我们之前学习挂起函数原理的时候知道,Continuation是底层元素,在挂起函数经过CPS转换后,会得到一个状态机模型,那个模型就是Continuation子类SuspendLambda的对象。所以这里的ContinuationInterceptor其实就是状态机的拦截器,我们可以对状态机对象进行拦截和处理。简单分析了上面几种关系,所以类的继承关系如下:这个关系图在后面分析时有很大的作用。默认的Dispatchers.Default我们还是以简单的代码为例,比如下面代码:fun main(){ testLaunch() Thread.sleep(10000) } private fun testLaunch(){ val scope = CoroutineScope(Job()) scope.launch { logX("Hello") delay(1000) logX("Kotlin") } } /** * 输出结果: ================================ Hello Thread:DefaultDispatcher-worker-1 @coroutine#1 ================================ ================================ Kotlin Thread:DefaultDispatcher-worker-1 @coroutine#1 ================================ * */ 在这里创建了一个CoroutineScope,但是并没有指定Dispatcher,我们从日志可以发现其执行的线程池为DefaultDispatcher,并且还可以打印出协程名字:@coroutine#1,这些是哪里帮我们完成的呢?我们来看一下launch{}的源码:public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { //注释1,获取新的上下文 val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine } 这里的几行代码,我们又一次来分析了,这一次我们重点关注协程上下文。首先就是第一个参数的默认值是EmptyCoroutineContext,在前面介绍CoroutineContext文章我们说过,CoroutineContext我们可以当成一个Map来使用,而这里的EmptyCoroutineContext就是一个空Map,也就等于没有传递;只不过Kotlin使用这个来代替null。然后就是注释1部分,这里会对传入的context进行处理,newCoroutineContext()方法源码如下:public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { //注释1 继承父协程上下文 val combined = coroutineContext.foldCopiesForChildCoroutine() + context //注释2 当是调试模式时,添加日志 val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined //注释3 添加Default return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null) debug + Dispatchers.Default else debug } 上面代码我们来一一分析:首先是newCoroutineContext()方法是CoroutineScope的扩展函数,所以注释1的coroutineContext就是该CoroutineScope的coroutineContext成员属性对象,这个关系我们之前说过,CoroutineScope是对CoroutineContext的一层封装。然后就是注释1的foldCopiesChildCoroutine()函数作用就是将CoroutineScope当中的所有上下文元素都拷贝出来,然后跟传入的context进行合并。这行代码,可以让子协程继承父协程的上下文元素。注释2的作用是在调试模式下,给协程对象增加唯一的ID,比如之前的@coroutine#1信息。注释3的判断条件要看清楚,当combined不等于Dispatchers.Default时,或者combined[ContinuationInterceptor]为null时,给上下文添加Deispatchers.Default。这里也印证了前面的继承观点,即:协程调度器在上下文中存储的Key是ContinuationInterceptor,且默认会设置为Dispatchers.Default。所以默认是Default线程池,因为Kotlin是支持多平台的,只有UI编程的平台比如Android才有Main主线程的概念,所以这里默认是Default一定不会错。intercepted拦截分析既然我们知道了在哪里设置了协程的调度器,那在哪里切换线程以及在特定线程上执行协程呢?还是回到我们熟悉的函数:public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) { createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit)) } 上面这个函数在之前文章已经分析很多遍了,至少我们知道createCoroutineUnintercepted(completion)就完成了协程的创建,或者说已经完成了协程Continuation状态机的创建。那么核心的再处理就是这里的intercepted()方法了:public actual fun <T> Continuation<T>.intercepted(): Continuation<T> = (this as? ContinuationImpl)?.intercepted() ?: this 对Continuation进行拦截处理,这里代码很简单,把Continuation强转为ContinuationImpl,然后调用其intercepted()方法。根据前面文章分析,我们知道这里的真正类型是SuspendLambda,它是ContinuationImapl的子类,所以这个转换一定能成功,我们接着来看intercepted()方法:public fun intercepted(): Continuation<Any?> = intercepted ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this) .also { intercepted = it } 这里首先会调用context中保存的ContinuationInterceptor,在前面分析中,我们可知这个context其实就是我们前面创建出来的协程上下文context。根据前面默认值分析,这里intercepted的值就会是我们前面所赋值的Dispatchers.Default。所以这里会调用Dispatchers.Default的interceptContinuation()方法,注意这里会将this即状态机自己传入到该函数中。Dispatchers.Default分析这里我们视角又绕回了Dispatchers.Default,我们通过查看可以发现interceptContinuation方法是定义在ContinuationInterceptor接口中的抽象方法,其实现地方是CoroutineDispatcher抽象类,如下:public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = DispatchedContinuation(this, continuation) 会发现这里会返回一个DispatchedContinuation实例对象,参数this就是Default,continuation就是状态机。到这里我们为了逻辑更清晰、分层,可以把前面的startCoroutineCancellable()方法改写一下:public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) { createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit)) } //改写如下 public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) { // 1 创建出状态机Continuation对象 val continuation = createCoroutineUnintercepted(completion) // 2 调用intercepted得到DispatchedContinuation对象 val dispatchedContinuation = continuation.intercepted() // 3 进行后续处理 dispatchedContinuation.resumeCancellableWith(Result.success(Unit)) } 所以我们分析的重点就来到了这个DispatchedContinuation类。在分析该类之前,我们来看一下Dispatchers.Default的具体继承关系://类型是CoroutineDispatcher,实现是DefaultScheduler public actual val Default: CoroutineDispatcher = DefaultScheduler //单例,继承至SchedulerCoroutineDispatcher internal object DefaultScheduler : SchedulerCoroutineDispatcher //继承至ExecutorCoroutineDispatcher internal open class SchedulerCoroutineDispatcher( private val corePoolSize: Int = CORE_POOL_SIZE, private val maxPoolSize: Int = MAX_POOL_SIZE, private val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS, private val schedulerName: String = "CoroutineScheduler", ) : ExecutorCoroutineDispatcher() //继承至CoroutineDispatcher public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher() 可以发现最终继承至CoroutineDispatcher抽象类,关于该类的关系,在前面我们分析过了。DispatchedContinuation类分析先来看一下这个DispatchedContinuation类的定义:internal class DispatchedContinuation<in T>( @JvmField val dispatcher: CoroutineDispatcher, @JvmField val continuation: Continuation<T> ) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {} 会发现这个类的定义还是非常复杂的,首先它实现了Continuation接口,但是使用了类委托,把接口实现都委托给了传递进来的continuation参数。其次参数dispatcher就是前面分析的默认参数Dispatchers.Default,continuation就是block协程实现类,具体类型是SuspendLambda。然后发现这个类还继承至DispatchedTask,我们来看一下这个类:internal abstract class DispatchedTask<in T>( @JvmField public var resumeMode: Int ) : SchedulerTask() {} internal actual typealias SchedulerTask = Task internal abstract class Task( @JvmField var submissionTime: Long, @JvmField var taskContext: TaskContext ) : Runnable{} 这里会发现DispatchedTask继承至SchedulerTask,该类继承至Task,而Task则继承至我们非常熟悉的Runnable接口,这也就意味着它可以被分到Java的线程当中去执行了。resumeCancellableWith()分析既然它是Runnable,我们就来看看如何进行分配,来看看resumeCancellableWith()的代码实现://DispatchedContinuation类 inline fun resumeCancellableWith( result: Result<T>, noinline onCancellation: ((cause: Throwable) -> Unit)? ) { val state = result.toState(onCancellation) //注释1 if (dispatcher.isDispatchNeeded(context)) { _state = state resumeMode = MODE_CANCELLABLE //注释2 dispatcher.dispatch(context, this) } else { //注释3 executeUnconfined(state, MODE_CANCELLABLE) { if (!resumeCancelled(state)) { resumeUndispatchedWith(result) } } } } 这里注释1的isDispatchNeeded的意思是是否需要分发,这里只有当是Dispatchers.Unconfined时,才返回false,所以本例中代码是Dispatchers.Default,所以会进入注释2的逻辑。注释2就是使用线程池来进行分发,其中把this(实现了Runnable接口)传递。注释3是设置了Dispatcher.Unconfined的情况下,这里会直接在当前线程执行。这里调用的是dispatcher.dispatch(),其实我们知道,就是Dispatchers.Default.dispatch()方法,下面是我们回到Dispatchers.Default的源码:public actual val Default: CoroutineDispatcher = DefaultScheduler internal object DefaultScheduler : SchedulerCoroutineDispatcher( CORE_POOL_SIZE, MAX_POOL_SIZE, IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME ) {} DefaultScheduler则是一个单例,继承至SchedulerCoroutineDispatcher,internal open class SchedulerCoroutineDispatcher( private val corePoolSize: Int = CORE_POOL_SIZE, private val maxPoolSize: Int = MAX_POOL_SIZE, private val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS, private val schedulerName: String = "CoroutineScheduler", ) : ExecutorCoroutineDispatcher() { private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName) //这就是前面调用的dispatch方法 override fun dispatch(context: CoroutineContext, block: Runnable): Unit = coroutineScheduler.dispatch(block) } 而这里我们发现dispatch方法中会调用CoroutineScheduler中的dispatch()方法:internal class CoroutineScheduler( @JvmField val corePoolSize: Int, @JvmField val maxPoolSize: Int, @JvmField val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS, @JvmField val schedulerName: String = DEFAULT_SCHEDULER_NAME ) : Executor, Closeable { override fun execute(command: Runnable) = dispatch(command) //核心方法 fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) { trackTask() // 1 val task = createTask(block, taskContext) // 2 val currentWorker = currentWorker() // 3 val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch) if (notAdded != null) { if (!addToGlobalQueue(notAdded)) { throw RejectedExecutionException("$schedulerName was terminated") } } val skipUnpark = tailDispatch && currentWorker != null if (task.mode == TASK_NON_BLOCKING) { if (skipUnpark) return signalCpuWork() } else { signalBlockingWork(skipUnpark = skipUnpark) } } private fun currentWorker(): Worker? = (Thread.currentThread() as? Worker)?.takeIf { it.scheduler == this } // 内部类 Worker internal inner class Worker private constructor() : Thread() { } } 这里我们发现CoroutineScheduler其实就是Java并发包Executor的子类,它的execute方法也被转到了dispatch方法,所以终于到了Java线程处理部分了,上面代码有3个注释:注释1,将传入的Runnable类型的block,其实也就是DispatchedContinuation,包装成Task。注释2,currentWorker()就是拿到当前执行的线程,这里的Worker是一个内部类,它的本质是Java的Thread。注释3,currentWorker.submitToLocalQueue(),将当前的Task添加到Worker线程的本地队列中,等待执行。这里我们来分析一下Worker是如何执行Task的。下面是Worker代码,代码较多,只列出有用信息:internal inner class Worker private constructor() : Thread() { //重写了Thread的run方法 override fun run() = runWorker() @JvmField var mayHaveLocalTasks = false private fun runWorker() { var rescanned = false while (!isTerminated && state != WorkerState.TERMINATED) { // 1 找到task val task = findTask(mayHaveLocalTasks) if (task != null) { rescanned = false minDelayUntilStealableTaskNs = 0L // 2 执行task executeTask(task) continue } else { mayHaveLocalTasks = false } if (minDelayUntilStealableTaskNs != 0L) { if (!rescanned) { rescanned = true } else { rescanned = false tryReleaseCpu(WorkerState.PARKING) interrupted() LockSupport.parkNanos(minDelayUntilStealableTaskNs) minDelayUntilStealableTaskNs = 0L } continue } tryPark() } tryReleaseCpu(WorkerState.TERMINATED) } } 这里Worker会重写Thread的run()方法,然后把执行流程交由给runWorkder(),这里代码注意2点:注释1,会在while循环中,一直尝试从Worker的本地队列中取出Task。注释2,executeTask方法,来执行其对应的Task。接下来就是关键的执行Task代码:internal inner class Worker private constructor() : Thread() { private fun executeTask(task: Task) { val taskMode = task.mode idleReset(taskMode) beforeTask(taskMode) // 1 执行task runSafely(task) afterTask(taskMode) } } fun runSafely(task: Task) { try { // 2 调用run方法 task.run() } catch (e: Throwable) { val thread = Thread.currentThread() thread.uncaughtExceptionHandler.uncaughtException(thread, e) } finally { unTrackTask() } } internal abstract class Task( @JvmField var submissionTime: Long, @JvmField var taskContext: TaskContext ) : Runnable { constructor() : this(0, NonBlockingContext) inline val mode: Int get() = taskContext.taskMode // TASK_XXX } 这里我们调用runSafely方法,然后在这个方法中我们执行了task.run(),而Task的本质是Runnable,到目前就代表了我们的协程任务真正执行了。注意又回到前面了,这里的run执行的具体逻辑是啥,从前面类的继承关系来看,这里执行的是DispatchedTask.run()方法,而这个类实际上是DispatchedContinuation的子类,所以会调用下面代码:internal class DispatchedContinuation<in T>( @JvmField val dispatcher: CoroutineDispatcher, @JvmField val continuation: Continuation<T> ) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation { //会回调该方法 public final override fun run() { val taskContext = this.taskContext var fatalException: Throwable? = null try { val delegate = delegate as DispatchedContinuation<T> val continuation = delegate.continuation withContinuationContext(continuation, delegate.countOrElement) { val context = continuation.context val state = takeState() val exception = getExceptionalResult(state) val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null if (job != null && !job.isActive) { // 1 val cause = job.getCancellationException() cancelCompletedResult(state, cause) continuation.resumeWithStackTrace(cause) } else { if (exception != null) { // 2 continuation.resumeWithException(exception) } else { // 3 continuation.resume(getSuccessfulResult(state)) } } } } catch (e: Throwable) { fatalException = e } finally { val result = runCatching { taskContext.afterTask() } handleFatalException(fatalException, result.exceptionOrNull()) } } } 上面代码主要就看3个注释点:注释1,在协程代码执行之前,首先判断协程是否已经被取消,如果已经取消,则通过resumeWithStackTrace把具体原因传出去。注释2,判断协程是否发生了异常,如果发生了异常,则通过resumeWithException将异常传递出去。注释3,如果一切正常,则调用resume启动协程,并且执行launch中传入的lambda表达式。到这里,我们就完全分析完了整个流程。总结本篇文章分析了launch的流程,而其中与线程交互重点就是Dispatchers,主要有下面几个步骤:第一步,createCoroutineUnintercepted(completion)创建了协程的Continuation实例,接着调用intercepted()方法,将其封装为DispatchedContinuation对象。第二步,DispatchedContinuation会持有CoroutineDispatcher、以及前面创建的Continuation对象,比如文中的CoroutineDispatcher就是Default线程池。第三步,执行DispatchedContinuation的resumeCancellableWith()方法,会执行dispatcher.dispatch()方法,这个会将Continuation封装为Task,添加到线程中去执行。在这一步,协程就已经完成了线程切换。第四步,线程run方法会调用DispatchedContinuation的run方法,会调用continuation.resume方法,它将执行原本launch当中生成的SuspendLambda子类,这时候协程的代码就在线程上执行了。分析完会发现,协程框架设计是非常巧妙的,将线程池信息保存在协程上下文中,把创建完的协程SuspendLambda实例实现Runnable接口,然后封装为Task,在指定线程池中运行,从而完成线程切换。
0
0
0
浏览量228
德州安卓

协程(22) | Channel原理解析

前言在前面文章我们介绍过Channel的使用,Channel主要用于协程间的通信,相比于Flow,它还是热的,即不管有没有消费者,它都会往Channel中发射数据,即发射端一直会工作,就和一位热情的服务员一样。那本篇文章,就来解析一波Channel的原理,看看是如何实现在协程间通信的,以及探究"热"的原因。正文我们还是以简单例子入手,来逐步分析。Channel()顶层函数我们创建一个没有缓存容量的Channel,如下:fun main() { val scope = CoroutineScope(Job()) //创建管道,都使用默认参数 val channel = Channel<Int>() scope.launch { //在一个单独的协程当中发送管道消息 repeat(3) { channel.send(it) println("Send: $it") } channel.close() } scope.launch { //在一个单独的协程当中接收管道消息 repeat(3) { val result = channel.receive() println("Receive $result") } } println("end") Thread.sleep(2000000L) } /* 输出结果: end Receive 0 Send: 0 Send: 1 Receive 1 Receive 2 Send: 2 */ 在这里会发现输出结果是交替执行的,这是因为Channel的send和receive是挂起函数,而默认参数创建的Channel是没有缓存容量的,所以调用完send后,如果没有消费者来消费,就会挂起;同理receive也是如此,这些知识点我们在之前学习Channel文章时,已经说过这些特性了。再结合挂起函数的本质,这种交替执行的输出结果,我相信都能明白。本篇文章,就来探索一下,Channel到底是如何实现的。和我们之前分析的CoroutineScope、Job等类似,Channel()也是一个顶层函数充当构造函数使用的案例,该方法代码如下://顶层函数充当构造函数使用 public fun <E> Channel( //容量 capacity: Int = RENDEZVOUS, //背压策略 onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, //元素投递失败回调 onUndeliveredElement: ((E) -> Unit)? = null ): Channel<E> = when (capacity) { //根据容量分类 RENDEZVOUS -> { //默认参数下,所创建的Channel if (onBufferOverflow == BufferOverflow.SUSPEND) RendezvousChannel(onUndeliveredElement) else //背压策略是非挂起情况下的实现 ArrayChannel(1, onBufferOverflow, onUndeliveredElement) } CONFLATED -> { ... ConflatedChannel(onUndeliveredElement) } UNLIMITED -> LinkedListChannel(onUndeliveredElement) //容量为2,默认也是ArrayChannel BUFFERED -> ArrayChannel( if (onBufferOverflow == BufferOverflow.SUSPEND) CHANNEL_DEFAULT_CAPACITY else 1, onBufferOverflow, onUndeliveredElement ) //其他自定义容量 else -> { if (capacity == 1 && onBufferOverflow == BufferOverflow.DROP_OLDEST) ConflatedChannel(onUndeliveredElement) else ArrayChannel(capacity, onBufferOverflow, onUndeliveredElement) } } 由该顶层函数我们可以看出,根据我们所传入的参数不同,会创建不同的Channel实例,比如RendezvousChannel、ArrayChannel等,我们等会以默认的RendezvousChannel为例来分析。这里有个小知识点,就是onUndeliveredElement参数,这里使用函数类型,即符合Kotlin的语法规则,又不用创建多余接口。但是(E) -> Unit这种函数类型是否会造成误解呢?因为毕竟丢失的元素可以用这个函数类型表示,那我再定义一个到达元素的回调呢,是不是也可以定义为(E) -> Unit。为了避免造成这种误解,我们看看是如何实现的,我们看看RendezvousChannel的定义:internal open class RendezvousChannel<E>(onUndeliveredElement: OnUndeliveredElement<E>?) : AbstractChannel<E>(onUndeliveredElement) 会发现这里参数类型居然是OnUndeliveredElement,这就很容易理解了。这里难道是定义了接口吗?我们查看一下:internal typealias OnUndeliveredElement<E> = (E) -> Unit 可以发现这里只是给类型起了一个别名,通过typealias可以给一些容易造成理解混乱的函数类型起个名字,这个小知识点,在实际业务中,还是蛮有用的。回到主线,我们来分析RendezvousChannel的继承关系://该类继承至AbstractChannel internal open class RendezvousChannel<E>(onUndeliveredElement: OnUndeliveredElement<E>?) : AbstractChannel<E>(onUndeliveredElement) //继承至AbstractSendChannel类,实现Channel接口 internal abstract class AbstractChannel<E>( onUndeliveredElement: OnUndeliveredElement<E>? ) : AbstractSendChannel<E>(onUndeliveredElement), Channel<E> //实现SendChannel接口 internal abstract class AbstractSendChannel<E>( @JvmField protected val onUndeliveredElement: OnUndeliveredElement<E>? ) : SendChannel<E> //Channel接口,继承至SendChannel和ReceiveChannel接口 public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> 乍一看,这里的接口和抽象类定义的有点复杂,但是我们稍微分析一下,就会发现这样定义挺合理:首先就是一个最基础的问题,接口和抽象类的区别? 从面向对象解读来看,以及使用角度来分析,接口是倾向于约束公共的功能,或者给一个类添加额外的功能,某个类实现了接口,它就有了一些额外的能力行为。同时约束了该类,有这些功能。 比如这里的SendChannel接口,就表示一个管道发送方,所以它约束了一些统一操作:send、trySend等。 而抽象类,更多的是公共代码的抽取,或者一个抽象事务的基本实现。比如这里的AbstractChannel<E>就代表传递E类型的抽象管道实现,在里面实现了大多数的公共函数功能。这里Channel接口,继承至SendChannel和ReceiveChannel,即把发送端和接收端给分开了,根据接口的定义,Channel就是具有发送端和接收端的管道。这里AbstractChannel代表发送方的抽象实现或者公共实现,构造函数的参数可以接收发送失败的回调处理。搞明白这几个抽象类,我们接下来就很好分析了。LockFreeLinkedList简析首先是AbstractChannel,为什么发送端单独需要抽离出一个抽象类呢?这也是因为,发送端的逻辑比较复杂,同时它还也是Channel是线程安全的核心实现点。在AbstractChannel中,有下面一个变量:internal abstract class AbstractSendChannel<E>( @JvmField protected val onUndeliveredElement: OnUndeliveredElement<E>? ) : SendChannel<E> { protected val queue = LockFreeLinkedListHead() ... 可以发现这是一个queue,即队列,同时它还是一个线程安全的队列,从LockFreeLinkedList就可以看出,它是一个没有使用锁Lock的LinkedList。//Head只是一个哨兵节点 public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() //线程安全的双向链表 public actual open class LockFreeLinkedListNode { private val _next = atomic<Any>(this) // Node | Removed | OpDescriptor private val _prev = atomic(this) // Node to the left (cannot be marked as removed) private val _removedRef = atomic<Removed?>(null) 关于这个数据结构,这里不做过多分析,等后面有时间可以专门研究一下,这个线程安全的数据结构,有如下特点:它是一个双向链表结构,按理说双向链表的插入可以从头或者尾都是可以的,但是在这里,定义了插入只能是尾部,即右边;而获取元素,只能从头部,即左边。它有一个哨兵节点,哨兵节点是不存储数据的,它的next节点是数据节点的头节点,它的pre节点是数据节点的尾节点,当数据节点为空时,依旧有哨兵节点。该数据结构中,保存数据使用了atomic,即CAS技术,这样可以保证这个链表的操作是线程安全的。到这里,我们已经知道了在AbstractChannel中存在一个线程安全的双向队列,至于节点保存的数据是什么,后面待会再分析。send流程分析我们以文章开始的测试代码为例,当调用send(0)时,实现方法就是AbstractChannel中://发送数据 public final override suspend fun send(element: E) { // fast path -- try offer non-blocking if (offerInternal(element) === OFFER_SUCCESS) return // slow-path does suspend or throws exception //挂起函数 return sendSuspend(element) } 在该方法中,有2个分支,当offerInternal方法返回结果为OFFER_SUCCESS时,就直接return,否则调用挂起发送函数sendSuspend。看到这个offerInternal(element)方法,我相信肯定会立马和前面所说的队列结合起来,因为offer这个单词就属于队列中的一种术语,表示增加的意思,和add一样,但是返回值不一样。所以我们可以大致猜出该方法作用:把element添加到队列中,如果添加成功,则直接返回,否则则挂起。我们来看看offerInternal()方法://尝试往buffer中增加元素,或者给消费者增加元素 protected open fun offerInternal(element: E): Any { while (true) { val receive = takeFirstReceiveOrPeekClosed() ?: return OFFER_FAILED val token = receive.tryResumeReceive(element, null) if (token != null) { assert { token === RESUME_TOKEN } receive.completeResumeReceive(element) return receive.offerResult } } } 该方法会往buffer中或者消费者增加数据,会成功返回数据,或者增加失败。根据前面我们设置的是默认Channel,是没有buffer的,且没有调用receive,即也没有消费者,所以这里会直接返回OFFER_FAILED。所以我们执行流程跳转到sendSuspend://send的挂起函数 private suspend fun sendSuspend(element: E): Unit = suspendCancellableCoroutineReusable sc@ { cont -> loop@ while (true) { //buffer是否已满,本例中,是满的 if (isFullImpl) { //封装为SendElement val send = if (onUndeliveredElement == null) SendElement(element, cont) else SendElementWithUndeliveredHandler(element, cont, onUndeliveredElement) //入队 val enqueueResult = enqueueSend(send) when { enqueueResult == null -> { // enqueued successfully cont.removeOnCancellation(send) return@sc } enqueueResult is Closed<*> -> { cont.helpCloseAndResumeWithSendException(element, enqueueResult) return@sc } enqueueResult === ENQUEUE_FAILED -> {} // try to offer instead enqueueResult is Receive<*> -> {} // try to offer instead else -> error("enqueueSend returned $enqueueResult") } } ... } } 这就是send的挂起函数方式实现,分析:这里使用suspendCancellableCoroutineReusable挂起函数,和我们之前所说的suspendCancellableCoroutine{}高阶函数一样,属于能接触到的最底层实现挂起函数的方法了,其中cont就是用来向挂起函数外部传递数据。在实现体中,首先判断isFullImpl即是否满了,由于本例测试代码的Channel是没有容量的,所以是满的。然后把element和cont封装为SendElement对象,这里的element就是我们之前所发送的0, 而continuation则代表后续的操作。 这个SendElement类定义如下://发送元素 internal open class SendElement<E>( override val pollResult: E, @JvmField val cont: CancellableContinuation<Unit> ) : Send() { override fun tryResumeSend(otherOp: PrepareOp?): Symbol? { val token = cont.tryResume(Unit, otherOp?.desc) ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() // finish preparations return RESUME_TOKEN } override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN) override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException) override fun toString(): String = "$classSimpleName@$hexAddress($pollResult)" } 从这里我们可以看出,这个Element就是把要发送的元素和Continuation给包装起来,而前面所说的双向链表中的元素也就是这种Element。接着调用enqueueSend方法,把上面这个Element入队,根据该方法的返回值定义,这里会返回null,表示插入成功。然后当入队成功时,会调用下面代码块:enqueueResult == null -> { // enqueued successfully cont.removeOnCancellation(send) return@sc } 这里先是给cont设置了一个监听://给CancellableContinuation设置监听 internal fun CancellableContinuation<*>.removeOnCancellation(node: LockFreeLinkedListNode) = invokeOnCancellation(handler = RemoveOnCancel(node).asHandler) //当Continuation被取消时,节点自动从队列中remove掉 private class RemoveOnCancel(private val node: LockFreeLinkedListNode) : BeforeResumeCancelHandler() { override fun invoke(cause: Throwable?) { node.remove() } override fun toString() = "RemoveOnCancel[$node]" } 这个监听作用就是当Continuation执行完成或者被取消时,该节点可以从双向队列中被移除。然后就是return@sc,这里是不是很疑惑呢?在以前我们实现挂起函数时,都是通过continuation的resume方法来传递挂起函数的值,同时也是恢复的步骤,这里居然没有恢复。那这个挂起函数该什么时候恢复呢?Channel是如何来恢复的呢?receive流程分析我们接着分析,其实就是当调用receive()的时候。receive()的实现,根据前面分析就是在AbstractChannel中://接收方法的实现 public final override suspend fun receive(): E { // fast path -- try poll non-blocking val result = pollInternal() @Suppress("UNCHECKED_CAST") if (result !== POLL_FAILED && result !is Closed<*>) return result as E // slow-path does suspend return receiveSuspend(RECEIVE_THROWS_ON_CLOSE) } 这里同样是类似的逻辑,首先是pollInternal方法,这里的poll同样和offer一样,属于队列的术语,有轮询的意思,和remove类似的意思,所以该方法就是从队列中取出元素,我们来看看实现://尝试从buffer或者发送端中取出元素 protected open fun pollInternal(): Any? { while (true) { //取出SendElement val send = takeFirstSendOrPeekClosed() ?: return POLL_FAILED //注释1 val token = send.tryResumeSend(null) if (token != null) { assert { token === RESUME_TOKEN } //注释2 send.completeResumeSend() return send.pollResult } // too late, already cancelled, but we removed it from the queue and need to notify on undelivered element send.undeliveredElement() } } 根据前面我们send的流程,这时可以成功取出我们之前入队的SendElement对象,然后调用注释2处的send.completeResumeSend()方法:override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN) 这里会调用continuation的completeResume方法,这里就需要结合前面文章所说的原理了,其实这个continuation就是状态机,它会回调CancellableContinuationImpl中的completeResume:override fun completeResume(token: Any) { assert { token === RESUME_TOKEN } dispatchResume(resumeMode) } 而该类的继承关系:internal open class CancellableContinuationImpl<in T>( final override val delegate: Continuation<T>, resumeMode: Int ) : DispatchedTask<T>(resumeMode), CancellableContinuation<T>, CoroutineStackFrame 这里相关的类,我们在线程调度那篇文章中有所提及,这里的dispatchResume:private fun dispatchResume(mode: Int) { if (tryResume()) return // completed before getResult invocation -- bail out // otherwise, getResult has already commenced, i.e. completed later or in other thread dispatch(mode) } internal fun <T> DispatchedTask<T>.dispatch(mode: Int) { ... if (dispatcher.isDispatchNeeded(context)) { dispatcher.dispatch(context, this) } ... } 这里最终会调用dispatcher.dispatch()方法,而这个我们在之前调度器文章说过,这个最后会在Java线程池上执行,从而开始状态机。既然该状态机恢复了,也就是前面send流程中的挂起也恢复了。当send挂起函数恢复后,再通过return send.pollResult 就可以获取我们之前发送的值0了。同样的,当pollInternal方法中,无法poll出SendElement,则会调用receiveSuspend挂起方法:private suspend fun <R> receiveSuspend(receiveMode: Int): R = suspendCancellableCoroutineReusable sc@ { cont -> val receive = if (onUndeliveredElement == null) ReceiveElement(cont as CancellableContinuation<Any?>, receiveMode) else ReceiveElementWithUndeliveredHandler(cont as CancellableContinuation<Any?>, receiveMode, onUndeliveredElement) while (true) { if (enqueueReceive(receive)) { removeReceiveOnCancel(cont, receive) return@sc } // hm... something is not right. try to poll val result = pollInternal() if (result is Closed<*>) { receive.resumeReceiveClosed(result) return@sc } if (result !== POLL_FAILED) { cont.resume(receive.resumeValue(result as E), receive.resumeOnCancellationFun(result as E)) return@sc } } } 和send类似,这里也会封装为ReceiveElement,同时入队到队列中,等待着send方法来恢复这个协程。"热"的探究分析完默认的Channel的发送和接收,我们来探究一下为什么Channel是热的。这里所说的热是因为Channel会在不管有没有接收者的情况下,都会执行发送端的操作,当策略为Suspend时,它会一直持续到管道容量满。这里我们还是拿之前文章的例子:fun main() = runBlocking { //创建管道 val channel = produce(capacity = 10) { (1 .. 3).forEach { send(it) logX("Send $it") } } logX("end") } 这里虽然没有调用receive方法,即没有消费者,send依旧会执行,也就是"热"的。根据前面所说的Channel()顶层函数源码,这里容量为10,策略不变,最终会创建出ArrayChannel实例。该类定义:internal open class ArrayChannel<E>( /** * Buffer capacity. */ private val capacity: Int, private val onBufferOverflow: BufferOverflow, onUndeliveredElement: OnUndeliveredElement<E>? ) : AbstractChannel<E>(onUndeliveredElement) 这里同样是AbstractChannel的子类,所以send方法还是依旧:public final override suspend fun send(element: E) { // fast path -- try offer non-blocking if (offerInternal(element) === OFFER_SUCCESS) return // slow-path does suspend or throws exception return sendSuspend(element) } 还是先尝试往队列中offer数据,当无法offer时,执行挂起;但是这里的offerInternal方法在ArrayChannel中被重写了://ArrayChannel中的方法 protected override fun offerInternal(element: E): Any { //接收者 var receive: ReceiveOrClosed<E>? = null //当多个线程都同时调用该方法时,为了容量安全,这里进行加锁 lock.withLock { //元素个数 val size = this.size.value //发送已经关闭,直接返回 closedForSend?.let { return it } // update size before checking queue (!!!) //在入队之前,更新管道容量,当元素小于管道容量,返回null //只有管道中的元素个数,大于管道容量时,该方法才会return //根据策略,会返回挂起或者丢弃或者失败等 updateBufferSize(size)?.let { return it } ... //容量没满时,把元素入队 enqueueElement(size, element) //返回入队成功 return OFFER_SUCCESS } ... } 在这里我们可以发现,不管有没有接收者的情况下,当我们多次调用send方法,当队列没满时,在这里都会返回OFFER_SUCCESS,即发送端已经在工作了,所以也就是我们所说的热的效果。总结Channel作为线程安全的管道,可以在协程之间通信,同时可以实现交替执行的效果,通过本篇文章学习,我相信已经知道其原因了。小小总结一下:Channel接口在设计时就非常巧妙,充分利用了接口和抽象,把发送端和接收端能力分开,这个值得我们学习。Channel的线程安全原因是发送端维护了一个线程安全的双向队列:LockFreeLinkedList,我们把值和continutaion封装为SendElement/ReceiveElement保存其中,这样就保证了线程安全。Channel的发送和接收挂起函数的恢复时机,是通过队列中的continuation控制,在CancellableContinuationImpl进行直接恢复,而不是我们常见的调用resumeWith方法。
0
0
0
浏览量1222
德州安卓

协程(8) | Channel

前言在前面介绍中,我们介绍了几种启动协程的方法,以及挂起函数,在使用async启动的协程中,我们可以通过await方法来获取协程的返回值。但是这些返回值都是只有一个,即当挂起的函数或者协程,在挂起后重新恢复时,只返回了一个结果。在普通业务中,这种模式是可行的,但是有些特殊业务,比如:前台应用不断更新手机GPS返回的经纬度,如果使用回调的话,我们可以非常容易实现,但是想用协程以同步方式优雅地实现,就需要引入新地东西,即本章的Channel可以解决。正文Channel直接翻译就是管道,我们可以利用这个概念来简单构建一个思维模型:通过这个思维模型,我们可以把Channel看成是一个封闭的管道,它只有发送方和接收方,数据从发送方发出可以被接收方收到,中间不会被修改。Channel的使用我们先来看看Channel的简单使用,实例代码如下:fun main() = runBlocking { //创建一个管道,可以传递Int类型的值 val channel = Channel<Int>() //开启一个协程 launch { //在该协程中发送消息 channel.send(1) logX("Send 1") channel.send(2) logX("Send 2") channel.send(3) logX("Send 3") } //开启一个协程 launch { //在另一个协程中接收管道消息 //这里之所以可以写出for循环的代码,这里也是根据Kotlin的约定,即运算符重载,重载了iterator(), //这个在集合篇中有详细说过 for (i in channel){ logX("Receive: $i") } } logX("end") } 在之前学习中,我们说过launch适合启动一些一劳永逸、不需要返回结果的任务协程,但是在这里,我们获取协程里面的代码执行结果而不必那么费劲了,必须使用async了,这里的Channel就像是一个管道,管道的俩头分别在俩个协程中,即Channel可以轻松实现协程间的通信。在上面代码中,首先就是接收数据的协程中,使用for循环来获取管道的值,这个是运算符重载的简写。其次,俩个协程都是运行在runBlocking启动的协程中,特点是会阻塞线程且会等待其子协程运行完成。我们来看一下上面代码运行结果:这里我们可以发现end先打印,这时因为launch启动的协程就如射箭,它们不会阻塞父协程代码执行。然后就是Receive和Send的打印交替执行,甚至还出现了Receive 1比Send 1先打印(打印的代码会被挂起),这是因为这俩个协程是交替执行的,send发送数据函数和接收数据函数是挂起函数。挂起函数的本质是Callback,且挂起的是协程后面的代码,在上面代码默认情况下:调用完channel.send(1),发送数据的协程就会挂起,等待接收协程把管道中的数据取走后,再恢复打印log和调用channel.send(2)。理解了挂起函数后,我相信上面利用挂起函数来实现的交替执行都不难理解了。但是从打印中也会发现一个问题,就是程序在输出完所有结果后并没有退出,即主线程不会结束,整个程序还会处于运行状态。关闭Channel出现这个原因是因为Channel无法知道数据有没有发送完,依旧会挂起等待,想解决上面问题也非常简单,发送完关闭这个Channel即可:fun main() = runBlocking { val channel = Channel<Int>() launch { (1 .. 3).forEach{ channel.send(it) logX("Send $it") } //这里加一句代码,发送完数据,及时关闭channel channel.close() } launch { for (i in channel){ logX("Receive: $i") } } logX("end") } 所以从这里可以看出Channel其实是一种协程资源,在使用完Channel以后,如果不主动关闭的话,会造成不必要的资源浪费。Channel简析现在我们来分析一下Channel的“构造函数”,来看看如何修改参数来达到不同的效果。代码如下:public fun <E> Channel( capacity: Int = RENDEZVOUS, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, onUndeliveredElement: ((E) -> Unit)? = null ): Channel<E> 可以看到当调用Channel()时看着像是在调用一个构造函数,其实它却是一个顶层函数,注意这里使用比较特殊,是把顶层函数当作构造函数来用,在这种情况下,函数首字母需要大写。这个函数带有一个泛型参数E,另外还有3个参数,我们一一来看一下这3个参数。capacity这个代表管道的容量,这个非常好理解,就比如我们生活中见到的管道也是有容量的,当接收方不把数据取走时,管道本身也可以保存一些数据。默认情况下是RENDEZVOUS,表示Channel的容量是0。capacity还有其他几种情况,分别如下:UNLIMITED:代表无限容量;CONFLATED:代表容量为1,新的数据会替代旧的数据;BUFFRED:代表一定缓存容量,默认是64;这里的默认值RENDEZOUS,很有意思,它的翻译是约会、会面的意思。onBufferOverflow这个是当指定了capacity的容量,等Channel的容量满了之后,Channel所应对的策略,这里主要有3种做法:SUSPEND:当管道的容量满了以后,如果发送方继续发送数据,我们会挂起当前的send()方法。由于它是一个挂起函数,所以我们可以非阻塞的方式将发送方的流程挂起,等管道容量有空闲位置以后再恢复。这个逻辑非常好理解,就和Java实现的阻塞队列一样。DROP_OLDEST:顾名思义,就是丢弃掉最旧的那个数据;DROP_LATEST丢掉最新的数据,这里指还没有进入管道的数据;这里有一张图,可以更好地表达上面几种做法:onUndeliveredElement这个相当于一个异常处理回调,当管道种某些数据没有被成功接收的时候,这个回调就会被调用。这里其实也需要注意,是数据已经发送了,但是没有被接收,才会触发回调;而不是管道满了,把数据丢了,这种情况不会触发回调。这种只有非常在意数据是否传输正常的业务,才会用到这个回调。测试例子说完上面各个参数的作用,我们来看一些例子。设置容量capacity = UNLIMITED,即容量无限,代码如下:fun main() = runBlocking { //创建管道,容量设置为无限容量 val channel = Channel<Int>(capacity = Channel.Factory.UNLIMITED) launch { //在一个单独地协程中发送管道消息 channel.send(1) logX("Send 1") channel.send(2) logX("Send 2") channel.send(3) logX("Send 3") } launch { //在另一个协程中接收管道消息 for (i in channel){ logX("Receive: $i") } } logX("end") } 这里的代码只改变一点,就是容量无限大,然后onBufferOverflow策略不变,我们可以想一下,第一个子协程在运行时,会发现管道容量很大,所以3个send()方法执行完后,管道还没有满,协程不会被挂起,子协程1执行完成。然后协程2开始遍历取出channel中的数据,所以结果如下:是先连续发送3个,再挨个接收3个。设置capacity = CONFLATED,即容量为1,但是根据定义,新的数据会替代旧的数据,这个还是要注意的,这个和你自己设置容量为1的情况大不相同,比如下面代码是设置capacity = CONFLATED的代码和运行结果://创建管道,容量为 CONFLATED val channel = Channel<Int>(capacity = Channel.Factory.CONFLATED) 代码运行结果如下:这里会发现即使channel已经满了,但是依旧会发送数据,而不是挂起,所以会发现3个数据,接收方只接收了一个最新值。下面代码仅仅是设置capacity = 1的情况://创建管道 val channel = Channel<Int>(capacity = 1) 代码运行结果如下:这里就符合正常的管道容量为1且默认满的时候是挂起的逻辑了。所以当设置为CONFLATED时,onBufferOverflow的挂起将会失效,会替换新值。这里我们结合onBufferOverflow策略,当使用DROP_OLDEST策略即丢弃最老值和同时设置capacity值为1时,就实现了capacity为CONFLATED一样的效果:val channel = Channel<Int>(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) 这里我们可以想一下,为什么capacity需要搞一个CONFLATED这个模式呢因为在我们的业务中,经常会出现一个场景就是我只需要最新的值;比如Android中的页面UI数据,在页面在后台时,我们就可以把无用数据丢弃,只需要最新值即可。所以CONFLATED非常合理。再看一下onBufferOverflow使用DROP_LATEST策略,这里注意使用这个策略,就不会再出现发送方挂起的情况了,所以我们在日常使用时尽量多使用SUSPEND默认策略,下面是使用DROP_LATEST策略:fun main() = runBlocking { //创建管道,容量为3,当管道满的时候,会丢弃最新的 val channel = Channel<Int>(capacity = 3, onBufferOverflow = BufferOverflow.DROP_LATEST) launch { //在一个单独地协程中发送管道消息 channel.send(1) logX("Send 1") channel.send(2) logX("Send 2") channel.send(3) logX("Send 3") channel.send(4) //会被丢弃 logX("Send 4") } launch { //在另一个协程中接收管道消息 for (i in channel) { logX("Receive: $i") } } logX("end") } 比如上面代码容量为3的管道,发送了4个数据,数据4会被丢弃,结果打印如下:在这种情况下,就会发现这个管道只是用来连接2个协程通信而已,不会有挂起、恢复等操作,在实际业务中,这种使用场景可能不会太多。最后看看这个onUndeliveredElement这个参数,就是当消息没有传递成功的回调,比如下面代码://创建管道,容量为3,背压策略是丢弃最新的,未传递策略是打印 val channel = Channel<Int>(capacity = 3, onBufferOverflow = BufferOverflow.DROP_LATEST){ println("传递失败 $it") } launch { //在一个单独地协程中发送管道消息 channel.send(1) logX("Send 1") channel.send(2) logX("Send 2") channel.send(3) logX("Send 3") channel.send(4) logX("Send 4") } launch { //在另一个协程中接收管道消息 channel.receive() channel.cancel() } 我们在一个子协程上发送了4个数据,但是管道容量为3,所以数据4直接会被丢弃,这个不算是传递失败,然后在另一个子协程中就receive()了一次,然后取消管道,这就导致有2个会传递失败,运行打印如下:Channel 关闭引发的问题在前面代码中我们一直有个问题没有解决,就是使用channel,假如忘记调用close(),会导致程序一直无法终止。produce{}生产数据Kotlin的官方也想到了这个问题,所以提供了另一种创建Channel的方式,即produce{}函数。fun main() = runBlocking { //创建管道 val channel = produce { (1 .. 3).forEach { send(it) logX("Send $it") } } launch { //在另一个协程中接收管道消息 for (i in channel){ logX("Receive $i") } } logX("end") } 这里使用produce高阶函数创建了一个Channel,并且发送了3个数据,然后该代码执行结果如下:可以看出程序可以正常退出。receive()前面我们从Channel中取出数据用的是for循环遍历的方式,其实还有一个方法:receive(),它是和send()方法对应的,比如上面代码我们把for循环遍历取出数据的方式改成receive(),并且一次性调用4次:launch { //在另一个协程中接收管道消息 channel.receive() channel.receive() channel.receive() channel.receive() } 这里由于我们发了3个数据,因为produce特性发完3个后会关闭Channel,所以前面代码报出异常:抛出了这个异常,也就说明Channel确实被关闭了。同时我们发现receive()是一个挂起函数,它的定义是当Channel不为空时取出一个数据,而当Channel为空时,会挂起,所以直接使用receive()函数时容易会导致程序永久挂起。isClosedForReceive和isClosedForSend那我就想使用receive()函数呢 这里有2个函数isClosedForReceive和isClosedForSend可以判断在发送时和接收时Channel是否关闭。比如下面代码:fun main() = runBlocking { //创建管道 val channel = produce { (1 .. 3).forEach { send(it) logX("Send $it") } } launch { //在另一个协程中接收管道消息 while (!channel.isClosedForReceive){ println("Receive ${channel.receive()}") } } logX("end") } 上面代码看起来天衣无缝,在接收协程中,循环判断Channel是否关闭,但是结果运行如下:会发现还是会崩溃,所以最好不要使用channel.receive(),即使配合isClosedForReceive()也不要使用。consumeEach{}消费数据那有没有除了for循环其他的方式呢,这里Kotlin为我们提供了一个高阶函数:channel.consumeEach{},我们来看下面的例子:fun main() = runBlocking { //创建管道,使用produce生产数据 val channel = produce { (1 .. 3).forEach { send(it) logX("Send $it") } } launch { //使用consumeEach消费数据 channel.consumeEach { print("Receive $it") } } logX("end") } 上面代码就不会出现异常。综上所述,当使用Channel时,我们建议发送方使用produce{}高阶函数,接收方使用for循环或者consumeEach{}高阶函数。Channel是"热的"前面介绍我们知道Channel就是一个管道,而管道中是数据流,是多个数据组合形成流。如果把挂起函数、async返回的数据比喻成水滴,那channel则像是自来水管。在Kotlin中我们经常拿Channel和Flow做比较,而说Channel是"热"的,那这个"热"该如何理解呢?热情、主动其实我觉得可以直接翻译为热情、主动的意思,比如我们可以下面代码:fun main() = runBlocking { //创建管道 val channel = produce(capacity = 10) { (1 .. 3).forEach { send(it) logX("Send $it") } } logX("end") } 我们定义了一个容量为10的管道,然后发送数据,但是我们没有接收数据,这里的运行结果如下:依旧会让管道中发送消息,而这种不管有没有接收方,发送方都会工作"的模式,我们就认为是"热"的原因。这里就可以类比为热情的饭店服务员,不管你有没有提出要求,服务员都会给你端茶递水,把茶水摆在饭桌上,想喝水的时候,直接从桌上拿即可。也可以类比为前面说的水龙头,Channel的发送方就好比是自来水厂,不管用不用水,自来水厂都会把水送到管道中来,当想喝水的时候,打开水龙头就能喝到水。总结Channel的出现给我们在协程之间通信提供了便利性,这里我们把Channel看成一条密闭的管道,通过produce{}和consumeEach{}来进行产生和消费数据。同时Channel的默认挂起策略,可以让我们更方便的处理数据。最后就是相比于Flow,Channel是热的,也就是不论消费者是否工作,它都会发送数据,会导致资源浪费。
0
0
0
浏览量1447
德州安卓

协程(7) | CoroutineContext

前言前面文章介绍了协程的非阻塞特性和结构化并发特性,在启动协程的函数定义中我们都见到了一个非常熟悉的类:CoroutineContext,这个类可以说是贯穿了整个协程框架,理解CoroutineContext的使用以及常用的类和CoroutineContext的关系对后面理解协程原理非常重要。正文CoroutineContext直接翻译就是协程上下文,这里我们先来看个简单的问题:什么是Context。其实这是一个很有意思的问题,在我们平时编程中很常见各种Context,而翻译为上下文我一直觉得有点问题。假如你在读一篇小说,我突然给你中间一章内容,问你说为什么主角要这样干,你肯定不理解,因为前后章节我都没有看过,即不知道上下文,我也就无法解答这个问题。这是文章中上下文的意思我们很容易理解,但是程序中的上下文该如何理解呢 我觉得把Context翻译为环境更为合适,即代码执行到代码段A中,这个A需要哪些信息才能正确执行下去,这些信息就保存在Context中,因为我们通常只需要上文就可以了,下文一般不需要。所以通俗来理解,Context就可以看成是一个容器,程序需要的一些信息没地方保存就可以保存在Context中。或者更为直接的是,你在编码过程中发现你这个代码需要一大堆配置信息,你大可把这些信息都保存在Context中。比如整个Android应用都要用到某个变量,我就把它定义在ApplicationContext中,这个变量只在协程框架中用到,我就定义在CoroutineContext中。是不是这样理解完就通透了,context就是一堆变量的集合。指定协程运行的线程池这里先不说CoroutineContext的设计,先来看一个ConroutineContext子类的使用,即Dispatchers来指定协程运行的线程。比如下面代码,我想指定协程运行的线程池:fun main() = runBlocking { val user = getUserInfo() logX(user) } suspend fun getUserInfo(): String { logX("Before IO Context.") withContext(Dispatchers.IO) { logX("In IO Context.") delay(1000L) } logX("After IO Context.") return "BoyCoder" } 比如上面的代码,在main()函数中调用挂起函数,但是在挂起函数的执行过程中,可以使用withContext方法来指定运行的线程,withContext函数如下:public suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): T { } 可以发现这里Dispatchers.IO就是一个CoroutineContext,好我们来运行一下代码:可以发现在切换IO线程前,协程是运行在main线程上,当切换了IO线程后,协程运行在worker-1线程上,然后在执行完后,又切换了回来。不止这个withContext,包括runBlocking,launch的第一个参数都是CoroutineContext,我们可以用来指定运行的线程。比如下面代码:fun main() = runBlocking(Dispatchers.IO) { val user = getUserInfo() logX(user) } suspend fun getUserInfo(): String { logX("Before IO Context.") withContext(Dispatchers.Default) { logX("In IO Context.") delay(1000L) } logX("After IO Context.") return "BoyCoder" } 在runBlocking中指定运行的线程,这里我们指定的IO线程,下面指定的是Default线程,我们来看一下代码运行结果:会发现有的在worker-1中,有的在worker-3中,但是都是DefaultDispatcher的线程池,按理说应该是IO的工作线程才对啊,这就要明白Dispatchers几个内置的分别是啥意思。内置的DispatcherKotlin官方提供了几种内置的Dispatcher,分别如下:Dispatchers.Main:它只在UI编程平台才有意义,比如Android平台,这种平台只有Main线程才能用于UI绘制。Dispatchers.Unconfined:代表无所谓,当前协程可以运行在任意线程上。Dispatchers.Default:用于CPU密集型任务的线程池,一般来说它内部的线程个数与机器CPU核心数量保持一致。Dispatchers.IO:用于IO密集型任务的线程池,内部线程数量较多,一般为64个。需要特别注意的是,Dispatchers.IO底层是可能复用Dispatchers.Default中的线程,比如上面截图中的结果,就都是复用Default线程池中的线程。自定义Dispatcher这里除了Kotlin官方内置的几个Dispathcer可以选择外,还可以自定义Dispatcher,比如下面代码:val mySingleDispatcher = Executors.newSingleThreadExecutor { Thread(it,"MySingleThread").apply { isDaemon = true } }.asCoroutineDispatcher() 我们创建了一个单线程的线程池,然后通过asCoroutineDispathcer方法转换为Dispatcher,然后我们来进行使用如下:fun main() = runBlocking(mySingleDispatcher) { val user = getUserInfo() logX(user) } suspend fun getUserInfo(): String { logX("Before IO Context.") withContext(Dispatchers.IO) { logX("In IO Context.") delay(1000L) } logX("After IO Context.") return "BoyCoder" } 这里的runBlocking就传入我们自定义的Dispather,然后运行结果如下:由于只有1个线程的线程池,所以切换IO线程时,会复用Default中的线程池。这其实也就印证了协程是运行在线程上的Task这种说法。万物皆为Context前面我们说了CoroutineDispatcher就是一个CoroutineContext,其实在协程中我们见到的几乎所有重要概念它都是CoroutineContext,比如Job、CoroutineScope等,是不是觉得有点不可思议,我们先来看看这些类和CoroutineContext的关系,后面再说协程为什么这样设计。CoroutineScope协程作用域,我们前面文章说了launch和async都是协程作用域CoroutineScope的扩展函数,我们来看一下这个CoroutineScope:public interface CoroutineScope { public val coroutineContext: CoroutineContext } 会发现它就是一个简单的接口,而这个接口的唯一成员就是CoroutineContext,所以CoroutineScope只是对CoroutineContext做了一层简单封装而已,其核心能力还是CoroutineContext。(涉及后面文章更新感悟,可以暂不理解:这里的Context其实就是协程运行的必要环境信息,而Scope把这个Context封装了一层,然后在launch启动时,就会获取父协程的环境变量信息,从而让子协程和父协程产生关系,这也是结构化并发的原因,在后面CoroutineScope原理解析时会详细说明。)而协程作用域的最大作用就是可以方便批量控制协程,说道批量控制协程,不由得想起来上篇文章所说的结构化并发,看下面代码:fun main() = runBlocking { // 仅用于测试,生成环境不要使用这么简易的CoroutineScope val scope = CoroutineScope(Job()) scope.launch { logX("First start!") delay(1000L) logX("First end!") // 不会执行 } scope.launch { logX("Second start!") delay(1000L) logX("Second end!") // 不会执行 } scope.launch { logX("Third start!") delay(1000L) logX("Third end!") // 不会执行 } delay(500L) scope.cancel() delay(1000L) } 可以发现这里创建了一个scope,然后用这个scope启动了3个协程,当协程没有执行完成时,通过调用scope的cancel方法便可以取消这3个协程,上述代码打印如下:这同样体现了结构化并发的理念。Job前面说了CoroutineScope还是封装的CoroutineContext的话,那Job就是一个真正的CoroutineContext了,我们来看一下源码:public interface Job : CoroutineContext.Element {} public interface Element : CoroutineContext {} 可以发现这里通过2层继承,Job就是CoroutineContext的子类。Dispatcher前面使用案例中说了内置的Dispatcher也是一个CoroutineContext,我们来看一下是如何关联的,下面是Dispatchers的源码:public actual object Dispatchers { @JvmStatic public actual val Default: CoroutineDispatcher = DefaultScheduler @JvmStatic public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher @JvmStatic public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined @JvmStatic public val IO: CoroutineDispatcher = DefaultIoScheduler } 会发现这是一个单例,这样很符合我们的使用,而这里提供了几个线程池,比如Default,它的类型是CoroutineDispathcer,我们来看一下:public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor public interface ContinuationInterceptor : CoroutineContext.Element 根据这个CoroutineDispatcher的继承关系,最终发现还是继承至CoroutineContext。CoroutineNmae从名字就可以看出这个是用来给协程命名的,比如下面代码:fun main() = runBlocking { val scope = CoroutineScope(Job() + mySingleDispatcher) scope.launch(CoroutineName("MyCoroutine")) { logX(coroutineContext[CoroutineDispatcher] == mySingleDispatcher) delay(1000L) logX("MyCoroutine end!") } delay(500L) scope.cancel() delay(1000L) } 这里我们在启动协程时传入一个名字,那打印如下:会发现这里在线程名后@的协程名,就是我们命名的,而#2是一个自增的ID。CoroutineExceptionHandler这个也是从名字就可以看出其作用,即协程异常处理,我们看一下使用,代码如下:fun main() = runBlocking { val scope = CoroutineScope(Job() + mySingleDispatcher) val myExceptionHandler = CoroutineExceptionHandler{_,thorwable -> println("Catch exception : $thorwable") } scope.launch(CoroutineName("MyCoroutine") + myExceptionHandler) { logX(coroutineContext[CoroutineDispatcher] == mySingleDispatcher) val s: String? = null s!!.length delay(1000L) logX("MyCoroutine end!") } delay(500L) scope.cancel() delay(1000L) } 这里在scope启动协程时,传递了协程名Context和异常处理Context,通过加号进行连接,然后这个协程的异常就可以被这个异常处理器捕获,打印如下:CoroutineContext接口设计看了上面我们不免发现协程中居然这么多重要的概念都是CoroutineContext的子类,我们来看看该类的源码:public interface CoroutineContext { //get方法通过operator可以简写为[Key] public operator fun <E : Element> get(key: Key<E>): E? public fun <R> fold(initial: R, operation: (R, Element) -> R): R //plus方法通过operator可以简写为 + public operator fun plus(context: CoroutineContext): CoroutineContext public fun minusKey(key: Key<*>): CoroutineContext //仅仅是接口Key,没有任何属性和方法 public interface Key<E : Element> //继承至CoroutineContext的接口 public interface Element : CoroutineContext { //接口属性,Kotlin的接口属性相当于Java的抽象方法 public val key: Key<*> //根据唯一Key,返回Element public override operator fun <E : Element> get(key: Key<E>): E? = @Suppress("UNCHECKED_CAST") if (this.key == key) this as E else null public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = operation(initial, this) public override fun minusKey(key: Key<*>): CoroutineContext = if (this.key == key) EmptyCoroutineContext else this } } 这个接口设计的非常巧妙,首先是充分利用了Kotlin接口可以带属性和默认实现的特性,其次这种无属性和无抽象方法的Key接口也是很精巧的设计。这里就需要发挥我们对泛型的理解,首先类是对事务的抽象,而泛型可以看成对代码的抽象。就比如本例中,其实就是实现类似Map的效果,通过key来获取对应的Element对象。第一种做法是key使用字符串,然后存和取都是利用这个字符串key,但是这种效果必须要我们手动实现一个Map来保存,来遍历。第二种做法就是协程框架这种,Key接口对象自己就是一个key,这时子类在实现Element时,就必须会有一个Key类型的对象,把这个对象作为key。(这里Key表示接口类型,key表示键值对的键)然后多种类型的Key,直接抽象为Key<T>即可,关于这种用法,我们后面仔细分析,先看一下接口设计。这里的接口设计看着非常像是集合,确切的说很像集合中的Map结构,对比如下图:所以我们完全可以把CoroutineContext当作Map来使用,都是可以通过get(简写为[])、plus(简写为+)来表示,为什么这么设计呢?1、类似Map结构,可以方便运算符重载,快速构造CoroutineContext,比如下面代码fun main() = runBlocking { //手动创建一个CoroutineScope对象 val scope = CoroutineScope(Job() + mySingleDispatcher) scope.launch { //在协程内部,获取coroutineContext上下文对象 logX(coroutineContext[CoroutineDispatcher] == mySingleDispatcher) delay(1000L) logX("First end!") } delay(500L) scope.cancel() delay(1000L) } 这里创建了一个CoroutineScope,使用Job() + mySingleDispatcher这种方式,这里之所以能使用加号,是因为运算符重载。具体原因我们前面分析过,Job就是一个Element对象,而mySingleDispatcher也是一个Element对象,根据接口种定义,所以可以这样操作。Job() + mySingleDispatcher这种方式创建的CoroutineScope其实也就制定了Job和Dispatcher,这种+号就很像集合操作符。由于launch的block是CoroutineScope接收者的高阶函数类型,所以在里面我们可以获取该协程的上下文coroutineContext对象,通过[CoroutineDispatcher]获取Dispatcher。这里又有一个非常有意思的点,这里coroutineContext[CoroutineDispatcher]为什么get()方法传入的是CoroutineDispatcher就可以获取其调度器呢?我们来看一下CoroutineDispatcher源码:public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { //这里伴生对象类Key public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>( ContinuationInterceptor, { it as? CoroutineDispatcher }) ... } //实际该抽象类还是实现Key接口 public abstract class AbstractCoroutineContextKey<B : Element, E : B>( baseKey: Key<B>, private val safeCast: (element: Element) -> E? ) : Key<E> 根据前面我们对接口中Key的理解,已经companion object的本质:声明类的同时,并且创建对象,这里我们应该使用coroutineContext[CoroutineDispatcher.Key]才是合理的,因为根据伴生对象的调用规则:这里的CoroutineContext[CoroutineDispatcher.Key就等于CoroutineContext[CoroutineDispatcher.Key.INSTANCE,毕竟get方法的参数需要一个Key的对象。那为什么这里可以直接传递CoroutineDispatcher作为参数呢?这也是一种伴生对象的简写,因为一个类只能定义一个伴生对象,就比如本类的Key单例,所以可以这样简写。捋清楚这一套操作后,会发现这样设计背后的逻辑也非常精妙。2、从Context本质出发,在文章开始说了,它其实就是一大堆环境变量的集合,而这些比如Job、CoroutineDispatcher等都可以看成是协程运行的辅助环境变量。同时,在前面接口设计中,Element虽然也是继承至CoroutineContext,但是从各种子类的继承规律来看,设计还是精巧的:CoroutineScope是一个范围,它包含一个环境变量的集合,所以它是封装了CoroutineContext。而其他的,比如Dispatcher、ExceptionHandler等等更像是每一个环境变量,所以他们都是继承至Element,可以互相通过算数运算符操作,更符合逻辑。理解这个可以借鉴后面的有篇说Continuation的文章,里面有个协程框架架构,其实CoroutineContext是最底层的概念,而launch、Dispatcher等则是中间层的概念,毫不夸张的说不要这些中间层API,协程一样可以运行,但是就没有这么多特性。所以说这些中层概念之所以为Context,其实都可以看成是协程运行的辅助环境变量。总结本篇文章没有太多深入,只是介绍了一些我们常见的类和CoroutineContext的关系,其实这里涉及到了协程框架的设计,等后面说原理时再讨论。下面做个简单总结:CoroutineContext本身是一个接口,而它的接口设计和Map的API极为相似,在使用过程中,可以当成Map来使用。这种设计的优点也就是方便我们使用运算符重载来创建我们希望的各种协程组件。协程中非常多的类,本身就是CoroutineContext,它们通过继承Element,比如Job,Deferred,Dispatcher,ContinuationInterceptor、CoroutineNmae、CoroutineExceptionHandler,也正是由于这些是一个接口的子类,所以可以使用操作符重载来写出灵活的代码。协程的CoroutineScope是CoroutineContext的一层简单封装,这个作用域可以访问这些Context,从而可以构建父子协程关系。挂起函数也和CoroutineContext有关系,后面再说。所以可以用下面这个图做个总结:
0
0
0
浏览量87
德州安卓

协程(23) | Flow原理解析

前言在前面文章中,我们说过在Flow出来之前,Kotlin的协程可能还不足以有绝对优势,在Flow发布之后,协程才真正被大家认可。在文章 # 协程(10) | Flow 中,我们介绍了Flow的简单使用,那么本篇文章就来看看Flow的原理。正文区别于上一篇文章所说的Channel是热的,Flow的最大特点就是"冷",这里的"冷"还包括"懒",即当Flow有终止操作符时,上游才开始发射数据,且一次只发射一个。Flow原理探究我们还是以最简单的测试代码入手:private suspend fun testFlow() { //上游操作符,创建Flow的同时,发射数据 flow { emit(1) emit(2) emit(3) emit(4) emit(5) } //终止操作符,接收数据 .collect { logX(it) } } 直接看一下flow{}高阶函数:public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block) 分析如下:flow{}的返回值是Flow类型,其实是返回一个SafeFlow对象。block的参数是suspend FlowCollector<T>.() -> Unit,可知这段lambda是挂起函数,最终会被编译成SuspendLambda类型对象;其次它的接收者类型是FlowCollector<T>,也就是可以把block看成FlowCollector的成员方法。这里的SafeFlow定义://private修饰,内部使用 private class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : AbstractFlow<T>() { override suspend fun collectSafely(collector: FlowCollector<T>) { collector.block() } } SafeFlow继承至AbstractFlow,即Flow的抽象类(可以看成是Flow的基础抽象实现),重写了collectSafely方法,注意该方法中,会调用collector.block(),而这个动作就会触发lambda代码块的执行。所以说这是一个重点方法,我们看看什么地方会调用该方法,分析其父类AbstractFlow://代码1 //Flow抽象类 public abstract class AbstractFlow<T> : Flow<T>, CancellableFlow<T> { //实现Flow中的唯一接口 public final override suspend fun collect(collector: FlowCollector<T>) { //注释1,简单封装 val safeCollector = SafeCollector(collector, coroutineContext) try { //注释2 collectSafely(safeCollector) } finally { safeCollector.releaseIntercepted() } } public abstract suspend fun collectSafely(collector: FlowCollector<T>) } 该抽象类实现了Flow接口,Flow接口只有唯一的collect方法,会在这里实现。注释1处,collector就是调用collect方法时,传入的FlowCollector对象。注释1处,coroutineContext是协程上下文,这是因为collect本身就是一个挂起函数,是挂起函数就肯定有Continuation对象,而这里不对外暴露Continuation对象,但是可以通过编译器优化,我们可以拿到上下文对象。在封装为safeCollector后,就会调用collectSafely(safeCollector)方法,根据前面分析,就会执行上游的lambda中的操作。即分析到现在,我们知道flow{}的lambda中,可以调用emit发射方法,而该对象就是这个safeCollector,我们称为上游的FlowCollector。暂时先不分析SafeCollector,我们来看看下游终止操作符collect{},根据前面分析我们可知flow{}会创建一个SafeFlow的对象,所以我们可以调用其实现接口Flow的唯一方法collect:public suspend fun collect(collector: FlowCollector<T>) 其实最开始的测试代码的写法,是经过简化的,其实效果如下://把collect简写复原 flow { emit(1) emit(2) emit(3) emit(4) emit(5) } .collect(object: FlowCollector<Int>{ override suspend fun emit(value: Int) { logX(value) } }) 这里使用collect方法创建了一个下游FlowCollector对象,而Flow中的数据是通过回调该下游FlowCollector对象的emit方法收集到。再回到代码1的AbstractFlow中,是不是有一种恍然大悟的感觉:只有调用了collect方法,才会让上游的lambda执行,这也就是"冷"的表现。那么还剩一个问题:在AbstractFlow的collect(collector: FlowCollector)方法中,通知下游数据是通过下游操作符的collector的emit方法,而发送数据是通过上游操作符的safeCollector的emit方法,这是如何结合起来的呢?谜底就在SafeCollector类,该类定义://注释1,函数引用 private val emitFun = FlowCollector<Any?>::emit as Function3<FlowCollector<Any?>, Any?, Continuation<Unit>, Any?> //实现类,该类又实现了FlowCollector接口 internal actual class SafeCollector<T> actual constructor( //终止操作符的FlowCollector @JvmField internal actual val collector: FlowCollector<T>, //协程上下文 @JvmField internal actual val collectContext: CoroutineContext ) : FlowCollector<T>, ContinuationImpl(NoOpContinuation, EmptyCoroutineContext), CoroutineStackFrame { ... //注释2,上游操作符中的FlowCollector会调用的发射方法 override suspend fun emit(value: T) { return suspendCoroutineUninterceptedOrReturn sc@{ uCont -> try { //发射数据 emit(uCont, value) } catch (e: Throwable) { lastEmissionContext = DownstreamExceptionElement(e) throw e } } } //内部方法 private fun emit(uCont: Continuation<Unit>, value: T): Any? { val currentContext = uCont.context currentContext.ensureActive() // This check is triggered once per flow on happy path. val previousContext = lastEmissionContext if (previousContext !== currentContext) { checkContext(currentContext, previousContext, value) } completion = uCont //注释3 return emitFun(collector as FlowCollector<Any?>, value, this as Continuation<Unit>) } ... } 这里省略了部分方法实现,只展示重要代码段,分析:首先是注释2,由于SafeCollector是实现FlowCollector接口,所以注释2处的emit就是上游操作符中的emit方法,即flow{ emit(0) }就是调用该方法。该方法中,会使用suspendCoroutineUninterceptedOrReturn来实现挂起函数,即发送数据,调用私有的内部emit方法。在私有的emit方法的注释3处,会调用emit()方法,方法参数分别为:collector也就是下游操作符中的FlowCollector,value也就是发射的数据0,this是Continuation对象。结合注释1处的定义,这里使用了函数引用,用到了挂起函数CPS的原理,即FlowCollector的emit挂起函数,其CPS后的函数引用就是Function3<FlowCollector<Any?>, Any?, Continuation<Unit>, Any?> ,表示意思是:参数分别是接收者FlowCollector,发射的值Any>,拼接在参数后面的Continuation对象,和返回值Any?。所以这里注释3处,就是调用了下游操作符的FlowCollector的emit方法,这样也就可以完美收集到数据了。这里我们可以总结一下:下游调用了collect方法,传递下游FlowCollector对象,才会触发上游数据发射。上游数据发射,即上游的FlowCollector调用emit发射的数据,会通过转换调用下游的FlowCollector的emit方法来接收数据。这里第一点解释了Flow冷的原因,第二点解释了Flow懒惰的原因:一次只能发送和接收一个数据。Flow中间操作符Flow的强大之处不仅仅是"冷"的特性,还因为其有方便的中间操作符。根据前面的思想:下游操作符触发上游操作符的动作执行,上游操作符再把数据传递给下游操作符,我们是否可以设想一下中间操作符的运作规则。比如下面代码:private suspend fun testFlow() { flow { emit(1) emit(2) emit(3) emit(4) emit(5) } .filter { it > 2 } .map { it * 2 } .collect { value -> logX(value) } } 由于还是"冷"的特性,collect{}会触发map{}的执行,map{}会触发filter{}的执行,filter{}会触发flow{}的执行,然后flow{}把数据传递给filter{},filter{}把数据传递给map{},最后map{}把数据传递给collect{}收集到。这个流程还是符合Flow的"冷"的特性,也符合"懒"的特性,因为每次只处理一个数据。我们就以filter为例,来看看其实现代码://inline函数,参数为crossinline类型,返回值类型为Flow,调用transform函数 public inline fun <T> Flow<T>.filter(crossinline predicate: suspend (T) -> Boolean): Flow<T> = transform { value -> if (predicate(value)) return@transform emit(value) } //这里会调用unsafeTransform,而不是transform函数 //这里会继续调用unsafeFlow函数 internal inline fun <T, R> Flow<T>.unsafeTransform( @BuilderInference crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit ): Flow<R> = unsafeFlow { //注释2 collect { value -> return@collect transform(value) } } //会返回一个Flow对象 internal inline fun <T> unsafeFlow(@BuilderInference crossinline block: suspend FlowCollector<T>.() -> Unit): Flow<T> { return object : Flow<T> { //注释1 override suspend fun collect(collector: FlowCollector<T>) { collector.block() } } } 上述代码看起来平平无奇,但是如果对Kotlin的高阶函数理解不透彻的话,还是很难理解,我们就来一一分析:首先看最后的unsafeFlow函数,通过object返回一个Flow对象,这时如果在filter{}后调用collect{}函数时,这里就会触发注释1出的方法执行。在该方法中,重点是collector.block(),这里会触发代码块block()的执行,这里的block代码块的类型是suspend FlowCollector.() -> Unit类型,是FlowCollector的成员对象挂起函数。该block就是unsafeTransform中的lambda代码块,即注释2中4行lambda代码块。在注释2的lambda中,会调用collect方法,这是因为该方法是Flow的扩展函数,这也就会导致上游操作符执行,即flow{}.filter{}.collect{}中flow{}将会执行,而这其中value就是发射出来的值。注释2中的value,会经过其参数transform(value)处理,这个参数就是if(predicate(value)) emit(value),即flow{}发射出的数据,再经过filter{}后,重新调用emit()方法。看到这个emit()方法,其实也就是回到了上面逻辑了,这个就是下游collect{}的上游FlowCollector了。这里可以看出,调用中间操作符filter{}会创建出新的Flow对象,而且会对数据重新进行发射。总结这么一分析完,其实可以发现Flow还是非常简单的,实现思路就类似与Callbck传递,终止操作符collect{}设置Callback(FlowCollector),触发上游flow{}的Callback(FlowCollector)发射数据。而中间操作符,也是一样的思想:触发上游,接收上游数据。本篇重点分析了filter{},会发现最后还是返回一个Flow对象,在fitler{}实现中会调用collect{}方法,然后调用emit方法。
0
0
0
浏览量1582

履历