注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Jackson 之 LocalDateTime 序列化与反序列化

前言 在 Java 8 中对 LocalDateTime、LocalDate 的序列化和反序列化有很多种操作 全局 在 ObjectMapper 对象中配置 JavaTimeModule,此为全局配置。 @Bean public ObjectM...
继续阅读 »

前言


在 Java 8 中对 LocalDateTime、LocalDate 的序列化和反序列化有很多种操作


全局


ObjectMapper 对象中配置 JavaTimeModule,此为全局配置。


    @Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();

// other serializer and deSerializer config ...

JavaTimeModule javaTimeModule = new JavaTimeModule();

javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));

javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

objectMapper.registerModule(javaTimeModule);
return objectMapper;
}

DateTimeFormatter.ofPattern 可以设置不同的时间日期模板,来实现不同的效果


局部


使用 @JsonFormat 注解


pattern 可以配置不同的时间格式模板


@Data
public static class Article {
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime date;
}

Serializer 和 DeSerializer


Jackson 提供了默认的 LocalDate 和 LocalDateTime 的 Serializer 和 DeSerializer,不过需要引入额外的 maven 依赖


<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>2.9.5</version>
</dependency>

@Data
public static class Article {
@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
private LocalDateTime date;
}

与此同时,还可以自定义 Serializer 和 DeSerializer,以满足某些独特场景中的时间日期格式。
比如对任意格式的时间同一反序列化为标准的 LocalDateTime 对象。


public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {

@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return this.deserialize(p.getText().trim());
}

private LocalDateTime deserialize(String source) {
if (StringUtils.isBlank(source)) {
return null;
} else if (source.matches("^\\d{4}-\\d{1,2}$")) {
// yyyy-MM
return LocalDateTime.parse(source + "-01T00:00:00.000", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}T{1}\\d{1,2}")) {
// yyyy-MM-ddTHH
return LocalDateTime.parse(source + ":00:00.000", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
} else {
// yyyy-MM-ddTHH:mm:ss.SSS
return LocalDateTime.parse(source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}

}

作者:菁芜
链接:https://juejin.cn/post/7025160932653268999
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

高并发场景下JVM调优实践之路

一、背景 2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验。 通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图: 可以看出,在观察周期里: 平均每10分钟Young&n...
继续阅读 »

一、背景


2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验。


通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图:




可以看出,在观察周期里:




  • 平均每10分钟Young GC次数66次,峰值为470次;




  • 平均每10分钟Full GC次数0.25次,峰值5次;




可见Full GC非常频繁,Young GC在特定的时段也比较频繁,存在较大的优化空间。由于对GC停顿的优化是降低接口的P99时延一个有效的手段,所以决定对该核心服务进行JVM调优。


二、优化目标




  • 接口P99时延降低30%




  • 减少Young GC和Full GC次数、停顿时长、单次停顿时长




由于GC的行为与并发有关,例如当并发比较高时,不管如何调优,Young GC总会很频繁,总会有不该晋升的对象晋升触发Full GC,因此优化的目标根据负载分别制定:


目标1:高负载(单机1000 QPS以上)




  • Young GC次数减少20%-30% ,Young GC累积耗时不恶化;




  • Full GC次数减少50%以上,单次、累积Full GC耗时减少50%以上,服务发布不触发Full GC。




目标2:中负载(单机500-600)




  • Young GC次数减少20%-30% ,Young GC累积耗时减少20%;




  • Full GC次数不高于4次/天,服务发布不触发Full GC。




目标3:低负载(单机200 QPS以下)




  • Young GC次数减少20%-30% ,Young GC累积耗时减少20%;




  • Full GC次数不高于1次/天,服务发布不触发Full GC。




三、当前存在的问题


当前服务的JVM配置参数如下:


-Xms4096M -Xmx4096M -Xmn1024M
-XX:PermSize=512M
-XX:MaxPermSize=512M

单纯从参数上分析,存在以下问题:


**未显示指定收集器 **


JDK 8默认搜集器为ParrallelGC,即Young区采用Parallel Scavenge,老年代采用Parallel Old进行收集,这套配置的特点是吞吐量优先,一般适用于后台任务型服务器。


比如批量订单处理、科学计算等对吞吐量敏感,对时延不敏感的场景,当前服务是视频与用户交互的门户,对时延非常敏感,因此不适合使用默认收集器ParrallelGC,应选择更合适的收集器。



Young区配比不合理


当前服务主要提供API,这类服务的特点是常驻对象会比较少,绝大多数对象的生命周期都比较短,经过一次或两次Young GC就会消亡。


再看下当前JVM配置


整个堆为4G,Young区总共1G,默认-XX:SurvivorRatio=8,即有效大小为0.9G,老年代常驻对象大小约400M。


这就意味着,当服务负载较高,请求并发较大时,Young区中Eden + S0区域会迅速填满,进而Young GC会比较频繁。


另外会引起本应被Young GC回收的对象过早晋升,增加Full GC的频率,同时单次收集的区域也会增大,由于Old区使用的是ParralellOld,无法与用户线程并发执行,导致服务长时间停顿,可用性下降, P99响应时间上升。


未设置


-XX:MetaspaceSize和-XX:MaxMetaspaceSize


Perm区在jdk 1.8已经过时,被Meta区取代,
因此-XX:PermSize=512M -XX:MaxPermSize=512M配置会被忽略,
真正控制Meta区GC的参数为
-XX:MetaspaceSize:
Metaspace初始大小,64位机器默认为21M左右

-XX:MaxMetaspaceSize:
Metaspace的最大值,64位机器默认为18446744073709551615Byte,
可以理解为无上限

-XX:MaxMetaspaceExpansion:
增大触发metaspace GC阈值的最大要求

-XX:MinMetaspaceExpansion:
增大触发metaspace GC阈值的最小要求,默认为340784Byte

这样服务在启动和发布的过程中,元数据区域达到21M时会触发一次Full GC (Metadata GC Threshold),随后随着元数据区域的扩张,会夹杂若干次Full GC (Metadata GC Threshold),使服务发布稳定性和效率下降。


此外如果服务使用了大量动态类生成技术的话,也会因为这个机制产生不必要的Full GC (Metadata GC Threshold)。




四、优化方案/验证方案


上面已分析出当前配置存在的较为明显的不足,下面优化方案主要先针对性解决这些问题,之后再结合效果决定是否继续深入优化。


当前主流/优秀的搜集器包含:




  • Parrallel Scavenge + Parrallel Old:吞吐量优先,后台任务型服务适合;




  • ParNew + CMS:经典的低停顿搜集器,绝大多数商用、延时敏感的服务在使用;




  • G1:JDK 9默认搜集器,堆内存比较大(6G-8G以上)的时候表现出比较高吞吐量和短暂的停顿时间;




  • ZGC:JDK 11中推出的一款低延迟垃圾回收器,目前处在实验阶段;





结合当前服务的实际情况(堆大小,可维护性),我们选择ParNew + CMS方案是比较合适的。


参数选择的原则如下:


1)Meta区域的大小一定要指定,且MetaspaceSize和MaxMetaspaceSize大小应设置一致,具体多大要结合线上实例的情况,通过jstat -gc可以获取该服务线上实例的情况。


# jstat -gc 31247
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
37888.0 37888.0 0.0 32438.5 972800.0 403063.5 3145728.0 2700882.3 167320.0 152285.0 18856.0 16442.4 15189 597.209 65 70.447 667.655

可以看出MU在150M左右,因此-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M是比较合理的。


2)Young区也不是越大越好


当堆大小一定时,Young区越大,Young GC的频率一定越小,但Old区域就会变小,如果太小,稍微晋升一些对象就会触发Full GC得不偿失。


如果Young区过小,Young GC就会比较频繁,这样Old区就会比较大,单次Full GC的停顿就会比较大。因此Young区的大小需要结合服务情况,分几种场景进行比较,最终获得最合适的配置。


基于以上原则,以下为4种参数组合:


1.ParNew +CMS,Young区扩大1倍


-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

**2.ParNew +CMS,**Young区扩大1倍,


去除-XX:+CMSScavengeBeforeRemark(使用【-XX:CMSScavengeBeforeRemark】参数可以做到在重新标记前先执行一次新生代GC)。


因为老年代和年轻代之间的对象存在跨代引用,因此老年代进行GC Roots追踪时,同样也会扫描年轻代,而如果能够在重新标记前先执行一次新生代GC,那么就可以少扫描一些对象,重新标记阶段的性能也能因此提升。)


-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC

3.ParNew +CMS,Young区扩大0.5倍


-Xms4096M -Xmx4096M -Xmn1536M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

4.ParNew +CMS,Young区不变


-Xms4096M -Xmx4096M -Xmn1024M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

下面,我们需要在压测环境,对不同负载下4种方案的实际表现进行比较,分析,验证。


4.1 压测环境验证/分析


高负载场景(1100 QPS)GC表现



可以看出,在高负载场景,4种ParNew + CMS的各项指标表现均远好于Parrallel Scavenge + Parrallel Old。其中:




  • 方案4(Young区扩大0.5倍)表现最佳,接口P95,P99延时相对当前方案降低50%,Full GC累积耗时减少88%, Young GC次数减少23%,Young GC累积耗时减少4%,Young区调大后,虽然次数减少了,但Young区大了,单次Young GC的耗时也大概率会上升,这是符合预期的。




  • Young区扩大1倍的两种方案,即方案2和方案3,表现接近,接口P95,P99延时相对当前方案降低40%,Full GC累积耗时减少81%, Young GC次数减少43%,Young GC累积耗时减少17%,略逊于Young区扩大0.5倍,总体表现不错,这两个方案进行合并,不再区分。




Young区不变的方案在新方案里,表现最差,淘汰。所以在中负载场景,我们只需要对比方案2和方案4。


中负载场景(600 QPS)GC表现



可以看出,在中负载场景,2种ParNew + CMS(方案2和方案4)的各项指标表现也均远好于Parrallel Scavenge + Parrallel Old。




  • Young区扩大1倍的方案表现最佳,接口P95,P99延时相对当前方案降低32%,Full GC累积耗时减少93%, Young GC次数减少42%,Young GC累积耗时减少44%;




  • Young区扩大0.5倍的方案稍逊一些。




综合来看,两个方案表现十分接近,原则上两种方案都可以,只是Young区扩大0.5倍的方案在业务高峰期的表现更佳,为尽量保证高峰期服务的稳定和性能,目前更倾向于选择ParNew + CMS,Young区扩大0.5倍方案。


4.2 灰度方案/分析


为保证覆盖业务的高峰期,选择周五、周六、周日分别从两个机房随机选择一台线上实例,线上实例的指标符合预期后,再进行全量升级。


目标组  xx.xxx.60.6


采用方案2,即目标方案


-Xms4096M -Xmx4096M -Xmn1536M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

对照组1  xx.xxx.15.215


采用原始方案


-Xms4096M -Xmx4096M -Xmn1024M
-XX:PermSize=512M
-XX:MaxPermSize=512M

对照组2  xx.xxx.40.87


采用方案4,即候选目标方案


-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

灰度3台机器。


我们先分析下Young GC相关指标:


Young GC次数



Young GC累计耗时



Young GC单次耗时



可以看出,与原始方案相比,目标方案的YGC次数减少50%,累积耗时减少47%,吞吐量提升的同时,服务停顿的频率大大降低,而代价是单次Young GC的耗时增长3ms,收益是非常高的。


对照方案2即Young区2G的方案整体表现稍逊与目标方案,再分析Full GC指标。


老年代内存增长情况



Full GC次数



Full GC累计/单次耗时



与原始方案相比,使用目标方案时,老年代增长的速度要缓慢很多,基本在观测周期内Full GC发生的次数从155次减少至27次,减少82%,停顿时间均值从399ms减少至60ms,减少85%,毛刺也非常少。


对照方案2即Young区2G的方案整体表现逊于目标方案。到这里,可以看出,目标方案从各个维度均远优于原始方案,调优目标也基本达成。


但细心的同学会发现,目标方案相对原始方案,"Full GC"(实际上是CMS Background GC)耗时更加平稳,但每个若干次"Full GC"后会有一个耗时很高的毛刺出现,这意味这个用户请求在这个时刻会停顿2-3s,能否进一步优化,给用户一个更加极致的体验呢?



4.3 再次优化


这里首先要分析这现象背后的逻辑。



对于CMS搜集器,采用的搜集算法为Mark-Sweep-[Compact]。


CMS搜集器GC的种类:


CMS Background GC


这种GC是CMS最常见的一类,是周期性的,由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,采用的是Mark-Sweep方式,由于没有Compact这种耗时操作,且可以与用户进程并行,所以CMS的停顿会比较低,GC日志中出现GC (CMS Initial Mark)字样就代表发生了一次CMS Background GC。


Background GC由于采用的是Mark-Sweep,会导致老年代内存碎片,这也是CMS最大的弱点。


CMS Foreground GC


这种GC是CMS搜集器里真正意义上的Full GC,采用Serial Old或Parralel Old进行收集,出现的频率就较低,当往往出现后就会造成较大的停顿。


触发CMS Foreground GC的场景有很多,场景的如下:




  • System.gc();




  • jmap -histo:live pid;




  • 元数据区域空间不足;




  • 晋升失败,GC日志中的标志为ParNew(promotion failed);




  • 并发模式失败,GC日志中的标志为councurrent mode failure字样。




不难推断,目标方案中的毛刺是晋升失败或并发模式失败造成的,由于线上没有开启打印gc日志,但也无妨,因为这两种场景的根因是一致的,就是若干次CMS Backgroud GC后造成的老年代内存碎片。


我们只需要尽可能减少由于老年代碎片触发晋升失败、并发模式失败即可。


CMS Background GC由JVM的常驻线程定时扫描老年代的使用率,当使用率超过阈值时触发,该阈值由-XX:CMSInitiatingOccupancyFraction; -XX:+UseCMSInitiatingOccupancyOnly两个参数控制,不设置,默认首次为92%,后续会根据历史情况进行预测,动态调整。


如果我们固定阈值的大小,将该阈值设置为一个相对合理的值,既不使GC过于频繁,又可以降低晋升失败或并发模式失败的概率,就可以大大缓解毛刺产生的频率。


目标方案的堆分布如下:




  • Young区 1.5G




  • Old区 2.5G




  • Old区常驻对象 约400M




按经验数据,75%,80%是比较折中的,因此我们选择-XX:CMSInitiatingOccupancyFraction=75 -


XX:+UseCMSInitiatingOccupancyOnly进行灰度观察(我们也对80%的场景做了对照实验,75%优于80%)。


最终目标方案的配置为:


-Xms4096M -Xmx4096M -Xmn1536M 
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

如上配置,灰度 xx.xxx.60.6 一台机器;



从再次优化的结果上看,CMS Foreground GC引起的毛刺基本消失,符合预期。


因此,视频服务最终目标方案的配置为;


-Xms4096M -Xmx4096M -Xmn1536M 
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

五、结果验收


灰度持续7天左右,覆盖工作日与周末,结果符合预期,因此符合在线上开启全量的条件,下面对全量后的结果进行评估。


Young GC次数



Young GC累计耗时



单次Young GC耗时



从Young GC指标上看,调整后Young GC次数平均减少30%,Young GC累积耗时平均减少17%,Young GC单次耗时平均增加约7ms,Young GC的表现符合预期。


除了技术手段,我们也在业务上做了一些优化,调优前实例的Young GC会出现明显的、不规律的(定时任务不一定分配到当前实例)毛刺,这里是业务上的一个定时任务,会加载大量数据,调优过程中将该任务进行分片,分摊到多个实例上,进而使Young GC更加平滑。


Full GC单次/累积耗时




从"Full GC"的指标上看,"Full GC"的频率、停顿极大减少,可以说基本上没有真正意义上的Full GC了。


核心接口-A (下游依赖较多) P99响应时间,减少19%(从 3457 ms下降至 2817 ms);



核心接口-B (下游依赖中等)  P99响应时间,减少41%(从 1647ms下降至 973ms);



核心接口-C (下游依赖最少) P99响应时间,减少80%(从 628ms下降至 127ms);



综合来看,整个结果是超出预期的。Young GC表现与设定的目标非常吻合,基本上没有真正意义上的Full GC,接口P99的优化效果取决于下游依赖的多少,依赖越少,效果越明显。


六、写在最后


由于GC算法复杂,影响GC性能的参数众多,并且具体参数的设置又取决于服务的特点,这些因素都很大程度增加了JVM调优的难度。


本文结合视频服务的调优经验,着重介绍调优的思路和落地过程,同时总结出一些通用的调优流程,希望能给大家提供一些参考。



作者:vivo互联网技术
链接:https://juejin.cn/post/7025410482341150751
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter 图片库高燃新登场

背景 去年,闲鱼图片库在大规模的应用下取得了不错的成绩,但也遇到了一些问题和诉求,需要进一步的演进,以适应更多的业务场景与最新的 flutter 特性。比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存...
继续阅读 »

背景


去年,闲鱼图片库在大规模的应用下取得了不错的成绩,但也遇到了一些问题和诉求,需要进一步的演进,以适应更多的业务场景与最新的 flutter 特性。比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如我们在相册中,需要在图片库之外再搭建图片通道。


这次,我们巧妙地将外接纹理与 FFi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。没错,Power 系列将新增一员,我们将新的图片库命名为 「PowerImage」!


我们将新增以下核心能力:


•支持加载 ui.Image 能力。在去年基于外接纹理的方案中,使用方无法拿到真正的 ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。•支持图片预加载能力。正如原生precacheImage一样。这在某些对图片展示速度要求较高的场景下非常有用。•新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。•支持模拟器。在 flutter-1.23.0-18.1.pre之前的版本,模拟器无法展示 Texture Widget。•完善自定义图片类型通道。解决业务自定义图片获取诉求。•完善的异常捕获与收集。•支持动图。


去年图片方案可以参考《闲鱼Flutter图片框架架构演进(超详细)》


Flutter 原生方案


在我们新方案开始之前,先简单回忆一下 flutter 原生图片方案。



原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilderloadingBuilder,最终在图片加载成功后,会 rebuildRawImageRawImage 会通过 RenderImage 来绘制,整个绘制的核心是 ImageInfo 中的 ui.Image


Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。 ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。 ImageStream:图片资源加载的对象。


在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?


新的方案


我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。


FFI


正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是 ui.Image。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image


首先 native 侧先获取必要的参数(以 iOS 为例):


    _rowBytes = CGImageGetBytesPerRow(cgImage);

CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
CFDataRef rawDataRef = CGDataProviderCopyData(dataProvider);
_handle = (long)CFDataGetBytePtr(rawDataRef);

NSData *data = CFBridgingRelease(rawDataRef);
self.data = data;
_length = data.length;

dart 侧拿到后


@override
FutureOr<ImageInfo> createImageInfo(Map map) {
Completer<ImageInfo> completer = Completer<ImageInfo>();
int handle = map['handle'];
int length = map['length'];
int width = map['width'];
int height = map['height'];
int rowBytes = map['rowBytes'];
ui.PixelFormat pixelFormat =
ui.PixelFormat.values[map['flutterPixelFormat'] ?? 0];
Pointer<Uint8> pointer = Pointer<Uint8>.fromAddress(handle);
Uint8List pixels = pointer.asTypedList(length);
ui.decodeImageFromPixels(pixels, width, height, pixelFormat,
(ui.Image image) {
ImageInfo imageInfo = ImageInfo(image: image);
completer.complete(imageInfo);
//释放 native 内存
PowerImageLoader.instance.releaseImageRequest(options);
}, rowBytes: rowBytes);
return completer.future;
}

我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过 ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。


这里有两个优化方向:


1.解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。2.与 flutter 官方讨论,尝试从内部减少这次内存拷贝。


FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。


Texture


Texture 方案与原生结合有一些难度,这里涉及到没有 ui.Image 只有 textureId。这里有几个问题需要解决:


问题一:Image Widget 需要 ui.Image 去 build RawImage 从而绘制,这在本文前面的Flutter 原生方案介绍中也提到了。 问题二:ImageCache 依赖 ImageInfo 中 ui.Image 的宽高进行 cache 大小计算以及缓存前的校验。 问题三:native 侧 texture 生命周期管理


都有解决方案:


问题一:通过自定义 Image 解决,透出 imageBuilder 来让外部自定义图片 widget 问题二:为 Texture 自定义 ui.image,如下:


import 'dart:typed_data';
import 'dart:ui' as ui show Image;
import 'dart:ui';

class TextureImage implements ui.Image {
int _width;
int _height;
int textureId;
TextureImage(this.textureId, int width, int height)
: _width = width,
_height = height;

@override
void dispose() {
// TODO: implement dispose
}

@override
int get height => _height;

@override
Future<ByteData> toByteData(
{ImageByteFormat format = ImageByteFormat.rawRgba}) {
// TODO: implement toByteData
throw UnimplementedError();
}

@override
int get width => _width;
}

这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。 实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。这个问题可以具体看 @皓黯 的 ISSUE[1] 与 PR[2]。


问题三:关于 native 侧感知 flutter image 释放时机的问题


• flutter 在 2.2.0 之后,ImageCache 提供了释放时机,可以直接复用,无需修改。•< 2.2.0 版本,需要修改 ImageCache,获取 cache 被丢弃的时机,在 cache 被丢弃的时候,通知 native 进行释放。


修改的 ImageCache 释放如下(部分代码):


typedef void HasRemovedCallback(dynamic key, dynamic value);

class RemoveAwareMap<K, V> implements Map<K, V> {
HasRemovedCallback hasRemovedCallback;
...
}
//------
final RemoveAwareMap<Object, _PendingImage> _pendingImages = RemoveAwareMap<Object, _PendingImage>();
//------
void hasImageRemovedCallback(dynamic key, dynamic value) {
if (key is ImageProviderExt) {
waitingToBeCheckedKeys.add(key);
}
if (isScheduledImageStatusCheck) return;
isScheduledImageStatusCheck = true;
//We should do check in MicroTask to avoid if image is remove and add right away
scheduleMicrotask(() {
waitingToBeCheckedKeys.forEach((key) {
if (!_pendingImages.containsKey(key) &&
!_cache.containsKey(key) &&
!_liveImages.containsKey(key)) {
if (key is ImageProviderExt) {
key.dispose();
}
}
});
waitingToBeCheckedKeys.clear();
isScheduledImageStatusCheck = false;
});
}

整体架构


我们将两种解决方案非常优雅地结合在了一起:



我们抽象出了 PowerImageProvider ,对于 external(ffi)、texture,分别生产自己的 ImageInfo 即可。它将通过对 PowerImageLoader 的调用,提供统一的加载与释放能力。


蓝色实线的 ImageExt 即为自定义的 Image Widget,为 texture 方式透出了 imageBuilder。


蓝色虚线 ImageCacheExt 即为 ImageCache 的扩展,仅在 flutter < 2.2.0 版本才需要,它将提供 ImageCache 释放时机的回调。


这次,我们也设计了超强的扩展能力。除了支持网络图、本地图、flutter 资源、native 资源外,我们提供了自定义图片类型的通道,flutter 可以传递任何自定义的参数组合给 native,只要 native 注册对应类型 loader,比如「相册」这种场景,使用方可以自定义 imageType 为 album ,native 使用自己的逻辑进行加载图片。有了这个自定义通道,甚至图片滤镜都可以使用 PowerImage 进行展示刷新。


除了图片类型的扩展,渲染类型也可进行自定义。比如在上面 ffi 中说的,为了降低内存拷贝带来的峰值问题,使用方可以在 flutter 侧进行解码,当然这需要 native 图片库提供解码前的数据。


数据对比


FFI vs Texture:



机型:iPhone 11 Pro,图片:300 张网络图,行为:在listView中手动滚动到底部再滚动到顶部,native Cache:100MB,flutter Cache:100MB
复制代码

这里有两个现象:


Texture:    395MB波动,内存较平滑
FFI: 480MB波动,内存有毛刺

Texture 方案在内存方面表现优于 FFI,在内存水位与毛刺两方面:


•内存水位:由于 Texture 方案在 flutter 侧的 cache 为占位空壳,没有实际占用内存,因此只在 native 图片库的内存缓存中存在一份,所以 flutter 侧内存缓存实际上比 ffi 方案少了 100MB•毛刺:由于 ffi 方案不能避免 flutter 侧内存拷贝,会有先拷贝再释放的过程,所以会有毛刺。


结论:


1.Texture 适用于日常场景,优先选择;2.FFI 更适用于


1.flutter <= 1.23.0-18.1.pre 版本中,在模拟器上显示图片2.获取 ui.Image 图片数据3.flutter 侧解码,解码前的数据拷贝影响较小。(比如集团 Hummer 的外接解码库)


滚动流畅性分析:



设备: Android OnePlus 8t,CPU和GPU进行了锁频。
case: GridView每行4张图片,300张图片,从上往下,再从下往上,滑动幅度从500,1000,1500,2000,2500,5轮滑动。重复20次。
方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑数据,获取TimeLine数据并分析。

结论:


•UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI方式波动比较大。•Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize了,其他方式加载的是原图。


更精简的代码:


dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。


FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。


未来


相信很多人注意到了,上文中少了动图部分。当前动图部分正在开发中,内部的 Pre Release 版本中,在 load 的时候返回的实际上是 OneFrameImageStreamCompleter,对于动图,我们将替换为 MultiFrameImageStreamCompleter,后面如何做,只是一些策略问题,并不难。顺便抛个另一种方案:可以把动图解码前的数据给 flutter 侧解码与渲染,但支持的格式不如原生丰富。


我们希望能将 PowerImage 贡献给社区,为了实现这一目标,我们提供了详细的设计文档、接入文档、性能报告,另外我们也在完善单元测试,在代码提交后或者 CR 时,都会进行单元测试。


最后,也是大家最关心的:我们计划在今年十二月底将代码开源在 「XianyuTech[3]」。


References


[1] ISSUE: github.com/flutter/flu…

[2] PR: github.com/flutter/flu…

[3] XianyuTech: github.com/XianyuTech


作者:闲鱼技术
链接:https://juejin.cn/post/7024314939271544863
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

@OnLifecycleEnvent 被废弃,替代方案更简单

近期 androidx.lifecycle 发布了 2.4.0 版本,此次更新中 @OnLifecycleEvent 注解被废弃,官方建议使用 LifecycleEventObserver 或者 DefaultLifecycleObserver 替代 现...
继续阅读 »

近期 androidx.lifecycle 发布了 2.4.0 版本,此次更新中 @OnLifecycleEvent 注解被废弃,官方建议使用 LifecycleEventObserver 或者 DefaultLifecycleObserver 替代





现代的 Android 应用中都少不了 Lifecycle 的身影,正是各种 lifecycle-aware 组件的存在保证了程序的健壮性。


Lifecycle 本质是一个观察者模式的最佳实践,通过实现 LifecycleObserver 接口,开发者可以自自定 lifecycle-aware 组件,感知 Activity 或 Fragment 等 LifecycleOwner 的生命周期回调。


趁新版本发布之际,我们再回顾一下 Lifecycle 注解的使用以及废弃后的替代方案


Lifecycle Events & States


Lifecyce 使用两组枚举分别定义了 EventState



  • Events

    • ON_CREATE

    • ON_START

    • ON_RESUME

    • ON_PAUSE

    • ON_STOP

    • ON_DESTROY

    • ON_ANY



  • States

    • INITIALIZED

    • CREATED

    • STARTED

    • RESUMED

    • DESTROYED




Events 对应了 Activity 等原生系统组件的生命后期回调, 每当 Event 发生时意味着这些 LifecycleOwner 进入到一个新的 State



作为 观察者的 LifecycleObserver 可以感知到 被观察者的 LifecycleOwner 其生命周期 State 变化时的 Event。定义 LifecycleObserver 有三种方式:



  1. 实现 LifecycleEventObserver 接口

  2. 使用 @OnLifecycleEvent 注解


实现 LifecycleEventObserver


public interface LifecycleEventObserver extends LifecycleObserver {
void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event);
}

LifecycleEventObserver 是一个单方法接口,在 Kotlin 中可转为写法更简洁的 Lambda
进行声明


val myEventObserver = LifecycleEventObserver { source, event ->
when(event) {
Lifecycle.Event.ON_CREATE -> TODO()
Lifecycle.Event.ON_START -> TODO()
else -> TODO()
}
}

LifecycleEventObserver 本身就是 LifecycleObserver 的派生,使用时直接 addObserver 到 LivecycleOwner 的 Lifecycle 即可。


需要在 onStateChanged 中写 swich / case 自己分发事件。相对于习惯重写 Activity 或者 Fragment 的 onCreateonResume 等方法,稍显啰嗦。


因此 Lifecycle 给我们准备了 @OnLifecycleEvent 注解


使用 @OnLifecycleEvent 注解


使用方法很简单,继承 LifecycleObserver 接口,然后在成员方法上添加注解即可


val myEventObserver = object : LifecycleObserver {

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onStart() {
TODO()
}

@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreat() {
TODO()
}
}

添加注册后,到 LifecycleOwner 的 Event 分发时,会自动回调注解匹配的成员方法,由于省去了手动 switch/case 的过程,深受开发者喜欢


注解解析过程


Event 分发时,怎么就会回到到注解对应的方法的?


通过 addObserver 添加的 LifecycleObserver ,都会转为一个 LifecycleEventObserver ,LifecycleOwner 通过调用其 onStateChanged 分发 Event


Lifecycling#lifecycleEventObserver 中处理转换


public class Lifecycling {

@NonNull
static LifecycleEventObserver lifecycleEventObserver(Object object) {
boolean isLifecycleEventObserver = object instanceof LifecycleEventObserver;
boolean isFullLifecycleObserver = object instanceof FullLifecycleObserver;
// 观察者是 FullLifecycleObserver
if (isLifecycleEventObserver && isFullLifecycleObserver) {
return new FullLifecycleObserverAdapter((FullLifecycleObserver) object,
(LifecycleEventObserver) object);
}

// 观察者是 LifecycleEventObserver
if (isFullLifecycleObserver) {
return new FullLifecycleObserverAdapter((FullLifecycleObserver) object, null);
}

if (isLifecycleEventObserver) {
return (LifecycleEventObserver) object;
}

final Class<?> klass = object.getClass();
int type = getObserverConstructorType(klass);

// 观察者是通过 apt 产生的类
if (type == GENERATED_CALLBACK) {
List<Constructor<? extends GeneratedAdapter>> constructors =
sClassToAdapters.get(klass);
if (constructors.size() == 1) {
GeneratedAdapter generatedAdapter = createGeneratedAdapter(
constructors.get(0), object);
return new SingleGeneratedAdapterObserver(generatedAdapter);
}
GeneratedAdapter[] adapters = new GeneratedAdapter[constructors.size()];
for (int i = 0; i < constructors.size(); i++) {
adapters[i] = createGeneratedAdapter(constructors.get(i), object);
}
return new CompositeGeneratedAdaptersObserver(adapters);
}

// 观察者需要通过反射生成一个 wrapper
return new ReflectiveGenericLifecycleObserver(object);
}

...

public static String getAdapterName(String className) {
return className.replace(".", "_") + "_LifecycleAdapter";
}
}

逻辑很清晰,根据 LifecycleObserver 类型不用转成不同的 LifecycleEventObserver,


用一段伪代码梳理如下:


if (lifecycleObserver is FullLifecycleObserver) {
return FullLifecycleObserverAdapter // 后文介绍
} else if (lifecycleObserver is LifecycleEventObserver) {
return this
} else if (type == GENERATED_CALLBACK) {
return GeneratedAdaptersObserver
} else {// type == REFLECTIVE_CALLBACK
return ReflectiveGenericLifecycleObserver
}

注解有两种使用用途。


场景一:runtime 时期使用反射生成 wrapper


class ReflectiveGenericLifecycleObserver implements LifecycleEventObserver {
private final Object mWrapped;
private final CallbackInfo mInfo;

ReflectiveGenericLifecycleObserver(Object wrapped) {
mWrapped = wrapped;
mInfo = ClassesInfoCache.sInstance.getInfo(mWrapped.getClass());
}

@Override
public void onStateChanged(LifecycleOwner source, Event event) {
mInfo.invokeCallbacks(source, event, mWrapped);
}
}

CallbackInfo 是关键,通过反射收集当前 LifecycleObserver 的回调信息。onStateChanged 中通过反射调用时,不会因为因为缺少 method 报错。


场景二:编译时使用 apt 生成 className + _LifecycleAdapter


除了利用反射, Lifecycle 还提供了 apt 方式处理注解。


添加 gradle 依赖:


dependencies {
// java 写法
annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.3.1"
// kotlin 写法
kapt "androidx.lifecycle:lifecycle-compiler:2.3.1"
}

这样在编译器就会根据 LifecyceObserver 类名生成一个添加 _LifecycleAdapter 后缀的类。 比如我们加了 onCreatonStart 的注解,生成的代码如下:


public class MyEventObserver_LifecycleAdapter implements GeneratedAdapter {
final MyEventObserver mReceiver;

MyEventObserver_LifecycleAdapter(MyEventObserver receiver) {
this.mReceiver = receiver;
}

@Override
public void callMethods(LifecycleOwner owner, Lifecycle.Event event, boolean onAny,
MethodCallsLogger logger) {
boolean hasLogger = logger != null;
if (onAny) {
return;
}
if (event == Lifecycle.Event.ON_CREATE) {
if (!hasLogger || logger.approveCall("onCreate", 1)) {
mReceiver.onCreate();
}
return;
}
if (event == Lifecycle.Event.ON_START) {
if (!hasLogger || logger.approveCall("onStart", 1)) {
mReceiver.onStart();
}
return;
}
}
}

apt 减少了反射的调用,性能更好,当然会牺牲一些编译速度。


为什么要使用注解


生命周期的 Event 种类很多,我们往往不需要全部实现,如过不使用注解,可能需要实现所有方法,产生额外的无用代码


上面代码中的 FullLifecycleObserver 就是一个全部方法的接口


interface FullLifecycleObserver extends LifecycleObserver {

void onCreate(LifecycleOwner owner);

void onStart(LifecycleOwner owner);

void onResume(LifecycleOwner owner);

void onPause(LifecycleOwner owner);

void onStop(LifecycleOwner owner);

void onDestroy(LifecycleOwner owner);
}

从接口不是 public 的( java 代码 ) 可以看出,官方也无意让我们使用这样的接口,增加开发者负担。


遭废弃的原因


既然注解这么好,为什么又要废弃呢?



This annotation required the usage of code generation or reflection, which should be avoided.



从官方文档的注释可以看到,注解要么依赖反射降低运行时性能,要么依靠 APT 降低编译速度,不是完美的方案。


我们之所引入注解,无非是不想多实现几个空方法。早期 Android 工程不支持 Java8 编译,接口没有 default 方法, 现如今 Java8 已经是默认配置,可以为接口添加 default 方法,此时注解已经失去了存在的意义。


如今官方推荐使用 DefaultLifecycleObserver 接口来定义你的 LifecycleObserver


public interface DefaultLifecycleObserver extends FullLifecycleObserver {

@Override
default void onCreate(@NonNull LifecycleOwner owner) {
}

@Override
default void onStart(@NonNull LifecycleOwner owner) {
}

@Override
default void onResume(@NonNull LifecycleOwner owner) {
}

@Override
default void onPause(@NonNull LifecycleOwner owner) {
}

@Override
default void onStop(@NonNull LifecycleOwner owner) {
}

@Override
default void onDestroy(@NonNull LifecycleOwner owner) {
}
}

FullLifecycleObserverAdapter, 无脑回调 FullLifecycleObserver 即可


class FullLifecycleObserverAdapter implements GenericLifecycleObserver {

private final FullLifecycleObserver mObserver;

FullLifecycleObserverAdapter(FullLifecycleObserver observer) {
mObserver = observer;
}

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
switch (event) {
case ON_CREATE:
mObserver.onCreate(source);
break;
case ON_START:
mObserver.onStart(source);
break;
case ON_RESUME:
mObserver.onResume(source);
break;
case ON_PAUSE:
mObserver.onPause(source);
break;
case ON_STOP:
mObserver.onStop(source);
break;
case ON_DESTROY:
mObserver.onDestroy(source);
break;
case ON_ANY:
throw new IllegalArgumentException("ON_ANY must not been send by anybody");
}
}
}

需要注意 DefaultLifecycleObserver 在 2.4.0 之前也是可以使用的, 存在于 androidx.lifecycle.lifecycle-common-java8 这个库中, 2.4.0 开始 统一移动到 androidx.lifecycle.lifecycle-common 了 ,已经没有 java8 单独的扩展库了。


作者:fundroid
链接:https://juejin.cn/post/7025407355093254151
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

「如何优雅的不写注释?」每个工程师都要不断追求的漫漫长路

引言 ✨ 作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。 说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并...
继续阅读 »

引言 ✨


作为一个软件开发者,我们需要进行协同,在这个协同的过程中我们需要让其他开发者可以读懂我们的代码,所以注释可能就变成了一个重要的代码解读标注手段。


说到这,大家可能就会感知到注释确实很重要,他可以是开发者有效沟通的渠道,但是我在这里想表达的是注释其实并不是银弹,也不是最好的手段,在我的眼里,注释更多承担的是一个兜底的作用。


于是便有了本篇文章,我想通过更多亲身经历和书籍参考来阐述证明我的观点:



  • 什么是好注释

  • 什么是坏注释

  • 怎么不通过注释提高编码可读性「易于上手版」

  • 挖一个关于前端架构的大坑「待我能力提升」



注意我并不是贬低使用注释,而是想给大家推销一种思想,即更多的用代码阐述自己的想法,把注释作为一种保底手段,而不是银弹。而且我切实的知道很多历史因素导致代码极其难以阅读,利用注释去表达信息也是没有办法的。但是希望大家读完本文之后可以更多去考量如何让代码更可读,而不是怎么写注释



不堪回首的摸爬滚打 🌧️


image.png


我毕业工作这一年以来,一直在不断加深对整洁编码的理解,我也经历过迷惑和自我怀疑。但是通过不断的实践,思考,回顾,我渐渐的有了自己的理解。


希望大家在阅读完成我的经历与摸爬滚打后,有所收获,如果觉得没啥参考性,就当作看了一场平平无奇的故事。


求学 🎓


曾经在求学过程中,大家的老师应该都会鼓励大家去多些注释去阐述你的编码思想,我便一直认为写注释是一个标准,是我在开发中必须要去做的事情。


然而现在我回过头去看曾经老师的教诲,我会觉得是出于以下三点考虑:



  • 对于一个计算机初学者,写注释可以让你去梳理你的编码思路

  • 写注释的时候也会对代码逻辑进行思考,类比伪代码

  • 很现实的一点,帮助老师理解你的代码,毕竟有的代码不写注释老师都不知道判卷的时候该给分还是不给分


实习 🧱


之后我便开始了我的第一段职业生涯,大四实习,我来到我现在所在的公司呆了两个月,做的也是比较简单的工作,改一改前人留下的遗产「bug」,到即将返校的时候,我接到了一个开发任务,当时我开发了一天,晚上进行 code-review,我记得当时我信心满满,给每个子方法和关键变量都写了注释,本来信心满满,但是最后我的代码被前辈批到自闭,其中不乏关于代码设计和命名相关的建议,也是在这次 review 中我听到了一句足以毁灭我学习生涯所建立的世界观的话:



好的代码是不需要注释的



工作 💻


工作之后,最开始我接触的项目也是一个恶臭代码的重灾区了,充斥着各种难以阅读的代码逻辑,但是那时候我还没有一个评判好坏的能力,一度去怀疑自己。



是不是我太菜了,才读不懂别人那些高端的代码



没错,我真的这么想过,甚至十分的自我怀疑,但是后期我经历了几件事情让我对这个自我怀疑的想法消除了:



  • 在导师的指导下对代码的反复 code-review 并重写



当时我们发现该项目存在需求遗漏,于是这个需求便来到了我的头上,即使项目紧急,导师还是给我细心 review,最后这个功能我重写了三四次,也让我对什么样的代码是好的有了一个粗略的概念。




  • 对某一个模块的完全重新设计与编码



经历了从设计到评审,再到编码,最后 review 的过程。




  • 相关书籍的阅读



本篇文章不做书籍推荐了,只是表达对于如何整洁编码是存在很多前人经验与指导原则存在的,新人可以优先阅读《代码整洁之道




  • 丰富的自我实践与思考



在我后来参与产线内部平台建设,负责安全运维大模块,负责冬奥会项目过程中也是不断的在追求整洁编码。思考,实践,回顾,一直伴随着我的职业道路,对于代码如何编写的更整洁也渐渐有了自己的想法。



在工作的过程中,我对于“好的代码是不需要注释的”这句话的理解也在不断加深。当然,如果对于某些难以处理的遗留问题,注释也是一个不错的方法对其进行注解描述。


总结 🔍


最开始我觉得注释是必要的,后经过经验的积累,前辈的教导,自己的学习,不断的思考与回顾,到现在有了自己的一套思想。当然我不会去说我的思想是正确的,可能过几年之后我会回来打我自己的脸,其实想法的改变,也能代表一种成长吧~


有关注释的杂七杂八 🌲


image.png



别给糟糕的代码加注释,重新写吧




  • 什么也比不上放置良好的注释来的有用

  • 什么也不会比乱七八糟的注释更有本事搞乱一个模块

  • 什么也不会比陈旧,提供错误信息的注释更具破坏性


若编程语言有足够的表达力,或者我们长于用这些语言来表达意图,就不那么需要注释 —— 也许根本不需要。


上面的话引自《代码整洁之道》。但是从事这个行业越久我越无法否认其正确性,我们必须要知道的一件事是代码具有实时性,即你现在项目中的代码总是当前最新的,否则也无法正确运行。然而上面的注释我们根本无法知道是什么时候写的,不具备实时性。



  • 代码是在变动,演化的。然而注释并不能随之变动

  • 程序员不会长期维护注释

  • 注释会撒谎,而代码不会

  • 不准确的注释比没注释坏的多

  • ...


所以我的想法很坚定,注释无法美化糟糕的代码,与其花时间为糟糕的代码编写解释不如花时间把糟糕的代码变得整洁。



用代码来阐述思想一直是最好的办法。



当然总有些注释是必须的或是有利的,还有一些注释是有害的,下面和大家聊一聊什么是好注释,什么是坏注释。


好注释 🌈



  • 法律信息



比如版权或者著作权的声明




  • 提供信息的注释



比如描述一个方法的返回值,但是其实可以利用函数名来传达信息




  • 阐释



把某些晦涩难懂的参数或者返回值翻译成某种可读的形式,更好的方式是让参数和返回值自身就足够清楚




  • TODO 注释



这个可能大家都会经常用,用来记录我们哪里还有任务没有完成




  • 放大



比如一个看似不起眼却很重要的变量,我们可以用注释凸显它的重要性




  • ...


坏注释 😈



  • 喃喃自语



只有作者读的懂的注释,当你打算开始写注释,就要讲清楚原委,和读者有良好的沟通




  • 多余的注释



有的注释写不写没啥作用,很简单的方法都要写注释,甚至读代码都比看注释快




  • 误导性注释



程序员都已经够辛苦了,你还要用注释欺骗人家




  • 循规式注释



要求每个方法每个变量都要有注释,很多废话只会扰乱读者




  • 位置标记



比如打了一堆 ****** 来标注位置,这个我上学的时候经常干




  • 废话注释



毫无作用的废话




  • 注释掉的代码



很多读者会想,代码依然留在那一定有原因,最后不敢删除畏手畏脚




  • 信息过多的注释



注释中包含很多无关的细节,其实对读者完全没有必要




  • ...


优雅的不写注释 🌿


image.png


首先我再次阐述之前说过的话,编码实际上是一种社会行为,是需要沟通的。而如何让我们不借助注释来阐述我们的思想,其实是需要我们长期探索并在实践中积累经验的,从我的经验与视角出发,其实让我们的代码库更加整洁其实主要从以下两个方面考量:



  • 整洁编码

  • 前端架构


下面我分开来讲~



注意,编码不是一个人的事情,在我眼里如何做到团队成员编码风格的相近才是最具成效且需要长期努力的任务,也是相对理想且难以做到的。正所谓,就算我们写的代码很烂,但是烂的我们的成员可以相互理解,也是一种优秀「瞎说的,哈哈哈,代码可维护性还是要团队成员一起追求的」。



整洁编码 📚


首先我先引用几位前辈的话,带大家感受一下,什么样的代码是整洁的:



  • Bjarne:我喜欢优雅和高效的代码,代码的逻辑应当直接了当,叫缺陷难以隐藏们。尽量减少依赖关系,使之便于维护,依据某种分层战略完善错误处理代码,性能调至最优,省得引诱别人做没有规矩的优化,搞出一堆混乱出来,整洁的代码只做好一件事。

  • Grady: 整洁的代码简单直接,整洁的代码从不隐藏设计者的意图,充满干净利落的抽象和直截了当的控制语句。


对于整洁编码可以先简单总结:



  • 尽量减少依赖关系,便于维护

  • 简单直接,充满了干净利落逻辑处理和直截了当的控制语句。

  • 能够全部运行通过,并配有单元测试和验收测试

  • 没有重复的代码

  • 写的代码能够完全提现我们的设计理念「这个可以通过类、方法、属性的命名,代码逻辑编码的清晰来体现



在我们日常编码中,命名和函数可以说是我们最常接触的,也是最能影响我们代码整洁度的。于是本文中,我将围绕这两个方向为大家介绍几种易于上手的整洁编码方案。



下文参考我之前写过的一篇文章:关于整洁代码与重构的摸爬滚打


命名 🌟



  • 只要命名需要通过注释来补充,就表示我们的命名还是存在问题

  • 所有的命名都要有实际意义,命名会告诉你它为什么存在,它做什么事情,应该怎么用


比如列举一段曾经上学的时候可能写出的代码:


#include <stdio.h>  
int main(){
printf("Hello, C! \n");
int i = 10;
int m = 1;
for(int j = 0; j < i; j+=m){
for(int n = 0; n< i-j;n++){
printf("*");
}
printf("\n");
}
return 0;
}

我们看这里命名都是一大堆 i,m,n,j之类的根本不知道这些变量用来干嘛,其实这段代码最后仅仅打印出来的是 * 组成的直角三角形。但是当时写代码我确实就是这样,i, j,m,n等等字母用了一遍,也不包含什么语义上的东西,变量命名就是字母表里面选。


当然现在的命名就高端多了,开始从词典里面找了,还要排列组合,比如 getUserisAdmin。语义上提升了,通过命名我们也可以直观的判断这个方法是干嘛的,这个变量是干嘛的。


这样看其实命名是很有学问的事情,下面我开始列举几点命名中可以快速提升代码整洁度的方法:



  • 避免引起误导



不要用不该作为变量的专有名词来为变量命名,一组账号accountList ,却不是List类型,也是存在误导。命名过于相似:比如 XYZHandlerForAORBORC 和XYZControllerForAORBORDORC,谁能一眼就看出来呢~




  • 做有意义的区分


let fn = (array1,array2) =>{ 
for(let i =0 ;i<array1.length;i++){
array2[i] = array1[i];
}
}


比如上面 array1array2 就不是有意义的区分,这只是一个赋值操作,完全可以是 sourceArrayDesArray

再比如 起的名字:userInfouserData都是这种的,我们很难读懂这两个有啥子区别,这种区分也没啥意义,说白了这只是单词拼写的区分,而不是在语义上区分开了。




  • 使用读的出来的名称



编程是社会活动,免不了与人交流对话,使用难以轻松读出来的声音会导致你的思想难于传达。并且人类的大脑中有专门处理语言的区域,可以辅助你理解问题,不加以运用简直暴殄天物。简单举个例子:getYMDHMS,这个方法就是获取时间,然而就是难以阅读,是不好的命名。




  • 使用可以搜索的名称



之前的代码,我用个 i 作为变量。如果代码很长,我这想要追踪一下这个i的变化,简直折磨。同理我不喜欢直接以 valuedatainfo 等单词直接做变量,因为他们经常以其他变量的组成部分出现,难以追踪。




  • 程序中有意义的数字或者字符串应该用常量进行替换,方便查找


export const DEFAULT_ORDERBY = '-updateTime' 
export const DEFAULT_CHECKEDNUM = 0


比如采用上面的方式,既可以让代码更加语义化也方便集中修改




  • 类名和对象名应为名词或名词词组,方法名应为动词或动词词组



比如我们常用的 updatexxxfilteredXXX 都是这样的命名规则




  • 属性命名添加有用必要的语境,但是短名称如果足够用清楚,就比长名称好,别添加不必要的语境

  • 每个概念对应一个词



比如 taglabelticketworkOrder 各种混着用岂不是乱糟糟的,这读者容易混淆,也会为以后造成负担,也可能会隐藏一些 bug。所以我们在项目开发前可以确定一个名词术语表来避免这种情况发生。




  • ...


函数 🌟



大师写代码是在讲故事,而不是在写程序。




  • 短小:20封顶最佳

  • 函数的缩进层级尽可能的少

  • 函数参数尽量少

  • 使用具有描述性的函数名



当然函数越短小,功能越集中,就越便于取好名字




  • 抽取异常处理,即 try-catch 就是一个函数 ,函数应该只做一件事,错误处理就是一件事

  • 标识参数丑陋不堪


const updateList = (flag) {
if(flag){
// ...
} else {
// ...
}
}


比如一个方法,定义成上面这个样子,我们很难通过方法定义直接了解其能力以及参数的含义。




  • 函数名是动词,参数是名词,并保证顺序



比如 saveField(name)assertExpectedEqualsActual(expected,actual)




  • 无副作用



比如一个方法名是 updateList,后来者应该顺理成章的认为这个方法只会更新列表,然而开发者在这个方法中加入了其他的逻辑,可能导致后来者在使用这个方法后导致副作用,而代码报错无法正常运行。




  • 重复是软件中一切邪恶的根源,拒绝重复的代码

  • ...


写代码和写文章一样,先去想你要写什么,最后再去打磨,初稿也许粗糙无序,那就要斟酌推敲,直到达成心中的样子。编程艺术也是语言设计的艺术。


前端架构 🎋



本人现在工作一年有余,一年半不足,对于前端架构并不能很好的输出给大家,所以在此给大家先挖一个大坑,本章节中如有错误理解,请大家不吝赐教,与我探讨交流,感谢。



首先,我先解释一下我为什么要把前端架构放在这样的一篇文章中,其实是存在两条原因:



  • 从个人开发角度来看,优秀的前端架构可以增强代码的维护性



试想一个组织结构恶臭的项目,一定会影响你的阅读的,杂乱不堪的组件划分原则,不清晰的边界通通都会成为巨大的阻力。




  • 最近换了组,到了天擎终端平台组,新的 leader 也分享了很多关于组件化的经验与理解



浅薄无知的小寒草🌿,在线求鞭策。



那么,大家在提到前端架构的时候,会想到什么呢,我反正会想到以下几点:



  • 组件化

  • 架构模式

  • 规范 / 约定

  • 分层

  • ...


下面我逐条来讲~


架构模式 ✨


组件化我先跳过,最后再说,先说说架构模式,大家脑子里一定会想到 MVVMMVC 等模式,比如我们常用的 Vue 框架中的 MVVM ,以及普遍在 Spring 那一套中被提及并在在 nest.js 中有所应用的 MVC。但是关于架构模式前端说的可能还是相对较少,我的水平也有限,而且说起来可能就会跑题了,于是也不在本文过多赘述。


规范&约定 ✨


关于规范或者约定,常见的包括:



  • 目录结构

  • 命名规范

  • 术语表

  • ...


其实这几点我们很好理解,我们会通过约定或者脚手架等方式来规范化我们的目录结构,使我们同一个产线下项目的目录结构保证一致。以及我们在开发前的设计阶段可能也需要出具一份术语表,这个前文也听到过一个含义用一个确定的词来表示,否则可能会导致代码的混乱。


关于命名规范,首先我们需要去约定一个统一的命名规则,我们常见的是变量命名为小驼峰,文件命名为连字符。但是这个命名规范其实我们可以做的事情不止这些,比如我说几个例子:



  • 前端命名规范是小驼峰,服务端命名是下划线,我们怎么处理让前端编码中屏蔽掉命名规则差异。

  • 同一个含义我们可以用很多命名来表示,比如:handleStaffUpdate / updateStaff。在项目初期我们完全可以对其进行约束使用哪种命名风格,以让我们项目一致性加强。

  • ...


分层 ✨


关于分层,大家的差异可能会比较大,比如我们可能会把我们的前端项目分为以下几层:



  • 业务层

  • 服务层

  • 模型层「可能有也可能没有」


业务层就是我们比较熟悉的,各种业务代码。


服务层「server」不知道大家的项目中有没有,我们项目使用 grpc 接口,由于接口粒度较高,我们通常会在 server 层对接口再次处理,合并,或者在这个层去完成一些服务端不合理设计的屏蔽。


模型层「model」不常有,但是一些复杂的又需要复用的逻辑可能有这个层,就相当于逻辑的抽象,脱离于视图,之后如果我们需要复用这里的逻辑,而视图不同,我们就可以使用这个 model


合理的分层可以让我们的项目更清晰,减少代码冗杂,提升可维护性。


组件化 ✨


其实组件化一直都是前端架构中的大课题,首先我们可以通过组件化能得到什么,其实最重要的可能就是:



  • 复用


不知道大家的项目有没有统计代码复用率,我们是有的,而且这也是前端工程质量很重要的一个指标。然而在追求组件化的过程中其实我们很少会拥有一个衡量标准:



  • 什么情况需要拆分组件

  • 什么情况不需要拆分组件


团队对这个问题没有一个统一认知的情况下很容易造成:



  • 五花八门的组件拆分原则导致代码结构混乱

  • 无效的组件拆分导致文件过多,维护困难

  • 过深的组件嵌套层级「经历过的人一定会对此深恶痛绝」

  • ...


其实我最开始的时候也喜欢把组件按照很细的粒度进行拆分,想的是总会有用到的时候嘛,但是从架构整洁的角度出发,过细或者过于粗糙的组件拆分都会导致维护困难,复用困难等问题,现在的我可能更会从复用性角度出发:



  • 这个东西会不会复用


只从复用性考量很容易的就会把组件区分为两大类:



  • 需要复用的组件

  • 几乎不会被复用的组件



注意我没有说什么组件是肯定不会被复用的,而是几乎不会被复用。



所以我们就可以坐下来思考,把我们工作中常见的场景拎出来,过一遍,因为我们工作的业务场景不同,所以我肯定还是以我的业务场景出发,那么我可以把我的组件分成几种:



  • page 组件

  • layout 组件

  • 业务组件


其中我认为,page 组件是几乎不会复用的组件,layout 组件和业务组件在我眼里是可以复用的组件。



这只是很粗糙的的区分,之后还有很多问题:



  • 如何把业务组件写的好用

  • 如何确定一个组件的边界

  • ...


这些我们就要从消费者角度考量了。



当然其实组件化也可以和分层一起考虑,因为组件其实也会有层级,比如:



  • 基础 ui 组件[参考element-ui]

  • 基础业务组件


基础业务组件也可以按照是否跨模块等原则继续进行分层,这个可以按照大家的业务场景自行考量。


总结 ✨


从实际经验出发,合理的架构确实是项目易于维护「从而优雅的不写注释🌿」,而这是一个自顶向下分析决策的过程,本章节篇幅有限,加上我水平有限,无法在此过多赘述,还请大家持续期待我的分享。


结束语 ☀️


image.png


那么本篇文章就结束了,涵盖了我个人经历上的摸爬滚打,解析什么样的注释是好的,什么样的注释是坏的,并从编码整洁度与前端架构的角度出发来考量如何提升代码的可维护性。以此来论述我的观点:



注释不是维护代码的银弹,而便于维护的代码需要从整洁编码前端架构两个「或者更多」层面入手。



我工作的时间不长也不短了,已经一年出头了,我一直秉承着编码是社会性工作,需要协同合作,代码的可维护性也是一名职业软件工程师需要持续追求的观点。


思考,实践,回顾的过程没有停歇,我在此也希望大家多思考,作为一名工程师我们需要追求的不仅仅只有:



收起阅读 »

Node 之一个进程的死亡

人固有一死,一个 Node 进程亦是如此,总有万般不愿也无法避免。从本篇文章我们看看一个进程灭亡时如何从容离去。 一个 Node 进程,除了提供 HTTP 服务外,也绝少不了跑脚本的身影。跑一个脚本拉取配置、处理数据以及定时任务更是家常便饭。在一些重要流程中能...
继续阅读 »

人固有一死,一个 Node 进程亦是如此,总有万般不愿也无法避免。从本篇文章我们看看一个进程灭亡时如何从容离去。


一个 Node 进程,除了提供 HTTP 服务外,也绝少不了跑脚本的身影。跑一个脚本拉取配置、处理数据以及定时任务更是家常便饭。在一些重要流程中能够看到脚本的身影:



  1. CI,用以测试、质量保障及部署等

  2. Cron,用以定时任务

  3. Docker,用以构建镜像


如果在这些重要流程中脚本出错无法及时发现问题,将有可能引发更加隐蔽的问题。如果在 HTTP 服务出现问题时,无法捕获,服务异常是不可忍受的。


最近观察项目镜像构建,会偶尔发现一两个镜像虽然构建成功,但容器却跑不起来的情况究其原因,是因为 一个 Node 进程灭亡却未曾感知到的问题


Exit Code



什么是 exit code?



exit code 代表一个进程的返回码,通过系统调用 exit_group 来触发。


POSIX 中,0 代表正常的返回码,1-255 代表异常返回码,在业务实践中,一般主动抛出的错误码都是 1。在 Node 应用中调用 API process.exitCode = 1 来代表进程因期望外的异常而中断退出。


这里有一张关于异常码的附表 Appendix E. Exit Codes With Special Meanings


异常码在操作系统中随处可见,以下是一个关于 cat 进程的异常以及它的 exit code,并使用 strace 追踪系统调用。


$ cat a
cat: a: No such file or directory

# 使用 strace 查看 cat 的系统调用
# -e 只显示 write 与 exit_group 的系统调用
$ strace -e write,exit_group cat a
write(2, "cat: ", 5cat: ) = 5
write(2, "a", 1a) = 1
write(2, ": No such file or directory", 27: No such file or directory) = 27
write(2, "\n", 1
) = 1
exit_group(1) = ?
+++ exited with 1 +++

strace 追踪进程显示的最后一行可以看出,该进程的 exit code 是 1,并把错误信息输出到 stderr (stderr 的 fd 为2) 中


如何查看 exit code


strace 中可以来判断进程的 exit code,但是不够方便过于冗余,更无法第一时间来定位到异常码。


有一种更为简单的方法,通过 echo $? 来确认返回码


$ cat a
cat: a: No such file or directory

$ echo $?
1

$ node -e "preocess.exit(52)"
$ echo $?
52

未曾感知的痛苦何在: throw new ErrorPromise.reject 区别


以下是两段代码,第一段抛出一个异常,第二段 Promise.reject,两段代码都会如下打印出一段异常信息,那么两者有什么区别?


function error () {
throw new Error('hello, error')
}

error()

// Output:

// /Users/shanyue/Documents/note/demo.js:2
// throw new Error('hello, world')
// ^
//
// Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)

async function error () {
return new Error('hello, error')
}

error()

// Output:

// (node:60356) UnhandledPromiseRejectionWarning: Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)
// at Object.<anonymous> (/Users/shanyue/Documents/note/demo.js:5:1)
// at Module._compile (internal/modules/cjs/loader.js:701:30)
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)

在对上述两个测试用例使用 echo $? 查看 exit code,我们会发现 throw new Error()exit code 为 1,而 Promise.reject() 的为 0。


从操作系统的角度来讲,exit code 为 0 代表进程成功运行并退出,然而此时即使有 Promise.reject,操作系统也会视为它执行成功。


这在 DockerfileCI 中执行脚本时将留有安全隐患。


Dockerfile 在 Node 镜像构建时的隐患


当使用 Dockerfile 构建镜像或者 CI 时,如果进程返回非0返回码,构建就会失败。


这是一个浅显易懂的含有 Promise.reject() 问题的镜像,我们从这个镜像来看出问题所在。


FROM node:12-alpine

RUN node -e "Promise.reject('hello, world')"

构建镜像过程如下,最后两行提示镜像构建成功:即使在构建过程打印出了 unhandledPromiseRejection 信息,但是镜像仍然构建成功。


$ docker build -t demo .
Sending build context to Docker daemon 33.28kB
Step 1/2 : FROM node:12-alpine
---> 18f4bc975732
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 79a6d53c5aa6
(node:1) UnhandledPromiseRejectionWarning: hello, world
(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:1) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Removing intermediate container 79a6d53c5aa6
---> 09f07eb993fe
Successfully built 09f07eb993fe
Successfully tagged demo:latest

但如果是在 node 15 镜像内,镜像会构建失败,至于原因以下再说。


FROM node:15-alpine

RUN node -e "Promise.reject('hello, world')"

$ docker build -t demo .
Sending build context to Docker daemon 2.048kB
Step 1/2 : FROM node:15-alpine
---> 8bf655e9f9b2
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 4573ed5d5b08
node:internal/process/promises:245
triggerUncaughtException(err, true /* fromPromise */);
^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "hello, world".] {
code: 'ERR_UNHANDLED_REJECTION'
}
The command '/bin/sh -c node -e "Promise.reject('hello, world')"' returned a non-zero code: 1

Promise.reject 脚本解决方案


能在编译时能发现的问题,绝不要放在运行时。所以,构建镜像或 CI 中需要执行 node 脚本时,对异常处理需要手动指定 process.exitCode = 1 来提前暴露问题


runScript().catch(() => {
process.exitCode = 1
})

在构建镜像时,Node 也有关于异常解决方案的建议:



(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see nodejs.org/api/cli.htm…). (rejection id: 1)



根据提示,--unhandled-rejections=strict 将会把 Promise.reject 的退出码设置为 1,并在将来的 node 版本中修正 Promise 异常退出码。


而下一个版本 Node 15.0 已把 unhandled-rejections 视为异常并返回非0退出码。


$ node --unhandled-rejections=strict error.js 

Signal


在外部,如何杀死一个进程?答:kill $pid


而更为准确的来说,一个 kill 命令用以向一个进程发送 signal,而非杀死进程。大概是杀进程的人多了,就变成了 kill。



The kill utility sends a signal to the processes specified by the pid operands.



每一个 signal 由数字表示,signal 列表可由 kill -l 打印


# 列出所有的 signal
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

这些信号中与终端进程接触最多的为以下几个,其中 SIGTERM 为 kill 默认发送信号,SIGKILL 为强制杀进程信号


在 Node 中,process.on 可以监听到可捕获的退出信号而不退出。以下示例监听到 SIGINT 与 SIGTERM 信号,SIGKILL 无法被监听,setTimeout 保证程序不会退出


console.log(`Pid: ${process.pid}`)

process.on('SIGINT', () => console.log('Received: SIGINT'))
// process.on('SIGKILL', () => console.log('Received: SIGKILL'))
process.on('SIGTERM', () => console.log('Received: SIGTERM'))

setTimeout(() => {}, 1000000)

运行脚本,启动进程,可以看到该进程的 pid,使用 kill -2 97864 发送信号,进程接收到信号并未退出


$ node signal.js
Pid: 97864
Received: SIGTERM
Received: SIGTERM
Received: SIGTERM
Received: SIGINT
Received: SIGINT
Received: SIGINT

容器中退出时的优雅处理


当在 k8s 容器服务升级时需要关闭过期 Pod 时,会向容器的主进程(PID 1)发送一个 SIGTERM 的信号,并预留 30s 善后。如果容器在 30s 后还没有退出,那么 k8s 会继续发送一个 SIGKILL 信号。如果古时皇帝白绫赐死,教你体面。


其实不仅仅是容器,CI 中脚本也要优雅处理进程的退出。


当接收到 SIGTERM/SIGINT 信号时,预留一分钟时间做未做完的事情。


async function gracefulClose(signal) {
await new Promise(resolve => {
setTimout(resolve, 60000)
})

process.exit()
}

process.on('SIGINT', gracefulClose)
process.on('SIGTERM', gracefulClose)

这个给脚本预留时间是比较正确的做法,但是如果是一个服务有源源不断的请求过来呢?那就由服务主动关闭吧,调用 server.close() 结束服务


const server = http.createServer(handler)

function gracefulClose(signal) {
server.close(() => {
process.exit()
})
}

process.on('SIGINT', gracefulClose)
process.on('SIGTERM', gracefulClose)

总结



  1. 当进程结束的 exit code 为非 0 时,系统会认为该进程执行失败

  2. 通过 echo $? 可查看终端上一进程的 exit code

  3. Node 中 Promise.reject 时 exit code 为 0

  4. Node 中可以通过 process.exitCode = 1 显式设置 exit code

  5. 在 Node12+ 中可以通过 node --unhandled-rejections=strict error.js 执行脚本,视 Promise.rejectexit code 为 1,在 Node15 中修复了这一个问题

  6. Node 进程退出时需要优雅退出

  7. k8s 关闭 POD 时先发一个 SIGTERM 信号,留 30s 时间处理未完成的事,如若 POD 没有正常退出,30s 过后发送 SIGKILL 信号


收起阅读 »

Android Gradle 基础自定义构建

win7 Android Studio 2.1.3基础自定义构建 Basic Build Customization本章目的理解Gradle文件build tasks入门自定义构建理解Gradle文件在Android Studio中新建一个项目后,会自动创建3...
继续阅读 »

win7 Android Studio 2.1.3

基础自定义构建 Basic Build Customization

本章目的

  • 理解Gradle文件
  • build tasks入门
  • 自定义构建

理解Gradle文件

在Android Studio中新建一个项目后,会自动创建3个Gradle文件。

MyApp
├── build.gradle
├── settings.gradle
└── app
└── build.gradle

每个文件都有自己的作用

settings.gradle文件

新建工程的settings文件类似下面这样

include ':app'

Gradle为每个settings文件创建Settings对象,并调用其中的方法。

The top-level build file 最外层的构建文件

能对工程中所有模块进行配置。如下

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.3'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

buildscript代码块是具体配置的地方,引用JCenter仓库。 本例中,一个仓库代表着依赖库,换句话说是app可以从中下载使用库文件。
JCenter是一个有名的 Maven 仓库。

dependencies代码块用来配置依赖。上面注释说明了,不要在此添加依赖,而应该到独立的模块 中去配置依赖。

allprojects能对所有模块进行配置。

模块中的build文件

模块中的独立配置文件,会覆盖掉top-level的build.gradle文件

apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "25.0.2"

defaultConfig {
applicationId "com.xxx.rust.newproj"
minSdkVersion 18
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:25.1.0'
}

下面来看3个主要的代码块。

plugin

第一行应用了Android 应用插件。Android插件由Google团队开发维护。该插件提供构建,测试,打包应用和模块需要的所有的task。

android

最大的一个区域。defaultConfig区域对app核心进行配置,会配置覆盖AndroidManifest.xml中的配置。

applicationId复写掉manifest文件中的包名。但applicationId和包名有区别。
manifest中的包名,在源代码和R文件中使用。所以package name在android studio中理解为一个查询类的路径比较合理。
applicationId在Android系统中是作为应用的唯一标识,即在一个Android设备中所有的应用程序的applicationId都是唯一的。

dependencies

是Gradle标准配置的一部分。 Android中用来配置使用到的库。

定制化构建 Customizing the build

BuildConfig and resources

自从SDK17以来,构建工具会生成一个BuildConfig类,包含着静态变量DEBUG和一些信息。
如果你想在区分debug和正式版,比如打log,这个BuildConfig类很有用。
可以通过Gradle来扩展这个类,让它拥有更多的静态变量。

以NewProj工程为例,app\build.gradle

android {
compileSdkVersion 25
buildToolsVersion "25.0.2"

defaultConfig {
applicationId "com.xxx.rust.newproj"
minSdkVersion 18
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
buildTypes {
debug {
buildConfigField("String", "BASE_URL", "\"http://www.baidu.com\"")
buildConfigField("String", "A_CONTENT", "\"debug content\"")
resValue("string", "str_version", "debug_ver")
}
release {
buildConfigField("String", "BASE_URL", "\"http://www.qq.com\"")
buildConfigField("String", "A_CONTENT", "\"release content\"")
resValue("string", "str_version", "release_ver")

minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

上面的buildConfigFieldresValue在编译后,能在源代码中使用
注意上面那个转义的分号不可少;注意里面的大小写,这里传入的参数就像是直接填入的代码一样

下面是编译后生成的BuildConfig文件,可以看到buildConfigField的东西已经在里面了

public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.xxx.rust.newproj";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";
// Fields from build type: debug
public static final String A_CONTENT = "debug content";
public static final String BASE_URL = "http://www.baidu.com";
}

resValue会被添加到资源文件中

mTv2.setText(R.string.str_version);

通过 build.gradle 增加获取 applicationId 的方式

模块build.gradle中添加属性applicationId,会被编译到BuildConfig中

project.afterEvaluate {
project.android.applicationVariants.all { variant ->
def applicationId = [variant.mergedFlavor.applicationId, variant.buildType.applicationIdSuffix].findAll().join()
}
}

在代码中可以直接使用

String appID = BuildConfig.APPLICATION_ID;

获取时间的方法

模块build.gradle中添加方法getTime(),并在buildTypes中添加域。

// 获取当前时间
static def getTime() {
String timeNow = new Date().format('YYYYMMdd-HHmmss')
return timeNow
}

android {
// ...
buildTypes {
debug {
buildConfigField "String", "BUILD_TIME", "\"" + getTime() + "\""
}
release {
buildConfigField "String", "BUILD_TIME", "\"" + getTime() + "\""
// ...
}
}
}

BuildConfig.java中得到这个域。

  // Fields from build type: debug
public static final String BUILD_TIME = "20180912-100335";

修改release apk文件名的方法

gradle版本3.1.4。使用了上面的方法getTime()

android {
// ...
// 修改release的apk名字
applicationVariants.all { variant ->
variant.outputs.all {
if (variant.buildType.name == 'release') {
outputFileName = "xxx_release_${defaultConfig.versionName}_${getTime()}.apk"
}
}
}
}

以前的方法可能会遇到问题:Cannot set the value of read-only property 'outputFile' for ApkVariantOutputImpl_Decorated
参考:stackoverflow.com/questions/4…

工程范围的设置

如果一个工程中有多个模块,可以对整个工程应用设置,而不用去修改每一个模块。

NewProj\build.gradle

allprojects {
repositories {
jcenter()
}
}

ext {
compileSDKVersion = 25
local = 'Hello from the top-level build'
}

每一个build.gradle文件都能定义额外的属性,在ext代码块中。

在一个模块的libmodule\build.gradle文件中,可以引用rootProject的ext属性

android {
compileSdkVersion rootProject.ext.compileSDKVersion
buildToolsVersion "25.0.2"
// ....
}

工程属性 Project properties

定义properties的地方

  • ext代码块
  • gradle.properties文件
  • 命令行 -P 参数

工程build.gradle文件

ext {
compileSDKVersion = 25
local = 'Hello from the top-level build'
}

/**
* Print properties info
*/

task aPrintSomeInfo {
println(local)
println('project dir: ' + projectDir)
println(projectPropertiesFileText)
}

task aPrintAllProperites() {
println('\nthis is aPrintAllProperites task\n')
Iterator pIt = properties.iterator()
while (pIt.hasNext()) {
println(pIt.next())
}
}

gradle.properties文件中增加

projectPropertiesFileText = Hello there from gradle.properties

在as的Gradle栏上双击执行aPrintSomeInfo,会连带下一个task也执行

13:08:10: Executing external task 'aPrintSomeInfo'...
Hello from the top-level build
project dir: G:\openSourceProject\NewProj
Hello there from gradle.properties

this is aPrintAllProperites task
......
BUILD SUCCESSFUL

Total time: 1.025 secs
13:08:11: External task execution finished 'aPrintSomeInfo'.

参考:Gradle for Android Kevin Pelgrims


收起阅读 »

Android Handler解读

Handler通常都会面被问到这几个问题1.一个线程有几个Handler?2.一个线程有几个Looper?如何保证?3.Handler内存泄漏原因?4.子线程中可以new Handler吗?5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?...
继续阅读 »

Handler通常都会面被问到这几个问题

  • 1.一个线程有几个Handler?
  • 2.一个线程有几个Looper?如何保证?
  • 3.Handler内存泄漏原因?
  • 4.子线程中可以new Handler吗?
  • 5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?
  • 6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?
  • 7.我们使用Message时应该如何创建它

Handler的总体框架

Handler的流程

这是我在网上看到的一张图,很形象的体现Handler的工作流程,也说明了Handler几个关键类之间的关系 Handler 只负责将message放到MessageQueue,然后再从MessageQueue取出message发送出去 MessageQueue 就是传送带,上面一直传送的许多message Looper 就是传送带的轮子,他带动这MessageQueue一直跑动 Thread 就是动力,要是没有线程,整个传送都不会开始,并且Looper还提供了一个开关给Thread,开启才会传送

image.png

MessageQueue 和 Message

添加消息

只要你使用handler发送消息,最后都会走到handler#enqueueMessag 然后调用MessageQueue#enqueueMessage,可以看到方法需要传入一个Message的

handler#enqueueMessage handler#enqueueMessag

MessageQueue#enqueueMessage MessageQueue#enqueueMessage

而且MessageQueue里面还存放了一个mMessage变量,有什么作用呢,让我们先来看一下Message 是什么 image.png

Message就是我们所发送的一个个消息体,而在这个类中 可以看到,一个Message变量里,又存放一个Message叫做next,存放下一个Message的,这又有啥用呢 image.png

再次回到MessageQueue#enqueueMessage,看一看这些变量到底有什么作用 image.png

首先第一个msg1进入时,p = mMessage = null,所以进入第一个if语句 所以msg1.next = p = null,mMessage = msg1

而第二个msg2进入时,假设msg2的执行时间when是在msg1之后的, 此时p = mMessage = msg1,而when(msg2.when) > p.when(msg1.when) 则if语句就不成立了,会进入else语句的for循环 image.png

此时的prev = p = mMessage = msg1, 而p = p.next(p就是msg1,msg1.next = null),此时的p就为null 所以break出去后,for循环也结束了

最后两句就是做了下图的操作 msg2.next = p = null prev.next(msg1.next) = msg2 image.png

结构就像这样,通过这样的赋值操作,这样就形成了一个链表结构 所以MessageQueue就相当于是一个仓库,里面存放着由许许多多的Message组成的链条 image.png

取消息

取消息的方法是MessageQueue#next()方法,里面的代码先不做分析, 我们知道发送消息是handler调用的 那么取消息是谁调用的呢 image.png

根据一开始的图很容易知道,是Loop#loop()调用了该方法 而在这个方法拿到msg后 会调用 msg.target.dispatchMessage(msg)将消息发送出去,这里的msg.target 就是 handler image.png

image.png

所以他们形成了这样一种模式,一种生产者消费者模型

image.png

也就是说要调用Looper.loop()才会取出消息去分发,但是我们再主线程的时候,都是直接使用Handler,是哪里帮我们调用了Looper.loop()函数呢,直接看到主线程的main函数就能看到,也就是说app一启动,主线程就帮我们调用了Looper.loop()函数 image.png

知道流程后,回到一开始的问题

1.一个线程有几个Handler?

这个问题其实不用说都知道,难道主线程不能使用多个Handler吗

2.一个线程有几个Looper?如何保证?

答案很简单,一个线程只有一个Looper,但是怎么保证的呢?

我们先来看看Looper是怎么创建的,是谁创建的 可以看到,Looper的构造函数只在prepare这里使用过,而且系统也有提示我们, image.png

但是Looper存放在了sThreadLocal变量中,所以先看看sThreadLocal是什么 查阅到就是Looper中的一个静态变量的ThreadLocal类,好像看不出什么

image.png

那就进入sThreadLocal.set(Looper)方法看一下

image.png

  • 1.可以看到set方法中,首先获取了当前线程,则prepare() --> set() --> 当前线程

也就是说,Thread1调用prepare方法,获取的当前线程也就是Thread1,不可能为其他线程。

  • 2.然后通过getMap(当前线程)获得ThreadLocalMap,也就是说Thead和ThreadLocalMap有关系。也可以看到Thread中有ThreadLocalMap的变量

image.png

  • 3.最后将this(当前ThreadLocal)与传入的Looper保存在ThreadLocalMap中
  • 4.ThreadLocalMap就是一个保存<key,value>键值对的

所以看一下 Thread,ThreadLocalMap,ThreadLocal,Looper的关系 image.png

所以这里保证了一个Thread对应一个ThreadLocalMap,而ThreadLocalMap又保存这该Thread的ThreadLocal。问题来了<key,vaule>中key是唯一的,但是value是可以代替的,怎么能做到<ThreadLocal,Looper>保存之后Looper不会被代替呢

再回到prepare函数,可以看到在new Looper之前,还有一个get()操作 image.png

get函数做了一个操作,就是查看当前Thread对应的ThreadLocal,在ThreadLocalMap有没有值,有值则在prepare抛出异常 也就是说,prepare在一个线程中,只能够调用一次,也就保证了Looper只能生成一次,也就是唯一的

image.png

3.Handler内存泄漏原因?

我们知道,handler不能作为内部类存在,不然有可能会导致内存泄漏。为什么其他内部类不会呢?

通过java语法我们知道:匿名内部类持有外部类的对象 比如这个,handler是持有HandlerActivity的,不然也不能够调用到其中的方法,而系统是直接帮我们省略了HandlerActivity.this部分的 这就表示** Handler ---持有--> this.Activity --持有--> Activity的一切内容 = 大量内存**

image.png

首先我们知道,一个message是通过handler发送的,然后MessageQueue会保存 也就是说 MessageQueue ---持有--> message

接着我们再看看handler#enqueueMessage,我认为红框就是造成内存泄漏的最主要原因,我们通过代码可以看到 message.traget = this 这就意味着 message ---持有--> Handler对象

image.png

将三条链路拼接在一起 MessageQueue ---持有--> message ---持有--> Handler对象 ---持有--> this.Activity --持有--> Activity的一切内容 = 大量内存

当Handler发送了一个延迟10s的message。但是5s的时候,Activity销毁了。 此时的message是没有人处理的,即使他已经从MessageQueue扔出去了,但是Activity销毁了没人接收,也就是说这个message一只存在,则上面的这条链路是一只存在的。所以这持有的大量内存一直没人处理,虚拟机也会认为你这块内存是被持有的,他不会回收,就这样造成了内存泄漏。

所以说,Handler的内存泄漏,是说是因为匿名内部类是不够全面的

4.子线程中可以new Handler吗?

答案是可以的。 主线程和子线程都是线程,凭啥子线程不行呢,而且看了这么多代码也没看到什么地方必须要做主线程执行的方法。

下面用一段代码演示一下怎么在子线程创建Handler 首先要自定义自己的线程,在线程中创建出自己的Looper image.png

然后再将子线程的Looper传给Handler,这样创建的Handler就是子线程的了 image.png

但是这样写会有问题吗,显然是有的 我们知道子线程是异步的,而在子线程生成和获取Looper,你怎么知道他什么时候能创建好,怎么知道在Handler创建时,Looper是有值的呢?这一下变成了线程同步问题了,很简单,线程同步就加锁呗。实际上,系统已经写好了一个能在子线程创建Handler的 HandlerThread

可以看到总体还是和我们自己写的差不多的,不过在自己获取Looper和暴露给外界获取Looper加上了锁 也就是说,如果我们在looper还没创建出来时调用getLooper会执行wait(),释放锁且等待 直到run方法拿到锁之后,获取到Looper后去notiftAll()唤醒他 这样就能保证在Handler创建时,Looper是一定有的

image.png

5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?主线程呢?

我们知道Looper会帮我们在MessageQueue里面取消息,当MessageQueue没有消息了,Looper会做什么呢

首先看到获取消息的next()方法,他会调用到native层的方法nativePollOnce,当nativePollOnce取不到消息时,他就会让线程等待

image.png

所以此时的Looper.loop()方法中,系统也提示我们,会在这里阻塞住 而Looper.loop()是在子线程的run中运行的,要是一直没消息,他就会一直阻塞,run方法一直没办法结束,线程也没办法释放,就会造成内存泄露了

image.png

所以Looper给我们提供了一个方法quitSafely,而他会调用到MessageQueue的方法

image.png

他会让mQuitting = true;,接着清除message,接着nativeWake, 这与nativePollOnce是一对的,他会唤醒nativePollOnce继续执行

image.png

所以quitSafely后,next()方法会继续,因为msg = null,mQuitting = true,导致next()直接返回 null

image.png

然后再看调用next()方法的Looper.loop(),msg为null后直接return,for循环退出,loop方法也结束了。这样线程也能得到释放了

image.png

6.既然可以存在多个Handler往MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?

我们知道Looper创建时,会创建一个MessageQueue,且是唯一对应的 这也就说明一个Thread,Looper,MessageQueue都是唯一对应的关系 image.png

那么在添加消息时,synchronized (this) 的this 就是MessageQueue,而根据对应关系,这里加锁,其实就等于锁住了当前线程。就一个线程内算多个Handler同时添加消息,他们也会被锁限制,从而保证了消息添加的有序性,取消息同理

image.png

7.我们使用Message时应该如何创建它

不知道你们有没有人使用new Message()去创建消息。虽然是可以的,但是如果疯狂的new Message,你每new一个,就占用一块内存,会占用大量的内存和内存碎片

系统也提供了新建Message的方法,发现还是new Message(),那又有什么不同呢。 不同的就是sPool,他也是一个Message变量

image.png

我们回到Looper,没处理完一个消息后,他会调用Message的方法

image.png

而这个方法就是将当前的Message的所有参数清空,变成一个空的Message对象,然后放到sPool中去。等你一下需要Message变量时,他就可以重复里面

image.png

收起阅读 »

Android不使用反射完成LiveDataBus

LiveDataBus大家都很熟悉了,网上也有很多通过反射实现的LiveDataBus。但是通过反射实现的代码比较混乱,也比较难以理解。这里给出一版通过代码实现的。更加的简洁优雅~首先来看一下LiveData原理一般我们都是这样使用的,创建一个LiveData...
继续阅读 »

LiveDataBus大家都很熟悉了,网上也有很多通过反射实现的LiveDataBus。但是通过反射实现的代码比较混乱,也比较难以理解。这里给出一版通过代码实现的。更加的简洁优雅~

首先来看一下LiveData原理

一般我们都是这样使用的,创建一个LiveData去发送数据,在你想观察的地方去注册。这样只要数据发射,你就能拿到你想要的数据了。 

下面就是你再使用红框语句时的调用流程 

先进入 observe 方法看一看

 这样我们创建的LifecycleBoundObserver(observe方法中的new Observer)就和宿主(observe方法中传入的this) 建立了联系。

所以宿主每次生命周期的变化都会调用到 LifecycleBoundObserver的onStateChanged 而从代码中也可以看到,在宿主生命周期是DESTROYED时,会主动移除掉当前mObserver,完成自动反注册,这里注意要把mObservers 和 mObserver分清楚

这里有几点需要注意一下 1.LiveData中的mObservers是一个Map,还有一个mVersion字段默认等于 -1 

2.LifecycleBoundObserver 继承 ObserverWrapper 里面有mObserver,其实就是保存自己 还有一个mLastVersion字段,默认等于 -1 

接下来继续进入activeStateChanged方法,其他方法不多解释。 这里直接进入dispatchingValue 可以看到dispatchingValue不管走哪边都会进入considerNotify 

接下来看considerNotify 

看到这里我相信你已经知道,我们为啥能再onChanged拿到数据了

    viewModel.liveDataObject.observe(this, new Observer<Bean>() {
@Override
public void onChanged(Bean data) {
接收数据
}
});

接下来看一下,postValue和setValue

可以看到setValue是有注解MainThread的,表示只能在主线程中使用
而postValue没有,把某事件抛到主线程去了 

再来来看一下,postValue在切换到主线程中都干了些啥,我们发现他的Runnable中的方法,最终还是执行了setValue。 

所以这样看 postValue只不过是可以在子线程执行,但是消息发送最终还是要到主线程,且执行setValue 而setValue就只能在主线程执行了

在执行了setValue或者postValue后,mVersion+1,接着直接进入到considerNotify 

黏性事件怎么来的?

为了造成黏性事件,我再注册观察者之前就将数据发送出去,然后通过按钮点击再去注册一个观察者,我们能发现,即使是之前发送的数据,仍然能够接受得到,这就是黏性事件。 

造成的原因就是mLastVersion 和 mVersion

实现自己的LiveDataBus

LiveData基本的都了解过了,接下来自己实现一个,既可以接受黏性事件,又可以接受普通事件。

怎么控制黏性事件

其实原本的代码就是可以发送事件的,只不过不能自由的控制黏性事件 如果我们能用一个变量去标志就好了,比如这样标志一个receiveSticky变量 为true就是接受黏性事件,那么调用方法发送数据
为false的话就会,跳过此方法

if (observer.mLastVersion >= mVersion) { 
return;
}
observer.mLastVersion = mVersion;
if(receiveSticky){
observer.mObserver.onChanged((T) mData);
} else {
// 处理普通事件
}

在假设我们能直接在源码添加这个字段的话,那这个receiveSticky从哪里来呢?

1.发送者的角度:从 postValue 和 setValue 入手

比如改写成 postValue(data,receiveSticky) 这样有个弊端,这样只能统一发送黏性或者非黏性,这样如果多个宿主监听同一个消息,而有些需要黏性,有些不需要,这样就很难控制

2.从接收者的角度:从observer入手

我们知道我们传入的observer,在包装成LifecycleBoundObserver后,才有mLastVersion。那我们可以参考一下这种思路

比如:LifecycleBoundObserver包装一下,有了mLastVersion
那么:我们将传入的Observer也包装一层,在创建的时候传入receiveSticky就好了
就像这样:(当然这不是完整版,这只是记录一下思路)

怎么保证接收的是同一个事件

        liveData.postValue
viewModel.liveData.observer(this , Observer {

})

一般我们都是这样发送接收的,这个LiveData都是同一个才能接收同一份数据

所以我们也必须在LiveDataBus保证是同一个LiveData才行。

还是同样的思路,要区分LiveData,就给LiveData加名字就行了呗

那我就再给LiveData包装一层,让调用者传入名字去生成

生成完了就保存下来,以后就用名字去找到对应的LiveData

就好比:

那么他的用法就是这样的:接收消息的有点过于复杂了。 

既然observer是LiveData里面的方法 而每次发送消息时间都是StickyObserver(sticky, Observer()) 这样我们就可以在我们的包装类中去复写一下observer,比如: 

这样发送接收数据就会变成,比之前稍微好一些 

如何解决普通事件的接收

在上面我们其实没对普通事件做处理 我们通过sticky能判断接不接受黏性事件 但是我们不知道在我们注册之前,有没有消息事件发送

override fun onChanged(t: T) {
if (sticky) {
observer.onChanged(t)
} else {
// 普通事件
}
}

回想一下,黏性事件是怎么产生的?

简单的认为,observer.mLastVersion(Observer的) < mVersion(LiveData的) 就会产生黏性事件

所以我们也可以模仿一下,弄两个变量去判断 

在我们的LiveDate中 

在我们的observe中会被改写成这样 

所以显然不能完全完全按照源码照抄

那黏性事件之所以会被发送出去

就是在StickyObserver初始化时mLastVersionmLiveDataVersion没对齐,

导致if (mLastVersion >= stickyLiveData.mLiveDataVersion) {} 没进入

所以进入if条件就有黏性事件,所以我们要改成这样

代码

object LiveDataBus {

// LiveDataBus.with<String>("TestLiveDataBus").postStickyData("测试!")
// LiveDataBus.with<String>("TestLiveDataBus") .observerSticky(this, false) {
//
// }

private val mStickyMap = ConcurrentHashMap<String, StickyLiveData<*>>()

fun <T> with(eventName: String): StickyLiveData<T> {
var stickyLiveData = mStickyMap[eventName]
if (stickyLiveData == null) {
stickyLiveData = StickyLiveData<T>(eventName)
mStickyMap[eventName] = stickyLiveData
}

return stickyLiveData as StickyLiveData<T>
}


/**
* 将发射出去的LiveData包装一下,再做一些数据保存
*/
class StickyLiveData<T>(private var eventName: String) : LiveData<T>() {

var mLiveDataVersion = 0
var mStickyData: T? = null

fun setStickyData(stickyData: T) {
mStickyData = stickyData
setValue(stickyData)
}

fun postStickyData(stickyData: T) {
mStickyData = stickyData
postValue(stickyData)
}

override fun setValue(value: T) {
mLiveDataVersion++
super.setValue(value)
}

override fun postValue(value: T) {
super.postValue(value)
}

fun observerSticky(owner: LifecycleOwner, sticky: Boolean, observer: Observer<in T>) {
// 移除自己保存的StickyLiveData
owner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
mStickyMap.remove(eventName)
}
})

super.observe(owner, StickyObserver(this, sticky, observer))
}

/**
* 重写LiveData的observer,把传入的observer包装一下
*/
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
observerSticky(owner,false, observer)
}
}

class StickyObserver<T>(
private val stickyLiveData: StickyLiveData<T>,
private val sticky: Boolean,
private val observer: Observer<in T>
) : Observer<T> {

/**
* 打个比方:
* 一条数据,名称为TestName,
* 对应一个 StickyLiveData, 也就对应一个version, 初始的值为0,且这个可以复用
* 且会创建StickyObserver,对应一个 mLastVersion, 初始的值为0
*
* 如果 StickyLiveData#version 和 StickyObserver#mLastVersion 没有对齐
* LastVersion < version --> 直接发送数据,就会产生黏性事件
*
* 源码就是这样没对齐,所以无法控制黏性事件
*
* 因为源码的流程
* 将传入的observer包装成LifecycleBoundObserver(继承ObserverWrapper)会将传入的observer做保存和保存在hashMap
* 最后在considerNotify遍历hashMap,活跃的观察者会调用observer.onChanged(t)去发送数据
*
* 所以这里把传入的observer包装成StickyObserver 进入源码后 --> 再变成LifecycleBoundObserver
* 所以最终发送数据会调用StickyObserver的onChanged 就可以做黏性事件的处理了
*
*/
private var mLastVersion = stickyLiveData.mLiveDataVersion

override fun onChanged(t: T) {

if (mLastVersion >= stickyLiveData.mLiveDataVersion) {
if (sticky && stickyLiveData.mStickyData != null) {
observer.onChanged(stickyLiveData.mStickyData)
}
return
}
observer.onChanged(t)
}
}


}

收起阅读 »

Jetpack Compose 自定义 Loading

自学Jetpack Compose 半月有余了,写了一个Loading加载动效效果图实现思路拆分将正方形均分为4份 确定4个符号的中心点位置BoxWithConstraints(modifier = modifier) {    val ...
继续阅读 »

自学Jetpack Compose 半月有余了,写了一个Loading加载动效

效果图

loading02.gif

实现思路拆分

  1. 将正方形均分为4份 确定4个符号的中心点位置

image.png

BoxWithConstraints(modifier = modifier) {
   val circleSizeDp = minOf(maxWidth, maxHeight)
   val density = LocalDensity.current.density
   val circleSizePx = circleSizeDp.value * density
   //均分4份
   val radius = circleSizePx / 4
//right 和 bottom x,y
   val centerOffset = radius * 3

//加号中心点
   var plusOffset by remember { mutableStateOf(Offset(radius, radius)) }
//减号中心点
   var minusOffset by remember { mutableStateOf(Offset(centerOffset, radius)) }
//乘号中心点
   var timesOffset by remember { mutableStateOf(Offset(centerOffset, centerOffset)) }
//除号中心点
   var divOffset by remember { mutableStateOf(Offset(radius, centerOffset)) }
 
}  
  1. 根据4个符号的中心点绘制符号
     //符号长度
val offset = radius / 2 + 15.dp.value
Canvas(modifier = modifier.requiredSize(size = circleSizeDp)) {
//加号
drawLine(
color = lineColor,
start = Offset(plusOffset.x - offset, plusOffset.y),
end = Offset(plusOffset.x + offset, plusOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)

drawLine(
color = lineColor,
start = Offset(plusOffset.x, plusOffset.y - offset),
end = Offset(plusOffset.x, plusOffset.y + offset),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)

//减号
drawLine(
color = lineColor,
start = Offset(minusOffset.x - offset, minusOffset.y),
end = Offset(minusOffset.x + offset, minusOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
//乘号
rotate(degrees = 45F, pivot = timesOffset) {
drawLine(
color = lineColor,
start = Offset(timesOffset.x - offset, timesOffset.y),
end = Offset(timesOffset.x + offset, timesOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
}
rotate(degrees = 135F, pivot = timesOffset) {
drawLine(
color = lineColor,
start = Offset(timesOffset.x - offset, timesOffset.y),
end = Offset(timesOffset.x + offset, timesOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
}
//除号
drawLine(
color = lineColor,
start = Offset(divOffset.x - offset, divOffset.y),
end = Offset(divOffset.x + offset, divOffset.y),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
)
//除法2个圆点
drawCircle(
color = lineColor,
style = Fill,
radius = circleRadius,
center = Offset(divOffset.x, divOffset.y - radius / 3)
)
drawCircle(
color = lineColor,
style = Fill,
radius = circleRadius,
center = Offset(divOffset.x, divOffset.y + radius / 3)
)
}

静态绘制效果
image.png

  1. 使用动画动起来

根据4个符号的中心点 构成一个正方形,每次偏移是正方形的边长

image.png

使用rememberInfiniteTransition() 无限循环动画 不断执行0到正方形的边长的动画运算 不断改变4个符号的中心点位置

//移动长度
val animateSize = radius * 2
//记录旋转次数
var currentCount by remember { mutableStateOf(0) }
//rememberInfiniteTransition() 无限动画
val animateValue by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = animateSize,
// keyframes 分时间分段计算返回
// LinearEasing 平滑过渡
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 800
0f at 80 with LinearEasing
0.1f * animateSize at 150 with LinearEasing
0.2f * animateSize at 200 with LinearEasing
0.3f * animateSize at 250 with LinearEasing
0.4f * animateSize at 300 with LinearEasing
0.5f * animateSize at 400 with LinearEasing
0.6f * animateSize at 500 with LinearEasing
0.7f * animateSize at 600
0.8f * animateSize at 700
0.9f * animateSize at 750
animateSize at 800
},
repeatMode = RepeatMode.Restart
)
)
//监听动画结果变化 对4个断
LaunchedEffect(animateValue) {
//根据animateValue ==0 来判断 动画的每次重新执行(无奈、没有相关监听接口)
if (animateValue == 0f) {
//每次重新开始就累加1
currentCount += 1
if (currentCount > 4) {
currentCount = 1
}
}
val plus = radius + animateValue
val minus = centerOffset - animateValue
// 根据 currentCount 标记出动画运行到哪个阶段
when (currentCount) {
1 -> {//加号从左往右
plusOffset = Offset(plus, radius)
minusOffset = Offset(centerOffset, plus)

timesOffset = Offset(minus, centerOffset)
divOffset = Offset(radius, minus)
}
2 -> {//加号从右往下
plusOffset = Offset(centerOffset, plus)
minusOffset = Offset(minus, centerOffset)

timesOffset = Offset(radius, minus)
divOffset = Offset(plus, radius)
}
3 -> {//加号从下往左
plusOffset = Offset(minus, centerOffset)
minusOffset = Offset(radius, minus)

timesOffset = Offset(plus, radius)
divOffset = Offset(centerOffset, plus)
}
4 -> {
plusOffset = Offset(radius, minus)
minusOffset = Offset(plus, radius)

timesOffset = Offset(centerOffset, plus)
divOffset = Offset(minus, centerOffset)
}
}
}

动画实现这个过程有点痛苦,目前Compose 在对动画细粒度监听上没有更好的支持,rememberInfiniteTransition()是无限循环动画,但是没有对动画Restart、 start、 end暴露监听接口 同时差值器提供的也不能满足需求,只能通过keyframes 去一点一点的计算出来 如果有工友有好的方式 还望不要吝啬告知 到这里就基本上完成了

loading02.gif

扩展

使用 ModifierdrawWithContent实现未读消息红点提示

fun Modifier.redPoint(num: String): Modifier = drawWithContent {
drawContent()
drawIntoCanvas {
val padding = 6.dp.toPx()
val topPadding = 3.dp.toPx()

val paint = Paint().apply {
color = Color.Red
}
val paintTextSize= 14.sp.toPx()
//绘制文本用FrameworkPaint
val textPaint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
isDither = true
color=Color.White.toArgb()
textSize = paintTextSize
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
textAlign = android.graphics.Paint.Align.CENTER
}
//测量出文本的宽度
val textWidth = textPaint.measureText(num)

val radius =20.dp.toPx()
val offset=(textWidth+padding*2)
//绘制背景
it.drawRoundRect(
left = size.width-offset,
top = 0f,
right = size.width,
bottom = radius,
radiusX= 10.dp.toPx(),
radiusY= 10.dp.toPx(),
paint = paint
)
//绘制文本
it.nativeCanvas.drawText(num, size.width-offset/2, radius-(radius-paintTextSize)/2-topPadding, textPaint)
}
}

调用

@Composable
fun ImageDemo() {
Image(
painter = painterResource(id = R.drawable.message),
contentDescription = "",
modifier = Modifier
.size(width = 56.dp, height = 56.dp)
.redPoint("99"),
contentScale = ContentScale.FillBounds,
alignment = Alignment.CenterEnd,
)
}

image.png

收起阅读 »

Swift 可选链

可选链(Optional Chaining)是一种可以请求和调用属性、方法和子脚本的过程,用于请求或调用的目标可能为nil。可选链返回两个值:如果目标有值,调用就会成功,返回该值如果目标为nil,调用将返回nil多次请求或调用可以被链接成一个链,如果任意一个节...
继续阅读 »

可选链(Optional Chaining)是一种可以请求和调用属性、方法和子脚本的过程,用于请求或调用的目标可能为nil。

可选链返回两个值:

  • 如果目标有值,调用就会成功,返回该值

  • 如果目标为nil,调用将返回nil

多次请求或调用可以被链接成一个链,如果任意一个节点为nil将导致整条链失效。


可选链可替代强制解析

通过在属性、方法、或下标脚本的可选值后面放一个问号(?),即可定义一个可选链。

可选链 '?'感叹号(!)强制展开方法,属性,下标脚本可选链
? 放置于可选值后来调用方法,属性,下标脚本! 放置于可选值后来调用方法,属性,下标脚本来强制展开值
当可选为 nil 输出比较友好的错误信息当可选为 nil 时强制展开执行错误

使用感叹号(!)可选链实例

class Person {
var residence: Residence?
}

class Residence {
var numberOfRooms = 1
}

let john = Person()

//将导致运行时错误
let roomCount = john.residence!.numberOfRooms

以上程序执行输出结果为:

fatal error: unexpectedly found nil while unwrapping an Optional value

想使用感叹号(!)强制解析获得这个人residence属性numberOfRooms属性值,将会引发运行时错误,因为这时没有可以供解析的residence值。

使用问号(?)可选链实例

class Person {
var residence: Residence?
}

class Residence {
var numberOfRooms = 1
}

let john = Person()

// 链接可选residence?属性,如果residence存在则取回numberOfRooms的值
if let roomCount = john.residence?.numberOfRooms {
print("John 的房间号为 \(roomCount)。")
} else {
print("不能查看房间号")
}

以上程序执行输出结果为:

不能查看房间号

因为这种尝试获得numberOfRooms的操作有可能失败,可选链会返回Int?类型值,或者称作"可选Int"。当residence是空的时候(上例),选择Int将会为空,因此会出现无法访问numberOfRooms的情况。

要注意的是,即使numberOfRooms是非可选Int(Int?)时这一点也成立。只要是通过可选链的请求就意味着最后numberOfRooms总是返回一个Int?而不是Int。


为可选链定义模型类

你可以使用可选链来多层调用属性,方法,和下标脚本。这让你可以利用它们之间的复杂模型来获取更底层的属性,并检查是否可以成功获取此类底层属性。

实例

定义了四个模型类,其中包括多层可选链:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

通过可选链调用方法

你可以使用可选链的来调用可选值的方法并检查方法调用是否成功。即使这个方法没有返回值,你依然可以使用可选链来达成这一目的。

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()


if ((john.residence?.printNumberOfRooms()) != nil) {
print("输出房间号")
} else {
print("无法输出房间号")
}

以上程序执行输出结果为:

无法输出房间号

使用if语句来检查是否能成功调用printNumberOfRooms方法:如果方法通过可选链调用成功,printNumberOfRooms的隐式返回值将会是Void,如果没有成功,将返回nil。


使用可选链调用下标脚本

你可以使用可选链来尝试从下标脚本获取值并检查下标脚本的调用是否成功,然而,你不能通过可选链来设置下标脚本。

实例1

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()
if let firstRoomName = john.residence?[0].name {
print("第一个房间名 \(firstRoomName).")
} else {
print("无法检索到房间")
}

以上程序执行输出结果为:

无法检索到房间

在下标脚本调用中可选链的问号直接跟在 john.residence 的后面,在下标脚本括号的前面,因为 john.residence 是可选链试图获得的可选值。

实例2

实例中创建一个 Residence 实例给 john.residence,且在他的 rooms 数组中有一个或多个 Room 实例,那么你可以使用可选链通过 Residence 下标脚本来获取在 rooms 数组中的实例了:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()
let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

let johnsAddress = Address()
johnsAddress
.buildingName = "The Larches"
johnsAddress
.street = "Laurel Street"
john
.residence!.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {
print("John 所在的街道是 \(johnsStreet)。")
} else {
print("无法检索到地址。 ")
}

以上程序执行输出结果为:

John 所在的街道是 Laurel Street

通过可选链接调用来访问下标

通过可选链接调用,我们可以用下标来对可选值进行读取或写入,并且判断下标调用是否成功。

实例

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
print("第一个房间名为\(firstRoomName)")
} else {
print("无法检索到房间")
}

以上程序执行输出结果为:

第一个房间名为客厅

访问可选类型的下标

如果下标返回可空类型值,比如Swift中Dictionary的key下标。可以在下标的闭合括号后面放一个问号来链接下标的可空返回值:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores
["Dave"]?[0] = 91
testScores
["Bev"]?[0]++
testScores
["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]

上面的例子中定义了一个testScores数组,包含了两个键值对, 把String类型的key映射到一个整形数组。

这个例子用可选链接调用把"Dave"数组中第一个元素设为91,把"Bev"数组的第一个元素+1,然后尝试把"Brian"数组中的第一个元素设为72。

前两个调用是成功的,因为这两个key存在。但是key"Brian"在字典中不存在,所以第三个调用失败。


连接多层链接

你可以将多层可选链连接在一起,可以掘取模型内更下层的属性方法和下标脚本。然而多层可选链不能再添加比已经返回的可选值更多的层。

如果你试图通过可选链获得Int值,不论使用了多少层链接返回的总是Int?。 相似的,如果你试图通过可选链获得Int?值,不论使用了多少层链接返回的总是Int?。

实例1

下面的例子试图获取john的residence属性里的address的street属性。这里使用了两层可选链来联系residence和address属性,它们两者都是可选类型:

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

if let johnsStreet = john.residence?.address?.street {
print("John 的地址为 \(johnsStreet).")
} else {
print("不能检索地址")
}

以上程序执行输出结果为:

不能检索地址

实例2

如果你为Address设定一个实例来作为john.residence.address的值,并为address的street属性设定一个实际值,你可以通过多层可选链来得到这个属性值。

class Person {
var residence: Residence?
}

class Residence {

var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
get{
return rooms[i]
}
set {
rooms
[i] = newValue
}
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

class Room {
let name: String
init
(name: String) { self.name = name }
}

class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
john
.residence?[0] = Room(name: "浴室")

let johnsHouse = Residence()
johnsHouse
.rooms.append(Room(name: "客厅"))
johnsHouse
.rooms.append(Room(name: "厨房"))
john
.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
print("第一个房间是\(firstRoomName)")
} else {
print("无法检索房间")
}

以上实例输出结果为:

第一个房间是客厅

对返回可选值的函数进行链接

我们还可以通过可选链接来调用返回可空值的方法,并且可以继续对可选值进行链接。

实例

class Person {
var residence: Residence?
}

// 定义了一个变量 rooms,它被初始化为一个Room[]类型的空数组
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript
(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms
() {
print("房间号为 \(numberOfRooms)")
}
var address: Address?
}

// Room 定义一个name属性和一个设定room名的初始化器
class Room {
let name: String
init
(name: String) { self.name = name }
}

// 模型中的最终类叫做Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier
() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}

let john = Person()

if john.residence?.printNumberOfRooms() != nil {
print("指定了房间号)")
} else {
print("未指定房间号")
}

以上程序执行输出结果为:

未指定房间号
收起阅读 »

Swift 析构过程

在一个类的实例被释放之前,析构函数被立即调用。用关键字deinit来标示析构函数,类似于初始化函数用init来标示。析构函数只适用于类类型。析构过程原理Swift 会自动释放不再需要的实例以释放资源。Swift 通过自动引用计数(ARC)处理实例的内存管理。通...
继续阅读 »

在一个类的实例被释放之前,析构函数被立即调用。用关键字deinit来标示析构函数,类似于初始化函数用init来标示。析构函数只适用于类类型。


析构过程原理

Swift 会自动释放不再需要的实例以释放资源。

Swift 通过自动引用计数(ARC)处理实例的内存管理。

通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。

例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前关闭该文件。

语法

在类的定义中,每个类最多只能有一个析构函数。析构函数不带任何参数,在写法上不带括号:

deinit {
// 执行析构过程
}

实例

var counter = 0;  // 引用计数器
class BaseClass {
init
() {
counter
+= 1;
}
deinit
{
counter
-= 1;
}
}

var show: BaseClass? = BaseClass()
print(counter)
show
= nil
print(counter)

以上程序执行输出结果为:

1
0

当 show = nil 语句执行后,计算器减去 1,show 占用的内存就会释放。

var counter = 0;  // 引用计数器

class BaseClass {
init
() {
counter
+= 1;
}

deinit
{
counter
-= 1;
}
}

var show: BaseClass? = BaseClass()

print(counter)
print(counter)

以上程序执行输出结果为:

1
1
收起阅读 »

Swift 构造过程

构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务。Swift 构造函数使用 init() 方法。与 Objective-C 中的构造器不同,Swift 的构造器无需返回...
继续阅读 »

构造过程是为了使用某个类、结构体或枚举类型的实例而进行的准备过程。这个过程包含了为实例中的每个属性设置初始值和为其执行必要的准备和初始化任务。

Swift 构造函数使用 init() 方法。

与 Objective-C 中的构造器不同,Swift 的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。

类实例也可以通过定义析构器(deinitializer)在类实例释放之前执行清理内存的工作。


存储型属性的初始赋值

类和结构体在实例创建时,必须为所有存储型属性设置合适的初始值。

存储属性在构造器中赋值时,它们的值是被直接设置的,不会触发任何属性观测器。

存储属性在构造器中赋值流程:

  • 创建初始值。

  • 在属性定义中指定默认属性值。

  • 初始化实例,并调用 init() 方法。


构造器

构造器在创建某特定类型的新实例时调用。它的最简形式类似于一个不带任何参数的实例方法,以关键字init命名。

语法

init()
{
// 实例化后执行的代码
}

实例

以下结构体定义了一个不带参数的构造器 init,并在里面将存储型属性 length 和 breadth 的值初始化为 6 和 12:

struct rectangle {
var length: Double
var breadth: Double
init
() {
length
= 6
breadth
= 12
}
}
var area = rectangle()
print("矩形面积为 \(area.length*area.breadth)")

以上程序执行输出结果为:

矩形面积为 72.0

默认属性值

我们可以在构造器中为存储型属性设置初始值;同样,也可以在属性声明时为其设置默认值。

使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型。

以下实例我们在属性声明时为其设置默认值:

struct rectangle {
    
// 设置默认值
var length = 6
var breadth = 12
}
var area = rectangle()
print("矩形的面积为 \(area.length*area.breadth)")

以上程序执行输出结果为:

矩形面积为 72

构造参数

你可以在定义构造器 init() 时提供构造参数,如下所示:

struct Rectangle {
var length: Double
var breadth: Double
var area: Double

init
(fromLength length: Double, fromBreadth breadth: Double) {
self.length = length
self.breadth = breadth
area
= length * breadth
}

init
(fromLeng leng: Double, fromBread bread: Double) {
self.length = leng
self.breadth = bread
area
= leng * bread
}
}

let ar = Rectangle(fromLength: 6, fromBreadth: 12)
print("面积为: \(ar.area)")

let are = Rectangle(fromLeng: 36, fromBread: 12)
print("面积为: \(are.area)")

以上程序执行输出结果为:

面积为: 72.0
面积为: 432.0

内部和外部参数名

跟函数和方法参数相同,构造参数也存在一个在构造器内部使用的参数名字和一个在调用构造器时使用的外部参数名字。

然而,构造器并不像函数和方法那样在括号前有一个可辨别的名字。所以在调用构造器时,主要通过构造器中的参数名和类型来确定需要调用的构造器。

如果你在定义构造器时没有提供参数的外部名字,Swift 会为每个构造器的参数自动生成一个跟内部名字相同的外部名。

struct Color {
let red, green, blue: Double
init
(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init
(white: Double) {
red
= white
green
= white
blue
= white
}
}

// 创建一个新的Color实例,通过三种颜色的外部参数名来传值,并调用构造器
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)

print("red 值为: \(magenta.red)")
print("green 值为: \(magenta.green)")
print("blue 值为: \(magenta.blue)")

// 创建一个新的Color实例,通过三种颜色的外部参数名来传值,并调用构造器
let halfGray = Color(white: 0.5)
print("red 值为: \(halfGray.red)")
print("green 值为: \(halfGray.green)")
print("blue 值为: \(halfGray.blue)")

以上程序执行输出结果为:

red 值为: 1.0
green
值为: 0.0
blue
值为: 1.0
red
值为: 0.5
green
值为: 0.5
blue
值为: 0.5

没有外部名称参数

如果你不希望为构造器的某个参数提供外部名字,你可以使用下划线_来显示描述它的外部名。

struct Rectangle {
var length: Double

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}
//不提供外部名字
init
(_ area: Double) {
length
= area
}
}

// 调用不提供外部名字
let rectarea = Rectangle(180.0)
print("面积为: \(rectarea.length)")

// 调用不提供外部名字
let rearea = Rectangle(370.0)
print("面积为: \(rearea.length)")

// 调用不提供外部名字
let recarea = Rectangle(110.0)
print("面积为: \(recarea.length)")

以上程序执行输出结果为:

面积为: 180.0
面积为: 370.0
面积为: 110.0

可选属性类型

如果你定制的类型包含一个逻辑上允许取值为空的存储型属性,你都需要将它定义为可选类型optional type(可选属性类型)。

当存储属性声明为可选时,将自动初始化为空 nil。

struct Rectangle {
var length: Double?

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}

init
(_ area: Double) {
length
= area
}
}

let rectarea = Rectangle(180.0)
print("面积为:\(rectarea.length)")

let rearea = Rectangle(370.0)
print("面积为:\(rearea.length)")

let recarea = Rectangle(110.0)
print("面积为:\(recarea.length)")

以上程序执行输出结果为:

面积为:Optional(180.0)
面积为:Optional(370.0)
面积为:Optional(110.0)

构造过程中修改常量属性

只要在构造过程结束前常量的值能确定,你可以在构造过程中的任意时间点修改常量属性的值。

对某个类实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。

尽管 length 属性现在是常量,我们仍然可以在其类的构造器中设置它的值:

struct Rectangle {
let length: Double?

init
(frombreadth breadth: Double) {
length
= breadth * 10
}

init
(frombre bre: Double) {
length
= bre * 30
}

init
(_ area: Double) {
length
= area
}
}

let rectarea = Rectangle(180.0)
print("面积为:\(rectarea.length)")

let rearea = Rectangle(370.0)
print("面积为:\(rearea.length)")

let recarea = Rectangle(110.0)
print("面积为:\(recarea.length)")

以上程序执行输出结果为:

面积为:Optional(180.0)
面积为:Optional(370.0)
面积为:Optional(110.0)

默认构造器

默认构造器将简单的创建一个所有属性值都设置为默认值的实例:

以下实例中,ShoppingListItem类中的所有属性都有默认值,且它是没有父类的基类,它将自动获得一个可以为所有属性设置默认值的默认构造器

class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()


print("名字为: \(item.name)")
print("数理为: \(item.quantity)")
print("是否付款: \(item.purchased)")

以上程序执行输出结果为:

名字为: nil
数理为: 1
是否付款: false

结构体的逐一成员构造器

如果结构体对所有存储型属性提供了默认值且自身没有提供定制的构造器,它们能自动获得一个逐一成员构造器。

我们在调用逐一成员构造器时,通过与成员属性名相同的参数名进行传值来完成对成员属性的初始赋值。

下面例子中定义了一个结构体 Rectangle,它包含两个属性 length 和 breadth。Swift 可以根据这两个属性的初始赋值100.0 、200.0自动推导出它们的类型Double。

struct Rectangle {
var length = 100.0, breadth = 200.0
}
let area = Rectangle(length: 24.0, breadth: 32.0)

print("矩形的面积: \(area.length)")
print("矩形的面积: \(area.breadth)")

由于这两个存储型属性都有默认值,结构体 Rectangle 自动获得了一个逐一成员构造器 init(width:height:)。 你可以用它来为 Rectangle 创建新的实例。

以上程序执行输出结果为:

矩形的面积: 24.0
矩形的面积: 32.0

值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重复。

以下实例中,Rect 结构体调用了 Size 和 Point 的构造过程:

struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}

struct Rect {
var origin = Point()
var size = Size()
init
() {}
init
(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init
(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}


// origin和size属性都使用定义时的默认值Point(x: 0.0, y: 0.0)和Size(width: 0.0, height: 0.0):
let basicRect = Rect()
print("Size 结构体初始值: \(basicRect.size.width, basicRect.size.height) ")
print("Rect 结构体初始值: \(basicRect.origin.x, basicRect.origin.y) ")

// 将origin和size的参数值赋给对应的存储型属性
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
size
: Size(width: 5.0, height: 5.0))

print("Size 结构体初始值: \(originRect.size.width, originRect.size.height) ")
print("Rect 结构体初始值: \(originRect.origin.x, originRect.origin.y) ")


//先通过center和size的值计算出origin的坐标。
//然后再调用(或代理给)init(origin:size:)构造器来将新的origin和size值赋值到对应的属性中
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size
: Size(width: 3.0, height: 3.0))

print("Size 结构体初始值: \(centerRect.size.width, centerRect.size.height) ")
print("Rect 结构体初始值: \(centerRect.origin.x, centerRect.origin.y) ")

以上程序执行输出结果为:

Size 结构体初始值: (0.0, 0.0) 
Rect 结构体初始值: (0.0, 0.0)
Size 结构体初始值: (5.0, 5.0)
Rect 结构体初始值: (2.0, 2.0)
Size 结构体初始值: (3.0, 3.0)
Rect 结构体初始值: (2.5, 2.5)

构造器代理规则

值类型类类型
不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给本身提供的其它构造器。 你可以使用self.init在自定义的构造器中引用其它的属于相同值类型的构造器。它可以继承自其它类,这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。

类的继承和构造过程

Swift 提供了两种类型的类构造器来确保所有类实例中存储型属性都能获得初始值,它们分别是指定构造器和便利构造器。

指定构造器便利构造器
类中最主要的构造器类中比较次要的、辅助型的构造器
初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。可以定义便利构造器来调用同一个类中的指定构造器,并为其参数提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入的实例。
每一个类都必须拥有至少一个指定构造器只在必要的时候为类提供便利构造器
Init(parameters) {
statements
}
convenience init(parameters) {
statements
}

指定构造器实例

class mainClass {
var no1 : Int // 局部存储变量
init
(no1 : Int) {
self.no1 = no1 // 初始化
}
}
class subClass : mainClass {
var no2 : Int // 新的子类存储变量
init
(no1 : Int, no2 : Int) {
self.no2 = no2 // 初始化
super.init(no1:no1) // 初始化超类
}
}

let res = mainClass(no1: 10)
let res2 = subClass(no1: 10, no2: 20)

print("res 为: \(res.no1)")
print("res2 为: \(res2.no1)")
print("res2 为: \(res2.no2)")

以上程序执行输出结果为:

res 为: 10
res
为: 10
res
为: 20

便利构造器实例

class mainClass {
var no1 : Int // 局部存储变量
init
(no1 : Int) {
self.no1 = no1 // 初始化
}
}

class subClass : mainClass {
var no2 : Int
init
(no1 : Int, no2 : Int) {
self.no2 = no2
super.init(no1:no1)
}
// 便利方法只需要一个参数
override convenience init(no1: Int) {
self.init(no1:no1, no2:0)
}
}
let res = mainClass(no1: 20)
let res2 = subClass(no1: 30, no2: 50)

print("res 为: \(res.no1)")
print("res2 为: \(res2.no1)")
print("res2 为: \(res2.no2)")

以上程序执行输出结果为:

res 为: 20
res2
为: 30
res2
为: 50

构造器的继承和重载

Swift 中的子类不会默认继承父类的构造器。

父类的构造器仅在确定和安全的情况下被继承。

当你重写一个父类指定构造器时,你需要写override修饰符。

class SuperClass {
var corners = 4
var description: String {
return "\(corners) 边"
}
}
let rectangle = SuperClass()
print("矩形: \(rectangle.description)")

class SubClass: SuperClass {
override init() { //重载构造器
super.init()
corners
= 5
}
}

let subClass = SubClass()
print("五角型: \(subClass.description)")

以上程序执行输出结果为:

矩形: 4 
五角型: 5

指定构造器和便利构造器实例

接下来的例子将在操作中展示指定构造器、便利构造器和自动构造器的继承。

它定义了包含两个个类MainClass、SubClass的类层次结构,并将演示它们的构造器是如何相互作用的。

class MainClass {
var name: String

init
(name: String) {
self.name = name
}

convenience init
() {
self.init(name: "[匿名]")
}
}
let main = MainClass(name: "Runoob")
print("MainClass 名字为: \(main.name)")

let main2 = MainClass()
print("没有对应名字: \(main2.name)")

class SubClass: MainClass {
var count: Int
init
(name: String, count: Int) {
self.count = count
super.init(name: name)
}

override convenience init(name: String) {
self.init(name: name, count: 1)
}
}

let sub = SubClass(name: "Runoob")
print("MainClass 名字为: \(sub.name)")

let sub2 = SubClass(name: "Runoob", count: 3)
print("count 变量: \(sub2.count)")

以上程序执行输出结果为:

MainClass 名字为: Runoob
没有对应名字: [匿名]
MainClass 名字为: Runoob
count
变量: 3

类的可失败构造器

如果一个类,结构体或枚举类型的对象,在构造自身的过程中有可能失败,则为其定义一个可失败构造器。

变量初始化失败可能的原因有:

  • 传入无效的参数值。

  • 缺少某种所需的外部资源。

  • 没有满足特定条件。

为了妥善处理这种构造过程中可能会失败的情况。

你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在init关键字后面加添问号(init?)。

实例

下例中,定义了一个名为Animal的结构体,其中有一个名为species的,String类型的常量属性。

同时该结构体还定义了一个,带一个String类型参数species的,可失败构造器。这个可失败构造器,被用来检查传入的参数是否为一个空字符串,如果为空字符串,则该可失败构造器,构建对象失败,否则成功。

struct Animal {
let species: String
init
?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}

//通过该可失败构造器来构建一个Animal的对象,并检查其构建过程是否成功
// someCreature 的类型是 Animal? 而不是 Animal
let someCreature = Animal(species: "长颈鹿")

// 打印 "动物初始化为长颈鹿"
if let giraffe = someCreature {
print("动物初始化为\(giraffe.species)")
}

以上程序执行输出结果为:

动物初始化为长颈鹿

枚举类型的可失败构造器

你可以通过构造一个带一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员。

实例

下例中,定义了一个名为TemperatureUnit的枚举类型。其中包含了三个可能的枚举成员(Kelvin,Celsius,和 Fahrenheit)和一个被用来找到Character值所对应的枚举成员的可失败构造器:

enum TemperatureUnit {
    
// 开尔文,摄氏,华氏
case Kelvin, Celsius, Fahrenheit
init
?(symbol: Character) {
switch symbol {
case "K":
self = .Kelvin
case "C":
self = .Celsius
case "F":
self = .Fahrenheit
default:
return nil
}
}
}


let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
print("这是一个已定义的温度单位,所以初始化成功。")
}

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
print("这不是一个已定义的温度单位,所以初始化失败。")
}

以上程序执行输出结果为:

这是一个已定义的温度单位,所以初始化成功。
这不是一个已定义的温度单位,所以初始化失败。

类的可失败构造器

值类型(如结构体或枚举类型)的可失败构造器,对何时何地触发构造失败这个行为没有任何的限制。

但是,类的可失败构造器只能在所有的类属性被初始化后和所有类之间的构造器之间的代理调用发生完后触发失败行为。

实例

下例子中,定义了一个名为 StudRecord 的类,因为 studname 属性是一个常量,所以一旦 StudRecord 类构造成功,studname 属性肯定有一个非nil的值。

class StudRecord {
let studname: String!
init
?(studname: String) {
self.studname = studname
if studname.isEmpty { return nil }
}
}
if let stname = StudRecord(studname: "失败构造器") {
print("模块为 \(stname.studname)")
}

以上程序执行输出结果为:

模块为 失败构造器

覆盖一个可失败构造器

就如同其它构造器一样,你也可以用子类的可失败构造器覆盖基类的可失败构造器。

者你也可以用子类的非可失败构造器覆盖一个基类的可失败构造器。

你可以用一个非可失败构造器覆盖一个可失败构造器,但反过来却行不通。

一个非可失败的构造器永远也不能代理调用一个可失败构造器。

实例

以下实例描述了可失败与非可失败构造器:

class Planet {
var name: String

init
(name: String) {
self.name = name
}

convenience init
() {
self.init(name: "[No Planets]")
}
}
let plName = Planet(name: "Mercury")
print("行星的名字是: \(plName.name)")

let noplName = Planet()
print("没有这个名字的行星: \(noplName.name)")

class planets: Planet {
var count: Int

init
(name: String, count: Int) {
self.count = count
super.init(name: name)
}

override convenience init(name: String) {
self.init(name: name, count: 1)
}
}

以上程序执行输出结果为:

行星的名字是: Mercury
没有这个名字的行星: [No Planets]

可失败构造器 init!

通常来说我们通过在init关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以使用通过在init后面添加惊叹号的方式来定义一个可失败构造器(init!)。实例如下:

struct StudRecord {
let stname: String

init
!(stname: String) {
if stname.isEmpty {return nil }
self.stname = stname
}
}

let stmark = StudRecord(stname: "Runoob")
if let name = stmark {
print("指定了学生名")
}

let blankname = StudRecord(stname: "")
if blankname == nil {
print("学生名为空")
}

以上程序执行输出结果为:

指定了学生名
学生名为空
收起阅读 »

Swift 继承

继承我们可以理解为一个类获取了另外一个类的方法和属性。当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)在 Swift 中,类可以调用和访问超类的方法,属性和下标脚本,并且可以重写它们。我们也可以为类中继承来的属性添加属性观察器。基类没有继承其它类的...
继续阅读 »

继承我们可以理解为一个类获取了另外一个类的方法和属性。

当一个类继承其它类时,继承类叫子类,被继承类叫超类(或父类)

在 Swift 中,类可以调用和访问超类的方法,属性和下标脚本,并且可以重写它们。

我们也可以为类中继承来的属性添加属性观察器。


基类

没有继承其它类的类,称之为基类(Base Class)。

以下实例中我们定义了基类 StudDetails ,描述了学生(stname)及其各科成绩的分数(mark1、mark2、mark3):

class StudDetails {
var stname: String!
var mark1: Int!
var mark2: Int!
var mark3: Int!
init
(stname: String, mark1: Int, mark2: Int, mark3: Int) {
self.stname = stname
self.mark1 = mark1
self.mark2 = mark2
self.mark3 = mark3
}
}
let stname = "swift"
let mark1 = 98
let mark2 = 89
let mark3 = 76

let sds = StudDetails(stname:stname, mark1:mark1, mark2:mark2, mark3:mark3);

print(sds.stname)
print(sds.mark1)
print(sds.mark2)
print(sds.mark3)

以上程序执行输出结果为:

swift
98
89
76

子类

子类指的是在一个已有类的基础上创建一个新的类。

为了指明某个类的超类,将超类名写在子类名的后面,用冒号(:)分隔,语法格式如下

class SomeClass: SomeSuperclass {
// 类的定义
}

实例

以下实例中我们定义了超类 StudDetails,然后使用子类 Tom 继承它:

class StudDetails
{
var mark1: Int;
var mark2: Int;

init
(stm1:Int, results stm2:Int)
{
mark1
= stm1;
mark2
= stm2;
}

func show
()
{
print("Mark1:\(self.mark1), Mark2:\(self.mark2)")
}
}

class Tom : StudDetails
{
init
()
{
super.init(stm1: 93, results: 89)
}
}

let tom = Tom()
tom
.show()

以上程序执行输出结果为:

Mark1:93, Mark2:89

重写(Overriding)

子类可以通过继承来的实例方法,类方法,实例属性,或下标脚本来实现自己的定制功能,我们把这种行为叫重写(overriding)。

我们可以使用 override 关键字来实现重写。

访问超类的方法、属性及下标脚本

你可以通过使用super前缀来访问超类的方法,属性或下标脚本。

重写访问方法,属性,下标脚本
方法super.somemethod()
属性super.someProperty()
下标脚本super[someIndex]

重写方法和属性

重写方法

在我们的子类中我们可以使用 override 关键字来重写超类的方法。

以下实例中我们重写了 show() 方法:

class SuperClass {
func show
() {
print("这是超类 SuperClass")
}
}

class SubClass: SuperClass {
override func show() {
print("这是子类 SubClass")
}
}

let superClass = SuperClass()
superClass
.show()

let subClass = SubClass()
subClass
.show()

以上程序执行输出结果为:

这是超类 SuperClass
这是子类 SubClass

重写属性

你可以提供定制的 getter(或 setter)来重写任意继承来的属性,无论继承来的属性是存储型的还是计算型的属性。

子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。所以你在重写一个属性时,必需将它的名字和类型都写出来。

注意点:

  • 如果你在重写属性中提供了 setter,那么你也一定要提供 getter。

  • 如果你不想在重写版本中的 getter 里修改继承来的属性值,你可以直接通过super.someProperty来返回继承来的值,其中someProperty是你要重写的属性的名字。

以下实例我们定义了超类 Circle 及子类 Rectangle, 在 Rectangle 类中我们重写属性 area:

class Circle {
var radius = 12.5
var area: String {
return "矩形半径 \(radius) "
}
}

// 继承超类 Circle
class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}

let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("Radius \(rect.area)")

以上程序执行输出结果为:

Radius 矩形半径 25.0  ,但现在被重写为 3

重写属性观察器

你可以在属性重写中为一个继承来的属性添加属性观察器。这样一来,当继承来的属性值发生改变时,你就会监测到。

注意:你不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。

class Circle {
var radius = 12.5
var area: String {
return "矩形半径为 \(radius) "
}
}

class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}


let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("半径: \(rect.area)")

class Square: Rectangle {
override var radius: Double {
didSet
{
print = Int(radius/5.0)+1
}
}
}


let sq = Square()
sq
.radius = 100.0
print("半径: \(sq.area)")
半径: 矩形半径为 25.0  ,但现在被重写为 3
半径: 矩形半径为 100.0 ,但现在被重写为 21

防止重写

我们可以使用 final 关键字防止它们被重写。

如果你重写了final方法,属性或下标脚本,在编译时会报错。

你可以通过在关键字class前添加final特性(final class)来将整个类标记为 final 的,这样的类是不可被继承的,否则会报编译错误。

final class Circle {
final var radius = 12.5
var area: String {
return "矩形半径为 \(radius) "
}
}
class Rectangle: Circle {
var print = 7
override var area: String {
return super.area + " ,但现在被重写为 \(print)"
}
}

let rect = Rectangle()
rect
.radius = 25.0
rect
.print = 3
print("半径: \(rect.area)")

class Square: Rectangle {
override var radius: Double {
didSet
{
print = Int(radius/5.0)+1
}
}
}

let sq = Square()
sq
.radius = 100.0
print("半径: \(sq.area)")

由于以上实例使用了 final 关键字不允许重写,所以执行会报错:

error: var overrides a 'final' var
override var area: String {
^
note
: overridden declaration is here
var area: String {
^
error
: var overrides a 'final' var
override var radius: Double {
^
note
: overridden declaration is here
final var radius = 12.5
^
error
: inheritance from a final class 'Circle'
class Rectangle: Circle {
^
收起阅读 »

Swift 下标脚本

下标脚本 可以定义在类(Class)、结构体(structure)和枚举(enumeration)这些目标中,可以认为是访问对象、集合或序列的快捷方式,不需要再调用实例的特定的赋值和访问方法。举例来说,用下标脚本访问一个数组(Array)实例中的元素可以这样写...
继续阅读 »

下标脚本 可以定义在类(Class)、结构体(structure)和枚举(enumeration)这些目标中,可以认为是访问对象、集合或序列的快捷方式,不需要再调用实例的特定的赋值和访问方法。

举例来说,用下标脚本访问一个数组(Array)实例中的元素可以这样写 someArray[index] ,访问字典(Dictionary)实例中的元素可以这样写 someDictionary[key]。

对于同一个目标可以定义多个下标脚本,通过索引值类型的不同来进行重载,而且索引值的个数可以是多个。


下标脚本语法及应用

语法

下标脚本允许你通过在实例后面的方括号中传入一个或者多个的索引值来对实例进行访问和赋值。

语法类似于实例方法和计算型属性的混合。

与定义实例方法类似,定义下标脚本使用subscript关键字,显式声明入参(一个或多个)和返回类型。

与实例方法不同的是下标脚本可以设定为读写或只读。这种方式又有点像计算型属性的getter和setter:

subscript(index: Int) -> Int {
get {
// 用于下标脚本值的声明
}
set(newValue) {
// 执行赋值操作
}
}

实例 1

import Cocoa

struct subexample {
let decrementer: Int
subscript
(index: Int) -> Int {
return decrementer / index
}
}
let division = subexample(decrementer: 100)

print("100 除以 9 等于 \(division[9])")
print("100 除以 2 等于 \(division[2])")
print("100 除以 3 等于 \(division[3])")
print("100 除以 5 等于 \(division[5])")
print("100 除以 7 等于 \(division[7])")

以上程序执行输出结果为:

100 除以 9 等于 11
100 除以 2 等于 50
100 除以 3 等于 33
100 除以 5 等于 20
100 除以 7 等于 14

在上例中,通过 subexample 结构体创建了一个除法运算的实例。数值 100 作为结构体构造函数传入参数初始化实例成员 decrementer。

你可以通过下标脚本来得到结果,比如 division[2] 即为 100 除以 2。

实例 2

import Cocoa

class daysofaweek {
private var days = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "saturday"]
subscript
(index: Int) -> String {
get {
return days[index] // 声明下标脚本的值
}
set(newValue) {
self.days[index] = newValue // 执行赋值操作
}
}
}
var p = daysofaweek()

print(p[0])
print(p[1])
print(p[2])
print(p[3])

以上程序执行输出结果为:

Sunday
Monday
Tuesday
Wednesday

用法

根据使用场景不同下标脚本也具有不同的含义。

通常下标脚本是用来访问集合(collection),列表(list)或序列(sequence)中元素的快捷方式。

你可以在你自己特定的类或结构体中自由的实现下标脚本来提供合适的功能。

例如,Swift 的字典(Dictionary)实现了通过下标脚本对其实例中存放的值进行存取操作。在下标脚本中使用和字典索引相同类型的值,并且把一个字典值类型的值赋值给这个下标脚来为字典设值:

import Cocoa

var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs
["bird"] = 2

print(numberOfLegs)

以上程序执行输出结果为:

["ant": 6, "bird": 2, "cat": 4, "spider": 8]

上例定义一个名为numberOfLegs的变量并用一个字典字面量初始化出了包含三对键值的字典实例。numberOfLegs的字典存放值类型推断为Dictionary。字典实例创建完成之后通过下标脚本的方式将整型值2赋值到字典实例的索引为bird的位置中。


下标脚本选项

下标脚本允许任意数量的入参索引,并且每个入参类型也没有限制。

下标脚本的返回值也可以是任何类型。

下标脚本可以使用变量参数和可变参数。

一个类或结构体可以根据自身需要提供多个下标脚本实现,在定义下标脚本时通过传入参数的类型进行区分,使用下标脚本时会自动匹配合适的下标脚本实现运行,这就是下标脚本的重载

import Cocoa

struct Matrix {
let rows: Int, columns: Int
var print: [Double]
init
(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
print = Array(repeating: 0.0, count: rows * columns)
}
subscript
(row: Int, column: Int) -> Double {
get {
return print[(row * columns) + column]
}
set {
print[(row * columns) + column] = newValue
}
}
}
// 创建了一个新的 3 行 3 列的Matrix实例
var mat = Matrix(rows: 3, columns: 3)

// 通过下标脚本设置值
mat
[0,0] = 1.0
mat
[0,1] = 2.0
mat
[1,0] = 3.0
mat
[1,1] = 5.0

// 通过下标脚本获取值
print("\(mat[0,0])")
print("\(mat[0,1])")
print("\(mat[1,0])")
print("\(mat[1,1])")

以上程序执行输出结果为:

1.0
2.0
3.0
5.0

Matrix 结构体提供了一个两个传入参数的构造方法,两个参数分别是rows和columns,创建了一个足够容纳rows * columns个数的Double类型数组。为了存储,将数组的大小和数组每个元素初始值0.0。

你可以通过传入合适的row和column的数量来构造一个新的Matrix实例。

收起阅读 »

CSS 奇技淫巧 | 巧妙实现文字二次加粗再加边框

需求背景 - 文字的二次加粗 今天遇到这样一个有意思的问题: 在文字展示的时候,利用了 font-weight: bold 给文字进行加粗,但是觉得还是不够粗,有什么办法能够让文字更粗一点呢? emm,不考虑兼容性的话,答案是可以利用文字的 -webkit...
继续阅读 »

需求背景 - 文字的二次加粗


今天遇到这样一个有意思的问题:



  1. 在文字展示的时候,利用了 font-weight: bold 给文字进行加粗,但是觉得还是不够粗,有什么办法能够让文字更粗一点呢?


emm,不考虑兼容性的话,答案是可以利用文字的 -webkit-text-stroke 属性,给文字二次加粗。


MDN - webkit-text-stroke: 该属性为文本字符添加了一个边框(笔锋),指定了边框的颜色, 它是 -webkit-text-stroke-width-webkit-text-stroke-color 属性的缩写。


看下面的 DEMO,我们可以利用 -webkit-text-stroke,给文字二次加粗:


<p>文字加粗CSS</p>
<p>文字加粗CSS</p>
<p>文字加粗CSS</p>
<p>文字加粗CSS</p>

p {
font-size: 48px;
letter-spacing: 6px;
}
p:nth-child(2) {
font-weight: bold;
}
p:nth-child(3) {
-webkit-text-stroke: 3px red;
}
p:nth-child(4) {
-webkit-text-stroke: 3px #000;
}

对比一下下面 4 种文字,最后一种利用了 font-weight: bold-webkit-text-stroke,让文字变得更为



CodePen Demo -- font-weight: bold 和 -webkit-text-stroke 二次加粗文字


如何给二次加粗的文字再添加边框?


OK,完成了上述第一步,事情还没完,更可怕的问题来了。


现在文字要在二次加粗的情况下,再添加一个不同颜色的边框。


我们把原本可能可以给文字添加边框的 -webkit-text-stroke 属性用掉了,这下事情变得有点棘手了。这个问题也可以转变为,如何给文字添加 2 层不同颜色的边框?


当然,这也难不倒强大的 CSS(SVG),让我们来尝试下。


尝试方法一:使用文字的伪元素放大文字


第一种尝试方法,有点麻烦。我们可以对每一个文字进行精细化处理,利用文字的伪元素稍微放大一点文字,将原文字和访达后的文字贴合在一起。



  1. 将文字拆分成一个一个独立元素处理

  2. 利用伪元素的 attr() 特性,利用元素的伪元素实现同样的字

  3. 放大伪元素的字

  4. 叠加在原文字之下


上代码:


<ul>
<li data-text="文">文</li>
<li data-text="字">字</li>
<li data-text="加">加</li>
<li data-text="粗">粗</li>
<li data-text="C">C</li>
<li data-text="S">S</li>
<li data-text="S">S</li>
</ul>

ul {
display: flex;
flex-wrap: nowrap;
}

li {
position: relative;
font-size: 64px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 3px #000;

&::before {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
color: red;
-webkit-text-stroke: 3px #f00;
z-index: -1;
transform: scale(1.15);
}
}

可以简单给上述效果加个动画,一看就懂:



CodePen Demo -- 利用伪元素给加粗文字添加边框


看着不错,但是实际上仔细观察,边框效果很粗糙,文字每一处并非规则的被覆盖,效果不太能接受:



尝试方法二:利用 text-shadow 模拟边框


第一种方法宣告失败,我们继续尝试第二种方式,利用 text-shadow 模拟边框。


我们可以给二次加粗的文字添加一个文字阴影:


<p>文字加粗CSS</p>

p {
font-size: 48px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 1px #000;
text-shadow: 0 0 2px red;
}

看看效果:


image


好吧,这和边框差的也太远了,它就是阴影。


不过别着急,text-shadow 是支持多重阴影的,我们把上述的 text-shadow 多叠加几次:


p {
font-size: 48px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 1px #000;
- text-shadow: 0 0 2px red;
+ text-shadow: 0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red,0 0 2px red;
}


Wow,不仔细看的话,利用这种叠加多层 text-shadow 的方式,还真的非常像边框!


当然,如果我们放大来看,瑕疵就比较明显了,还是能看出是阴影:



CodePen Demo -- 利用 text-shadow 给文字添加边框


尝试方法四:利用多重 drop-shadow()


在尝试了 text-shadow 之后,自然而然的就会想到多重 filter: drop-shadow(),主观上认为会和多重 text-shadow 的效果应该是一致的。


不过,实践出真知。


在实际测试中,发现利用 filter: drop-shadow() 的效果比多重 text-shadow 要好,模糊感会弱一些:


p {
font-weight: bold;
-webkit-text-stroke: 1px #000;
filter:
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red)
drop-shadow(0 0 0.25px red);
}

效果如下:


image


我们甚至可以利用它制作文字二次加粗后的多重边框:


p {
font-weight: bold;
-webkit-text-stroke: 1px #000;
filter:
drop-shadow(0 0 0.2px red)
// 重复 N 次
drop-shadow(0 0 0.2px red)
drop-shadow(0 0 0.25px blue)
// 重复 N 次
drop-shadow(0 0 0.25px blue);
}

效果如下:



然而,在不同屏幕下(高清屏和普通屏),drop-shadow() 的表现效果差别非常之大,实则也难堪重用。


我们没有办法了吗?不,还有终极杀手锏 SVG。


尝试方法四:利用 SVG feMorphology 滤镜给文字添加边框


其实利用 SVG 的 feMorphology 滤镜,可以非常完美的实现这个需求。


这个技巧,我在 有意思!不规则边框的生成方案 这篇文章中也有提及。


借用 feMorphology 的扩张能力给不规则图形添加边框


直接上代码:


<p>文字加粗CSS</p>

<svg width="0" height="0">
<filter id="dilate">
<feMorphology in="SourceAlpha" result="DILATED" operator="dilate" radius="2"></feMorphology>
<feFlood flood-color="red" flood-opacity="1" result="flood"></feFlood>
<feComposite in="flood" in2="DILATED" operator="in" result="OUTLINE"></feComposite>

<feMerge>
<feMergeNode in="OUTLINE" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</svg>

p {
font-size: 64px;
letter-spacing: 6px;
font-weight: bold;
-webkit-text-stroke: 2px #000;
filter: url(#dilate);
}

效果如下:



我们可以通过 SVG feMorphology 滤镜中的 radius 控制边框大小,feFlood 滤镜中的 flood-color 控制边框颜色。并且,这里的 SVG 代码可以任意放置,只需要在 CSS 中利用 filter 引入即可。


本文不对 SVG 滤镜做过多的讲解,对 SVG 滤镜原理感兴趣的,可以翻看我上述提到的文章。


至此,我们就完美的实现了在已经利用 font-weight: bold-webkit-text-stroke 的基础上,再给文字添加不一样颜色的边框的需求。


放大了看,这种方式生成的边框,是真边框,不带任何的模糊:



CodePen Demo -- 利用 SVG feMorphology 滤镜给文字添加边框


最后


OK,本文到此结束,介绍了一些 CSS 中的奇技淫巧去实现文字二次加粗后加边框的需求,实际需求中,如果不是要求任意字都要有这个效果,其实我更推荐切图大法,高保真,不丢失细节。


当然,可能还有更便捷更有意思的解法,欢迎在评论区不吝赐教。


希望本文对你有所帮助 :)


想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄


更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。


如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。


作者:chokcoco
链接:https://juejin.cn/post/7023940690476269605

收起阅读 »

大道至简,繁在人心:在浏览器控制台安装npm包是什么操作?

  我们都知道,npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。虽然作为命令行工具的 npm 近年来逐渐式微,但是作为广泛使用的存储库的 npm,却依然...
继续阅读 »

  我们都知道,npm 是 JavaScript 世界的包管理工具,并且是 Node.js 平台的默认包管理工具。通过 npm 可以安装、共享、分发代码,管理项目依赖关系。虽然作为命令行工具的 npm 近年来逐渐式微,但是作为广泛使用的存储库的 npm,却依然如日中天,还是世界上最大的软件注册表


  通常,我们通过npm install xxx在 React、Vue、Angular 等现代前端项目中安装依赖,但是前端项目在本质上还是运行在浏览器端的 HTML、JavaScript 和 CSS,那么,我们有办法在浏览器控制台直接安装 npm 包并使用吗?


  如果你对这个问题感兴趣,不妨跟着我通过本文一探究竟,也许最终你会发现:越是“复杂”的东西,其原理越趋向“简单”


通过 <script /> 引入 cdn 资源


  在浏览器控制台安装 npm 包,看起来是个天马行空的想法,让人觉得不太切实际。如果我换一个方式进行提问:如何在浏览器/HTML 中引入 JavaScript 呢?也许你马上就有了答案:<script />标签。没错,我们的第一步就是通过 <script />标签在 HTML 页面上引入 cdn 资源。


  那么,又该如果在控制台在页面上插入<script />标签来引入 CDN 资源呢?这个问题可难不倒你


// 在页面中插入<script />标签
const injectScript = (url) => {
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
};

  我们还得在资源引入后以及出现错误时,给用户一些提示:


script.onload = () => {
console.log(pkg_name_origin, ' 安装成功。');
};
script.onerror = () => {
console.log(pkg_name_origin, ' 安装失败。');
};

  这么以来,我们就可以直接在控制台引入 cdn 资源了,你可以再额外补充一些善后工作的处理逻辑,比如把<script />标签移除。当然,你也完全可以通过创建<link />标签来引入css样式库,这里不过多赘述。


根据包名安装 npm 包


  上面实现了通过<script /> 引入 cdn 资源,但是我们安装 npm 包一般都是通过npm install后面直接跟包名来完成的,显然单靠<script />的方式难以达到我们的饿预期,那么,有没有一种方式,可以将我们的包名直接转换成 cdn 资源地址呢?


  答案当然是:有。否则我写个屁啊 🤔,cdnjs就提供了这样的能力。


  cdnjs 提供了一个简单的 API,允许任何人快速查询 CDN 上的资源。具体使用读者可参考官方链接,这里给出一个根据包名查询 CDN 资源链接的示例,可以直接在浏览器地址栏打开这个链接查看:https://api.cdnjs.com/libraries?search=jquery,这是一个 get 请求,你将看到类似下面的页面,数组的第一项为名称/功能最相近的资源的最新 CDN 资源地址


jquery


  是以,根据包名搜索 cdn 资源 URL 便有如下的实现:


const cdnjs = async (name) => {
const searchPromise = await fetch(
`https://api.cdnjs.com/libraries?search=${name}`,
// 不显示referrer的任何信息在请求头中
{ referrerPolicy: 'no-referrer' }
);
const { results, total } = await searchPromise.json();
if (total === 0) {
console.error('Sorry, ', name, ' not found, please try another keyword.');
return;
}

// 取结果中最相关的一条
const { name: exactName, latest: url } = results[0];
if (name !== exactName) {
// 如果名称和你传进来的不一样
console.log(name, ' not found, import ', exactName, ' instead.');
}
// 通过<script />标签插入
injectScript(url);
};

安装特定版本的 npm 包


  我们在 npm 中还可以通过类似npm install jquery@3.5.1的语法安装特定版本的 npm 包,而 cdnjs 只能返回特定版本的详细信息(不含 cdn 资源链接)。


  UNPKG在此时可以帮我们一个大忙。unpkg 是一个快速的全球内容分发网络,适用于 npm 上的所有内容。使用它可以使用以下 URL 快速轻松地从任何包加载任何文件unpkg.com/:package@:version/:file


  例如,访问https://unpkg.com/jquery@3.5.1会自动重定向到https://unpkg.com/jquery@3.5.1/dist/jquery.js,并返回v3.5.1版本的jQuery文件内容(如果不带版本号,会返回最新的资源):


jquery_unpkg


  也就是说,我们可以将https://unpkg.com/包名直接丢给<script />标签来加载资源:


const unpkg = (name) => {
injectScript(`https://unpkg.com/${name}`);
};

完整代码


  将上面的代码简单整理,并通过一个统一的入口方法npmInstall进行调用:


// 存储原始传入的名称
let pkg_name_origin = null;
const npmInstall = (originName) => {
// Trim string
const name = originName.trim();
pkg_name_origin = name;
// 三种引入方式
// 如果是一个有效的URL,直接通过<script />标签插入
if (/^https?:\/\//.test(name)) return injectScript(name);
// 如果指定了版本,尝试使用unpkg加载
if (name.indexOf('@') !== -1) return unpkg(name);
// 否则,尝试使用cdnjs搜索
return cdnjs(name);
};

// 在页面中插入<script />标签
const injectScript = (url) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => {
console.log(pkg_name_origin, ' 安装成功。');
};
script.onerror = () => {
console.log(pkg_name_origin, ' 安装失败。');
};
document.body.appendChild(script);
// document.body.removeChild(script);
};

const unpkg = (name) => {
injectScript(`https://unpkg.com/${name}`);
};

const cdnjs = async (name) => {
const searchPromise = await fetch(
`https://api.cdnjs.com/libraries?search=${name}`,
// 不显示referrer的任何信息在请求头中
{ referrerPolicy: 'no-referrer' }
);
const { results, total } = await searchPromise.json();
if (total === 0) {
console.error('Sorry, ', name, ' not found, please try another keyword.');
return;
}

// 取结果中最新的一条
const { name: exactName, latest: url } = results[0];
if (name !== exactName) {
console.log(name, ' not found, import ', exactName, ' instead.');
}
// 通过<script />标签插入
injectScript(url);
};

  我们可以使用类似npmInstall('moment')的方式在控制台进行调用:


console


  下面这些调用方式自然也是支持的:


npmInstall('jquery'); // 直接引入
npmInstall('jquery@2'); // 指定版本
npmInstall('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'); // cdn地址

不每次都写这些函数行不行


  看了上面的操作,确实很简单,但是也许你会说:每次要使用时,我都得在控制台定义和调用函数,有些麻烦,不每次都写这些函数行不行?那自然是行的啦,你完全可以自己写一个浏览器插件,将这些JS代码注入页面,详情可参考7分钟学会写一个浏览器插件——突破某SDN未登录禁止复制的限制


  如果你实在不想写,其实有人已经为你写好了,那便是Console Importer,它可以让你的浏览器控制台成为更强大的实验场



  • 使用示例:


import



  • 效果图:


Console Importer



链接:Console Importer | Chrome 插件地址



可以干什么


  那么,本文介绍的方法和工具到底有什么用呢?


  平时开发中,我们经常会想要在项目里尝试一些操作或者验证一些库的方法、打印结果,通过本文的学习,以后你完全可以直接在控制台引入loadsh、moment、jQuery、React 等来进行使用和验证,减少在项目中进行console.log验证后再删除的频次。



  • 你可以通过引入jQuery方便的进行一些项目、页面中的DOM操作;

  • 你可以通过引入axios进行一些简单的接口请求;

  • 你可以通过引入moment.js来验证一些时间格式化方法的使用;

  • 你可以通过引入loadsh并调用它的方法完成一些便捷的计算;

  • ...


可以学到什么


unpkg


  unpkg 是一个内容源自 npm 的前端常用全球快速 CDN,它能以快速、简洁、优雅的方式提供任意包、任意文件的访问,在流行的类库、框架文档中常常能看到它的身影。使用方式一般是unpkg.com/:package@:version/:file。或者更简洁一点:https://unpkg.com/包名,包名包含版本号时,你将获得对应版本的js文件,不包含版本号时,你将获得这个库的最新版js文件。


cdnjs


  cdnjs是一种免费的开源 CDN 服务,受到超过 12.5% 的网站的信任,每月处理超过 2000 亿次请求,由 Cloudflare 提供支持。它类似 Google CDN 和微软CDN服务,但是速度比这二者更加快。CDNJS 上提供了众多 JavaScript 库,你可以直接在网页上引用这些 JS 文件,实现用户浏览网站的最佳速度体验。


  你还可以通过它的查询APIhttps://api.cdnjs.com/libraries?search=xxx进行特定库的cdn地址的查找,这个API还会给你返回一些你所查询的库的替代品


大道至简,繁在人心


  越是“复杂”的东西,其原理也许越是趋向“简单”,大道至简,繁在人心,祝每一个努力攀登者,终能豁然开朗,释然于心。


作者:獨釣寒江雪
链接:https://juejin.cn/post/7023916328637431816

收起阅读 »

微信小程序统一分享,全局接管页面分享消息的一些技巧

分享功能非常的重要,当某一个功能或文章打动用户的时候,能把这个小程序分享出去,就能带来裂变传播的效果。 全局接管分享事件 而随着功能越来越多,页面越来越多,每一个页面都需要添加分享的回调方法吗? onShareAppMessage: function () {...
继续阅读 »

分享功能非常的重要,当某一个功能或文章打动用户的时候,能把这个小程序分享出去,就能带来裂变传播的效果。


全局接管分享事件


而随着功能越来越多,页面越来越多,每一个页面都需要添加分享的回调方法吗?


onShareAppMessage: function () {
return {
title: '分享的标题',
path: '分享的页面路径'
}
},

有没有办法能全局统一接管分享呢?写一次,所有页面就都可以分享了。


能!


由于onShareAppMessage是一个函数,在用户点击右上角...时触发,或者<button open-type='share'>时触发。所以我们只要在这之前替换掉这个函数就可以了。


通过wx.onAppRoute(cb)这个方法,我们可以监听到微信小程序页面栈的变化。


//在小程序启动时添加全局路由变化的监听
onLaunch(){
wx.onAppRoute(()=>{
console.log('page stack changed');
console.log(getCurrentPages());
});
}

onAppRoute会在页面栈改变后被触发,这个时候通过getCurrentPages()方法,我们可以拿到小程序中全部的页面栈。


数组最后一个就是当前页面


image.png


现在直接给当前页面这个对象赋值onShareAppMessage即可


var pages = getCurrentPages();
var curPage = pages[pages.length-1];

curPage.onShareAppMessage=()=>{
return {
title:"被接管了"
}
}

再分享时我们就会发现被接管了


image.png


获取当前页面的地址


page参数不传的话,默认转发出去就是当前页面的地址。当然通过curPage.route也可以获取该页面地址。


var pages = getCurrentPages();
var curPage = pages[pages.length-1];

curPage.onShareAppMessage=()=>{
return {
title:"被接管了",
page:curPage.route
}
}

小技巧


如果就这样分享出去,用户打开的时候,就会直接展示这个分享的页面。直接返回,或者左滑屏幕,都会直接退出到聊天界面。用户主动分享一次产生的裂变不容易,我希望这个分享带来的价值最大化,让接到分享的微信用户看到更多页面的话怎么办呢?


永远先进首页,首页检查启动参数后再跳转相关页面


curPage.onShareAppMessage=()=>{
return {
title:"被接管了",
page:"/pages/home/home?url="+curPage.route
}
}


作者:大帅老猿
链接:https://juejin.cn/post/7024046727820738573

收起阅读 »

我阅读源码的五步速读法

阅读代码是程序员最重要的技能之一,我们每天都在读同事的代码或者第三方库的代码,那怎么高效的阅读代码呢?分享下我的源码阅读方法。 我的阅读源码的方法分为五步: 第一步,通过文档和测试用例了解代码的功能 阅读源码前要先了解代码的功能,可以通过文档或者测试用例,了解...
继续阅读 »

阅读代码是程序员最重要的技能之一,我们每天都在读同事的代码或者第三方库的代码,那怎么高效的阅读代码呢?分享下我的源码阅读方法。


我的阅读源码的方法分为五步:


第一步,通过文档和测试用例了解代码的功能


阅读源码前要先了解代码的功能,可以通过文档或者测试用例,了解代码做了什么,输入和输出是什么。


了解功能是阅读源码的基础,后面才会有方向感。


第二步,自己思考功能的实现方式


知道了源码有啥功能之后,要先思考下如果自己实现会怎么做。有个大概的思路就行。


如果想不通可以看下源码用到了哪些依赖库,这些依赖库都有啥功能,再想下应该怎么实现。


如果还想不通也没关系,重要的是要先自己思考下实现方式。


第三步,粗读源码理清实现思路


你已经有了一个大概的实现思路,然后再去读源码,看下它是怎么实现的。和你思路类似的地方很快就可以掠过去,而且印象也很深,和你思路不一样的地方,通过读代码搞清楚它的实现思路。


这一步不用关心细节,知道某段代码是干啥的就行,关键是和自己的思路做 diff,理清它的整体实现思路。


第四步,通过 debugger 理清实现细节


粗读源码理清了实现思路之后,对于一些部分的具体实现可能还不是很清楚,这时候就可以通过 debugger 来断点调试了。


构造一个能触发该功能的测试用例,在关心的代码处打一个断点,通过 debugger 运行代码。


这时候你已经知道这部分代码是干啥的了,单步调试也很容易理清每一条语句的功能,这样一条语句一条语句的搞懂之后,你就很容易能把这部分代码的实现细节理清楚。


这样一部分一部分的通过 debugger 理清细节实现之后,你就对整体代码的思路和细节的实现都有了比较好的掌握。


第五步,输出文章来讲述源码实现思路


当你觉得对源码的实现有了比较好的掌握的时候,可以输出一篇文章的方式来讲述源码的整体思路。


因为可能会有一些部分是你没注意到的,而在输出的过程中,会进行更全面的思考,这时候如果发现了一些没有读到的点,可以再通过前面几步去阅读源码,直到能清晰易懂的把源码的实现讲清楚。这样才算真正的把代码读懂了。


这就是我觉得比较高效的阅读源码的方法。


总结


我阅读源码的方法分为五步:



  1. 通过文档和测试用例了解代码的功能

  2. 自己思考功能的实现方式

  3. 粗读源码理清实现思路

  4. 通过 debugger 理清实现细节

  5. 输出文章来讲述源码实现思路


这五步缺一不可:



  • 缺了第一步,不了解功能就开始读源码,那读代码会没有方向感

  • 缺了第二步,不经过思考直接读源码,理解代码实现思路的效率会降低

  • 缺了第三步,不理清整体思路就开始 debugger,会容易陷入细节,理不清整体的思路

  • 缺了第四步,不 debugger 只大概理下整体思路,这样不能从细节上真正理清楚

  • 缺了第五步,不通过输出文章来检验,那是否真的理清了整体思路和实现细节是没底的


当然,这是我个人的阅读源码的方法,仅供参考。


作者:zxg_神说要有光
链接:https://juejin.cn/post/7024084789929967646

收起阅读 »

复杂web动画,不慌,选择 web Animations API

说动前端动画,我们熟知的有两种 CSS 动画 (requestAnimation/setTimeout/setInterval + 属性改变) 动画 当然有人可能会说canvas动画,从运动本质了还是第二种。 今天说的是第三种 Web Animations...
继续阅读 »

说动前端动画,我们熟知的有两种



  1. CSS 动画

  2. (requestAnimation/setTimeout/setInterval + 属性改变) 动画


当然有人可能会说canvas动画,从运动本质了还是第二种。


今天说的是第三种 Web Animations API, 也有简称为 WAAPI 的。


与纯粹的声明式CSS不同,JavaScript还允许我们动态地将属性值设置为持续时间。 对于构建自定义动画库和创建交互式动画,Web动画API可能是完成工作的完美工具。


举两个栗子


落球


点击之后,球体下落


ballFall2.gif


const ballEl = document.querySelector(".ball");
ballEl.addEventListener("click", function () {
let fallAni = ballEl.animate({
transform: ['translate(0, 0)', 'translate(20px, 8px)', 'translate(50px, 200px)']
}, {
easing: "cubic-bezier(.68,.08,.89,-0.05)",
duration: 2000,
fill: "forwards"
})
});

直播的世界消息或者弹幕


这是一个我们项目中一个实际的例子, 直播的弹幕。

我们需要消息先运动到屏幕中间,消息最少需要在停留2秒,如果消息过长,消息还需要 匀速滚动 ,之后再滑出屏幕。



  1. 滑入

  2. 暂停,如果消息过长,消息还需要匀速滚动

  3. 滑出


难点就在于,暂停阶段,消息滚动的时间并不是确定的,需要计算。 这个时候,纯CSS3的动画,难度就有些高了,采用 Web Animations API,天然的和JS亲和,那就简单多了。


先看看效果
longDan2.gif


shortDan.gif


代码也就简单的分为三段:滑入,暂停,滑出。

因为其天然支持Promise, 代码很简洁,逻辑也很清晰。


async function startAnimate() {
// 滑入
const totalWidth = stageWidth + DANMU_WITH;
const centerX = stageWidth * 0.5 - DANMU_WITH * 0.5;
const kfsIn = {
transform: [`translateX(${totalWidth}px)`, `translateX(${centerX}px)`]
}
await danmuEl.animate(kfsIn, {
duration: 2000,
fill: 'forwards',
easing: 'ease-out'
}).finished;

// 暂停部分
const contentEl = danmuEl.querySelector(".danmu-content");
const itemWidth = contentEl.getBoundingClientRect().width;
const gapWidth = Math.max(0, itemWidth - DANMU_WITH);
const duration = Math.max(0, Math.floor(gapWidth / 200) * 1000);

const translateX = duration > 0 ? gapWidth : 0;
const kfsTxt = {
transform: [`translateX(0px)`, `translateX(-${gapWidth}px)`]
};
await contentEl.animate(kfsTxt, {
duration,
delay: 2000,
fill: 'forwards',
easing: 'linear',
}).finished;

// 滑出
const kfsOut = {
transform: [`translateX(${centerX}px)`, `translateX(-${DANMU_WITH}px)`]
};
await danmuEl.animate(kfsOut, {
duration: 2000,
fill: "forwards",
easing: 'ease-in'
}).finished;

if (danmuEl) {
stageEl.removeChild(danmuEl);
}
isAnimating = false
}

web Animations API 两个核心的对象



  1. KeyframeEffect 描述动画属性

  2. Animation 控制播放


KeyframeEffect


描述动画属性的集合,调用keyframesAnimation Effect Timing Properties。 然后可以使用 Animation 构造函数进行播放。


其有三种构建方式,着重看第二种,参数后面说。



new KeyframeEffect(target, keyframes);

new KeyframeEffect(target, keyframes, options)

new KeyframeEffect(source)



当然我们可以显示的去创建 KeyframeEffect, 然后交付给Animation去播放。 但是我们通常不需要这么做, 有更加简单的API, 这就是接后面要说的 Element.animate


看一个KeyframeEffect复用的例子,new KeyframeEffect(kyEffect)基于当前复制,然后多处使用。


const box1ItemEl = document.querySelector(".box1");
const box2ItemEl = document.querySelector(".box2");

const kyEffect = new KeyframeEffect(null, {
transform: ['translateX(0)', 'translateX(200px)']
},
{ duration: 3000, fill: 'forwards' })

const ky1 = new KeyframeEffect(kyEffect);
ky1.target = box1ItemEl;

const ky2 = new KeyframeEffect(kyEffect);
ky2.target = box2ItemEl;

new Animation(ky1).play();
new Animation(ky2).play();


kf2.gif


Animation


提供播放控制、动画节点或源的时间轴。 可以接受使用 KeyframeEffect 构造函数创建的对象作为参数。


const box1ItemEl = document.querySelector(".box1");

const kyEffect = new KeyframeEffect(box1ItemEl, {
transform: ['translateX(0)', 'translateX(200px)']
},
{ duration: 3000, fill: 'forwards' })

const ani1 = new Animation(kyEffect);
ani1.play();

ani1.gif


常用的方法



Animation 事件监听


监听有两种形式:



  1. event 方式


因其继承于EventTarget,所有依旧有两种形式


animation.onfinish = function() {
element.remove();
}

animation.addEventListener("finish", function() {
element.remove();
}


  1. Promise形式


animation.finished.then(() =>
element.remove()
)

比如一个很有用的场景,所有动画完成后:


Promise.all( element.getAnimations().map(ani => ani.finished)
).then(function() {
// do something cool
})

常用事件回调




便捷的 Element.animate


任何 Element都具备该方法, 其语法:



animate(keyframes, options)



其参数和 new KeyframeEffect(target, keyframes, options)的后两个参数基本一样, 返回的是一个Animation对象。


第一个参数 keyframes


keyframes有两种形式,一种是数组形式,一种是对象形式。


数组形式


一组对象(关键帧) ,由要迭代的属性和值组成。

关键帧的偏移可以通过提供一个offset来指定 ,值必须是在 [0.0, 1.0] 这个区间内,且须升序排列。简单理解就是进度的百分比的小数值。


element.animate([ { opacity: 1 },
{ opacity: 0.1, offset: 0.7 },
{ opacity: 0 } ],
2000);

并非所有的关键帧都需要设置offset。 没有指定offset的关键帧将与相邻的关键帧均匀间隔。


对象形式


一个包含key-value键值的对象需要包含动画的属性和要循环变化的值数组


element.animate({
opacity: [ 0, 0.9, 1 ],
offset: [ 0, 0.8 ], // [ 0, 0.8, 1 ] 的简写
easing: [ 'ease-in', 'ease-out' ],
}, 2000);

第二个参数 options


new KeyframeEffect(target, keyframes, options)的第三个参数基本一致,但是多了一个可选属性,就是id,用来标记动画,也方便 在Element.getAnimations结果中精确的查找。





















































后续四个特性相对高级,掌握好了可以玩出花来,本章主要讲基本知识,后续会出高级版本。


更多细节可以参见 KeyframeEffect


Element.getAnimations


我们通过Element.animate或者创建Animation给Element添加很多动画,通过这个方法可以获得所有Animation的实例。


在需要批量修改参数,或者批量停止动画的时候,那可是大杀器。


比如批量暂停动画:


box1ItemEl.getAnimations()
.forEach(el=> el.pause()) // 暂停全部动画

优势



  1. 相对css动画更加灵活

  2. 相对requestAnimation/setTimeout/setInterval 动画,性能更好,代码更简洁

  3. 天然支持Promise,爽爽爽!!!


你有什么理由拒绝她呢?


对比 CSS Animation


动画参数属性键对照表


参数设置值上的区别



  1. duration 参数只支持毫秒

  2. 迭代次数无限使用的是 JS的Infinity,不是字符串 "infinite"

  3. 默认动画的贝塞尔是linear,而不是css的ease


兼容性


整体还不错,Safari偏差。

如果不行, 加个垫片 web-animations-js


我们在实际的桌面项目上已经使用,非常灵活, nice!
image.png


总结


web Animations API 和 css动画,不是谁替换谁。结合使用,效果更佳。


复杂的逻辑动画,因为web Animations API和JS天然的亲和力,是更优的选择。



收起阅读 »

List常用操作比for循环更优雅的写法

list常用的lamada表达式-单list操作 引言 使用JDK1.8之后,大部分list的操作都可以使用lamada表达式去写,可以让代码更简洁,开发更迅速。以下是我在工作中常用的lamada表达式对list的常用操作,喜欢建议收藏。 以用户表为例,用户实...
继续阅读 »

list常用的lamada表达式-单list操作


引言


使用JDK1.8之后,大部分list的操作都可以使用lamada表达式去写,可以让代码更简洁,开发更迅速。以下是我在工作中常用的lamada表达式对list的常用操作,喜欢建议收藏。

以用户表为例,用户实体代码如下:


public class User {
private Integer id; //id

private String name; //姓名

private Integer age; //年龄

private Integer departId; //所属部门id
}

List<User> list = new ArrayList<>();

简单遍历


使用lamada表达式之前,如果需要遍历list时,一般使用增强for循环,代码如下:


List<User> list = new ArrayList<>();
for (User u:list) {
System.out.println(u.toString());
}

使用lamada表达式之后,可以缩短为一行代码:


list.forEach(u-> System.out.println(u.toString()));

筛选符合某属性条件的List集合


以筛选年龄在15-17之间的用户为例,for循环写法为:


List<User> users = new ArrayList<>();
for (User u : list) {
if (u.getAge() >= 15 && u.getAge() <= 17) {
users.add(u);
}
}

使用lamada表达式写法为:


List<User> users = list.stream()
.filter(u -> u.getAge() >= 15 && u.getAge() <= 17)
.collect(Collectors.toList());

获取某属性返回新的List集合


以获取id为例,项目中有时候可能会需要根据用户id的List进行查询或者批量更新操作,这时候就需要用户id的List集合,for循环写法为:


List<Integer> ids = new ArrayList<>();
for (User u:list) {
ids.add(u.getId());
}

lamada表达式写法为:


List<Integer> ids = list.stream()
.map(User::getId).collect(Collectors.toList());

获取以某属性为key,其他属性或者对应对象为value的Map集合


以用户id为key(有时可能需要以用户编号为key),以id对应的user作为value构建Map集合,for循环写法为:


Map<Integer,User> userMap = new HashMap<>();
for (User u:list) {
if (!userMap.containsKey(u.getId())){
userMap.put(u.getId(),u);
}
}

lamada表达式写法为:


Map<Integer,User> map = list.stream()
.collect(Collectors.toMap(User::getId,
Function.identity(),
(m1,m2)->m1));


Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

(m1,m2)-> m1此处的意思是当转换map过程中如果list中有两个相同id的对象,则map中存放的是第一个对象,此处可以根据项目需要自己写。



以某个属性进行分组的Map集合


以部门id为例,有时需要根据部门分组,筛选出不同部门下的人员,如果使用for循环写法为:


Map<Integer,List<User>> departGroupMap = new HashMap<>();
for (User u:list) {
if (departGroupMap.containsKey(u.getDepartId())){
departGroupMap.get(u.getDepartId()).add(u);
}else {
List<User> users1 = new ArrayList<>();
users1.add(u);
departGroupMap.put(u.getDepartId(),users1);
}
}

使用lamada表达式写法为:


Map<Integer,List<User>> departGroupMap = list.stream()
.collect(Collectors
.groupingBy(User::getDepartId));

其他情况


可以根据需要结合stream()进行多个操作,比如筛选出年龄在15-17岁的用户,并且根据部门进行分组分组,如果使用for循环,代码如下:


Map<Integer,List<User>> departGroupMap = new HashMap<>();
for (User u:list) {
if (u.getAge() >= 15 && u.getAge() <= 17) {
if (departGroupMap.containsKey(u.getDepartId())){
departGroupMap.get(u.getDepartId()).add(u);
}else {
List<User> users1 = new ArrayList<>();
users1.add(u);
departGroupMap.put(u.getDepartId(),users1);
}
}
}

使用lamada表达式,代码如下:


Map<Integer,List<User>> departGroupMap = list.stream()
.filter(u->u.getAge() >= 15 && u.getAge() <= 17)
.collect(Collectors.groupingBy(User::getDepartId));

总结


上述部分是小编在工作中遇到的常用的单个List的操作,可能在项目中还会遇到更复杂的场景,可以根据需要进行多个方法的组合使用,我的感觉是使用lamada表达式代码更加简洁明了,当然各人有各人的编码习惯,不喜勿喷。


作者:Mr泰迪
链接:https://juejin.cn/post/7023760357407916063
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

还在傻乎乎得背MyISAM与InnoDB 的区别?一篇文章让你理解的明明白白

序言     相信不少的小伙伴在准备面试题的时候,必定会遇到这个面试题,MyISAM与InnoDB 的区别是什么?我们当时可谓是背一次忘一次,以至于很多的同学去找实习工作的时候,经常被这个问题卡脖子,那么今天我就系统的来...
继续阅读 »

序言


    相信不少的小伙伴在准备面试题的时候,必定会遇到这个面试题,MyISAM与InnoDB 的区别是什么?我们当时可谓是背一次忘一次,以至于很多的同学去找实习工作的时候,经常被这个问题卡脖子,那么今天我就系统的来说一说MyISAM与InnoDB 的区别,一问让你们彻底整明白!


🧡MySQL默认存储引擎的变迁


    熟悉MySQL的小伙伴们都知道,在MySQL 5.1之前的版本中,默认的搜索引擎是MyISAM,从MySQL 5.5之后的版本中,默认的搜索引擎变更为InnoDB。这也间接说明了,MySQL官方更推荐使用InnoDB。


💛MyISAM与InnoDB存储引擎的主要特点


💚MyISAM


    MyISAM存储引擎的特点是:表级锁、不支持事务和全文索引,适合一些CMS内容管理系统作为后台数据库使用,但是使用大并发、重负荷生产系统上,表锁结构的特性就显得力不从心。下图是MySQL 5.7 MyISAM存储引擎的版本特性。
image.png


💙InnoDB


    InnoDB存储引擎的特点是:行级锁、事务安全(ACID兼容)、支持外键、不支持FULLTEXT类型的索引(5.6.4以后版本开始支持FULLTEXT类型的索引)。InnoDB存储引擎提供了具有提交、回滚和崩溃恢复能力的事务安全存储引擎。InnoDB是为处理巨大量时拥有最大性能而设计的。它的CPU效率可能是任何其他基于磁盘的关系数据库引擎所不能匹敌的。以下是MySQL 5.7 InnoDB存储引擎的版本特性。


image.png


💜MyISAM与InnoDB性能测试


    MyISAM与InnoDB谁的性能更高,其实官方已经给了压测图
image.png


image.png


其实瞎眼可见的结果是:MyISAM被InnoDB直接按在地上摩擦!


🤎是否支持事务


    MyISAM是一种非事务性的引擎(不支持事务),使得MyISAM引擎的MySQL可以提供高速存储和检索,以及全文搜索能力,适合数据仓库等查询频繁的应用。


    InnoDB是事务安全的(支持事务),事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而MyISAM就不可以了。


🖤MyISAM与InnoDB表锁和行锁


    MySQL表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。什么意思呢,就是说对MyISAM表进行读操作时,它不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写操作;而对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作。


    InnoDB行锁是通过给索引项加锁来实现的,即只有通过索引条件检索数据,InnoDB才使用行级锁,否则将使用表锁!行级锁在每次获取锁和释放锁的操作需要消耗比表锁更多的资源。在InnoDB两个事务发生死锁的时候,会计算出每个事务影响的行数,然后回滚行数少的那个事务。当锁定的场景中不涉及Innodb的时候,InnoDB是检测不到的。只能依靠锁定超时来解决。


💔是否保存数据库表中表的具体行数


    InnoDB 中不保存表的具体行数,也就是说,执行select count(*) from table 时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。


💕如何选择


    虽然InnoDB很好,但是也不是无脑选,有些情况下MyISAM比InnoDB更好!


MyISAM适合:



  1. 做很多count 的计算;

  2. 插入不频繁,查询非常频繁,如果执行大量的SELECT,MyISAM是更好的选择;

  3. 没有事务。


InnoDB适合:



  1. 可靠性要求比较高,或者要求事务;

  2. 表更新和查询都相当的频繁,并且表锁定的机会比较大的情况指定数据引擎的创建;

  3. 如果你的数据执行大量的INSERT或UPDATE,出于性能方面的考虑,应该使用InnoDB表;

  4. DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的 删除;

  5. LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

作者:XiaoLin_Java
链接:https://juejin.cn/post/7023745049967984654
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

android常用命令介绍

adb命令 安装apk -f 表示强制安装 adb install -f apk 获取当前顶部activity名称方式 windows: adb shell dumpsys window | findstr mCurrentFocus mac: adb s...
继续阅读 »

adb命令


安装apk


-f 表示强制安装


adb install -f apk

获取当前顶部activity名称方式


windows:  adb shell dumpsys window | findstr mCurrentFocus
mac: adb shell dumpsys window | grep mCurrentFocus

apksigner命令


查看apk签名方式命令


apksigner verify -v apk

给apk进行签名



  1. 大于28的版本


apksigner sign --ks your_keystore_file --v1-signing-enabled 

true --v2-signing-enabled true --v3-signing-enabled false your_apk_file


  1. 小于28的版本


apksigner sign --ks your_keystore_file --v1-signing-enabled 

true --v2-signing-enabled true your_apk_file

apktool命令


反编译


apktool d apk

正编译


不指定输出的话,默认apk在目录的dist文件夹下


apktool b 反编译的目录名

keytool命令


查看证书信息


keytool -list -v -keystore your.keystore -storepass yourpassword

-storepass yourpassword不输入的话,执行命令的时候回提示输入密码


修改别名方法


keytool -changealias -keystore your.keystore -alias yourcurrentalias -destalias cert

其它


apk对齐命令


zipalign -v 4 源apk  目的apk

作者:安安安安卓
链接:https://juejin.cn/post/7023704345308823559
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Moshi踩坑之HashMap

Moshi 之HashMap就是这个错 moshi让你写自定义Adapter呢,报错摘要:No JsonAdapter for class java.util.HashMap, you should probably use Map ins...
继续阅读 »

Moshi 之HashMap

就是这个错 moshi让你写自定义Adapter呢,

报错摘要:

No JsonAdapter for class java.util.HashMap, you should probably use Map instead of HashMap (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.

解决方法

自定义 HashMap Adapter代码如下

class HashMapJsonAdapter<K, V>(moshi: Moshi, keyType: Type?, valueType: Type?) :
JsonAdapter<HashMap<K?, V?>?>() {
private val keyAdapter: JsonAdapter<K>
private val valueAdapter: JsonAdapter<V>

@Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: HashMap<K?, V?>?) {
writer.beginObject()
for (entry: Map.Entry<K?, V?> in value!!.entries) {
if (entry.key == null) {
throw JsonDataException("Map key is null at " + writer.path)
}
writer.promoteValueToName()
keyAdapter.toJson(writer, entry.key)
valueAdapter.toJson(writer, entry.value)
}
writer.endObject()
}

@Throws(IOException::class)
override fun fromJson(reader: JsonReader): HashMap<K?, V?> {
val result = HashMap<K?, V?>()
reader.beginObject()
while (reader.hasNext()) {
reader.promoteNameToValue()
val name = keyAdapter.fromJson(reader)
val value = valueAdapter.fromJson(reader)
val replaced = result.put(name, value)
if (replaced != null) {
throw JsonDataException(
"Map key '"
+ name
+ "' has multiple values at path "
+ reader.path
+ ": "
+ replaced
+ " and "
+ value
)
}
}
reader.endObject()
return result
}

override fun toString(): String {
return "JsonAdapter($keyAdapter=$valueAdapter)"
}

companion object {
val FACTORY: Factory =
Factory { type, annotations, moshi ->
val rawType = Types.getRawType(type)
if (annotations.isNotEmpty()) return@Factory null
if (rawType != java.util.HashMap::class.java) return@Factory null
val keyAndValue = if (type === java.util.Properties::class.java) arrayOf<Type>(
String::class.java,
String::class.java
) else {
arrayOf<Type>(Any::class.java, Any::class.java)
}
HashMapJsonAdapter<Any?, Any>(
moshi,
keyAndValue[0],
keyAndValue[1]
).nullSafe()
}
}

init {
keyAdapter = moshi.adapter(keyType)
valueAdapter = moshi.adapter(valueType)
}
}

其实完全就是在抄MoshiMapJsonAdapter 然后略微修改一下FACTORY

如何使用

Moshi.Builder()
.add(HashMapJsonAdapter.FACTORY)
.build()

完美解决

收起阅读 »

Adnroid 卡顿分析与布局优化

1 卡顿分析1 SystraceSystrace是Android平台提供的一款工具,用于记录短期内的设备活动,其中汇总了Android内核中的数据,例如CPU调度程序,磁盘活动和应用程序,Systrace主要用来分析绘制性能方面的问题,在发生卡顿时,通过这份报...
继续阅读 »

1 卡顿分析

1 Systrace

Systrace是Android平台提供的一款工具,用于记录短期内的设备活动,其中汇总了Android内核中的数据,例如CPU调度程序,磁盘活动和应用程序,Systrace主要用来分析绘制性能方面的问题,在发生卡顿时,通过这份报告,可以知道当前整个系统所处的状态,从而帮助开发者更直观的分析系统瓶颈,改进系统性能

2 android profile 中的cpu监测

App层面监测卡顿 1 利用UI线程的Looper打印日志匹配 

2 使用Choreographer.FrameCallback 

Looper日志监测卡顿** 

Android 主线程更新UI,如果界面1室内刷新少于60次,即FPS小于60,用户就会产生卡顿的感觉,简单来说Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断去除message,调用其他绑定的UI线程执行,如果在_handler的dispatchMessage_方法里面有耗时操作,就会发生卡顿.

只要监测_msg.target.dispatchmessage_的执行时间,就能检车就能检测到部分UI线程是否有耗时的操作。

注意到这行 执行代码的前后,有两个logging.println函数,如果设置了logging,会分别打印出>>>>> Dispatching to和 

<<<<< Finished to 这样的日志,

这样我们就可以通过两次log的时间差值,来计算dispatchMessage的执行时 间,从而设置阈值判断是否发生了卡顿。 

Looper 提供了 setMessageLogging(@Nullable Printer printer) 方法,所以我们可以自己实现一个Printer,在 通过setMessageLogging()方法传入即可

package com.dy.safetyinspectionforengineer.block;

import android.os.Looper;
public class BlockCanary {
public static void install() {
LogMonitor logMonitor = new LogMonitor();
Looper.getMainLooper().setMessageLogging(logMonitor);
}
}


package com.dy.safetyinspectionforengineer.block;

import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.util.Printer;
import java.util.List;

public class LogMonitor implements Printer {

private StackSampler mStackSampler;
private boolean mPrintingStarted = false;
private long mStartTimestamp;
// 卡顿阈值
private long mBlockThresholdMillis = 3000;
//采样频率
private long mSampleInterval = 1000;

private Handler mLogHandler;

public LogMonitor() {
mStackSampler = new StackSampler(mSampleInterval);
HandlerThread handlerThread = new HandlerThread("block-canary-io");
handlerThread.start();
mLogHandler = new Handler(handlerThread.getLooper());
}
@Override
public void println(String x) {
//从if到else会执行 dispatchMessage,如果执行耗时超过阈值,输出卡顿信息
if (!mPrintingStarted) {
//记录开始时间
mStartTimestamp = System.currentTimeMillis();
mPrintingStarted = true;
mStackSampler.startDump();
} else {
final long endTime = System.currentTimeMillis();
mPrintingStarted = false;
//出现卡顿
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
mStackSampler.stopDump();
}
}
private void notifyBlockEvent(final long endTime) {
mLogHandler.post(new Runnable() {
@Override
public void run() {
//获得卡顿时主线程堆栈
List<String> stacks = mStackSampler.getStacks(mStartTimestamp, endTime);
for (String stack : stacks) {
Log.e("block-canary", stack);
}
}
});
}
private boolean isBlock(long endTime) {
return endTime - mStartTimestamp > mBlockThresholdMillis;
}
}


package com.dy.safetyinspectionforengineer.block;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class StackSampler {
public static final String SEPARATOR = "\r\n";
public static final SimpleDateFormat TIME_FORMATTER =
new SimpleDateFormat("MM-dd HH:mm:ss.SSS");

private Handler mHandler;
private Map<Long, String> mStackMap = new LinkedHashMap<>();
private int mMaxCount = 100;
private long mSampleInterval;
//是否需要采样
protected AtomicBoolean mShouldSample = new AtomicBoolean(false);

public StackSampler(long sampleInterval) {
mSampleInterval = sampleInterval;
HandlerThread handlerThread = new HandlerThread("block-canary-sampler");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
}
/**
* 开始采样 执行堆栈
*/
public void startDump() {
//避免重复开始
if (mShouldSample.get()) {
return;
}
mShouldSample.set(true);
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, mSampleInterval);
}
public void stopDump() {
if (!mShouldSample.get()) {
return;
}
mShouldSample.set(false);
mHandler.removeCallbacks(mRunnable);
}
public List<String> getStacks(long startTime, long endTime) {
ArrayList<String> result = new ArrayList<>();
synchronized (mStackMap) {
for (Long entryTime : mStackMap.keySet()) {
if (startTime < entryTime && entryTime < endTime) {
result.add(TIME_FORMATTER.format(entryTime)
+ SEPARATOR
+ SEPARATOR
+ mStackMap.get(entryTime));
}
}
}
return result;
}
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement s : stackTrace) {
sb.append(s.toString()).append("\n");
}
synchronized (mStackMap) {
//最多保存100条堆栈信息
if (mStackMap.size() == mMaxCount) {
mStackMap.remove(mStackMap.keySet().iterator().next());
}
mStackMap.put(System.currentTimeMillis(), sb.toString());
}
if (mShouldSample.get()) {
mHandler.postDelayed(mRunnable, mSampleInterval);
}
}
};
}


public class MyApplication extends Application{
@Override
public void onCreate() {
super.onCreate();
BlockCanary.install();
}
}

Choreographer.FrameCallback

Android系统每隔16ms发出VSYNC信号,来通知界面进行重绘、渲染,每一次同步的周期约为16.6ms,代表一帧 的刷新频率。通过Choreographer类设置它的FrameCallback函数,当每一帧被渲染时会触发回调 FrameCallback.doFrame (long frameTimeNanos) 函数。frameTimeNanos是底层VSYNC信号到达的时间戳 。

import android.os.Build;
import android.view.Choreographer;

import java.util.concurrent.TimeUnit;

public class ChoreographerHelper {

static long lastFrameTimeNanos = 0;

public static void start() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {

@Override
public void doFrame(long frameTimeNanos) {
//上次回调时间
if (lastFrameTimeNanos == 0) {
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
return;
}
long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
if (diff > 16.6f) {
//掉帧数
int droppedCount = (int) (diff / 16.6);
}
lastFrameTimeNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
}
}
}

通过 ChoreographerHelper 可以实时计算帧率和掉帧数,实时监测App页面的帧率数据,发现帧率过低,还可以自 动保存现场堆栈信息。 Looper比较适合在发布前进行测试或者小范围灰度测试然后定位问题,ChoreographerHelper适合监控线上环境 的 app 的掉帧情况来计算 app 在某些场景的流畅度然后有针对性的做性能优化。

布局优化
1 层级优化

可以使用工具layoutinspector 查看层级,或这看源码查看层级
Tools - layoutINspector

2 使用merge标签

当我们有一些布局元素需要被多处使用时,我们可以将其抽取成一个单独的布局文件,在需要的地方include加载,这是就可以使用merge标签,吧这些抽离的标签进行包裹

3 使用viewstub标签

在不显示及不可见的情况下 用viewstub来包裹,被包裹后,如果visible=gone 则该view不会立即加载,等到需要显示的时候,设置viewstub为visible 或调用其inflater()方法,该view才会初始化

过度渲染
1进入开发则选项
2调用调试GPU过度绘制
3 选择显示过度绘制区域
3.1 蓝色 为一次绘制 绿色为两次绘制 粉色为3次绘制,红色为4次或更多次绘制

解决过度绘制问题
1 移除不需要的背景
2 使视图层次结构扁平化
3 降低透明度

布局加载优化
1 异步加载
setContentView 时 可以异步加载

implementation "androidx.asynclayoutinflater:asynclayoutinflater:1.0.0" 

new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null,
new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid,
@Nullable ViewGroup parent) {
setContentView(view);
}
});

收起阅读 »

Android - Binder通信架构

Java应用层:  对于上层应用通过调用AMP.startService, 完全可以不用关心底层,经过层层调用,最终必然会调用到AMS.startService.Java IPC层:  Binder通信是采用C/S架构,...
继续阅读 »


image.png

  • Java应用层:  对于上层应用通过调用AMP.startService, 完全可以不用关心底层,经过层层调用,最终必然会调用到AMS.startService.
  • Java IPC层:  Binder通信是采用C/S架构, Android系统的基础架构便已设计好,Binder在Java framework层的Binder客户类,BinderProxy和服务类Binder;
  • Native IPC层:  对于Native层,如果需要直接使用Binder(比如media相关), 则可以直接使用BpBinder和BBinder(当然这里还有JavaBBinder)即可, 对于上一层Java IPC的通信也是基于这个层面.
  • Kernel物理层:  这里是Binder Driver, 前面3层都跑在用户空间,对于用户空间的内存资源是不共享的,每个Android的进程只能运行在自己进程所拥有的虚拟地址空间, 而内核空间却是可共享的. 真正通信的核心环节还是在Binder Driver.

通过startService的流程分析如图

image.png

AMP和AMN都是实现了IActivityManager接口,AMS继承于AMN. 其中AMP作为Binder的客户端,运行在各个app所在进程, AMN(或AMS)运行在系统进程system_server.

Binder IPC原理

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务.

image.png

可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程,Client端和Server端通信时都需要先获取Service Manager接口,才能开始通信服务, 当然查找到目标信息可以缓存起来则不需要每次都向ServiceManager请求。

  1. 注册服务:首先AMS注册到ServiceManager。该过程:AMS所在进程(system_server)是客户端,ServiceManager是服务端。
  2. 获取服务:Client进程使用AMS前,须先向ServiceManager中获取AMS的代理类AMP。该过程:AMP所在进程(app process)是客户端,ServiceManager是服务端。
  3. 使用服务: app进程根据得到的代理类AMP,便可以直接与AMS所在进程交互。该过程:AMP所在进程(app process)是客户端,AMS所在进程(system_server)是服务端。

Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder Driver进行交互的,从而实现IPC通信方式.

Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层.

通信原理

image.png

  1. 发起端线程向Binder Driver发起binder ioctl请求后, 便采用环不断talkWithDriver,此时该线程处于阻塞状态, 直到收到如下BR_XXX命令才会结束该过程.

    • BR_TRANSACTION_COMPLETE: oneway模式下,收到该命令则退出
    • BR_REPLY: 非oneway模式下,收到该命令才退出;
    • BR_DEAD_REPLY: 目标进程/线程/binder实体为空, 以及释放正在等待reply的binder thread或者binder buffer;
    • BR_FAILED_REPLY: 情况较多,比如非法handle, 错误事务栈, security, 内存不足, buffer不足, 数据拷贝失败, 节点创建失败, 各种不匹配等问题
    • BR_ACQUIRE_RESULT: 目前未使用的协议;
  2. 左图中waitForResponse收到BR_TRANSACTION_COMPLETE,则直接退出循环, 则没有机会执行executeCommand()方法, 故将其颜色画为灰色. 除以上5种BR_XXX命令, 当收到其他BR命令,则都会执行executeCommand过程.

  3. 目标Binder线程创建后, 便进入joinThreadPool()方法, 采用循环不断地循环执行getAndExecuteCommand()方法, 当bwr的读写buffer都没有数据时,则阻塞在binder_thread_read的wait_event过程. 另外,正常情况下binder线程一旦创建则不会退出.

通信协议

image.png

  • Binder客户端或者服务端向Binder Driver发送的命令都是以BC_开头,例如本文的BC_TRANSACTIONBC_REPLY, 所有Binder Driver向Binder客户端或者服务端发送的命令则都是以BR_开头, 例如本文中的BR_TRANSACTIONBR_REPLY.
  • 只有当BC_TRANSACTION或者BC_REPLY时, 才调用binder_transaction()来处理事务. 并且都会回应调用者一个BINDER_WORK_TRANSACTION_COMPLETE事务, 经过binder_thread_read()会转变成BR_TRANSACTION_COMPLETE.
  • startService过程便是一个非oneway的过程, 那么oneway的通信过程如下所述.

image.png

  • Java应用层:  对于上层应用通过调用AMP.startService, 完全可以不用关心底层,经过层层调用,最终必然会调用到AMS.startService.
  • Java IPC层:  Binder通信是采用C/S架构, Android系统的基础架构便已设计好,Binder在Java framework层的Binder客户类,BinderProxy和服务类Binder;
  • Native IPC层:  对于Native层,如果需要直接使用Binder(比如media相关), 则可以直接使用BpBinder和BBinder(当然这里还有JavaBBinder)即可, 对于上一层Java IPC的通信也是基于这个层面.
  • Kernel物理层:  这里是Binder Driver, 前面3层都跑在用户空间,对于用户空间的内存资源是不共享的,每个Android的进程只能运行在自己进程所拥有的虚拟地址空间, 而内核空间却是可共享的. 真正通信的核心环节还是在Binder Driver.

通过startService的流程分析如图

image.png

AMP和AMN都是实现了IActivityManager接口,AMS继承于AMN. 其中AMP作为Binder的客户端,运行在各个app所在进程, AMN(或AMS)运行在系统进程system_server.

Binder IPC原理

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务.

image.png

可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程,Client端和Server端通信时都需要先获取Service Manager接口,才能开始通信服务, 当然查找到目标信息可以缓存起来则不需要每次都向ServiceManager请求。

  1. 注册服务:首先AMS注册到ServiceManager。该过程:AMS所在进程(system_server)是客户端,ServiceManager是服务端。
  2. 获取服务:Client进程使用AMS前,须先向ServiceManager中获取AMS的代理类AMP。该过程:AMP所在进程(app process)是客户端,ServiceManager是服务端。
  3. 使用服务: app进程根据得到的代理类AMP,便可以直接与AMS所在进程交互。该过程:AMP所在进程(app process)是客户端,AMS所在进程(system_server)是服务端。

Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder Driver进行交互的,从而实现IPC通信方式.

Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层.

通信原理

image.png

  1. 发起端线程向Binder Driver发起binder ioctl请求后, 便采用环不断talkWithDriver,此时该线程处于阻塞状态, 直到收到如下BR_XXX命令才会结束该过程.

    • BR_TRANSACTION_COMPLETE: oneway模式下,收到该命令则退出
    • BR_REPLY: 非oneway模式下,收到该命令才退出;
    • BR_DEAD_REPLY: 目标进程/线程/binder实体为空, 以及释放正在等待reply的binder thread或者binder buffer;
    • BR_FAILED_REPLY: 情况较多,比如非法handle, 错误事务栈, security, 内存不足, buffer不足, 数据拷贝失败, 节点创建失败, 各种不匹配等问题
    • BR_ACQUIRE_RESULT: 目前未使用的协议;
  2. 左图中waitForResponse收到BR_TRANSACTION_COMPLETE,则直接退出循环, 则没有机会执行executeCommand()方法, 故将其颜色画为灰色. 除以上5种BR_XXX命令, 当收到其他BR命令,则都会执行executeCommand过程.

  3. 目标Binder线程创建后, 便进入joinThreadPool()方法, 采用循环不断地循环执行getAndExecuteCommand()方法, 当bwr的读写buffer都没有数据时,则阻塞在binder_thread_read的wait_event过程. 另外,正常情况下binder线程一旦创建则不会退出.

通信协议

image.png

  • Binder客户端或者服务端向Binder Driver发送的命令都是以BC_开头,例如本文的BC_TRANSACTIONBC_REPLY, 所有Binder Driver向Binder客户端或者服务端发送的命令则都是以BR_开头, 例如本文中的BR_TRANSACTIONBR_REPLY.
  • 只有当BC_TRANSACTION或者BC_REPLY时, 才调用binder_transaction()来处理事务. 并且都会回应调用者一个BINDER_WORK_TRANSACTION_COMPLETE事务, 经过binder_thread_read()会转变成BR_TRANSACTION_COMPLETE.
  • startService过程便是一个非oneway的过程, 那么oneway的通信过程如下所述.

收起阅读 »

JetPack Compose 主题配色太少怎么办? 来设计自己的颜色系统吧

引言JetPack Compose 正式版已经发布好几个月了,在这段时间里,除了业务相关需求之外,我也开始了 Compose 在实际项目中的落地实验,因为一旦要接入当前项目,那么遇到的问题其实远远大于新创建一个项目所需要的问题。本...
继续阅读 »

引言

JetPack Compose 正式版已经发布好几个月了,在这段时间里,除了业务相关需求之外,我也开始了 Compose 在实际项目中的落地实验,因为一旦要接入当前项目,那么遇到的问题其实远远大于新创建一个项目所需要的问题。

本篇要解决的就是 Compose 默认 Material 主题颜色太少,如何配置自己的业务颜色板,或者说,如何自定义自己的颜色系统,并由点入深,系统的分析相关实现方法与原理。

问题

在开始之前,我们先看看目前创建一个 Compose 项目,默认的 Material 主题为我们提供的颜色有哪些:

图片名称

对于一个普通的应用而言,默认的已基本满足开发使用,基本的主题配色已经足够。但是此时一个问题出现了,如果我存在其他的主题配色呢?

传统做法

在传统的 View 体系中,我们一般都会将颜色定义在 color.xml 文件中,在使用的时候直接读取即可,getColor(R.xx) ,这个大家都已经很熟悉了,那么在 Compose 中呢?

image-20211025151217426

Compose

在 Compose 中,google 将颜色数值统一放在了 theme 下的 color.kt 中,这其实也就是全局静态变量,乍一看好像没什么问题,那我的业务颜色放在那里呢,总不能都全局暴露吧?

image-20211025151546986

但是聪明的你肯定知道,我按照老办法放到 color.xml 里不就行哈,这样也不是不可以,但是随之而来的问题如下:

  • 切换主题时候,颜色怎么统一解决?
  • 在 Google 的 simple 里,color.xml 里往往不会写任何配置,即 Google 本身不建议在 compose 里这样用

那么我该怎么办,我去看看google的simple,看看他们怎么解决:

image-20211025152543420

simple果然是simple 😑 ,Google 完全按照 Material 的标准,即不会出现其他的非主题配色,那实际呢,我们开发怎么办。然后我搜了下目前github上大佬们写的一些开源项目,发现他们也都是按照 Material 去实现,但是很明显这样很不符合实际(国情)。🙃

解决思路

随心所欲写法(不推荐)

形容 没什么标准,直接卷起袖子撸代码,左脑思考,右手开敲,拿起 ⌨️ 就是干,又指新时代埋头苦干的 👷🏻‍♂️

既然官方没写怎么解决,那就自己想办法解决喽。

compose 中,对于数据的改变监听是使用 MutableState ,那么我自己自定义一个单例持有类,持有现有的主题配置,然后定义一个业务颜色类,并且定义相应的主题颜色类对象,最终根据当前单例的主题配置,去判断最终使用什么颜色板即可,更改业务主题时只需要更改这个单例主题配置字段即可。一想到如此简单,我可真是个抖机灵,说干就干 👨‍🔧‍

创建主题枚举
enum class ColorPallet {
// 默认就给两个颜色,根据需求可以定义多个
DARK, LIGHT
}
增加主题配置单例
object ColorsManager {
/** 使用一个变量维护当前主题 */
var pallet by mutableStateOf(ColorPallet.LIGHT)
}
增加颜色板
/** 共用的颜色 */
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

/** 业务颜色配置,如果需要增加其他主题业务色,直接定义相应的下述对象即可,如果某个颜色共用,则增加默认值 */
open class CkColor(val homeBackColor: Color, val homeTitleTvColor: Color = Color.Gray)

/** 提前定义好的业务颜色模板对象 */
private val CkDarkColor = CkColor(
homeBackColor = Color.Black
)

private val CkLightColor = CkColor(
homeBackColor = Color.White
)

/** 默认的颜色配置,即Md的默认配置颜色 */
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
)
增加统一调用入口

为了便于实际使用,我们增加一个 MaterialTheme.ckColor的扩展函数,以便使用我们自定义的颜色组:

/** 增加扩展 */
val MaterialTheme.ckColor: CkColor
get() = when (ColorsManager.pallet) {
ColorPallet.DARK -> CkDarkColor
ColorPallet.LIGHT -> CkLightColor
}
最终的主题如下
@Composable
fun CkTheme(
pallet: ColorPallet = ColorsManager.pallet,
content: @Composable() () -> Unit
) {
val colors = when (pallet) {
ColorPallet.DARK -> DarkColorPalette
ColorPallet.LIGHT -> LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

效果图

 

看效果也还成,简单粗暴,[看着] 也没什么问题,那有没有什么其他方式呢?我还是不相信官方没有写,可能是我疏忽了。

自定义颜色系统(官方)

就在我翻官方文档时,突然看见了这样几个小字,它实现了自定义颜色系统

image-20211025180600191

真是瞎了自己的眼,居然没看到这行字,有了官方示例,于是就赶紧去学习(抄)代码。

增加颜色模板
enum class StylePallet {
// 默认就给两个颜色,根据需求可以定义多个
DARK, LIGHT
}

// 示例,正确做法是放到color.kt下
val Blue50 = Color(0xFFE3F2FD)
val Blue200 = Color(0xFF90CAF9)
val A900 = Color(0xFF0D47A1)

/**
* 实际主题的颜色集,所有颜色都需要添加到其中,并使用相应的子类覆盖颜色。
* 每一次的更改都需要将颜色配置在下方 [CkColors] 中,并同步 [CkDarkColor] 与 [CkLightColor]
* */

@Stable
class CkColors(
homeBackColor: Color,
homeTitleTvColor: Color
) {
var homeBackColor by mutableStateOf(homeBackColor)
private set
var homeTitleTvColor by mutableStateOf(homeTitleTvColor)
private set

fun update(colors: CkColors) {
this.homeBackColor = colors.homeBackColor
this.homeTitleTvColor = colors.homeTitleTvColor
}

fun copy() = CkColors(homeBackColor, homeTitleTvColor)
}

/** 提前定义好的颜色模板对象 */
private val CkDarkColors = CkColors(
homeBackColor = A900,
homeTitleTvColor = Blue50,
)

private val CkLightColors = CkColors(
homeBackColor = Blue200,
homeTitleTvColor = Color.White,
)
增加 xxLocalProvider
@Composable
fun ProvideLcColors(colors: CkColors, content: @Composable () -> Unit) {
val colorPalette = remember {
colors.copy()
}
colorPalette.update(colors)
CompositionLocalProvider(LocalLcColors provides colorPalette, content = content)
}
增加 LocalLcColors 静态变量
// 创建静态 CompositionLocal ,通常情况下主题改动不会很频繁
private val LocalLcColors = staticCompositionLocalOf {
CkLightColors
}
增加主题配置单例
/* 针对当前主题配置颜色板扩展属性 */
private val StylePallet.colors: Pair<Colors, CkColors>
get() = when (this) {
StylePallet.DARK -> DarkColorPalette to CkDarkColors
StylePallet.LIGHT -> LightColorPalette to CkLightColors
}


/** CkX-Compose主题管理者 */
object CkXTheme {
/** 从CompositionLocal中取出相应的Local */
val colors: CkColors
@Composable
get() = LocalLcColors.current

/** 使用一个state维护当前主题配置,这里的写法取决于具体业务,
如果你使用了深色模式默认配置,则无需这个变量,即app只支持深色与亮色,
那么只需要每次读系统配置即可。但是compose本身可以做到快速切换主题,
那么维护一个变量是肯定没法避免的 */

var pallet by mutableStateOf(StylePallet.LIGHT)
}
最终主题代码
@Composable
fun CkXTheme(
pallet: StylePallet = CkXTheme.pallet,
content: @Composable () -> Unit
) {
val (colorPalette, lcColors) = pallet.colors
ProvideLcColors(colors = lcColors) {
MaterialTheme(
colors = colorPalette,
typography = Typography,
shapes = Shapes,
content = content
)
}
}

分析

最终的效果和上述的一致,也就不具体赘述了,我们主要来分析一下,为什么Google要这么写:

我们可以看到上述的示例里主要是使用了 CompositionLocalProvider 去保存当前的主题配置 ,而 CompositionLocalProvider 又继承自 CompositionLocal ,比如我们常用的 MaterialTheme 主题中的 Shapes ,typography 都是由此来管理。

CkColors 这个类上增加了 @Stable ,其代表着对于 Compose 而言,这个类是一个稳定的类,即每次更改不会引发重组,内部的颜色字段使用了 mustbaleStateOf 包装,以便当颜色更改时触发重组,内部还增加了 update() 与 copy() 方法,以便于管理与单向数据的更改。

其实如果我们去看的 Colors 类。就会发现上述示例中的 CkColors 和其是完全一样的设计方式。

image-20211027155601502

所以在Compose中自定义主题颜色,其实就是我们在 Colors 的基础上自己又写了一套自己的配色。😂

既然这样,那为什么我们不直接继承Colors去增加配色呢?使用的时候我强制一下不就行,这样不就不用再自己造什么 CompositionLocal 了?

其实很好理解,因为 Colors 中的 copy() 以及 update() 无法被重写,即没加 open ,而且其内部变量使用了 internal 修饰 set 方法。更重要的原因是这样 不符合Md的设计 ,所以这也就是为什么 需要我们去自定义自己的颜色系统,甚至于可以完全自定义自己的主题系统。前提是你觉得自定义的主题里面再包装一层 MaterialTheme 主题比较丑陋的话,当然相应的,你也需要考虑如何解决其他附带的问题。


收起阅读 »

小程序原理 及 优化

小程序使用的是双线程 在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 > 两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 Native 的 JS...
继续阅读 »

小程序使用的是双线程



在这种架构中,视图层使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境 >




两者都是独立的模块,并不具备数据直接共享的通道。视图层和逻辑层的数据传输,要由 NativeJSBrigde 做中转



小程序的启动过程



  • 1、小程序初始化: 微信初始化小程序环境:包括 Js 引擎WebView 进行初始化,并注入公共基础库。 这步是微信做的,在用户打开小程序之前就已经准备好了,是小程序运行环境预加载。

  • 2、下载小程序代码包 对小程序业务代码包进行下载:下载的不是小程序的源代码,而是编译、压缩、打包之后的代码。

  • 3、加载小程序代码包 对下载完成对代码包进行注入执行。 此时,app.js、页面所在的 Js 文件和所有其他被require 的 Js 文件会被自动执行一次,小程序基础库会完成所有页面的注册。

  • 4、初始化小程序首页 拉取数据,从逻辑层传递到视图层,进行渲染


setData 的工作原理



  • 1、调用setData方法;

  • 2、逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,将待传输数据转换成字符串并拼接到特定的JS脚本, 并通过 evaluateJavascript 执行脚本将数据传输到渲染层。

  • 3、渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染。

  • 4、WebView 线程开始执行渲染时,将 data setData 数据套用在WXML 片段上,得到一个新节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。最后,将 setData 数据合并到 data 中,并用新节点树替换旧节点树,用于下一次重渲染


小程序官方性能指标



  • 1、首屏时间不超过 5 秒

  • 2、渲染时间不超过 500ms

  • 3、每秒调用 setData 的次数不超过 20 次

  • 4、setData 的数据在 JSON.stringify 后不超过 256kb

  • 5、页面 WXML 节点少于 1000 个,节点树深度少于 30 层子节点数不大于 60 个

  • 6、所有网络请求都在 1 秒内返回结果;


小程序优化


1、分包并且使用


分包预加载(通过配置 preloadRule) 将访问率低的页面放入子包里,按需加载;启动时需要访问的页面及其依赖的资源文件应放在主包中。 不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转;可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;


image.png


2、采用独立分包技术(感觉开普勒黄金流程源码可以独立分包)


主包+子包的方式,,如果要跳到子包里,还是会加载主包然后加载子包;采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源;


3、异步请求可以在页面onLoad就加载


4、注意利用缓存


利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新


5、及时反馈


及时对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了 先反馈,再请求。比如说,点赞的按钮,可以先改变按钮的样式,再 发起异步请求。


6、可拆分的部分尽量使用自定义组件


自定义组件的更新并不会影响页面上其他元素的更新,各个组件具有独立的逻辑空间、数据、样式环境及 setData 调用


7、避免不当的使用onPageScroll


避免在onPageScroll 中执行复杂的逻辑,避免在onPageScroll中频繁使用setData,避免在onPageScroll中 频繁查询几点信息(selectQuery


8、减少在代码包中直接嵌入的资源文件;图片放在cdn,使用适当的图片格式


9、setData 优化


(1)与界面渲染无关的数据最好不要设置在 data 中,可以考虑设置在 page 对象的其他字段下;


this.setData({ 
a: '与渲染有关的字符串',
b: '与渲染无关的字符串'
})
// 可以优化为
this.setData({
a: '与渲染有关的字符串'
})
this.b = '与渲染无关的字符串'

(2)不要过于频繁调用 setData,将多次 setData 合并成一次 setData 调用


(3)数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示数据结构比较复杂包含长字符串,则不应使用setData来设置这些数据


(4)列表局部更新 在更新列表的某一个数据时。不要用 setData 进行全部数据的刷新。查找对应 id 的那条数据的下标(index是不会改变的),用 setData 进行局部刷新


this.setData({ 
`list[${index}]` = newList[index]
})

(5)切勿在后台页面进行setData(就是不要再页面跳转后使用setData) 页面跳转后,代码逻辑还在执行,此时多个webview是共享一个js进程;后台的setData操作会抢占前台页面的渲染资源;


10、避免过多的页面节点数


页面初始渲染时,渲染树的构建、计算节点几何信息以及绘制节点到屏幕的时间开销都跟页面节点数量成正相关关系,页面节点数量越多,渲染耗时越长。


每次执行 setData 更新视图,WebView JS 线程都要遍历节点树计算新旧节点数差异部分。当页面节点数量越多,计算的时间开销越大,减少节点树节点数量可以有效降低重渲染的时间开销。


11、事件使用不当


(1)去掉不必要的事件绑定(WXML中的bindcatch),从而减少通信的数据量和次数;
(2)事件绑定时需要传输targetcurrentTargetdataset,因而不要在节点的data前缀属性中放置过大的数据


12、逻辑后移,精简业务逻辑


就比如咱们生成分享图片,再比如领取新人券的时候将是否是新人是否符合风控条件和最终领券封装为一个接口


13、数据预拉取(重要


小程序官方为开发者提供了一个在小程序冷启动时提前拉取第三方接口的能力 developers.weixin.qq.com/miniprogram… 预拉取能够在小程序冷启动的时候通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间,从而提升小程序的打开速度


14、跳转时预拉取


可以在发起跳转前(如 wx.navigateTo 调用前),提前请求下一个页面的主接口并存储在全局 Promise 对象中,待下个页面加载完成后从 Promise 对象中读取数据即可


15、非关键渲染数据延迟请求


小程序会发起一个聚合接口请求来获取主体模块的数据,而非主体模块的数据则从另一个接口获取,通过拆分的手段来降低主接口的调用时延,同时减少响应体的数据量,缩减网络传输时间。


16、分屏渲染


在 主体模块 的基础上再度划分出 首屏模块 和 非首屏模块(比如京挑好货的猜你喜欢模块),在所有首屏模块都渲染完成后才会渲染非首屏模块和非主体模块,以此确保首屏内容以最快速度呈现


17、接口聚合,请求合并(主要解决小程序中针对 API 调用次数的限制)


在小程序中针对 API 调用次数的限制: wx.request (HTTP 连接)的最大并发限制是 10 个; wx.connectSocket (WebSocket 连接)的最大并发限制是 5 个;


18、事件总线,替代组件间数据绑定的通信方式


通过事件总线(EventBus),也就是发布/订阅模式,来完成由父向子的数据传递


19、大图裁剪为多块加载


20、长列表优化


(1)不要每次加载更多的时候 都用concat
每获取到新一页数据时,就把它们concatlist上去,这样就会导致每次setData时的list越来越长越来越长,渲染速度也就越来越慢
(2)分批setData,减少一次setData的数量。不要一次性setData list,而是把每一页一批一批地set Data到这个list中去


this.setData({ 
['feedList[' + (page - 1) + ']']: newVal,
})

(3)运用官方的 IntersectionObserver.relativeToViewport 将超出或者没进入可视区的 部分卸载掉(适用于一次加载很多的列表数据,超出了两屏高度所展示的内容)


image.png


this.extData.listItemContainer.relativeToViewport({ top: showNum * windowHeight, bottom: showNum * windowHeight }) 
.observe(`#list-item-${this.data.skeletonId}`, (res) => {
let { intersectionRatio } = res
if (intersectionRatio === 0) {
console.log('【卸载】', this.data.skeletonId, '超过预定范围,从页面卸载')
this.setData({
showSlot: false
})
} else {
console.log('【进入】', this.data.skeletonId, '达到预定范围,渲染进页面')
this.setData({
showSlot: true,
height: res.boundingClientRect.height
})
}
})

21、合理运用函数的防抖与节流,防止出现重复点击及重复请求出现 为避免频繁setData和渲染,做了防抖函数,时间是600ms


作者:甘草倚半夏
链接:https://juejin.cn/post/7023671521075806244

收起阅读 »

Vue 开发规范(下)

提供组件 API 文档 使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。 为什么?良好的文档可以让开发者比较容易的...
继续阅读 »

提供组件 API 文档


使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md 文件中进行说明。


为什么?

良好的文档可以让开发者比较容易的对组件有一个整体的认识,而不用去阅读组件的源码,也更方便开发者使用。

组件配置属性即组件的 API,对于组件的用户来说他们更感兴趣的是 API 而不是实现原理。

正式的文档会告诉开发者组件 API 变更以及向后的兼容性情况

README.md 是标准的我们应该首先阅读的文档文件。代码托管网站(GitHub、Bitbucket、Gitlab 等)会默认在仓库中展示该文件作为仓库的介绍。


怎么做?


在模块目录中添加 README.md 文件:


range-slider/
├── range-slider.vue
├── range-slider.less
└── README.md
在 README 文件中说明模块的功能以及使用场景。对于 vue 组件来说,比较有用的描述是组件的自定义属性即 API 的描述介绍。


提供组件 demo


添加 index.html 文件作为组件的 demo 示例,并提供不同配置情况的效果,说明组件是如何使用的。


为什么?

demo 可以说明组件是独立可使用的。

demo 可以让开发者预览组件的功能效果。

demo 可以展示组件各种配置参数下的功能。


对组件文件进行代码校验


代码校验可以保持代码的统一性以及追踪语法错误。.vue 文件可以通过使用 eslint-plugin-html插件来校验代码。你可以通过 vue-cli 来开始你的项目,vue-cli 默认会开启代码校验功能。


为什么?

保证所有的开发者使用同样的编码规范。

更早的感知到语法错误。


怎么做?


为了校验工具能够校验 *.vue文件,你需要将代码编写在

ESLint
ESLint 需要通过 ESLint HTML 插件来抽取组件中的代码。


通过 .eslintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项:


{
"extends": "eslint:recommended",
"plugins": ["html"],
"env": {
"browser": true
},
"globals": {
"opts": true,
"vue": true
}
}
运行 ESLint


eslint src/**/*.vue


JSHint
JSHint 可以解析 HTML(使用 --extra-ext命令参数)和抽取代码(使用 --extract=auto命令参数)。


通过 .jshintrc 文件来配置 ESlint,这样 IDE 可以更好的理解校验配置项。


{
"browser": true,
"predef": ["opts", "vue"]
}
运行 JSHint


jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/
注:JSHint 不接受 vue 扩展名的文件,只支持 html。


只在需要时创建组件


为什么?


Vue.js 是一个基于组件的框架。如果你不知道何时创建组件可能会导致以下问题:



  • 如果组件太大, 可能很难重用和维护;

  • 如果组件太小,你的项目就会(因为深层次的嵌套而)被淹没,也更难使组件间通信;


怎么做?



  • 始终记住为你的项目需求构建你的组件,但是你也应该尝试想到它们能够从中脱颖而出(独立于项目之外)。如果它们能够在你项目之外工作,就像一个库那样,就会使得它们更加健壮和一致。

  • 尽可能早地构建你的组件总是更好的,因为这样使得你可以在一个已经存在和稳定的组件上构建你的组件间通信(props & events)。


规则



  • 首先,尽可能早地尝试构建出诸如模态框、提示框、工具条、菜单、头部等这些明显的(通用型)组件。总之,你知道的这些组件以后一定会在当前页面或者是全局范围内需要。

  • 第二,在每一个新的开发项目中,对于一整个页面或者其中的一部分,在进行开发前先尝试思考一下。如果你认为它有一部分应该是一个组件,那么就创建它吧。

  • 最后,如果你不确定,那就不要。避免那些“以后可能会有用”的组件污染你的项目。它们可能会永远的只是(静静地)待在那里,这一点也不聪明。注意,一旦你意识到应该这么做,最好是就把它打破,以避免与项目的其他部分构成兼容性和复杂性。


Vue 组件规范


<!-- iview 等第三方公共组件,推荐大写开头 -->
<Button> from the top</Button>
<Row>
<Col span="24">
</Col>
</Row>


/** * 公共组件 项目内,自己开发的 推荐p开头 * import pLinkpage from 'public/module/linkage' */


<p-linkage v-model="form.pcarea"></p-linkage>


/** * 非公共组件 项目内,自己开发的推荐v开头 * import vSearch from './search' */
<v-search @search="params = $event"></v-search>

自闭合组件


在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。


自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。


不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。


// 反例
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent></MyComponent>


<!-- 在 DOM 模板中 -->
<my-component/>
// 好例子
<!-- 在单文件组件、字符串模板和 JSX 中 -->
<MyComponent/>


<!-- 在 DOM 模板中 -->
<my-component></my-component>

作者:_Battle
链接:https://juejin.cn/post/7023549490372182052/

收起阅读 »

Vue 开发规范(中)

上一篇:https://www.imgeek.org/article/825358938将 this 赋值给 component 变量 在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 c...
继续阅读 »

上一篇:https://www.imgeek.org/article/825358938

将 this 赋值给 component 变量


在 Vue.js 组件上下文中,this指向了组件实例。因此当你切换到了不同的上下文时,要确保 this 指向一个可用的 component 变量。


换句话说,如果你正在使用 ES6 的话,就不要再编写 var self = this; 这样的代码了,您可以安全地使用 Vue 组件。


为什么?



  • 使用 ES6,就不再需要将 this 保存到一个变量中了。

  • 一般来说,当你使用箭头函数时,会保留 this 的作用域。(译者注:箭头函数没有它自己的 this 值,箭头函数内的 this 值继承自外围作用域。)

  • 如果你没有使用 ES6,当然也就不会使用 箭头函数 啦,那你必须将 “this” 保存到到某个变量中。这是唯一的例外。


怎么做?


<script type="text/javascript"> 
export default { methods: { hello() { return 'hello'; }, printHello() { console.log(this.hello()); }, }, };
</script>
<!-- 避免 -->
<script type="text/javascript">
export default { methods: { hello() { return 'hello'; }, printHello() { const self = this; // 没有必要 console.log(self.hello()); }, }, };
</script>

组件结构化


按照一定的结构组织,使得组件便于理解。


为什么?



  • 导出一个清晰、组织有序的组件,使得代码易于阅读和理解。同时也便于标准化。

  • 按首字母排序 properties、data、computed、watches 和 methods 使得这些对象内的属性便于查找。

  • 合理组织,使得组件易于阅读。(name; extends; props, data 和 computed; components; watch 和 methods; lifecycle methods 等)。

  • 使用 name 属性。借助于 vue devtools 可以让你更方便的测试。

  • 合理的 CSS 结构,如 BEM 或 rscss - 详情?。

  • 使用单文件 .vue 文件格式来组件代码。


怎么做?


组件结构化


<template lang="html">
<div class="Ranger__Wrapper">
<!-- ... -->
</div>
</template>

<script type="text/javascript"> 
export default {
// 不要忘记了 name 属性 name: 'RangeSlider',
// 组合其它组件 extends: {},
// 组件属性、变量 props: { bar: {},
// 按字母顺序 foo: {}, fooBar: {}, },
// 变量 data() {}, computed: {},
// 使用其它组件 components: {},
// 方法 watch: {}, methods: {},
// 生命周期函数 beforeCreate() {}, mounted() {}, };
</script>

<style scoped> .Ranger__Wrapper { /* ... */ } </style>

组件事件命名


Vue.js 提供的处理函数和表达式都是绑定在 ViewModel 上的,组件的每一个事件都应该按照一个好的命名规范来,这样可以避免不少的开发问题,具体可见如下 为什么。


为什么?



  • 开发者可以随意给事件命名,即使是原生事件的名字,这样会带来迷惑性。

  • 过于宽松的事件命名可能与 DOM 模板不兼容。


怎么做?



  • 事件名也使用连字符命名。

  • 一个事件的名字对应组件外的一组意义操作,如:upload-success、upload-error 以及 dropzone-upload-success、dropzone-upload-error (如果需要前缀的话)。

  • 事件命名应该以动词(如 client-api-load) 或是 形容词(如 drive-upload-success)结尾。(出处)


避免 this.$parent


Vue.js 支持组件嵌套,并且子组件可访问父组件的上下文。访问组件之外的上下文违反了基于模块开发的第一原则。因此你应该尽量避免使用 this.$parent。


为什么?



  • 组件必须相互保持独立,Vue 组件也是。如果组件需要访问其父层的上下文就违反了该原则。

  • 如果一个组件需要访问其父组件的上下文,那么该组件将不能在其它上下文中复用。


怎么做?



  • 通过 props 将值传递给子组件。

  • 通过 props 传递回调函数给子组件来达到调用父组件方法的目的。

  • 通过在子组件触发事件来通知父组件。


谨慎使用 this.$refs


Vue.js 支持通过 ref 属性来访问其它组件和 HTML 元素。并通过 this.$refs 可以得到组件或 HTML 元素的上下文。在大多数情况下,通过 this.$refs来访问其它组件的上下文是可以避免的。在使用的的时候你需要注意避免调用了不恰当的组件 API,所以应该尽量避免使用 this.$refs


为什么?



  • 组件必须是保持独立的,如果一个组件的 API 不能够提供所需的功能,那么这个组件在设计、实现上是有问题的。

  • 组件的属性和事件必须足够的给大多数的组件使用。


怎么做?



  • 提供良好的组件 API

  • 总是关注于组件本身的目的。

  • 拒绝定制代码。如果你在一个通用的组件内部编写特定需求的代码,那么代表这个组件的 API 不够通用,或者你可能需要一个新的组件来应对该需求。

  • 检查所有的 props 是否有缺失的,如果有提一个 issue 或是完善这个组件。

  • 检查所有的事件。子组件向父组件通信一般是通过事件来实现的,但是大多数的开发者更多的关注于 props 从忽视了这点。

  • Props向下传递,事件向上传递!。以此为目标升级你的组件,提供良好的 API 和 独立性。

  • 当遇到 propsevents 难以实现的功能时,通过 this.$refs来实现。

  • 当需要操作 DOM 无法通过指令来做的时候可使用 this.$ref 而不是 JQuery、document.getElement*、document.queryElement

  • 基础使用准则是,能不用ParseError: KaTeX parse error: Expected 'EOF', got '就' at position 5: refs就̲尽量不用,如果用,尽量不要通过refs操作状态,可以通过$refs调用methods


<!-- 推荐,并未使用 this.$refs -->
<range :max="max" :min="min" @current-value="currentValue" :step="1"></range>

<!-- 使用 this.$refs 的适用情况-->
<modal ref="basicModal">
<h4>Basic Modal</h4>
<button class="primary" @click="$refs.basicModal.hide()">Close</button>
</modal>
<button @click="$refs.basicModal.open()">Open modal</button>

<!-- Modal component -->
<template>
<div v-show="active">
<!-- ... -->
</div>
</template>

<script> 
export default { // ... data() { return { active: false, }; }, methods: { open() { this.active = true; }, hide() { this.active = false; }, }, // ... };
</script>
<!-- 这里是应该避免的 -->
<!-- 如果可通过 emited 来做则避免通过 this.$refs 直接访问 -->
<template>
<range :max="max" :min="min" ref="range" :step="1"></range>
</template>

<script>
export default { // ... methods: { getRangeCurrentValue() { return this.$refs.range.currentValue; }, }, // ... };
</script>

使用组件名作为样式作用域空间


Vue.js 的组件是自定义元素,这非常适合用来作为样式的根作用域空间。可以将组件名作为 CSS 类的命名空间。


为什么?



  • 给样式加上作用域空间可以避免组件样式影响外部的样式。

  • 保持模块名、目录名、样式根作用域名一样,可以很好的将其关联起来,便于开发者理解。


怎么做?


使用组件名作为样式命名的前缀,可基于 BEM 或 OOCSS 范式。同时给 style 标签加上 scoped 属性。加上 scoped 属性编译后会给组件的 class 自动加上唯一的前缀从而避免样式的冲突。


<style scoped> /* 推荐 */ 
.MyExample { }
.MyExample li { }
.MyExample__item { }
/* 避免 */
.My-Example { }
/* 没有用组件名或模块名限制作用域, 不符合 BEM 规范 */
</style>


作者:_Battle
链接:https://juejin.cn/post/7023548108214648863

收起阅读 »

Vue 开发规范(上)

基于模块开发 始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。 Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。 怎么做? 每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的...
继续阅读 »

基于模块开发


始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。


Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。


怎么做?


每一个 Vue 组件(等同于模块)首先必须专注于解决一个单一的问题,独立的、可复用的、微小的 和 可测试的。


如果你的组件做了太多的事或是变得臃肿,请将其拆分成更小的组件并保持单一的原则。一般来说,尽量保证每一个文件的代码行数不要超过 100 行。也请保证组件可独立的运行。比较好的做法是增加一个单独的 demo 示例。


Vue 组件命名


组件的命名需遵从以下原则:



  • 有意义的: 不过于具体,也不过于抽象

  • 简短: 2 到 3 个单词

  • 具有可读性: 以便于沟通交流


同时还需要注意:




  • 必须符合自定义元素规范: 使用连字符分隔单词,切勿使用保留字。




  • app- 前缀作为命名空间: 如果非常通用的话可使用一个单词来命名,这样可以方便于其它项目里复用。




为什么?


组件是通过组件名来调用的。所以组件名必须简短、富有含义并且具有可读性。


如何做?


<!-- 推荐 -->
<app-header></app-header>
<user-list></user-list>
<range-slider></range-slider>

<!-- 避免 -->
<btn-group></btn-group> <!-- 虽然简短但是可读性差. 使用 `button-group` 替代 -->
<ui-slider></ui-slider> <!-- ui 前缀太过于宽泛,在这里意义不明确 -->
<slider></slider> <!-- 与自定义元素规范不兼容 -->

组件表达式简单化


Vue.js 的表达式是 100% 的 Javascript 表达式。这使得其功能性很强大,但也带来潜在的复杂性。因此,你应该尽量保持表达式的简单化。


为什么?




  • 复杂的行内表达式难以阅读。




  • 行内表达式是不能够通用的,这可能会导致重复编码的问题。




  • IDE 基本上不能识别行内表达式语法,所以使用行内表达式 IDE 不能提供自动补全和语法校验功能。




怎么做?


如果你发现写了太多复杂并难以阅读的行内表达式,那么可以使用 method 或是 computed 属性来替代其功能。


<!-- 推荐 -->
<template>
<h1>
{{ `${year}-${month}` }}
</h1>
</template>
<script type="text/javascript"> export default { computed: { month() { return this.twoDigits((new Date()).getUTCMonth() + 1); }, year() { return (new Date()).getUTCFullYear(); } }, methods: { twoDigits(num) { return ('0' + num).slice(-2); } }, }; </script>

<!-- 避免 -->
<template>
<h1>
{{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}
</h1>
</template>

组件 props 原子化


虽然 Vue.js 支持传递复杂的 JavaScript 对象通过 props 属性,但是你应该尽可能的使用原始类型的数据。尽量只使用 JavaScript 原始类型(字符串、数字、布尔值)和函数。尽量避免复杂的对象。


为什么?



  • 使得组件 API 清晰直观。

  • 只使用原始类型和函数作为 props 使得组件的 API 更接近于 HTML(5) 原生元素。

  • 其它开发者更好的理解每一个 prop 的含义、作用。

  • 传递过于复杂的对象使得我们不能够清楚的知道哪些属性或方法被自定义组件使用,这使得代码难以重构和维护。


怎么做?


组件的每一个属性单独使用一个 props,并且使用函数或是原始类型的值。







验证组件的 props


在 Vue.js 中,组件的 props 即 API,一个稳定并可预测的 API 会使得你的组件更容易被其他开发者使用。


组件 props 通过自定义标签的属性来传递。属性的值可以是 Vue.js 字符串(:attr="value" 或 v-bind:attr="value")或是不传。你需要保证组件的 props 能应对不同的情况。


为什么?


验证组件 props 可以保证你的组件永远是可用的(防御性编程)。即使其他开发者并未按照你预想的方法使用时也不会出错。


怎么做?



  • 提供默认值。

  • 使用 type 属性校验类型。

  • 使用 props 之前先检查该 prop 是否存在。


<template>
<input type="range" v-model="value" :max="max" :min="min">
</template>
<script type="text/javascript">
export default {
props: {
max: { type: Number, // 这里添加了数字类型的校验 default() { return 10; }, },
min: { type: Number, default() { return 0; }, },
value: { type: Number, default() { return 4; }, },
},
};
</script>

作者:_Battle
链接:https://juejin.cn/post/7023188232368029710

收起阅读 »

带你理解scoped、>>>、/deep/、::v-deep的原理

前言 平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理...
继续阅读 »

前言


平时开发项目我们在使用第三方插件时,必须使用element-ui的某些组件需要修改样式时,老是需要加上/deep/深度选择器,以前只是知道这样用,但是还不清楚他的原理。还有平时每个组件的样式都会加上scoped,但是也不知道他具体的原理。今天我就带大家理解理解理解


1. Scoped CSS的原理


1.1 区别


先带大家看一下无设置Scoped与设置Scoped的区别在哪


无设置Scoped


<div class="login">登录</div>
<style>
.login {
width: 100px;
height: 100px
}
</style>

打包之后的结果是跟我们的代码一摸一样的,没有区别。


设置Scoped


<div class="login">登录</div>
<style scoped>
.login {
width: 100px;
height: 100px
}
</style>

打包之后的结果是跟我们的代码就有所区别了。如下:


<div data-v-257dda99b class="login">登录</div>
<style scoped>
.login[data-v-257dda99b] {
width: 100px;
height: 100px
}
</style>


我们通过上面的例子,不难发现多了一个data-v-hash属性,也就是说加了scoped,PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,然后,给CSS选择器额外添加一个对应的属性选择器来选择该组件中dom,这种做法使得样式只作用于含有该属性的dom——组件内部dom,可以使得组件之间的样式不互相污染。



1.2 原理


Vue的作用域样式 Scoped CSS 的实现思路如下:



  1. 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件实例的标识符,我称它为组件实例标识,简称实例标识,记作 InstanceID;

  2. 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为 data-v-实例标识,示例:<div data-v-e0f690c0="" >

  3. 给组件的作用域样式 <style scoped> 的每一个选择器的最后一个选择器单元增加一个属性选择器 原选择器[data-v-实例标识] ,示例:假设原选择器为 .cls #id > div,则更改后的选择器为 .cls #id > div[data-v-e0f690c0]


1.3 特点




  1. 将组件的样式的作用范围限制在了组件自身的标签,即:组件内部,包含子组件的根标签,但不包含子组件的除根标签之外的其它标签;所以 组件的css选择器也不能选择到子组件及后代组件的中的元素(子组件的根元素除外);



    因为它给选择器的最后一个选择器单元增加了属性选择器 [data-v-实例标识] ,而该属性选择器只能选中当前组件模板中的标签;而对于子组件,只有根元素 即有 能代表子组件的标签属性 data-v-子实例标识,又有能代表当前组件(父组件)的 签属性 data-v-父实例标识,子组件的其它非根元素,仅有能代表子组件的标签属性 data-v-子实例标识





  2. 如果递归组件有后代选择器,则该选择器会打破特性1中所说的子组件限制,从而选中递归子组件的中元素;



    原因:假设递归组件A的作用域样式中有选择器有后代选择器 div p ,则在每次递归中都会为本次递归创建新的组件实例,同时也会为该实例生成对应的选择器 div p[data-v-当前递归组件实例的实例标识],对于递归组件的除了第一个递归实例之外的所有递归实例来说,虽然 div p[data-v-当前递归组件实例的实例标识] 不会选中子组件实例(递归子组件的实例)中的 p 元素(具体原因已在特性1中讲解),但是它会选中当前组件实例中所有的 p 元素,因为 父组件实例(递归父组件的实例)中有匹配的 div 元素;





2. >>>、/deep/、::v-deep深度选择器的原理


2.1 例子


实际开发中遇到的例子:当我们开发一个页面使用了子组件的时候,如果这时候需要改子组件的样式,但是又不影响其他页面使用这个子组件的样式的时候。比如:


父组件:Parent.vue


<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>

<child></child>
</div>
</template>

<style scoped>
.parent {
background-color: green;
}

.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
.child .dyx p {
background-color: red;
}
</style>

子组件:Child.vue


<template>
<div class="child">
<h1>我是子组件</h1>
<div class="dyx">
<p>我是子组件的段落</p>
</div>
</div>
</template>

<style scoped>
.child .dyx p {
background-color: blue;
}
</style>

这时候我们就会发现没有效果。但是如果我们使用>>>/deep/::v-deep三个深度选择器其中一个就能实现了。看代码:


<template>
<div class="parent" id="app">
<h1>我是父组件</h1>
<div class="gby">
<p>我是一个段落</p>
</div>

<child></child>
</div>
</template>

<style scoped>
.parent {
background-color: green;
}

.gby p {
background-color: red;
}
// 把子组件的背景变成红色,原组件不变
::v-deep .child .dyx p {
background-color: red;
}
</style>

2.2 原理


如果你希望 scoped 样式中的一个选择器能够选择到子组 或 后代组件中的元素,我们可以使用 深度作用选择器,它有三种写法:



  • >>>,示例: .gby div >>> #dyx p

  • /deep/,示例: .gby div /deep/ #dyx p.gby div/deep/ #dyx p

  • ::v-deep,示例: .gby div::v-deep #dyx p.gby div::v-deep #dyx p


它的原理与 Scoped CSS 的原理基本一样,只是第3步有些不同(前2步一样),具体如下:



  1. 为每个组件实例(注意:是组件的实例,不是组件类)生成一个能唯一标识组件的标识符,我称它为实例标识,记作 InstanceID;

  2. 给组件模板中的每一个标签对应的Dom元素(组件标签对应的Dom元素是该组件的根元素)添加一个标签属性,格式为 data-v-实例标识,示例:<div data-v-e0f690c0="" >

  3. 给组件的作用域样式 <style scoped> 的每一个深度作用选择器前面的一个选择器单元增加一个属性选择器[data-v-实例标识] ,示例:假设原选择器为 .cls #id >>> div,则更改后的选择器为 .cls #id[data-v-e0f690c0] div


因为Vue不会为深度作用选择器后面的选择器单元增加 属性选择器[data-v-实例标识],所以,后面的选择器单元能够选择到子组件及后代组件中的元素;



收起阅读 »

Moshi踩坑之ArrayList

就是这个错 moshi让你写自定义Adapter呢,No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Mo...
继续阅读 »

就是这个错 moshi让你写自定义Adapter呢,

No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.
java.lang.IllegalArgumentException: No JsonAdapter for class java.util.ArrayList, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.

解决方法

代码如下自己看吧,这几天就因为这个moshi搞死人哦。

abstract class MoshiArrayListJsonAdapter<C : MutableCollection<T>?, T> private constructor(
private val elementAdapter: JsonAdapter<T>
) :
JsonAdapter<C>() {
abstract fun newCollection(): C

@Throws(IOException::class)
override fun fromJson(reader: JsonReader): C {
val result = newCollection()
reader.beginArray()
while (reader.hasNext()) {
result?.add(elementAdapter.fromJson(reader)!!)
}
reader.endArray()
return result
}

@Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: C?) {
writer.beginArray()
for (element in value!!) {
elementAdapter.toJson(writer, element)
}
writer.endArray()
}

override fun toString(): String {
return "$elementAdapter.collection()"
}

companion object {
val FACTORY = Factory { type, annotations, moshi ->
val rawType = Types.getRawType(type)
if (annotations.isNotEmpty()) return@Factory null
if (rawType == ArrayList::class.java) {
return@Factory newArrayListAdapter<Any>(
type,
moshi
).nullSafe()
}
null
}

private fun <T> newArrayListAdapter(
type: Type,
moshi: Moshi
): JsonAdapter<MutableCollection<T>> {
val elementType =
Types.collectionElementType(
type,
MutableCollection::class.java
)

val elementAdapter: JsonAdapter<T> = moshi.adapter(elementType)

return object :
MoshiArrayListJsonAdapter<MutableCollection<T>, T>(elementAdapter) {
override fun newCollection(): MutableCollection<T> {
return ArrayList()
}
}
}
}
}

用法

本来不想写 但怕有人骂我代码不写全

    Moshi.Builder()
.add(MoshiArrayListJsonAdapter.FACTORY)
.build()


作者:锤子呀
链接:https://juejin.cn/post/7023318010500743181
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Android 序列化(Serializable和Parcelable)

什么是序列化 由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化。 🔥 为什么序列化 永久的保存对象数据(将对象数据保存在文件当中、或者是磁盘中)。 对象在网络中传递。 对象...
继续阅读 »

什么是序列化


由于存在于内存中的对象都是暂时的,无法长期驻存,为了把对象的状态保持下来,这时需要把对象写入到磁盘或者其他介质中,这个过程就叫做序列化。


🔥 为什么序列化



  • 永久的保存对象数据(将对象数据保存在文件当中、或者是磁盘中)。

  • 对象在网络中传递。

  • 对象在IPC间传递。


🔥 实现序列化的方式



  • 实现Serializable接口

  • 实现Parcelable接口


🔥 Serializable 和 Parcelable 区别




  • Serializable 是Java本身就支持的接口。




  • Parcelable 是Android特有的接口,效率比实现Serializable接口高效(可用于Intent数据传递,也可以用于进程间通信(IPC))。




  • Serializable的实现,只需要implements Serializable即可。这只是给对象打了一个标记,系统会自动将其序列化。




  • Parcelabel的实现,不仅需要implements Parcelabel,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator接口。




  • Serializable 使用I/O读写存储在硬盘上,而Parcelable是直接在内存中读写。




  • Serializable 会使用反射,序列化和反序列化过程需要大量I/O操作,Parcelable 自己实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在Native内存中,效率要快很多




💥 实现Serializable


import java.io.Serializable;

public class UserSerializable implements Serializable {
public String name;
public int age;
}

然后你会发现没有serialVersionUID


Android Studio 是默认关闭 serialVersionUID 生成提示的,我们需要打开Setting,执行如下操作:



再次回到UserSerializable类,有个提示,就可以添加serialVersionUID了。



效果如下:


public class UserSerializable implements Serializable {
private static final long serialVersionUID = 1522126340746830861L;
public String name;
public int age = 0;

}

💥 实现Parcelable


Parcelabel的实现,不仅需要实现Parcelabel接口,还需要在类中添加一个静态成员变量CREATOR,这个变量需要实现 Parcelable.Creator 接口,并实现读写的抽象方法。如下:


 public class MyParcelable implements Parcelable {
private int mData;

public int describeContents() {
return 0;
}

public void writeToParcel(Parcel out, int flags) {
out.writeInt(mData);
}

public static final Parcelable.Creator<MyParcelable> CREATOR
= new Parcelable.Creator<MyParcelable>() {
public MyParcelable createFromParcel(Parcel in) {
return new MyParcelable(in);
}

public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};

private MyParcelable(Parcel in) {
mData = in.readInt();
}
}

此时Android Studio 给我们了一个插件可自动生成Parcelable 。


🔥 自动生成 Parcelable


public class User {
String name;
int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

想进行序列化,但是自己写太麻烦了,这里介绍个插件操作简单易上手。


💥先下载



💥使用





💥效果


public class User implements Parcelable {
String name;
int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeInt(this.age);
}

public void readFromParcel(Parcel source) {
this.name = source.readString();
this.age = source.readInt();
}

public User() {
}

protected User(Parcel in) {
this.name = in.readString();
this.age = in.readInt();
}

public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}

@Override
public User[] newArray(int size) {
return new User[size];
}
};
}

搞定。


写完了咱就运行走一波。


🔥 使用实例


💥 Serializable



MainActivity.class
Bundle bundle = new Bundle();
UserSerializable userSerializable=new UserSerializable("SCC",15);
bundle.putSerializable("user",userSerializable);
Intent intent = new Intent(MainActivity.this, BunderActivity.class);
intent.putExtra("user",bundle);
startActivity(intent);

BunderActivity.class
Bundle bundle = getIntent().getBundleExtra("user");
UserSerializable userSerializable= (UserSerializable) bundle.getSerializable("user");
MLog.e("Serializable:"+userSerializable.name+userSerializable.age);

日志:
2021-10-25 E/-SCC-: Serializable:SCC15

💥 Parcelable



MainActivity.class
Bundle bundle = new Bundle();
bundle.putParcelable("user",new UserParcelable("SCC",15));
Intent intent = new Intent(MainActivity.this, BunderActivity.class);
intent.putExtra("user",bundle);
startActivity(intent);

BunderActivity.class
Bundle bundle = getIntent().getBundleExtra("user");
UserParcelable userParcelable= (UserParcelable) bundle.getParcelable("user");
MLog.e("Parcelable:"+userParcelable.getName()+userParcelable.getAge());

日志:
2021-10-25 E/-SCC-: Parcelable:SCC15

作者:Android帅次
链接:https://juejin.cn/post/7023569402519879693
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

看动画学算法之:队列queue

简介 队列Queue是一个非常常见的数据结构,所谓队列就是先进先出的序列结构。 想象一下我们日常的排队买票,只能向队尾插入数据,然后从队头取数据。在大型项目中常用的消息中间件就是一个队列的非常好的实现。 队列的实现 一个队列需要一个enQueue入队列操作和一...
继续阅读 »

简介


队列Queue是一个非常常见的数据结构,所谓队列就是先进先出的序列结构。


想象一下我们日常的排队买票,只能向队尾插入数据,然后从队头取数据。在大型项目中常用的消息中间件就是一个队列的非常好的实现。


队列的实现


一个队列需要一个enQueue入队列操作和一个DeQueue操作,当然还可以有一些辅助操作,比如isEmpty判断队列是否为空,isFull判断队列是否满员等等。



为了实现在队列头和队列尾进行方便的操作,我们需要保存队首和队尾的标记。


先看一下动画,直观的感受一下队列是怎么入队和出队的。


先看入队:



再看出队:



可以看到入队是从队尾入,而出队是从队首出。


队列的数组实现


和栈一样,队列也有很多种实现方式,最基本的可以使用数组或者链表来实现。


先考虑一下使用数组来存储数据的情况。


我们用head表示队首的index,使用rear表示队尾的index。


当队尾不断插入,队首不断取数据的情况下,很有可能出现下面的情况:



上面图中,head的index已经是2了,rear已经到了数组的最后面,再往数组里面插数据应该怎么插入呢?


如果再往rear后面插入数据,head前面的两个空间就浪费了。这时候需要我们使用循环数组。


循环数组怎么实现呢?只需要把数组的最后一个节点和数组的最前面的一个节点连接即可。



有同学又要问了。数组怎么变成循环数组呢?数组又不能像链表那样前后连接。


不急,我们先考虑一个余数的概念,假如我们知道了数组的capacity,当要想数组插入数据的时候,我们还是照常的将rear+1,但是最后除以数组的capacity, 队尾变到了队首,也就间接的实现了循环数组。


看下java代码是怎么实现的:


public class ArrayQueue {

//存储数据的数组
private int[] array;
//head索引
private int head;
//real索引
private int rear;
//数组容量
private int capacity;

public ArrayQueue (int capacity){
this.capacity=capacity;
this.head=-1;
this.rear =-1;
this.array= new int[capacity];
}

public boolean isEmpty(){
return head == -1;
}

public boolean isFull(){
return (rear +1)%capacity==head;
}

public int getQueueSize(){
if(head == -1){
return 0;
}
return (rear +1-head+capacity)%capacity;
}

//从尾部入队列
public void enQueue(int data){
if(isFull()){
System.out.println("Queue is full");
}else{
//从尾部插入
rear = (rear +1)%capacity;
array[rear]= data;
//如果插入之前队列为空,将head指向real
if(head == -1 ){
head = rear;
}
}
}

//从头部取数据
public int deQueue(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data= array[head];
//如果只有一个元素,则重置head和real
if(head == rear){
head= -1;
rear = -1;
}else{
head = (head+1)%capacity;
}
return data;
}
}
}

大家注意我们的enQueue和deQueue中使用的方法:


rear = (rear +1)%capacity
head = (head+1)%capacity

这两个就是循环数组的实现。


队列的动态数组实现


上面的实现其实有一个问题,数组的大小是写死的,不能够动态扩容。我们再实现一个能够动态扩容的动态数组实现。


    //因为是循环数组,这里不能做简单的数组拷贝
private void extendQueue(){
int newCapacity= capacity*2;
int[] newArray= new int[newCapacity];
//先全部拷贝
System.arraycopy(array,0,newArray,0,array.length);
//如果real<head,表示已经进行循环了,需要将0-head之间的数据置空,并将数据拷贝到新数组的相应位置
if(rear< head){
for(int i=0; i< head; i++){
//重置0-head的数据
newArray[i]= -1;
//拷贝到新的位置
newArray[i+capacity]=array[i];
}
//重置real的位置
rear= rear+capacity;
//重置capacity和array
capacity=newCapacity;
array=newArray;
}
}

需要注意的是,在进行数组扩展的时候,我们不能简单的进行拷贝,因为是循环数组,可能出现rear在head后面的情况。这个时候我们需要对数组进行特殊处理。


其他部分是和普通数组实现基本一样的。


队列的链表实现


除了使用数组,我们还可以使用链表来实现队列,只需要在头部删除和尾部添加即可。


看下java代码实现:


public class LinkedListQueue {
//head节点
private Node headNode;
//rear节点
private Node rearNode;

class Node {
int data;
Node next;
//Node的构造函数
Node(int d) {
data = d;
}
}

public boolean isEmpty(){
return headNode==null;
}

public void enQueue(int data){
Node newNode= new Node(data);
//将rearNode的next指向新插入的节点
if(rearNode !=null){
rearNode.next=newNode;
}
rearNode=newNode;
if(headNode == null){
headNode=newNode;
}
}

public int deQueue(){
int data;
if(isEmpty()){
System.out.println("Queue is empty");
return -1;
}else{
data=headNode.data;
headNode=headNode.next;
}
return data;
}
}

队列的时间复杂度


上面的3种实现的enQueue和deQueue方法,基本上都可以立马定位到要入队列或者出队列的位置,所以他们的时间复杂度是O(1)。


本文的代码地址:


learn-algorithm


作者:程序那些事
链接:https://juejin.cn/post/7023561574669352996
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

来讨论下 Android 面试该问什么?

经历过一些面试,也面过一些同学。 被面试官问到头皮发麻,也把候选人问得面红耳赤。 曾怨恨问题刁钻刻薄,也曾怀疑提问跑题超纲。 经历过攻守的角色转换后,沉下心,回顾过往,不由得发出感叹。如果要将“面试”作类比的话,我愿意将其比作“相亲”。 之所以这样类比,是因为...
继续阅读 »

经历过一些面试,也面过一些同学。


被面试官问到头皮发麻,也把候选人问得面红耳赤。


曾怨恨问题刁钻刻薄,也曾怀疑提问跑题超纲。


经历过攻守的角色转换后,沉下心,回顾过往,不由得发出感叹。如果要将“面试”作类比的话,我愿意将其比作“相亲”。


之所以这样类比,是因为看似客观的技术面试,其实充斥了各种各样的主观判断。“候选人合不合面试官胃口”可能比“候选人有多优秀”更重要一点。


世界这么大,Android 知识体系这么庞杂,我也时不时地怀疑自己,特别是当 pass 一个候选人之后,这种情感愈发强烈。“是不是自己的知识有局限性?”、“我认为关键的问题,真的这么关键吗?”


带着这样的怀疑,我对自己的面试偏好做了一下总结,在此抛砖引玉,欢迎各路大神指点迷津。


ps:本篇仅关注 Android 应用层开发相关面试。


八股文式问题



  1. Activity 有几种 launch mode?每一种有什么特点?

  2. Service 有几种类型?各有什么应用场景?

  3. 广播有几种注册方式?有什么区别?

  4. Activity 有哪些生命周期回调?

  5. Kotlin 中的扩展函数是什么?

  6. JVM 内存模型是怎么样的?

  7. GC 回收算法?

  8. Java 中有几种引用类型?


这类问题的特点是“只需百度即可立马获得答案”。候选人若做过充足的准备,刷过题,就可以倒背如流。但这些问题也是有价值的,可以快速判断候选人是否了解 Android 的基本概念。


上面的第 6,7 问,我不太喜欢问。原因是“掌握了这个问题对应用层开发能起到什么可见的好处?”


计算机的复杂度高,分层是常用的降低复杂度的方法,层与层之间形成了壁垒,也提高了层内的效率。将单独一层的复杂度吃透,都可能要花去毕生的精力。并不是否定深挖底层的价值,学有余力当然可以打通好几层,但作为 Android 应用层的面试,重点还是要关注应用层的技术细节。(个人愚见,欢迎拍砖~)


但如果面试中全都是八股文式问题,则不太公平,太过偏袒死记硬背者,也可能因此 pass 掉能力很强,但基本问题准备不太充分的候选人。


原理性问题


这类问题旨在考察候选人的技术深度,在会用的技术上,知道为什么用它,及其背后的实现原理。比如:



  1. Android 消息机制是怎么实现的?

  2. Android 触摸事件如何传递?

  3. Android 视图是怎么被绘制出来的?

  4. Android 如何在不同组件间通信?(跨进程,跨线程)

  5. Activity 启动流程?

  6. AMS、PMS、WMS 创建过程?

  7. 手写消息入 MessageQueue 的算法。

  8. RecyclerView 缓存机制?


原理性问题也可以被百度出来,但可能得多看几篇博客再消化一番,最后用自己的语言组织一下,才能在面试中对答如流。


这类问题不同于八股文的地方不仅在于考察了技术深度,还顺带便考察了理解分析能力和总结表达能力。把原理性的东西用简单精炼的语言表达出来并让人听懂也是一种能力。


我不太喜欢问 5、6 这样的问题,还是之前提到的那个原因,即“回答出这样的问题对应用层开发能起到什么可见的好处?”。若是 Android 系统开发工程的面试,倒是很有必要问。


第 7 问将原理性和算法结合,不是让默写算法,而是在考察理解原理的基础上的算法实现能力。若死记硬背原理,通常都写不出。


项目经历类问题


这类问题旨在考察候选人项目经历是否真实,技术栈情况。也可就某一个使用过的技术栈追问背后的原理。


这类问题对面试官要求最高,若是没有一定的技术广度和深度,很难就候选人的技术栈问出好问题。


场景类问题


场景类问题是指设计一个“待解决的问题”,让候选人当场解决。


所有前面的问题,都可以提前准备,若准备足够充分,全部拿下不是问题。而场景题是无法提前准备的。



  1. 如图所示:按住View,移到 View 边界外后松手。这个过程中,哪些触摸事件会被传递,它们是如何传递的?


image.png



  1. 要做一个 1MB * 10 的帧动画,有什么办法优化内存?

  2. 如何防止搜索框过度频繁地发起请求?

  3. 如何实现弹幕?

  4. 如何设计直播间礼物队列?

  5. 设计图片异步加载组件需要注意哪些方面?


第 1 问将原理性问题场景化了,对死记硬背不友好。


这些问题都是应用层开发过程中可能遇到的技术问题,场景类问题是开放性的,没有唯一解,考察候选人的思路、技术积累及综合运用能力,甚至是抗压能力。


但场景类问题也有致命的缺点,受到面试官知识及经验的限制,面试官见过多少世面,就能问出多少问题。若面试官经验恰好和候选人有交集则两情相悦,不然则可能话不投机。所以这类问题也不是公平的,就好像相亲,甲之蜜糖乙之砒霜是有可能出现的。


需求拆解估时问题


即把一个真真切切的迭代需求给到面试者,要求把业务需求拆解成技术步骤,然后为每个步骤精确估时。


不要小看“需求拆解”,首先得深入领会需求,能否把产品想表达的理解到位?,能否意会产品想表达而为表达之意?在实际迭代过程中,产品和研发对需求理解的不一致是屡见不鲜的,候选人会不会和产品成为好朋友?


在深入领会需求的基础上,能否将业务故事拆解成技术步骤?考察候选人掌握的技术栈及其综合运用能力,技术选型及实现方案是否合理?是否将扩展性或性能优化考虑在内?


“估时”可以看出候选人对技术实现细节的熟练程度,假设“用 ViewPager + Fragment 实现分页框架”的估时是 1 天,那说明虽然了解改用什么技术,但并未实践过。但此时的估时是理想化的,因为没有将应用的代码现状考虑在内。


这些问题也是候选者入职之后,在每次迭代时真真切切遇到的问题。“拆解合理,估时准确”不是一件容易的事情,即需要深入领会需求、有丰富的技术栈实战经验,还需要对现有代码框架了然于胸,这是一个成熟研发的标志。


没有找到比需求拆解估时问题更务实的面试题了。若相亲的第一感觉不可靠,那就试着约会一次。


总结


我对面试的偏好是按罗列顺序递进的,但水平有限,经验局限,对 Android 应用层的面试也只能停留在这个阶段。还望掘金大神点拨~


作者:唐子玄
链接:https://juejin.cn/post/7023508547832905758
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Swift 方法

Swift 方法是与某些特定类型相关联的函数在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。实例方法在 Swift 语言中,实例...
继续阅读 »

Swift 方法是与某些特定类型相关联的函数

在 Objective-C 中,类是唯一能定义方法的类型。但在 Swift 中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活的在你创建的类型(类/结构体/枚举)上定义方法。


实例方法

在 Swift 语言中,实例方法是属于某个特定类、结构体或者枚举类型实例的方法。

实例方法提供以下方法:

  • 可以访问和修改实例属性

  • 提供与实例目的相关的功能

实例方法要写在它所属的类型的前后大括号({})之间。

实例方法能够隐式访问它所属类型的所有的其他实例方法和属性。

实例方法只能被它所属的类的某个特定实例调用。

实例方法不能脱离于现存的实例而被调用。

语法

func funcname(Parameters) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

import Cocoa

class Counter {
var count = 0
func increment() {
count += 1
}
func incrementBy(amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
// 初始计数值是0
let counter = Counter()

// 计数值现在是1
counter.increment()

// 计数值现在是6
counter.incrementBy(amount: 5)
print(counter.count)

// 计数值现在是0
counter.reset()
print(counter.count)

以上程序执行输出结果为:

6
0

Counter类定义了三个实例方法:

  • increment 让计数器按 1 递增;
  • incrementBy(amount: Int) 让计数器按一个指定的整数值递增;
  • reset 将计数器重置为0。

Counter 这个类还声明了一个可变属性 count,用它来保持对当前计数器值的追踪。


方法的局部参数名称和外部参数名称

Swift 函数参数可以同时有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用

Swift 中的方法和 Objective-C 中的方法极其相似。像在 Objective-C 中一样,Swift 中方法的名称通常用一个介词指向方法的第一个参数,比如:with,for,by等等。

Swift 默认仅给方法的第一个参数名称一个局部参数名称;默认同时给第二个和后续的参数名称为全局参数名称。

以下实例中 'no1' 在swift中声明为局部参数名称。'no2' 用于全局的声明并通过外部程序访问。

import Cocoa

class division {
var count: Int = 0
func incrementBy(no1: Int, no2: Int) {
count = no1 / no2
print(count)
}
}

let counter = division()
counter.incrementBy(no1: 1800, no2: 3)
counter.incrementBy(no1: 1600, no2: 5)
counter.incrementBy(no1: 11000, no2: 3)

以上程序执行输出结果为:

600
320
3666

是否提供外部名称设置

我们强制在第一个参数添加外部名称把这个局部名称当作外部名称使用(Swift 2.0前是使用 # 号)。

相反,我们呢也可以使用下划线(_)设置第二个及后续的参数不提供一个外部名称。

import Cocoa

class multiplication {
var count: Int = 0
func incrementBy(first no1: Int, no2: Int) {
count = no1 * no2
print(count)
}
}

let counter = multiplication()
counter.incrementBy(first: 800, no2: 3)
counter.incrementBy(first: 100, no2: 5)
counter.incrementBy(first: 15000, no2: 3)

以上程序执行输出结果为:

2400
500
45000

self 属性

类型的每一个实例都有一个隐含属性叫做self,self 完全等同于该实例本身。

你可以在一个实例的实例方法中使用这个隐含的self属性来引用当前实例。

import Cocoa

class calculations {
let a: Int
let b: Int
let res: Int

init(a: Int, b: Int) {
self.a = a
self.b = b
res = a + b
print("Self 内: \(res)")
}

func tot(c: Int) -> Int {
return res - c
}

func result() {
print("结果为: \(tot(c: 20))")
print("结果为: \(tot(c: 50))")
}
}

let pri = calculations(a: 600, b: 300)
let sum = calculations(a: 1200, b: 300)

pri.result()
sum.result()

以上程序执行输出结果为:

Self 内: 900
Self 内: 1500
结果为: 880
结果为: 850
结果为: 1480
结果为: 1450

在实例方法中修改值类型

Swift 语言中结构体和枚举是值类型。一般情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确实需要在某个具体的方法中修改结构体或者枚举的属性,你可以选择变异(mutating)这个方法,然后方法就可以从方法内部改变它的属性;并且它做的任何改变在方法结束时还会保留在原始结构中。

方法还可以给它隐含的self属性赋值一个全新的实例,这个新实例在方法结束后将替换原来的实例。

import Cocoa

struct area {
var length = 1
var breadth = 1

func area() -> Int {
return length * breadth
}

mutating func scaleBy(res: Int) {
length *= res
breadth *= res

print(length)
print(breadth)
}
}

var val = area(length: 3, breadth: 5)
val.scaleBy(res: 3)
val.scaleBy(res: 30)
val.scaleBy(res: 300)

以上程序执行输出结果为:

9
15
270
450
81000
135000

在可变方法中给 self 赋值

可变方法能够赋给隐含属性 self 一个全新的实例。

import Cocoa

struct area {
var length = 1
var breadth = 1

func area() -> Int {
return length * breadth
}

mutating func scaleBy(res: Int) {
self.length *= res
self.breadth *= res
print(length)
print(breadth)
}
}
var val = area(length: 3, breadth: 5)
val.scaleBy(res: 13)

以上程序执行输出结果为:

39
65

类型方法

实例方法是被类型的某个实例调用的方法,你也可以定义类型本身调用的方法,这种方法就叫做类型方法。

声明结构体和枚举的类型方法,在方法的func关键字之前加上关键字static。类可能会用关键字class来允许子类重写父类的实现方法。

类型方法和实例方法一样用点号(.)语法调用。

import Cocoa

class Math
{
class func abs(number: Int) -> Int
{
if number < 0
{
return (-number)
}
else
{
return number
}
}
}

struct absno
{
static func abs(number: Int) -> Int
{
if number < 0
{
return (-number)
}
else
{
return number
}
}
}

let no = Math.abs(number: -35)
let num = absno.abs(number: -5)

print(no)
print(num)

以上程序执行输出结果为:

35
5
收起阅读 »

Swift 属性

Swift 属性将值跟特定的类、结构或枚举关联。属性可分为存储属性和计算属性:存储属性计算属性存储常量或变量作为实例的一部分计算(而不是存储)一个值用于类和结构体用于类、结构体和枚举存储属性和计算属性通常用于特定类型的实例。属性也可以直接用于类型本身,这种属性...
继续阅读 »

Swift 属性将值跟特定的类、结构或枚举关联。

属性可分为存储属性和计算属性:

存储属性计算属性
存储常量或变量作为实例的一部分计算(而不是存储)一个值
用于类和结构体用于类、结构体和枚举

存储属性和计算属性通常用于特定类型的实例。

属性也可以直接用于类型本身,这种属性称为类型属性。

另外,还可以定义属性观察器来监控属性值的变化,以此来触发一个自定义的操作。属性观察器可以添加到自己写的存储属性上,也可以添加到从父类继承的属性上。


存储属性

简单来说,一个存储属性就是存储在特定类或结构体的实例里的一个常量或变量。

存储属性可以是变量存储属性(用关键字var定义),也可以是常量存储属性(用关键字let定义)。

  • 可以在定义存储属性的时候指定默认值

  • 也可以在构造过程中设置或修改存储属性的值,甚至修改常量存储属性的值

import Cocoa

struct Number
{
var digits: Int
let pi = 3.1415
}

var n = Number(digits: 12345)
n.digits = 67

print("\(n.digits)")
print("\(n.pi)")

以上程序执行输出结果为:

67
3.1415

考虑以下代码:

let pi = 3.1415

代码中 pi 在定义存储属性的时候指定默认值(pi = 3.1415),所以不管你什么时候实例化结构体,它都不会改变。

如果你定义的是一个常量存储属性,如果尝试修改它就会报错,如下所示:

import Cocoa

struct Number
{
var digits: Int
let numbers = 3.1415
}

var n = Number(digits: 12345)
n.digits = 67

print("\(n.digits)")
print("\(n.numbers)")
n.numbers = 8.7

以上程序,执行会报错,错误如下所示:

error: cannot assign to property: 'numbers' is a 'let' constant
n.numbers = 8.7

意思为 'numbers' 是一个常量,你不能修改它。


延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。

在属性声明前使用 lazy 来标示一个延迟存储属性。

注意:
必须将延迟存储属性声明成变量(使用var关键字),因为属性的值在实例构造完成之前可能无法得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。

延迟存储属性一般用于:

  • 延迟对象的创建。

  • 当属性的值依赖于其他未知类

import Cocoa

class sample {
lazy var no = number() // `var` 关键字是必须的
}

class number {
var name = "Runoob Swift 教程"
}

var firstsample = sample()
print(firstsample.no.name)

以上程序执行输出结果为:

Runoob Swift 教程

实例化变量

如果您有过 Objective-C 经验,应该知道Objective-C 为类实例存储值和引用提供两种方法。对于属性来说,也可以使用实例变量作为属性值的后端存储。

Swift 编程语言中把这些理论统一用属性来实现。Swift 中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。

一个类型中属性的全部信息——包括命名、类型和内存管理特征——都在唯一一个地方(类型定义中)定义。


计算属性

除存储属性外,类、结构体和枚举可以定义计算属性,计算属性不直接存储值,而是提供一个 getter 来获取值,一个可选的 setter 来间接设置其他属性或变量的值。

import Cocoa

class sample {
var no1 = 0.0, no2 = 0.0
var length = 300.0, breadth = 150.0

var middle: (Double, Double) {
get{
return (length / 2, breadth / 2)
}
set(axis){
no1 = axis.0 - (length / 2)
no2 = axis.1 - (breadth / 2)
}
}
}

var result = sample()
print(result.middle)
result.middle = (0.0, 10.0)

print(result.no1)
print(result.no2)

以上程序执行输出结果为:

(150.0, 75.0)
-150.0
-65.0

如果计算属性的 setter 没有定义表示新值的参数名,则可以使用默认名称 newValue。


只读计算属性

只有 getter 没有 setter 的计算属性就是只读计算属性。

只读计算属性总是返回一个值,可以通过点(.)运算符访问,但不能设置新的值。

import Cocoa

class film {
var head = ""
var duration = 0.0
var metaInfo: [String:String] {
return [
"head": self.head,
"duration":"\(self.duration)"
]
}
}

var movie = film()
movie.head = "Swift 属性"
movie.duration = 3.09

print(movie.metaInfo["head"]!)
print(movie.metaInfo["duration"]!)

以上程序执行输出结果为:

Swift 属性
3.09

注意:

必须使用var关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let关键字只用来声明常量属性,表示初始化后再也无法修改的值。


属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,甚至新的值和现在的值相同的时候也不例外。

可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重载属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。

注意:
不需要为无法重载的计算属性添加属性观察器,因为可以通过 setter 直接监控和响应值的变化。

可以为属性添加如下的一个或全部观察器:

  • willSet在设置新的值之前调用
  • didSet在新的值被设置之后立即调用
  • willSet和didSet观察器在属性初始化过程中不会被调用
import Cocoa

class Samplepgm {
var counter: Int = 0{
willSet(newTotal){
print("计数器: \(newTotal)")
}
didSet{
if counter > oldValue {
print("新增数 \(counter - oldValue)")
}
}
}
}
let NewCounter = Samplepgm()
NewCounter.counter = 100
NewCounter.counter = 800

以上程序执行输出结果为:

计数器: 100
新增数 100
计数器: 800
新增数 700

全局变量和局部变量

计算属性和属性观察器所描述的模式也可以用于全局变量和局部变量。

局部变量全局变量
在函数、方法或闭包内部定义的变量。函数、方法、闭包或任何类型之外定义的变量。
用于存储和检索值。用于存储和检索值。
存储属性用于获取和设置值。存储属性用于获取和设置值。
也用于计算属性。也用于计算属性。

类型属性

类型属性是作为类型定义的一部分写在类型最外层的花括号({})内。

使用关键字 static 来定义值类型的类型属性,关键字 class 来为类定义类型属性。

struct Structname {
static var storedTypeProperty = " "
static var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

enum Enumname {
static var storedTypeProperty = " "
static var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

class Classname {
class var computedTypeProperty: Int {
// 这里返回一个 Int 值
}
}

注意:
例子中的计算型类型属性是只读的,但也可以定义可读可写的计算型类型属性,跟实例计算属性的语法类似。


获取和设置类型属性的值

类似于实例的属性,类型属性的访问也是通过点运算符(.)来进行。但是,类型属性是通过类型本身来获取和设置,而不是通过实例。实例如下:

import Cocoa

struct StudMarks {
static let markCount = 97
static var totalCount = 0
var InternalMarks: Int = 0 {
didSet {
if InternalMarks > StudMarks.markCount {
InternalMarks = StudMarks.markCount
}
if InternalMarks > StudMarks.totalCount {
StudMarks.totalCount = InternalMarks
}
}
}
}

var stud1Mark1 = StudMarks()
var stud1Mark2 = StudMarks()

stud1Mark1.InternalMarks = 98
print(stud1Mark1.InternalMarks)

stud1Mark2.InternalMarks = 87
print(stud1Mark2.InternalMarks)

以上程序执行输出结果为:

97
87
收起阅读 »

Swift 类

Swift 类是构建代码所用的一种通用且灵活的构造体。我们可以为类定义属性(常量、变量)和方法。与其他编程语言所不同的是,Swift 并不要求你为自定义类去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类,系统会自动生成面向其它代码的外部接口。...
继续阅读 »

Swift 类是构建代码所用的一种通用且灵活的构造体。

我们可以为类定义属性(常量、变量)和方法。

与其他编程语言所不同的是,Swift 并不要求你为自定义类去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类,系统会自动生成面向其它代码的外部接口。

类和结构体对比

Swift 中类和结构体有很多共同点。共同处在于:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义附属脚本用于访问值
  • 定义构造器用于生成初始化值
  • 通过扩展以增加默认实现的功能
  • 符合协议以对某类提供标准功能

与结构体相比,类还有如下的附加功能:

  • 继承允许一个类继承另一个类的特征
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 解构器允许一个类实例释放任何其所被分配的资源
  • 引用计数允许对一个类的多次引用

语法:

class classname {
Definition 1
Definition 2
……
Definition N
}

类定义

class student{
var studname: String
var mark: Int
var mark2: Int
}

实例化类:

let studrecord = student()

实例

import Cocoa

class MarksStruct {
var mark: Int
init(mark: Int) {
self.mark = mark
}
}

class studentMarks {
var mark = 300
}
let marks = studentMarks()
print("成绩为 \(marks.mark)")

以上程序执行输出结果为:

成绩为 300

作为引用类型访问类属性

类的属性可以通过 . 来访问。格式为:实例化类名.属性名

import Cocoa

class MarksStruct {
var mark: Int
init(mark: Int) {
self.mark = mark
}
}

class studentMarks {
var mark1 = 300
var mark2 = 400
var mark3 = 900
}
let marks = studentMarks()
print("Mark1 is \(marks.mark1)")
print("Mark2 is \(marks.mark2)")
print("Mark3 is \(marks.mark3)")

以上程序执行输出结果为:

Mark1 is 300
Mark2 is 400
Mark3 is 900

恒等运算符

因为类是引用类型,有可能有多个常量和变量在后台同时引用某一个类实例。

为了能够判定两个常量或者变量是否引用同一个类实例,Swift 内建了两个恒等运算符:

恒等运算符不恒等运算符
运算符为:===运算符为:!==
如果两个常量或者变量引用同一个类实例则返回 true如果两个常量或者变量引用不同一个类实例则返回 true

实例

import Cocoa

class SampleClass: Equatable {
let myProperty: String
init(s: String) {
myProperty = s
}
}
func ==(lhs: SampleClass, rhs: SampleClass) -> Bool {
return lhs.myProperty == rhs.myProperty
}

let spClass1 = SampleClass(s: "Hello")
let spClass2 = SampleClass(s: "Hello")

if spClass1 === spClass2 {// false
print("引用相同的类实例 \(spClass1)")
}

if spClass1 !== spClass2 {// true
print("引用不相同的类实例 \(spClass2)")
}

以上程序执行输出结果为:

引用不相同的类实例 SampleClass
收起阅读 »

Swift 结构体

Swift 结构体是构建代码所用的一种通用且灵活的构造体。我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。与 C 和 Objective C 不同的是:结构体不需要包含实现文件和接口。结构体允许我们创建一个单一文件,且系统会自动生成面向...
继续阅读 »

Swift 结构体是构建代码所用的一种通用且灵活的构造体。

我们可以为结构体定义属性(常量、变量)和添加方法,从而扩展结构体的功能。

与 C 和 Objective C 不同的是:

  • 结构体不需要包含实现文件和接口。

  • 结构体允许我们创建一个单一文件,且系统会自动生成面向其它代码的外部接口。

结构体总是通过被复制的方式在代码中传递,因此它的值是不可修改的。

语法

我们通过关键字 struct 来定义结构体:

struct nameStruct { 
Definition 1
Definition 2
……
Definition N
}

实例

我们定义一个名为 MarkStruct 的结构体 ,结构体的属性为学生三个科目的分数,数据类型为 Int:

struct MarkStruct{
var mark1: Int
var mark2: Int
var mark3: Int
}

我们可以通过结构体名来访问结构体成员。

结构体实例化使用 let 关键字:

import Cocoa

struct studentMarks {
var mark1 = 100
var mark2 = 78
var mark3 = 98
}
let marks = studentMarks()
print("Mark1 是 \(marks.mark1)")
print("Mark2 是 \(marks.mark2)")
print("Mark3 是 \(marks.mark3)")

以上程序执行输出结果为:

Mark1  100
Mark2 78
Mark3 98

实例中,我们通过结构体名 'studentMarks' 访问学生的成绩。结构体成员初始化为mark1, mark2, mark3,数据类型为整型。

然后我们通过使用 let 关键字将结构体 studentMarks() 实例化并传递给 marks。

最后我们就通过 . 号来访问结构体成员的值。

以下实例化通过结构体实例化时传值并克隆一个结构体:

import Cocoa

struct MarksStruct {
var mark: Int

init
(mark: Int) {
self.mark = mark
}
}
var aStruct = MarksStruct(mark: 98)
var bStruct = aStruct // aStruct 和 bStruct 是使用相同值的结构体!
bStruct
.mark = 97
print(aStruct.mark) // 98
print(bStruct.mark) // 97

以上程序执行输出结果为:

98
97

结构体应用

在你的代码中,你可以使用结构体来定义你的自定义数据类型。

结构体实例总是通过值传递来定义你的自定义数据类型。

按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:

  • 结构体的主要目的是用来封装少量相关简单数据值。
  • 有理由预计一个结构体实例在赋值或传递时,封装的数据将会被拷贝而不是被引用。
  • 任何在结构体中储存的值类型属性,也将会被拷贝,而不是被引用。
  • 结构体不需要去继承另一个已存在类型的属性或者行为。

举例来说,以下情境中适合使用结构体:

  • 几何形状的大小,封装一个width属性和height属性,两者均为Double类型。
  • 一定范围内的路径,封装一个start属性和length属性,两者均为Int类型。
  • 三维坐标系内一点,封装xyz属性,三者均为Double类型。

结构体实例是通过值传递而不是通过引用传递。

import Cocoa

struct markStruct{
var mark1: Int
var mark2: Int
var mark3: Int

init
(mark1: Int, mark2: Int, mark3: Int){
self.mark1 = mark1
self.mark2 = mark2
self.mark3 = mark3
}
}

print("优异成绩:")
var marks = markStruct(mark1: 98, mark2: 96, mark3:100)
print(marks.mark1)
print(marks.mark2)
print(marks.mark3)

print("糟糕成绩:")
var fail = markStruct(mark1: 34, mark2: 42, mark3: 13)
print(fail.mark1)
print(fail.mark2)
print(fail.mark3)

以上程序执行输出结果为:

优异成绩:
98
96
100
糟糕成绩:
34
42
13

以上实例中我们定义了结构体 markStruct,三个成员属性:mark1, mark2 和 mark3。结构体内使用成员属性使用 self 关键字。

从实例中我们可以很好的理解到结构体实例是通过值传递的。

收起阅读 »

动画曲线天天用,你能自己整一个吗?看完这篇你就会了!

前言最近在写动画相关的篇章,经常会用到 Curve 这个动画曲线类,那这个类到底怎么实现的?如果想自己来一个自定义的动画曲线该怎么弄?本篇我们就来一探究竟。Curve 类定义查看源码, Curve 类定义如下:abstr...
继续阅读 »

前言

最近在写动画相关的篇章,经常会用到 Curve 这个动画曲线类,那这个类到底怎么实现的?如果想自己来一个自定义的动画曲线该怎么弄?本篇我们就来一探究竟。

曲线

Curve 类定义

查看源码, Curve 类定义如下:

abstract class Curve extends ParametricCurve<double> {
const Curve();

@override
double transform(double t) {
if (t == 0.0 || t == 1.0) {
return t;
}
return super.transform(t);
}

Curve get flipped => FlippedCurve(this);
}

看上去好像没定义什么, 实际这里只是做了两个处理,一个是明确的数据类型为 double,另一个是对 transform 做了重载,也只是对参数 t 做了特殊处理,保证参数 t 的范围在0-1之间,且起点值0.0和终点值1.0不被转换函数转换。主要定义在上一层的ParametricCurve。文档是建议子类重载transformInternal方法,那我们就继续往上看ParametricCurve这个类的实现,代码如下:

abstract class ParametricCurve<T> {
const ParametricCurve();

T transform(double t) {
assert(t != null);
assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
return transformInternal(t);
}

@protected
T transformInternal(double t) {
throw UnimplementedError();
}

@override
String toString() => objectRuntimeType(this, 'ParametricCurve');
}

可以看到,实际上 transform 方法除了做参数合法性验证以外,其实就是调用了transformInternal方法,因此子类必须要实现该方法,否则会抛出UnimplementedError异常。

实例解析

上面的源码可以看到,关键在于参数 t。这个参数 t 代表什么呢?注释里说的是:

Returns the value of the curve at point t. — 返回 t 点的曲线对应的值。

因此 t 可以认为是曲线的横坐标,而为了保证曲线的一致性,做了归一化处理,也就是t的取值都是在0-1之间。这么说可能有点抽象,我们来看2个例子来对比就明白了,先看最简单 Curves.linear 的实现。

class _Linear extends Curve {
const _Linear._();

@override
double transformInternal(double t) => t;
}

超级简单吧,直接返回 t,其实对应我们的数学的函数就是:

y = f(t) = t

对应的曲线就是一条斜线。也就是说在设定的动画时间内,会完成从0-1的线性转变,也就是变化是均匀的。 线性这个很好理解,我们再来看一个减速曲线decelerate的实现。

class _DecelerateCurve extends Curve {
const _DecelerateCurve._();

@override
double transformInternal(double t) {
t = 1.0 - t;
return 1.0 - t * t;
}
}

我们先看一下_DecelerateCurve 的计算表达式是什么。减速公式1

回忆一下我们高中物理学的匀减速运动,加速度为负(即减速)的距离计算公式:减速公式2

上面的减速曲线其实就可以看做是初始速度是2,加速度也是2的减速运动。为什么要是2这个值呢,这是因为 t 的取值范围是0-1,这样计算完的结果的取值范围还是0-1。你肯定会问,为什么要保证曲线的计算结果要是0-1? 我们来假设计算结果不为0-1会发生什么情况,比如我们要在屏幕上移动一个组件为60像素。假设动画曲线初始值不为0。那就意味着一开始的移动距离是跳变的。同样的,如果结束值不为1.0,意味着在最后一个点的距离值不是60.0,那么就意味着结束时需要从最后一个点跳到最终的60像素的位置(动画需要保证最终的移动距离是60像素)这样意味着动画会出现跳变的效果,绘制曲线的话会是下的样子(绿色是正常的,红线是异常的)。这样的动画体验是很糟糕的!因此,这是一个关键点,如果你的自定义曲线的 transformInternal 方法的返回值范围不是0-1,就意味着动画会出现跳变,导致动画缺帧的感觉。

image.png

有了这个基础,我们就可以解释动画曲线的基本机制了,实际上就是在给定的动画时间(Duration)范围内,完成组件的初始状态到结束状态的转变,这个转变是沿着设定的 Curve 类完成的,而其横坐标是0-1.0,曲线的初始值和结束值分别是0和1.0,而至于中间值是可以低于0或超过1的。我们可以想像是我们沿着设定的曲线运动,最终无论如何都会达到设定的目的地,而至于怎么走,拐多少道弯,速度怎么变化都是曲线控制的。但是,如果你的曲线初始值不为0或结束值不为1,就像是跳悬崖的那种感觉!

正弦动画曲线

我们来一个正弦曲线的动画验证一下上面的说法。

class SineCurve extends Curve {
final int count;
const SineCurve({this.count = 1}) : assert(count > 0);

@override
double transformInternal(double t) {
return sin(2 * count* pi * t);
}
}

count 参数用于控制周期,即达到目的地之前可以多来几个来回。这里我们发现,初始值是0,但是一个周期(2π)结束值也是0,这样在动画结束前会出现跳变的结果。来看一下示例代码,这个示例是让圆形向下移动60像素。

AnimatedContainer(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(30.0),
),
transform: Matrix4.identity()..translate(0.0, up ? 60.0 : 0.0, 0.0),
duration: Duration(milliseconds: 3000),
curve: SineCurve(count: 1),
child: ClipOval(
child: Container(
width: 60.0,
height: 60.0,
color: Colors.blue,
),
),
)

运行效果如下,注意看最后一帧从0的位置直接跳到了60的位置。

跳动动画

这个怎么调呢,我们来看一下正弦曲线的样子。

正弦曲线

如果我们要满足0-1范围的要求,那么要往后再移动90度才能够达到。但是,这样还有个问题,这样破坏了周期性,比如设置 count=2的时候结果又不对了。我们来看一下规律,实际上只有第一个周期需要多移动90度(途中箭头指向的点),后面的都是按360度(即2π)为周期了。也就是角度其实是按2.5π,4.5π,6.5π……规律来的,对应的角度公式其实就是:调整后公式

所以调整后的正弦曲线代码为:

class SineCurve extends Curve {
final int count;
const SineCurve({this.count = 1}) : assert(count > 0);

@override
double transformInternal(double t) {
// 需要补偿pi/2个角度,使得起始值是0.终止值是1,避免出现最后突然回到0
return sin(2 * (count + 0.25) * pi * t);
}
}

再看调整后的效果,是不是丝滑般地过渡了?调整后动画

总结

本篇介绍了 Flutter 动画曲线类的原理和控制动画的机制,实际上 Curve 类就是在指定的时间内,沿曲线完成从起点到终点的过渡。但是为了保证动画平滑过渡,应该保证自定义曲线的transformInternal方法返回值的起始值和结束值分别是0和1。

收起阅读 »

Android协程(Coroutines)系列-深入理解suspend(挂起函数)关键字

Kotlin 协程把suspend 修饰符引入到了我们 Android 开发者的日常开发中。您是否好奇它的底层工作原理呢?编译器是如何转换我们的代码,使其能够挂起和恢复协程操作的呢?suspend挂起函数,是指把协程代码挂起不继续执行的函数,也叫协程被函数挂起...
继续阅读 »

Kotlin 协程把suspend 修饰符引入到了我们 Android 开发者的日常开发中。您是否好奇它的底层工作原理呢?编译器是如何转换我们的代码,使其能够挂起和恢复协程操作的呢?

suspend

挂起函数,是指把协程代码挂起不继续执行的函数,也叫协程被函数挂起了。协程中调用挂起函数时,协程所在的线程不会挂起也不会阻塞,但是协程被挂起了。也就是说,协程内挂起函数之后的代码停止执行了,直到挂起函数完成后恢复协程,协程才继续执行后续的代码。所有挂起函数都会通过suspend修饰符修饰。

suspend是协程的关键字,每一个被suspend修饰的方法都必须在另一个suspend函数或者Coroutine协程程序中进行调用。

挂起函数(由suspend关键字修饰)的目的是用来挂起协程的执行等待异步计算的结果,所以一个挂起函数通常有两个要点:挂起异步

这里涉及到一种机制俗称CPS(Continuation-Passing-Style:续体传递风格)。每一个suspend修饰的方法或者lambda表达式都会在代码调用的时候为其额外添加Continuation(续体)类型的参数。

Kotlin协程中使用了状态机,编译器会将协程体编译成一个匿名内部类,每一个挂起函数的调用位置对应一个挂起点。

挂起函数意义解释
join挂起当前协程,直到等待的子协程执行完毕通过当前协程返回的Job接口的join方法,可以单纯的挂起当前协程,等待子协程完成后再恢复继续执行
await挂起当前协程,直到等待的子协程返回结果和join的区别是,它属于Job接口的子接口Deferred的方法,可以等待子协程完成后,带着返回值恢复当前协程
delay挂起当前协程,直到指定时间后恢复当前协程单纯挂起当前协程,指定时长后恢复协程执行
withContext()挂起外部协程,直到自己内部协程全部返回后,才会恢复外部的协程。没有创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成并返回结果。类似async.await的效果

协程挂起流程详解

协程实现异步的核心原理就是通过挂起函数实现协程体的挂起,还不阻塞协程体所在的线程。

fun testInMain() {
Log.d("["+Thread.currentThread().name+"]testInMain start")
var job = CoroutineScope(Dispatchers.Main).launch { //启动协程job
Log.d("[" + Thread.currentThread().name+"]job start")
var job1 = async(Dispatchers.IO) { //启动协程job1
Log.d("["+Thread.currentThread().name+"]job1 start")
delay(3000) //挂起job1协程 3秒
Log.d("["+Thread.currentThread().name+"]job1 end ")
"job1-Return"
} //job1协程 续体执行完毕

var job2 = async(Dispatchers.Default) {
Log.d("["+Thread.currentThread().name+"]job2 start" )
delay(1000) //挂起job2协程 1秒
Log.d("["+Thread.currentThread().name+"]job2 end")
"job2-Return"
} //job2协程 续体执行完毕

Log.d("["+Thread.currentThread().name+"]before job1 return")
Log.d("["+Thread.currentThread().name+"]job1 result = " + job1.await()) //挂起job协程,等待job1返回结果;如果已有结果,不挂起,直接返回

Log.d("["+Thread.currentThread().name+"]before job2 return")
Log.d("["+Thread.currentThread().name+"]job2 result = " + job2.await()) //挂起job协程,等待job2返回结果;如果已有结果,不挂起,直接返回

Log.d("["+Thread.currentThread().name+"]job end ")
} //job协程 续体执行完毕

Log.d("["+Thread.currentThread().name+"]testInMain end")
} //testInMain

示例代码的log输出如下,我们需要重点关注Log输出的次序,和时间间隔:

10:15:04.046 26079-26079/com.example.myapplication D/TC: [main]testInMain start
10:15:04.067 26079-26079/com.example.myapplication D/TC: [main]testInMain end
10:15:04.080 26079-26079/com.example.myapplication D/TC:
[main]job start
10:15:04.083 26079-26079/com.example.myapplication D/TC:
[main]before job1 return
10:15:04.086 26079-26683/com.example.myapplication D/TC: [DefaultDispatcher-worker-1]job1 start
10:15:04.087 26079-26684/com.example.myapplication D/TC: [DefaultDispatcher-worker-2]job2 start
10:15:05.090 26079-26683/com.example.myapplication D/TC: [DefaultDispatcher-worker-1]job2 end
10:15:05.095 26079-26079/com.example.myapplication D/TC:
[main]button-2 onclick now
10:15:07.090 26079-26685/com.example.myapplication D/TC: [DefaultDispatcher-worker-3]job1 end
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job1 result = job1-Return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]before job2 return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job2 result = job2-Return
10:15:07.091 26079-26079/com.example.myapplication D/TC:
[main]job end
  • 步骤一:在主线程调用TestInMain,直接打印“[main]testInMain start”的log
  • 步骤二:TestInMain方法继续执行完毕,打印“[main]testInMain end”的log
  • 步骤三:job协程被主线程调度执行,打印“[main]job start”的log
  • 步骤四:job协程继续执行,打印“[main]before job1 return”的log
  • 步骤五:job协程被job1.await挂起函数中断执行,退出main线程,等待job1返回结果后再恢复执行
  • 步骤六:job1协程被异步调度到work-1子线程执行,打印“[DefaultDispatcher-worker-1]job1 start”的log,接着被delay挂起函数中断执行,退出work-1子线程,等待delay 3秒结束后再恢复执行
  • 步骤七:job2协程被异步调度到work-2子线程执行,打印“[DefaultDispatcher-worker-2]job2 start”的log,接着被delay挂起函数中断执行,退出work-2子线程,等待delay 1秒结束后再恢复执行
  • 步骤八:1秒钟后(从04秒-05秒),job2协程被delay挂起函数异步调度到[DefaultDispatcher-worker-1]子线程恢复执行,打印“[DefaultDispatcher-worker-1]job2 end”的log,job2续体结束执行,同时将结果存储到job2协程的result字段中。
  • 步骤九:main线程中button-2点击事件被处理,打印“[main]button-2 onclick now”的log
  • 步骤十:3秒钟后(从04秒-07秒),job1协程被delay挂起函数异步调度到[DefaultDispatcher-worker-3]子线程恢复执行,打印“[DefaultDispatcher-worker-3]job1 end”的log,job1续体结束执行,同时将结果存储到job1协程的result字段中。
  • 步骤十一:job1.await挂起函数得到结果,job协程被await挂起函数异步调度到main线程恢复执行,打印“[main]job1 result = job1-Return”的log
  • 步骤十二:job协程继续执行,打印“[main]before job2 return”的log
  • 步骤十三:job协程继续调用job2.await挂起函数,此时job2协程已经有result结果,所有它不会中断job协程的执行,而是直接返回结果,打印“[main]job2 result = job2-Return”的log
  • 步骤十四:job协程继续执行,打印“[main]job end”的log,job续体结束执行。

微信图片_20211025132142.jpg 从图中,我们可以清晰的得到几点结论:

  1. job协程内部,通过await 阻塞了后续代码的执行。job1和job2协程,通过delay阻塞了后续代码的执行。
  2. 协程job1,job2 启动后,保持并行执行。job2 并没有等待job1执行完才启动执行和恢复,而是在各自线程并行执行。
  3. job的后续代码被await 阻塞后,并没有阻塞main线程,main线程中其它模块的代码能同时被执行,并打印出"[main]button 2 onclick now"。
  4. job1 被delay阻塞后续代码执行时,并没有阻塞所在线程[DefaultDispatcher-worker-1],job2中的后续代码被恢复到此[DefaultDispatcher-worker-1]子线程中执行。
  5. job1 和 job2 协程在恢复执行时,并不能确保在原线程中执行后续代码。如log所示,job2在DefaultDispatcher-worker-2中启动和阻塞后,却在DefaultDispatcher-worker-1中恢复了后续的代码执行。

所以可以看出,协程的挂起,并不会阻塞协程所在的线程,而只是中断了协程后面的代码执行。然后等待挂起函数完成后,恢复协程的后续代码执行。这就是协程挂起最最基本的关键点。

协程挂起的实现原理

上节中的示例代码,经过反编译后的核心代码如下:

//TestCoroutin.decompiled.java
public final void testInMain() {
Log.d("cjf---", var10001.append("testInMain start").toString());

Job job = BuildersKt.launch$default( CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getMain()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
//单独拆分到下面,需要详细讲解
}

public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {/......./}

public final Object invoke(Object var1, Object var2){/......./}

}), 3, (Object)null);

Log.d("cjf---", var10001.append("testInMain end ").toString());
}

//job协程的SuspendLambda续体,其invokeSuspend方法代码
public final Object invokeSuspend(@NotNull Object $result) {
... ...
label17: {
Object var8 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
Log.d(var10001.append("job start ").toString());
Deferred job1 = BuildersKt.async$default(/......./);
job2 = BuildersKt.async$defaultdefault(/......./);
Log.d(var10001.append("before job1 return").toString());
var6 = var10001.append("job1 result =");
this.L$0 = job2;
this.L$1 = var5;
this.L$2 = var6;
this.label = 1;
var10000 = job1.await(this);
if (var10000 == var8) {
return var8;
}
break;
case 1:
var6 = (StringBuilder)this.L$2;
var5 = (String)this.L$1;
job2 = (Deferred)this.L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
case 2:
var6 = (StringBuilder)this.L$1;
var5 = (String)this.L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break label17;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

var7 = var10000;
Log.d(var5, var6.append((String)var7).toString());
Log.d(var10001.append("before wait job2 return").toString());
var6 = var10001.append("job2 result = ");
this.L$0 = var5;
this.L$1 = var6;
this.L$2 = null;
this.label = 2;
var10000 = job2.await(this);
if (var10000 == var8) {
return var8;
}
} //end of label17

Log.d(var5, var6.append((String)var7).toString());
Log.d("cjf---", var10001.append("job end ").toString());
return Unit.INSTANCE;
} //end of invokeSuspend

反编译后的主要区别在job协程,其Lambda代码块转换成了Function2 实现。

我们借助APK反编译工具,可以看到执行代码中,Function2 实际上被SuspendLambda 类继承实现。

微信图片_20211025132929.jpg

SuspendLambda实现类的关键逻辑在invokeSuspend方法中,而invokeSuspend方法中采用了CPS(Continuation-Passing-Style) 续体传递风格

续体传递风格会将job协程的Lambda代码块,通过label标签和switch分割成多个代码块。代码块分割的点,就是协程中调用suspend挂起函数的地方。

分支代码调用到await挂起函数时,如果返回了COROUTINE_SUSPENDED,就退出invokeSuspend,进入挂起状态。

我们用流程图来描述上面示例代码,转换后的续体传递风格代码,如下:

微信图片_20211025132944.jpg

我们可以看到,整个示例代码,被分割成了5个代码块。其中case1 代码块主要负责为label17 代码块进行参数转换;case2 代码块主要负责为最外层代码块进行参数转换;所以相当于2个await挂起函数,将lambda代码块分割成了3个实际执行的代码块。

而且job1.await和job2.await会根据挂起函数的返回值进行不同处理,如果返回挂起,则进行协程挂起,当前协程退出执行;如果返回其它值,则协程继续后续代码块的执行。

编译器在编译期间,会对所有suspend修饰的函数调用处进行续体传递风格变换, Continuation可以称之为协程续体,它提供了协程恢复的基本方法:resumeWith。Continuation续体声明很简单:

public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/

public val context: CoroutineContext

/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/

public fun resumeWith(result: Result<T>)
}

其具体实现在SuspendLambda的父类BaseContinuationImpl中:

//class BaseContinuationImpl 中 fun resumeWith 内部核心代码
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result<Any?> = //协程返回了结果,说明协程执行完毕
try {
val outcome = invokeSuspend(param)//执行协程的续体代码块
if (outcome === COROUTINE_SUSPENDED) return //挂起函数返回挂起标志,退出后续代码执行
Result.success(outcome) //没有返回挂起标志,将返回值outcome封装为Result返给外层outcome
} catch (exception: Throwable) {
Result.failure(exception)//将异常Result返给外层outcome
}
releaseIntercepted() // 释放当前协程的拦截器
if (completion is BaseContinuationImpl) {//如果上一层续体是一个单纯的续体,则将结果作为上一层续体的恢复参数,进行上一层续体的恢复
current = completion
param = outcome
} else {//上一层续体是一个协程,则调用协程的恢复函数,进行上一层的协程恢复
completion.resumeWith(outcome)
return
}
}
}

如果invokeSuspend函数返回中断标志时,会直接从函数中返回,等待后续继续被恢复执行。

如果invokeSuspend函数返回的是结果,且上一层续体不是单纯的续体而是协程体,它会调用参数completion的resumeWith函数,恢复上一层协程的invokeSuspend代码的执行。

协程被resumeWith恢复后,会继续调用invokeSuspend函数,根据label值执行下一个case分支代码块。按照这个恢复流程,直到所有invokeSuspend代码执行完,返回非COROUTINE_SUSPENDED的结果,协程就执行结束。

我们继续看job续体在invokeSuspend中调用到job1.await函数时,await是怎么实现返回挂起标志,和后续恢复job协程的。核心代码可以在awaitSuspend中查看:

// JobSupport.kt中 awaitSuspend方法
private suspend fun awaitSuspend(): Any? = suspendCoroutineUninterceptedOrReturn { uCont ->
val cont = AwaitContinuation(uCont.intercepted(), this)
cont.disposeOnCancellation(invokeOnCompletion(
ResumeAwaitOnCompletion(this, cont).asHandler))
cont.getResult()
}

// JobSupport.kt中 invokeOnCompletion方法
public final override fun invokeOnCompletion(...):DisposableHandle {
var nodeCache: JobNode<*>? = null
loopOnState { state ->
when (state) {
is Empty -> { // 没有completion handlers,直接创建Node放入state
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
if (_state.compareAndSet(state, node)) return nod
}
is Incomplete -> {// 有completion handlers,加入到node list列表
val list = state.list
val node = nodeCache ?: makeNode(handler, onCancelling).also { nodeCache = it }
if (!addLastAtomic(state, list, node)) return@loopOnState /
}
else -> { // 已经完成,不需要加入结果监听Node
if (invokeImmediately) handler.invokeIt((state as? CompletedExceptionally)?.cause) return NonDisposableHandle
}
}
}
}

// AbstractCoroutine.kt 中 resumeWith方法
// 通知state node,进行恢复
public final override fun resumeWith(result: Result<T>) {
// makeCompletingOnce 大致实现是修改协程状态,如果需要的话还会将结果返回给调用者协程,并恢复调用者协程
makeCompletingOnce(result.toState(), defaultResumeMode)
}

可以看出,job1.await()首先会通过getResult()去获取job1的结果,如果有结果则直接返回结果,否则立即返回中断标志,这样就实现了await挂起点挂起job协程了。await()挂起函数恢复job协程的流程是,将job 协程封装为 ResumeAwaitOnCompletion,并将其再次封装成handler 节点,添加job1协程的 state.list。

等job1协程完成后,会通知 handler 节点调用job协程的 resumeWith(result) 方法,从而恢复 job协程await 挂起点之后的代码块的执行。

我们再次结合示例代码, 来梳理这个挂起和恢复流程:

微信图片_20211025145009.jpg

note:绿色底色,表示在主线程执行;红色字体,表示调用挂起函数;

可以看到整个过程:

  • job协程没有阻塞调用者TestInMain,job协程会被post到主线程执行;
  • 子协程job1,job2会同时调度到不同子线程中执行,会并行执行;
  • job协程通过job1,和job2 的 await挂起函数等待异步结果。等待异步结果的时候,job协程也没有阻塞主线程。

通过续体传递风格的invokeSuspend代码,和续体之间形成的resumewith恢复链,协程得以实现挂起和恢复的核心流程。


收起阅读 »

实现一个 Coroutine 版 DialogFragment

Android 对话框有多种实现方法,目前比较推荐的是 DialogFragment,先比较与直接使用 AlertDialog,可以避免屏幕旋转等配置变化造成消失。但是其 API 建立在回调的基础上使用起来并不友好。接入 Coroutine...
继续阅读 »

Android 对话框有多种实现方法,目前比较推荐的是 DialogFragment,先比较与直接使用 AlertDialog,可以避免屏幕旋转等配置变化造成消失。但是其 API 建立在回调的基础上使用起来并不友好。接入 Coroutine 我们可以对其进行一番改造。

1. 使用 Coroutine 进行改造

自定义 AlertDialogFragment 继承自 DialogFragment 如下

class AlertDialogFragment : DialogFragment() {

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
_cont.resume(which)
}
return AlertDialog.Builder(context)
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}

private lateinit var _cont : Continuation<Int>
suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
_cont = cont
}
}

实现很简单,我们是使用 suspendCoroutine 将原本基于 listener 的回调转化为挂起函数。接下来我们可以用同步的方式获取 dialog 的返回值了:

button.setOnClickListener {
GlobalScope.launch {
val result = AlertDialogFragment().showAsSuspendable(supportFragmentManager)
Log.d("AlertDialogFragment", "$result Clicked")
}
}

2. 屏幕旋转时的崩溃

经过测试,发现上述代码存在问题。我们知道 DialogFragment 在屏幕旋转时可以保持不消失,但是此时如果点击 Dialog 的按钮,会出现崩溃:

kotlin.UninitializedPropertyAccessException: lateinit property _cont has not been initialized

如果了解 Fragment 和 Activity 销毁重建的过程就能轻松推理出发生问题的原因:

  1. 旋转屏幕时,Activity 将会重新创建。
  2. Activity 临终前会在 onSaveInstanceState() 中保存 DialogFragment 的状态 FragmentManagerState;
  3. 重建后的 Activity,在 onCreate() 中根据 savedInstanceState 所给予的 FragmentManagerState 自动重建 DialogFragment 并且 show() 出来

总结起来流程如下:

旋转屏幕 --> Activity.onSaveInstanceState() --> Activity.onCreate() --> DialogFragment.show()

重建后的 FragmentDialog 其成员变量 _cont 尚未初始化,此时对其访问自然发生 crash。

那么如果不使用 lateinit 就没问题了呢? 我们尝试引入 RxJava 对其进行改造


3. 二次改造: RxJava + Coroutine

通过 RxJava 的 Subject 避免了 lateinit 的出现,防止 crash :

//build.gradle
implementation "io.reactivex.rxjava2:rxjava:2.2.8"

新的 AlertDialogFragment 代码如下:

class AlertDialogFragment : DialogFragment() {

private val subject = SingleSubject.create<Int>()

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
subject.onSuccess(which)
}

return AlertDialog.Builder(requireContext())
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}

suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
subject.subscribe { it -> cont.resume(it) }
}
}

显示 dialog 时,通过订阅 SingleSubject 响应 listener 的回调。

经过修改,旋转屏幕后点击 Dialog 按钮时没有再发生 crash 的现象,但是仍然存在问题:屏幕旋转后我们无法接收到 Dialog 的返回值,即没有按预期的那样显示下面的日志

Log.d("AlertDialogFragment", "$result Clicked")

当 DialogFragment 重建后, Subject 也跟随重建,但是丢失了之前的 Subscriber ,所以点击按钮后,Rx 的下游无法响应。

有没有办法让 Subject 重建时能够恢复之前的 Subscriber 呢? 此时想到了借助 onSaveInstanceState 。

想要 subject 作为 Fragment 的 arguments 保存到 savedInstanceState,必须是一个 Serializable 或者 Parcelable


4. 三次改造: SerializableSingleSubject

令人高兴的是,查阅 SingleSubject 源码后发现其成员变量全是 Serializable 的子类,也就是只要 SingleSubject 实现 Serializable 接口就可以存入 savedInstanceState 了, 但可惜它不是,而且它是一个 final 类,只好拷贝源码出来,自己实现一个 SerializableSingleSubject :

/**
* 实现 Serializable 接口并增加 serialVersionUID
*/

public final class SerializableSingleSubject<T> extends Single<T> implements SingleObserver<T>, Serializable {
private static final long serialVersionUID = 1L;

final AtomicReference<SerializableSingleSubject.SingleDisposable<T>[]> observers;

@SuppressWarnings("rawtypes")
static final SerializableSingleSubject.SingleDisposable[] EMPTY = new SerializableSingleSubject.SingleDisposable[0];

@SuppressWarnings("rawtypes")
static final SerializableSingleSubject.SingleDisposable[] TERMINATED = new SerializableSingleSubject.SingleDisposable[0];

final AtomicBoolean once;
T value;
Throwable error;

// 以下代码同 SingleSubject,省略

基于 SerializableSingleSubject 重写 AlertDialogFragment 如下:

class AlertDialogFragment : DialogFragment() {

private var subject = SerializableSingleSubject.create<Int>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let {
subject = it["subject"] as SerializableSingleSubject<Int>
}

}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val listener = DialogInterface.OnClickListener { _: DialogInterface, which: Int ->
subject.onSuccess(which)
}

return AlertDialog.Builder(requireContext())
.setTitle("Title")
.setMessage("Message")
.setPositiveButton("Ok", listener)
.setNegativeButton("Cancel", listener)
.create()
}


override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putSerializable("subject", subject);

}

suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
show(fm, tag)
subject.subscribe { it -> cont.resume(it) }
}
}

重建后通过 savedInstanceState 恢复之前的 Subscriber ,下游顺利收到消息,日志正常输出。

需要注意的是,此时仍然存在隐患:屏幕旋转后,点击 dialog 虽然可以正常获得返回值,但是此时协程恢复的上下文是前一次 launch { ... } 的闭包

    GlobalScope.launch {
val frag = AlertDialogFragment()
val result = frag.showAsSuspendable(supportFragmentManager)
Log.d("AlertDialogFragment", "$result Clicked on $frag")
}

如上,此时打印的 frag 是重建之前的 DialogFragment,如果 launch{...} 里引用了外部 Activity(获取成员) ,那也是旧的 Activity,此处需要特别注意避免类似操作。


5. 纯 RxJava 方式

既然引入了 RxJava,最后捎带介绍一下不使用 Coroutine 只依靠 RxJava 的版本:

fun showAsSingle(fm: FragmentManager, tag: String? = null): Single<Int> {
show(fm, tag)
return subject.hide()
}

使用时,由 subscribe() 替代挂起函数的使用。

button.setOnClickListener {
AlertDialogFragment().showAsSingle(supportFragmentManager).subscribe { result ->
Log.d("AlertDialogFragment", "$result Clicked")
}
}


收起阅读 »

LeetCode刷题-合并区间

一、题目描述 难度:中等~ 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。 示例1: 输入:...
继续阅读 »

一、题目描述


难度:中等~

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。


示例1:


输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例2:


输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:
  1 <= intervals.length <= 10^4
  intervals[i].length == 2
  0 <= starti <= endi <= 10^4


作者:力扣 (LeetCode)
链接:leetcode-cn.com/leetbook/re…
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


二、题目解析


思路:
直接代码里注释!


三、代码


1.Python实现



初见的第一思路:
1.按左端点从小到大排序



2.有交集,更新右端点;无交集,则保存当前区间


class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
//将二维数组intervals按照其内每个子数组第一个元素从小到大排序
intervals.sort()
result = list()
for i in intervals:
//如果result中没有子数组或者当前两个数组无交集
//直接保存当前区间
if not result or result[-1][1] < i[0]:
result.append(i)
//否则有交集,取两个数组中第一个元素最大的值作为当前数组的第一个元素(即合并操作)
else:
result[-1][1] = max(result[-1][1], i[1])
return result

复杂度分析




  • 时间复杂度:O(n log n),其中 n 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(n log n)。




  • 空间复杂度:O(log n),其中 n 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(log n) 即为排序所需要的空间复杂度。




2.C实现


留空,等变再牛B点再来手写快排加合并!


3.C++实现


class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return {};
}
sort(intervals.begin(), intervals.end());
vector<vector<int>> merge;
for (int i = 0; i < intervals.size(); ++i) {
int L = intervals[i][0], R = intervals[i][1];
if (!merge.size() || merge.back()[1] < L) {
merge.push_back({L, R});
}
else {
merge.back()[1] = max(merge.back()[1], R);
}
}
return merge;
}
};

🔆In The End!


请添加图片描述








从现在做起,坚持下去,一天进步一小点,不久的将来,你会感谢曾经努力的你!

作者:孤寒者
链接:https://juejin.cn/post/7023000958921605127
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Swift 枚举

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:它声明在类中,可以通过实例化类来访问它的值。枚举也可以定义构造函数(ini...
继续阅读 »

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。

Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:

  • 它声明在类中,可以通过实例化类来访问它的值。

  • 枚举也可以定义构造函数(initializers)来提供一个初始成员值;可以在原始的实现基础上扩展它们的功能。

  • 可以遵守协议(protocols)来提供标准的功能。

语法

Swift 中使用 enum 关键词来创建枚举并且把它们的整个定义放在一对大括号内:

enum enumname {
// 枚举定义放在这里
}

例如我们定义以下表示星期的枚举:

import Cocoa

// 定义枚举
enum DaysofaWeek {
case Sunday
case Monday
case TUESDAY
case WEDNESDAY
case THURSDAY
case FRIDAY
case Saturday
}

var weekDay = DaysofaWeek.THURSDAY
weekDay = .THURSDAY
switch weekDay
{
case .Sunday:
print("星期天")
case .Monday:
print("星期一")
case .TUESDAY:
print("星期二")
case .WEDNESDAY:
print("星期三")
case .THURSDAY:
print("星期四")
case .FRIDAY:
print("星期五")
case .Saturday:
print("星期六")
}

以上程序执行输出结果为:

星期四

枚举中定义的值(如 SundayMonday……Saturday)是这个枚举的成员值(或成员)。case关键词表示一行新的成员值将被定义。

注意: 和 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的DaysofaWeek例子中,SundayMonday……Saturday不会隐式地赋值为01……6。相反,这些枚举成员本身就有完备的值,这些值是已经明确定义好的DaysofaWeek类型。

var weekDay = DaysofaWeek.THURSDAY 

weekDay的类型可以在它被DaysofaWeek的一个可能值初始化时推断出来。一旦weekDay被声明为一个DaysofaWeek,你可以使用一个缩写语法(.)将其设置为另一个DaysofaWeek的值:

var weekDay = .THURSDAY 

weekDay的类型已知时,再次为其赋值可以省略枚举名。使用显式类型的枚举值可以让代码具有更好的可读性。

枚举可分为相关值与原始值。

相关值与原始值的区别

相关值原始值
不同数据类型相同数据类型
实例: enum {10,0.8,"Hello"}实例: enum {10,35,50}
值的创建基于常量或变量预先填充的值
相关值是当你在创建一个基于枚举成员的新常量或变量时才会被设置,并且每次当你这么做得时候,它的值可以是不同的。原始值始终是相同的

相关值

以下实例中我们定义一个名为 Student 的枚举类型,它可以是 Name 的一个字符串(String),或者是 Mark 的一个相关值(Int,Int,Int)。

import Cocoa

enum Student{
case Name(String)
case Mark(Int,Int,Int)
}
var studDetails = Student.Name("Runoob")
var studMarks = Student.Mark(98,97,95)
switch studMarks {
case .Name(let studName):
print("学生的名字是: \(studName)。")
case .Mark(let Mark1, let Mark2, let Mark3):
print("学生的成绩是: \(Mark1),\(Mark2),\(Mark3)。")
}

以上程序执行输出结果为:

学生的成绩是: 98,97,95。

原始值

原始值可以是字符串,字符,或者任何整型值或浮点型值。每个原始值在它的枚举声明中必须是唯一的。

在原始值为整数的枚举时,不需要显式的为每一个成员赋值,Swift会自动为你赋值。

例如,当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个值没有被赋初值,将会被自动置为0。

import Cocoa

enum Month: Int {
case January = 1, February, March, April, May, June, July, August, September, October, November, December
}

let yearMonth = Month.May.rawValue
print("数字月份为: \(yearMonth)。")

以上程序执行输出结果为:

数字月份为: 5。
收起阅读 »

Swift 闭包

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。全局函数和嵌套函数其实就是特殊的闭包。闭包的形式有...
继续阅读 »

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。

Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。

全局函数和嵌套函数其实就是特殊的闭包。

闭包的形式有:

全局函数嵌套函数闭包表达式
有名字但不能捕获任何值。有名字,也能捕获封闭函数内的值。无名闭包,使用轻量级语法,可以根据上下文环境捕获值。

Swift中的闭包有很多优化的地方:

  1. 根据上下文推断参数和返回值类型
  2. 从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略return)
  3. 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数...)
  4. 提供了尾随闭包语法(Trailing closure syntax)
  5. 语法

    以下定义了一个接收参数并返回指定类型的闭包语法:

    {(parameters) -> return type in
    statements
    }

    实例

    import Cocoa

    let studname = { print("Swift 闭包实例。") }
    studname
    ()

    以上程序执行输出结果为:

    Swift 闭包实例。

    以下闭包形式接收两个参数并返回布尔值:

    {(Int, Int) -> Bool in
    Statement1
    Statement 2
    ---
    Statement n
    }

    实例

    import Cocoa

    let divide = {(val1: Int, val2: Int) -> Int in
    return val1 / val2
    }
    let result = divide(200, 20)
    print (result)

    以上程序执行输出结果为:

    10

    闭包表达式

    闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。


    sorted 方法

    Swift 标准库提供了名为 sorted(by:) 的方法,会根据您提供的用于排序的闭包函数将已知类型数组中的值进行排序。

    排序完成后,sorted(by:) 方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:) 方法修改。

    sorted(by:)方法需要传入两个参数:

    • 已知类型的数组
    • 闭包函数,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    // 使用普通函数(或内嵌函数)提供排序功能,闭包函数类型需为(String, String) -> Bool。
    func backwards
    (s1: String, s2: String) -> Bool {
    return s1 > s2
    }
    var reversed = names.sorted(by: backwards)

    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果第一个字符串 (s1) 大于第二个字符串 (s2),backwards函数返回true,表示在新的数组中s1应该出现在s2前。 对于字符串中的字符来说,"大于" 表示 "按照字母顺序较晚出现"。 这意味着字母"B"大于字母"A",字符串"S"大于字符串"D"。 其将进行字母逆序排序,"AT"将会排在"AE"之前。


    参数名称缩写

    Swift 自动为内联函数提供了参数名称缩写功能,您可以直接通过$0,$1,$2来顺序调用闭包的参数。

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted( by: { $0 > $1 } )
    print(reversed)

    $0和$1表示闭包中第一个和第二个String类型的参数。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果你在闭包表达式中使用参数名称缩写, 您可以在闭包参数列表中省略对其定义, 并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字同样也可以被省略.


    运算符函数

    实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。

    Swift 的String类型定义了关于大于号 (>) 的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。 而这正好与sort(_:)方法的第二个参数需要的函数类型相符合。 因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted(by: >)
    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    尾随闭包

    尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。

    func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
    }

    // 以下是不使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    ({
    // 闭包主体部分
    })

    // 以下是使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    () {
    // 闭包主体部分
    }

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    //尾随闭包
    var reversed = names.sorted() { $0 > $1 }
    print(reversed)

    sort() 后的 { $0 > $1} 为尾随闭包。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    注意: 如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。

    reversed = names.sorted { $0 > $1 }

    捕获值

    闭包可以在其定义的上下文中捕获常量或变量。

    即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

    Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。

    嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

    看这个例子:

    func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    一个函数makeIncrementor ,它有一个Int型的参数amout, 并且它有一个外部参数名字forIncremet,意味着你调用的时候,必须使用这个外部名字。返回值是一个()-> Int的函数。

    函数体内,声明了变量 runningTotal 和一个函数 incrementor。

    incrementor函数并没有获取任何参数,但是在函数体内访问了runningTotal和amount变量。这是因为其通过捕获在包含它的函数体内已经存在的runningTotal和amount变量而实现。

    由于没有修改amount变量,incrementor实际上捕获并存储了该变量的一个副本,而该副本随着incrementor一同被存储。

    所以我们调用这个函数时会累加:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    print(incrementByTen())

    // 返回的值为20
    print(incrementByTen())

    // 返回的值为30
    print(incrementByTen())

    以上程序执行输出结果为:

    10
    20
    30

    闭包是引用类型

    上面的例子中,incrementByTen是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值。

    这是因为函数和闭包都是引用类型。

    无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,incrementByTen指向闭包的引用是一个常量,而并非闭包内容本身。

    这也意味着如果您将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    incrementByTen
    ()

    // 返回的值为20
    incrementByTen
    ()

    // 返回的值为30
    incrementByTen
    ()

    // 返回的值为40
    incrementByTen
    ()

    let alsoIncrementByTen = incrementByTen

    // 返回的值也为50
    print(alsoIncrementByTen())
收起阅读 »

Swift 函数

Swift 函数用来完成特定任务的独立的代码块。Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。函数声明: 告诉编译器函数的名字,返回类型及参数。函数定义: 提供了函数的实体。Swift 函数包含了参数类型...
继续阅读 »

Swift 函数用来完成特定任务的独立的代码块。

Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。

  • 函数声明: 告诉编译器函数的名字,返回类型及参数。

  • 函数定义: 提供了函数的实体。

Swift 函数包含了参数类型及返回值类型:


函数定义

Swift 定义函数使用关键字 func

定义函数的时候,可以指定一个或多个输入参数和一个返回值类型。

每个函数都有一个函数名来描述它的功能。通过函数名以及对应类型的参数值来调用这个函数。函数的参数传递的顺序必须与参数列表相同。

函数的实参传递的顺序必须与形参列表相同,-> 后定义函数的返回值类型。

语法

func funcname(形参) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

以下我们定义了一个函数名为 runoob 的函数,形参的数据类型为 String,返回值也为 String:

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数调用

我们可以通过函数名以及对应类型的参数值来调用函数,函数的参数传递的顺序必须与参数列表相同。

以下我们定义了一个函数名为 runoob 的函数,形参 site 的数据类型为 String,之后我们调用函数传递的实参也必须 String 类型,实参传入函数体后,将直接返回,返回的数据类型为 String。

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数参数

函数可以接受一个或者多个参数,这些参数被包含在函数的括号之中,以逗号分隔。

以下实例向函数 runoob 传递站点名 name 和站点地址 site:

import Cocoa

func runoob
(name: String, site: String) -> String {
return name + site
}
print(runoob(name: "菜鸟教程:", site: "www.runoob.com"))
print(runoob(name: "Google:", site: "www.google.com"))

以上程序执行输出结果为:

菜鸟教程:www.runoob.com
Googlewww.google.com

不带参数函数

我们可以创建不带参数的函数。

语法:

func funcname() -> datatype {
return datatype
}

实例

import Cocoa

func sitename
() -> String {
return "菜鸟教程"
}
print(sitename())

以上程序执行输出结果为:

菜鸟教程

元组作为函数返回值

函数返回值类型可以是字符串,整型,浮点型等。

元组与数组类似,不同的是,元组中的元素可以是任意类型,使用的是圆括号。

你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。

下面的这个例子中,定义了一个名为minMax(_:)的函数,作用是在一个Int数组中找出最小值与最大值。

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("最小值为 \(bounds.min) ,最大值为 \(bounds.max)")

minMax(_:)函数返回一个包含两个Int值的元组,这些值被标记为min和max,以便查询函数的返回值时可以通过名字访问它们。

以上程序执行输出结果为:

最小值为 -6 ,最大值为 109

如果你不确定返回的元组一定不为nil,那么你可以返回一个可选的元组类型。

你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如(Int, Int)?或(String, Int, Bool)?

注意
可选元组类型如(Int, Int)?与元组包含可选类型如(Int?, Int?)是不同的.可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。

前面的minMax(_:)函数返回了一个包含两个Int值的元组。但是函数不会对传入的数组执行任何安全检查,如果array参数是一个空数组,如上定义的minMax(_:)在试图访问array[0]时会触发一个运行时错误。

为了安全地处理这个"空数组"问题,将minMax(_:)函数改写为使用可选元组返回类型,并且当数组为空时返回nil

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("最小值为 \(bounds.min),最大值为 \(bounds.max)")
}

以上程序执行输出结果为:

最小值为 -6,最大值为 109

没有返回值函数

下面是 runoob(_:) 函数的另一个版本,这个函数接收菜鸟教程官网网址参数,没有指定返回值类型,并直接输出 String 值,而不是返回它:

import Cocoa

func runoob
(site: String) {
print("菜鸟教程官网:\(site)")
}
runoob
(site: "http://www.runoob.com")

以上程序执行输出结果为:

菜鸟教程官网:http://www.runoob.com

函数参数名称

函数参数都有一个外部参数名和一个局部参数名。

局部参数名

局部参数名在函数的实现内部使用。

func sample(number: Int) {
println
(number)
}

以上实例中 number 为局部参数名,只能在函数体内使用。

import Cocoa

func sample
(number: Int) {
print(number)
}
sample
(number: 1)
sample
(number: 2)
sample
(number: 3)

以上程序执行输出结果为:

1
2
3

外部参数名

你可以在局部参数名前指定外部参数名,中间以空格分隔,外部参数名用于在函数调用时传递给函数的参数。

如下你可以定义以下两个函数参数名并调用它:

import Cocoa

func pow
(firstArg a: Int, secondArg b: Int) -> Int {
var res = a
for _ in 1..<b {
res
= res * a
}
print(res)
return res
}
pow
(firstArg:5, secondArg:3)

以上程序执行输出结果为:

125

注意
如果你提供了外部参数名,那么函数在被调用时,必须使用外部参数名。


可变参数

可变参数可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数,其数量是不确定的。

可变参数通过在变量类型名后面加入(...)的方式来定义。

import Cocoa

func vari
<N>(members: N...){
for i in members {
print(i)
}
}
vari
(members: 4,3,5)
vari
(members: 4.5, 3.1, 5.6)
vari
(members: "Google", "Baidu", "Runoob")

以上程序执行输出结果为:

4
3
5
4.5
3.1
5.6
Google
Baidu
Runoob

常量,变量及 I/O 参数

一般默认在函数中定义的参数都是常量参数,也就是这个参数你只可以查询使用,不能改变它的值。

如果想要声明一个变量参数,可以在参数定义前加 inout 关键字,这样就可以改变这个参数的值了。

例如:

func  getName(_ name: inout String).........

此时这个 name 值可以在函数中改变。

一般默认的参数传递都是传值调用的,而不是传引用。所以传入的参数在函数内改变,并不影响原来的那个参数。传入的只是这个参数的副本。

当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。

实例

import Cocoa

func swapTwoInts
(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a
= b
b
= temporaryA
}


var x = 1
var y = 5
swapTwoInts
(&x, &y)
print("x 现在的值 \(x), y 现在的值 \(y)")

swapTwoInts(_:_:) 函数简单地交换 a 与 b 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b。

需要注意的是,someInt 和 anotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀。

以上程序执行输出结果为:

x 现在的值 5, y 现在的值 1

函数类型及使用

每个函数都有种特定的函数类型,由函数的参数类型和返回类型组成。

func inputs(no1: Int, no2: Int) -> Int {
return no1/no2
}

inputs 函数类型有两个 Int 型的参数(no1、no2)并返回一个 Int 型的值。

实例如下:

import Cocoa

func inputs
(no1: Int, no2: Int) -> Int {
return no1/no2
}
print(inputs(no1: 20, no2: 10))
print(inputs(no1: 36, no2: 6))

以上程序执行输出结果为:

2
6

以上函数定义了两个 Int 参数类型,返回值也为 Int 类型。

接下来我们看下如下函数,函数定义了参数为 String 类型,返回值为 String 类型。

func inputstr(name: String) -> String {
return name
}

函数也可以定义一个没有参数,也没有返回值的函数,如下所示:

import Cocoa

func inputstr
() {
print("菜鸟教程")
print("www.runoob.com")
}
inputstr
()

以上程序执行输出结果为:

菜鸟教程
www
.runoob.com

使用函数类型

在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:

var addition: (Int, Int) -> Int = sum

解析:

"定义一个叫做 addition 的变量,参数与返回值类型均是 Int ,并让这个新变量指向 sum 函数"。

sum 和 addition 有同样的类型,所以以上操作是合法的。

现在,你可以用 addition 来调用被赋值的函数了:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

以上程序执行输出结果为:

输出结果: 129

函数类型作为参数类型、函数类型作为返回类型

我们可以将函数作为参数传递给另外一个参数:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

func another
(addition: (Int, Int) -> Int, a: Int, b: Int) {
print("输出结果: \(addition(a, b))")
}
another
(addition: sum, a: 10, b: 20)

以上程序执行输出结果为:

输出结果: 129
输出结果: 30

函数嵌套

函数嵌套指的是函数内定义一个新的函数,外部的函数可以调用函数内定义的函数。

实例如下:

import Cocoa

func calcDecrement
(forDecrement total: Int) -> () -> Int {
var overallDecrement = 0
func decrementer
() -> Int {
overallDecrement
-= total
return overallDecrement
}
return decrementer
}
let decrem = calcDecrement(forDecrement: 30)
print(decrem())

以上程序执行输出结果为:

-30
收起阅读 »

Swift 字典

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我...
继续阅读 »

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。

Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。

和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。

Swift 字典的key没有类型限制可以是整型或字符串,但必须是唯一的。

如果创建一个字典,并赋值给一个变量,则创建的字典就是可以修改的。这意味着在创建字典后,可以通过添加、删除、修改的方式改变字典里的项目。如果将一个字典赋值给常量,字典就不可修改,并且字典的大小和内容都不可以修改。


创建字典

我们可以使用以下语法来创建一个特定类型的空字典:

var someDict =  [KeyType: ValueType]()

以下是创建一个空字典,键的类型为 Int,值的类型为 String 的简单语法:

var someDict = [Int: String]()

以下为创建一个字典的实例:

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

访问字典

我们可以根据字典的索引来访问数组的元素,语法如下:

var someVar = someDict[key]

我们可以通过以下实例来学习如何创建,初始化,访问字典:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var someVar = someDict[1]

print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

修改字典

我们可以使用 updateValue(forKey:) 增加或更新字典的内容。如果 key 不存在,则添加值,如果存在则修改 key 对应的值。updateValue(_:forKey:)方法返回Optional值。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict.updateValue("One 新的值", forKey: 1)

var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

你也可以通过指定的 key 来修改字典的值,如下所示:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict[1]
someDict
[1] = "One 新的值"
var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

移除 Key-Value 对

我们可以使用 removeValueForKey() 方法来移除字典 key-value 对。如果 key 存在该方法返回移除的值,如果不存在返回 nil 。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var removedValue = someDict.removeValue(forKey: 2)

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

你也可以通过指定键的值为 nil 来移除 key-value(键-值)对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

someDict
[2] = nil

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

遍历字典

我们可以使用 for-in 循环来遍历某个字典中的键值对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict {
print("字典 key \(key) - 字典 value \(value)")
}

以上程序执行输出结果为:

字典 key 2 -  字典 value Two
字典 key 3 - 字典 value Three
字典 key 1 - 字典 value One

我们也可以使用enumerate()方法来进行字典遍历,返回的是字典的索引及 (key, value) 对,实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict.enumerated() {
print("字典 key \(key) - 字典 (key, value) 对 \(value)")
}

以上程序执行输出结果为:

字典 key 0 -  字典 (key, value)  (2, "Two")
字典 key 1 - 字典 (key, value) (3, "Three")
字典 key 2 - 字典 (key, value) (1, "One")

字典转换为数组

你可以提取字典的键值(key-value)对,并转换为独立的数组。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

let dictKeys = [Int](someDict.keys)
let dictValues = [String](someDict.values)

print("输出字典的键(key)")

for (key) in dictKeys {
print("\(key)")
}

print("输出字典的值(value)")

for (value) in dictValues {
print("\(value)")
}

以上程序执行输出结果为:

输出字典的键(key)
2
3
1
输出字典的值(value)
Two
Three
One

count 属性

我们可以使用只读的 count 属性来计算字典有多少个键值对:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]

print("someDict1 含有 \(someDict1.count) 个键值对")
print("someDict2 含有 \(someDict2.count) 个键值对")

以上程序执行输出结果为:

someDict1 含有 3 个键值对
someDict2
含有 2 个键值对

isEmpty 属性

Y我们可以通过只读属性 isEmpty 来判断字典是否为空,返回布尔值:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]
var someDict3:[Int:String] = [Int:String]()

print("someDict1 = \(someDict1.isEmpty)")
print("someDict2 = \(someDict2.isEmpty)")
print("someDict3 = \(someDict3.isEmpty)")

以上程序执行输出结果为:

someDict1 = false
someDict2
= false
someDict3
= true
收起阅读 »

使用 Kotlin Flow 优化你的网络请求框架,减少模板代码

一、以前封装的遗憾点 主要集中在如下2点上: Loading的处理 多余的LiveData 总而言之,就是需要写很多模板代码。 不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小. 1.1 Loading的处理 对于封装二,虽然...
继续阅读 »

一、以前封装的遗憾点


主要集中在如下2点上:




  • Loading的处理




  • 多余的LiveData




总而言之,就是需要写很多模板代码。



不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小.



1.1 Loading的处理


对于封装二,虽然解耦比封装一更彻底,但是关于Loading这里我觉得还是有遗憾。


试想一下:如果Activity中业务很多、逻辑复杂,存在很多个网络请求,在需要网络请求的地方都要手动去showLoading() ,然后在 observer() 中手动调用 stopLoading()


假如Activity中代码业务复杂,存在多个api接口,这样Activity中就存在很多个与loading有关的方法。


此外,如果一个网络请求的showLoading()方法和dismissLoading()方法相隔很远。会导致一个顺序流程的割裂。


请求开始前showLoading() ---> 请求网络 ---> 结束后stopLoading(),这是一个完整的流程,代码也应该尽量在一起,一目了然,不应该割裂存在。


如果代码量一多,以后维护起来,万一不小心删除了某个showLoading()或者stopLoading(),也容易导致问题。


还有就是每次都要手动调用这两个方法,麻烦。


1.2 重复的LiveData声明


个人认为常用的网络请求分为两大类:




  • 用完即丢




  • 需要监听数据变化




举个常见的例子,看下面这个页面:


image.png


用户一进入这个页面,绿色框里面内容基本不会变化,(不去纠结微信这个页面是不是webview之类的),这种ui其实是不需要设置一个LiveData去监听的,因为它几乎不会再更新了。


典型的还有:点击登录按钮,成功后就进去了下一个页面。


但是红色的框里面的ui不一样,需要实时刷新数据,也就用到LiveData监听,这种情况下观察者订阅者模式的好处才真正展示出来。并且从其他页面过来,LiveData也会把最新的数据自动更新。


对于用完即丢的网络请求,LoginViewModel会存在这种代码:


// LoginViewModel.kt
val loginLiveData = MutableLiveData<User?>()
val logoutLiveData = MutableLiveData<Any?>()
val forgetPasswordLiveData = MutableLiveData<User?>(

并且对应的Activity中也需要监听这3个LiveData。


这种模板代码让我写的很烦。


用了Flow优化后,完美的解决这2个痛点。



“Talk is cheap. Show me the code.”



二、集成Flow之后的用法


2.1 请求自带Loading&&不需要监听数据变化


需求:




  • 不需要监听数据变化,对应上面的用完即丢




  • 不需要在ViewModel中声明LiveData成员对象




  • 发起请求之前自动showLoading(),请求结束后自动stopLoading()




  • 类似于点击登录按钮,finish 当前页面,跳转到下一个页面




TestActivity 中示例代码:


// TestActivity.kt
private fun login() {
launchWithLoadingAndCollect({mViewModel.login("username", "password")}) {
onSuccess = { data->
showSuccessView(data)
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onError = {e ->
e.printStackTrace()
}
}
}

TestViewModel 中代码:


// TestViewModel中代码
suspend fun login(username: String, password: String): ApiResponse<User?> {
return repository.login(username, password)
}

2.2 请求不带Loading&&不需要声明LiveData


需求:




  • 不需要监听数据变化




  • 不需要在ViewModel中声明LiveData成员对象




  • 不需要Loading的展示




// TestActivity.kt
private fun getArticleDetail() {
launchAndCollect({ mViewModel.getArticleDetail() }) {
onSuccess = {
showSuccessView()
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onDataEmpty = {
showEmptyView()
}
}
}

TestViewModel 中代码和上面一样,这里就不写了。


是不是非常简单,一个方法搞定,将Loading的逻辑都隐藏了,再也不需要手动写 showLoading()stopLoading()


并且请求的结果直接在回调里面接收,直接处理,这样请求网络和结果的处理都在一起,看起来一目了然,再也不需要在 Activity 中到处找在哪监听的 LiveData


同样,它跟 LiveData 一样,也会监听 Activity 的生命周期,不会造成内存泄露。因为它是运行在ActivitylifecycleScope 协程作用域中的。


2.3 需要监听数据变化


需求:




  • 需要监听数据变化,要实时更新数据




  • 需要在 ViewModel 中声明 LiveData 成员对象




  • 例如实时获取最新的配置、最新的用户信息等




TestActivity 中示例代码:


// TestActivity.kt
class TestActivity : AppCompatActivity(R.layout.activity_api) {

private fun initObserver() {
mViewModel.wxArticleLiveData.observeState(this) {

onSuccess = { data: List<WxArticleBean>? ->
showSuccessView(data)
}

onDataEmpty = { showEmptyView() }

onFailed = { code, msg -> showFailedView(code, msg) }

onError = { showErrorView() }
}
}

private fun requestNet() {
// 需要Loading
launchWithLoading {
mViewModel.requestNet()
}
}
}

ViewModel 中示例代码:


class ApiViewModel : ViewModel() {

private val repository by lazy { WxArticleRepository() }

val wxArticleLiveData = StateMutableLiveData<List<WxArticleBean>>()

suspend fun requestNet() {
wxArticleLiveData.value = repository.fetchWxArticleFromNet()
}
}

本质上是通过FLow来调用LiveDatasetValue()方法,还是LiveData的使用。虽然可以完全用 Flow 来实现,但是我觉得这里用 Flow 的方式麻烦,不容易懂,还是怎么简单怎么来。


这种方式其实跟上篇文章中的封装二差不多,区别就是不需要手动调用Loading有关的方法。


三、拆封装


如果不抽取通用方法是这样写的:


// TestActivity.kt
private fun login() {
lifecycleScope.launch {
flow {
emit(mViewModel.login("username", "password"))
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}.collect { response ->
when (response) {
is ApiSuccessResponse -> showSuccessView(response.data)
is ApiEmptyResponse -> showEmptyView()
is ApiFailedResponse -> showFailedView(response.errorCode, response.errorMsg)
is ApiErrorResponse -> showErrorView(response.error)
}
}
}
}

简单介绍下Flow


Flow类似于RxJava,操作符都跟Rxjava差不多,但是比Rxjava简单很多,kotlin通过flow来实现顺序流和链式编程。


flow关键字大括号里面的是方法的执行,结果通过emit发送给下游。


onStart表示最开始调用方法之前执行的操作,这里是展示一个 loading ui


onCompletion表示所有执行完成,不管有没有异常都会执行这个回调。


collect表示执行成功的结果回调,就是emit()方法发送的内容,flow必须执行collect才能有结果。因为是冷流,对应的还有热流。


更多的Flow知识点可以参考其他博客和官方文档。


这里可以看出,通过Flow完美的解决了loading的显示与隐藏。


我这里是在Activity中都调用flow的流程,这样我们扩展BaseActivity即可。


为什么扩展的是BaseActivity?


因为startLoading()stopLoading()BaseActivity中。😂


3.1 解决 flow 的 Loading 模板代码


fun <T> BaseActivity.launchWithLoadingGetFlow(block: suspend () -> ApiResponse<T>): Flow<ApiResponse<T>> {
return flow {
emit(block())
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}
}

这样每次调用launchWithLoadingGetFlow方法,里面就实现了 Loading 的展示与隐藏,并且会返回一个 FLow 对象。


下一步就是处理 flow 结果collect里面的模板代码。


3.2 声明结果回调类


class ResultBuilder<T> {
var onSuccess: (data: T?) -> Unit = {}
var onDataEmpty: () -> Unit = {}
var onFailed: (errorCode: Int?, errorMsg: String?) -> Unit = { _, _ -> }
var onError: (e: Throwable) -> Unit = { e -> }
var onComplete: () -> Unit = {}
}

各种回调按照项目特性删减即可。


3.3 对ApiResponse对象进行解析


private fun <T> parseResultAndCallback(response: ApiResponse<T>, 
listenerBuilder: ResultBuilder<T>.() -> Unit) {
val listener = ResultBuilder<T>().also(listenerBuilder)
when (response) {
is ApiSuccessResponse -> listener.onSuccess(response.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(response.errorCode, response.errorMsg)
is ApiErrorResponse -> listener.onError(response.throwable)
}
listener.onComplete()
}

上篇文章这里的处理用的是继承LiveDataObserver,这里就不需要了,毕竟继承能少用就少用。


3.4 最终抽取方法


将上面的步骤连起来如下:


fun <T> BaseActivity.launchWithLoadingAndCollect(block: suspend () -> ApiResponse<T>, 
listenerBuilder: ResultBuilder<T>.() -> Unit) {
lifecycleScope.launch {
launchWithLoadingGetFlow(block).collect { response ->
parseResultAndCallback(response, listenerBuilder)
}
}
}

3.5 将Flow转换成LiveData对象


获取到的是Flow对象,如果想要变成LiveDataFlow原生就支持将Flow对象转换成不可变的LiveData对象。


val loginFlow: Flow<ApiResponse<User?>> =
launchAndGetFlow(requestBlock = { mViewModel.login("UserName", "Password") })
val loginLiveData: LiveData<ApiResponse<User?>> = loginFlow.asLiveData()

调用的是 Flow 的asLiveData()方法,原理也很简单,就是用了livedata的扩展函数:


@JvmOverloads
fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}

这里返回的是LiveData<ApiResponse<User?>>对象,如果想要跟上篇文章一样用StateLiveData,在observe的回调里面监听不同状态的callback


以前的方式是继承,有如下缺点:



  • 必须要用StateLiveData,不能用原生的LiveData,侵入性很强

  • 不只是继承LiveData,还要继承Observer,麻烦

  • 为了实现这个,写了一堆的代码


这里用 Kotlin 扩展实现,直接扩展 LiveData


@MainThread
inline fun <T> LiveData<ApiResponse<T>>.observeState(
owner: LifecycleOwner,
listenerBuilder: ResultBuilder<T>.() -> Unit
) {
val listener = ResultBuilder<T>().also(listenerBuilder)
observe(owner) { apiResponse ->
when (apiResponse) {
is ApiSuccessResponse -> listener.onSuccess(apiResponse.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(apiResponse.errorCode, apiResponse.errorMsg)
is ApiErrorResponse -> listener.onError(apiResponse.throwable)
}
listener.onComplete()
}
}

感谢Flywith24开源库提供的思路,感觉自己有时候还是在用Java的思路在写Kotlin。


3.6 进一步完善


很多网络请求的相关并不是只有 loading 状态,还需要在请求前和结束后处理一些特定的逻辑。


这里的方式是:直接在封装方法的参数加 callback,默认用是 loading 的实现。


fun <T> BaseActivity.launchAndCollect(
requestBlock: suspend () -> ApiResponse<T>,
startCallback: () -> Unit = { showLoading() },
completeCallback: () -> Unit = { dismissLoading() },
listenerBuilder: ResultBuilder<T>.() -> Unit
)

四、针对多数据来源


虽然项目中大部分都是单一数据来源,但是也偶尔会出现多数据来源,多数据源结合Flow的操作符,也非常的方便。


示例


假如同一份数据可以从数据库获取,可以从网络请求获取,TestRepository的代码如下:


// TestRepository.kt
suspend fun fetchDataFromNet(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = executeHttp { mService.getWxArticle() }
return flow { emit(response) }.flowOn(Dispatchers.IO)
}

suspend fun fetchDataFromDb(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = getDataFromRoom()
return flow { emit(response) }.flowOn(Dispatchers.IO)


Repository中的返回不再直接返回实体类,而是返回flow包裹的实体类对象。


为什么要这么做?


为了用神奇的flow操作符来处理。


flow组合操作符



  • combine、combineTransform


combine操作符可以连接两个不同的Flow。



  • merge


merge操作符用于将多个流合并。



  • zip


zip操作符会分别从两个流中取值,当一个流中的数据取完,zip过程就完成了。


关于 Flow 的基础操作符,徐医生大神的这篇文章已经写的很棒了,这里就不多余的写了。


根据操作符的示例可以看出,就算返回的不是同一个对象,也可以用操作符进行处理。


几年前刚开始学RxJava时,好几次都是入门到放弃,操作符太多了,搞的也很懵逼,Flow 真的比它简单太多了。


五、flow的奇淫技巧


flowWithLifecycle


需求:
Activity 的 onSume() 方法中请求最新的地理位置信息。


以前的写法:


// TestActivity.kt
override fun onResume() {
super.onResume()
getLastLocation()
}

override fun onDestory() {
super.onDestory()
// 释放获取定位的代码,防止内存泄露
}

这种写法没问题,也很正常,但是用了 Flow 之后,有一种新的写法。


用了 flow 的写法:


// TestActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getLastLocation()
}

@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private fun getLastLocation() {
if (LocationPermissionUtils.isLocationProviderEnabled() && LocationPermissionUtils.isLocationPermissionGranted()) {
lifecycleScope.launch {
SharedLocationManager(lifecycleScope)
.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}
}
}
}

onCreate中书写该函数,然后 flow 的链式调用中加入:


.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)


flowWithLifecycle能监听 Activity 的生命周期,在 Activity 的onResume开始请求位置信息,onStop 时自动停止,不会导致内存泄露。



flowWithLifecycle 会在生命周期进入和离开目标状态时发送项目和取消内部的生产者。



这个api需要引入 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01依赖库。


callbackFlow


有没有发现5.1中调用获取位置信息的代码很简单?


SharedLocationManager(lifecycleScope)
.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}

几行代码解决获取位置信息,并且任何地方都直接调用,不要写一堆代码。


这里就是用到callbackFlow,简而言之,callbackFlow就是将callback回调代码变成同步的方式来写。


这里直接上SharedLocationManager的代码,具体细节自行 Google,因为这就不是网络框架的内容。


这里附上主要的代码:


@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private val _locationUpdates: SharedFlow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
Log.d(TAG, "New location: ${result.lastLocation}")
trySend(result.lastLocation)
}

}
Log.d(TAG, "Starting location updates")

fusedLocationClient.requestLocationUpdates(
locationRequest,callback,Looper.getMainLooper())
.addOnFailureListener { e ->close(e)}

awaitClose {
Log.d(TAG, "Stopping location updates")
fusedLocationClient.removeLocationUpdates(callback)
}
}.shareIn(
externalScope,
replay = 0,
started = SharingStarted.WhileSubscribed()
)

完整代码见:GitHub


总结


上一篇文章# 两种方式封装Retrofit+协程,实现优雅快速的网络请求


加上这篇的 flow 网络请求封装,一共是三种对Retrofit+协程的网络封装方式。


对比下三种封装方式:




  • 封装一 (对应分支oneWay) 传递ui引用,可按照项目进行深度ui定制,方便快速,但是耦合高




  • 封装二 (对应分支master) 耦合低,依赖的东西很少,但是写起来模板代码偏多




  • 封装三 (对应分支dev) 引入了新的flow流式编程(虽然出来很久,但是大部分人应该还没用到),链式调用,loading 和网络请求以及结果处理都在一起,很多时候甚至都不要声明 LiveData 对象。




第二种封装我在公司的商业项目App中用了很长时间了,涉及几十个接口,暂时没遇到什么问题。


第三种是我最近才折腾出来的,在公司的新项目中(还没上线)使用,也暂时没遇到什么问题。


如果某位大神看到这篇文章,有不同意见,或者发现封装三有漏洞,欢迎指出,不甚感谢!


项目地址


FastJetpack


项目持续更新...


作者:零先生
链接:https://juejin.cn/post/7022823222928211975
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

MVVM 进阶版:MVI 架构了解一下~

MVI
前言 Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。 不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而...
继续阅读 »

前言


Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。

不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而MVI可以很好的解决一部分MVVM的痛点。

本文主要包括以下内容



  1. MVC,MVP,MVVM等经典架构介绍

  2. MVI架构到底是什么?

  3. MVI架构实战



需要重点指出的是,标题中说MVI架构是MVVM的进阶版是指MVIMVVM非常相似,并在其基础上做了一定的改良,并不是说MVI架构一定比MVVM适合你的项目

各位同学可以在分析比较各个架构后,选择合适项目场景的架构



经典架构介绍


MVC架构介绍


MVC是个古老的Android开发架构,随着MVPMVVM的流行已经逐渐退出历史舞台,我们在这里做一个简单的介绍,其架构图如下所示:



MVC架构主要分为以下几部分



  1. 视图层(View):对应于xml布局文件和java代码动态view部分

  2. 控制层(Controller):主要负责业务逻辑,在android中由Activity承担,同时因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。

  3. 模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源


由于androidxml布局的功能性太弱,Activity实际上负责了View层与Controller层两者的工作,所以在androidmvc更像是这种形式:



因此MVC架构在android平台上的主要存在以下问题:



  1. Activity同时负责ViewController层的工作,违背了单一职责原则

  2. Model层与View层存在耦合,存在互相依赖,违背了最小知识原则


MVP架构介绍


由于MVC架构在Android平台上的一些缺陷,MVP也就应运而生了,其架构图如下所示



MVP架构主要分为以下几个部分



  1. View层:对应于ActivityXML,只负责显示UI,只与Presenter层交互,与Model层没有耦合

  2. Presenter层: 主要负责处理业务逻辑,通过接口回调View

  3. Model层:主要负责网络请求,数据库处理等操作,这个没有什么变化


我们可以看到,MVP解决了MVC的两个问题,即Activity承担了两层职责与View层与Model层耦合的问题


MVP架构同样有自己的问题



  1. Presenter层通过接口与View通信,实际上持有了View的引用

  2. 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成View的接口会很庞大。


MVVM架构介绍


MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向数据绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然

MVVM架构图如下所示:



可以看出MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP


MVVM的双向数据绑定主要通过DataBinding实现,不过相信有很多人跟我一样,是不喜欢用DataBinding的,这样架构就变成了下面这样



  1. View观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性我其实并没有用到

  2. View通过调用ViewModel提供的方法来与ViewMdoel交互


小结



  1. MVC架构的主要问题在于Activity承担了ViewController两层的职责,同时View层与Model层存在耦合

  2. MVP引入Presenter层解决了MVC架构的两个问题,View只能与Presenter层交互,业务逻辑放在Presenter

  3. MVP的问题在于随着业务逻辑的增加,View的接口会很庞大,MVVM架构通过双向数据绑定可以解决这个问题

  4. MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP

  5. MVVM的双向数据绑定主要通过DataBinding实现,但有很多人(比如我)不喜欢用DataBinding,而是View通过LiveData等观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定


MVI架构到底是什么?


MVVM架构有什么不足?


要了解MVI架构,我们首先来了解下MVVM架构有什么不足

相信使用MVVM架构的同学都有如下经验,为了保证数据流的单向流动,LiveData向外暴露时需要转化成immutable的,这需要添加不少模板代码并且容易遗忘,如下所示


class TestViewModel : ViewModel() {
//为保证对外暴露的LiveData不可变,增加一个状态就要添加两个LiveData变量
private val _pageState: MutableLiveData<PageState> = MutableLiveData()
val pageState: LiveData<PageState> = _pageState
private val _state1: MutableLiveData<String> = MutableLiveData()
val state1: LiveData<String> = _state1
private val _state2: MutableLiveData<String> = MutableLiveData()
val state2: LiveData<String> = _state2
//...
}

如上所示,如果页面逻辑比较复杂,ViewModel中将会有许多全局变量的LiveData,并且每个LiveData都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM架构写比较复杂页面时最难受的点。

其次就是View层通过调用ViewModel层的方法来交互的,View层与ViewModel的交互比较分散,不成体系


小结一下,在我的使用中,MVVM架构主要有以下不足



  1. 为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘

  2. View层与ViewModel层的交互比较分散零乱,不成体系


MVI架构是什么?


MVIMVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示



其主要分为以下几部分



  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态

  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Intent的变化实现界面刷新(注意:这里不是ActivityIntent

  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求


单向数据流


MVI强调数据的单向流动,主要分为以下几步:



  1. 用户操作以Intent的形式通知Model

  2. Model基于Intent更新State

  3. View接收到State变化刷新UI。


数据永远在一个环形结构中单向流动,不能反向流动:


上面简单的介绍了下MVI架构,下面我们一起来看下具体是怎么使用MVI架构的


MVI架构实战


总体架构图




我们使用ViewModel来承载MVIModel层,总体结构也与MVVM类似,主要区别在于ModelView层交互的部分



  1. Model层承载UI状态,并暴露出ViewStateView订阅,ViewState是个data class,包含所有页面状态

  2. View层通过Action更新ViewState,替代MVVM通过调用ViewModel方法交互的方式


MVI实例介绍


添加ViewStateViewEvent


ViewState承载页面的所有状态,ViewEvent则是一次性事件,如Toast等,如下所示


data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)  

sealed class MainViewEvent {
data class ShowSnackbar(val message: String) : MainViewEvent()
data class ShowToast(val message: String) : MainViewEvent()
}


  1. 我们这里ViewState只定义了两个,一个是请求状态,一个是页面数据

  2. ViewEvent也很简单,一个简单的密封类,显示ToastSnackbar


ViewState更新


class MainViewModel : ViewModel() {
private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData()
val viewStates = _viewStates.asLiveData()
private val _viewEvents: SingleLiveEvent<MainViewEvent> = SingleLiveEvent()
val viewEvents = _viewEvents.asLiveData()

init {
emit(MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList()))
}

private fun fabClicked() {
count++
emit(MainViewEvent.ShowToast(message = "Fab clicked count $count"))
}

private fun emit(state: MainViewState?) {
_viewStates.value = state
}

private fun emit(event: MainViewEvent?) {
_viewEvents.value = event
}
}

如上所示



  1. 我们只需定义ViewStateViewEvent两个State,后续增加状态时在data class中添加即可,不需要再写模板代码

  2. ViewEvents是一次性的,通过SingleLiveEvent实现,当然你也可以用Channel当来实现

  3. 当状态更新时,通过emit来更新状态


View监听ViewState


    private fun initViewModel() {
viewModel.viewStates.observe(this) {
renderViewState(it)
}
viewModel.viewEvents.observe(this) {
renderViewEvent(it)
}
}

如上所示,MVI 使用 ViewStateState 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。


View通过Action更新State


class MainActivity : AppCompatActivity() {
private fun initView() {
fabStar.setOnClickListener {
viewModel.dispatch(MainViewAction.FabClicked)
}
}
}
class MainViewModel : ViewModel() {
fun dispatch(action: MainViewAction) =
reduce(viewStates.value, action)

private fun reduce(state: MainViewState?, viewAction: MainViewAction) {
when (viewAction) {
is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
MainViewAction.FabClicked -> fabClicked()
MainViewAction.OnSwipeRefresh -> fetchNews(state)
MainViewAction.FetchNews -> fetchNews(state)
}
}
}

如上所示,View通过ActionViewModel交互,通过 Action 通信,有利于 ViewViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控


总结


本文主要介绍了MVC,MVP,MVVMMVI架构,目前MVVM是官方推荐的架构,但仍然有以下几个痛点



  1. MVVMMVP的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源

  2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘

  3. ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护


MVI可以比较好的解决以上痛点,它主要有以下优势



  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯

  2. 使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. ViewModel通过ViewStateAction通信,通过浏览ViewStateAciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。


当然MVI也有一些缺点,比如



  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀

  2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销


软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。

但通过以上的分析与介绍,我相信使用MVI架构代替没有使用DataBindingMVVM是一个比较好的选择~


Sample代码


github.com/shenzhen201…


作者:RicardoMJiang
链接:https://juejin.cn/post/7022624191723601928
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »