注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

发布Android库至Maven Central详解

最近,使用compose编写了一个类QQ的image picker。完成android library的编写,在此记录下发布这个Library到maven central的流程以及碰到的问题。 maven:mvnrepository.com/artifact/...
继续阅读 »

最近,使用compose编写了一个类QQ的image picker。完成android library的编写,在此记录下发布这个Library到maven central的流程以及碰到的问题。


maven:mvnrepository.com/artifact/io…


github:github.com/huhx/compos…


Sonatype 账号


MavenCentral 和 Sonatype 的关系

















库平台运营商管理后台
MavenCentralSonatypes01.oss.sonatype.org

因此我们要发布Library到Maven Central的话,首先需要Sonatype的账号以及权限。


申请 Sonatype 账号


申请账号地址: issues.sonatype.org/secure/Sign…


登录账号创建issue


创建issue地址:issues.sonatype.org/secure/View…


image-20220805223118398.png


点击 Create 按钮, 然后会弹出 Create Issue的窗口:


image-20220805224204197.png


点击Configure Fields, 选择 Custom 选项


image-20220805224557793.png



grouId的话最好使用: io.github.github_name, 要不然使用其他的还需要在 DNS 配置中配置一个TXT记录来验证域名所有权



填写完所有的信息点击创建,一个新的issue就创建成功了,以下就是我创建的issue,附上链接:issues.sonatype.org/browse/OSSR…


image-20220805225725812.png


值得注意的是sonatype要求我们创建一个github仓库来验证我们的github账号。创建完仓库之后,我们回复热心的工作人员,接下来就是等他们的处理结果了。大概30分钟就能好吧


image-20220805230217988.png
收到这样的回复,代表一切ready了你可以上传package到maven central


编写gradle脚本上传Lib


这篇文章里面,我是使用的android library做例子的。如果你想要发布java的Library,可以参考:docs.gradle.org/current/use…


In module project, build.gradle file


// add maven-publish and signing gradle plugin
plugins {
id 'maven-publish'
id 'signing'
}

// add publish script
publishing {
publications {
release(MavenPublication) {
pom {
name = 'Image Picker Compose'
description = 'An Image Picker Library for Jetpack Compose'
url = 'https://github.com/huhx/compose_image_picker'

licenses {
license {
name = 'The Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}

developers {
developer {
id = 'huhx'
name = 'hongxiang'
email = 'gohuhx@gmail.com'
}
}

scm {
connection = 'https://github.com/huhx/compose_image_picker.git'
developerConnection = 'https://github.com/huhx/compose_image_picker.git'
url = 'https://github.com/huhx/compose_image_picker'
}
}

groupId "io.github.huhx"
artifactId "compose-image-picker"
version "1.0.2"

afterEvaluate {
from components.release
}
}
}
repositories {
maven {
url "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
credentials {
username ossrhUsername // ossrhUsername is your sonatype username
password ossrhPassword // ossrhUsername is your sonatype password
}
}
}
}

// signing, this need key, secret, we put it into gradle.properties
signing {
sign publishing.publications.release
}

ossrhUsernameossrhPassword 是我们在第一步注册的sonatype账号。用户名和密码是敏感信息,我们放在gradle.properties并且不会提交到github。因此在 gradle.properties文件中,我们添加了以下内容:


# signing information
signing.keyId=key
signing.password=password
signing.secretKeyRingFile=file path

# sonatype account
ossrhUsername=username
ossrhPassword=password

其中包含了签名的三个重要信息,这个我们会在下面详细讲解


创建gpg密钥


我使用的是mac,这里就拿mac来说明如何创建gpg密钥。以下是shell脚本


# 安佳 gpg
> brew install gpg

# 创建gpg key,过程中会提示你输入密码。
# 记住这里要输入的密码就是上述提到你需要配置的signing.password
> gpg --full-gen-key

# 切换目录到~/.gnupg/openpgp-revocs.d, 你会发现有一个 .rev文件。
# 这个文件名称的末尾8位字符就是上述提到你需要配置的signing.keyId
> cd ~/.gnupg/openpgp-revocs.d && ls

# 创建secretKeyRingFile, 以下命令会创建一个文件secret.gpg
# 然后~/.gnupg/secret.gpg就是上述提到你需要配置的signing.secretKeyRingFile
> cd ~/.gnupg/ && gpg --export-secret-keys -o secret.gpg

把signing相关的信息成功填写到gradle.properties之后,我们就可以借助maven-publish插件发布我们的andoird包到maven的中心仓库了


maven publish的gradle task


# 这个是发布到我们的本地,你可以在~/.m2/repository/的目录找到你发布的包
> ./gradlew clean publishToMavenLocal

# 这个是发布到maven的中心仓库,你可以在https://s01.oss.sonatype.org/#stagingRepositories找到
> ./gradlew clean publish

我们执行./gradlew clean publish命令之后,访问地址:s01.oss.sonatype.org/#stagingRep…


image-20220805233310825.png


可以看到我们的android包已经在nexus repository了,接下来你要做的两步就是Close and Release。


Maven检验以及发布


第一步:点击Close按钮,它会触发对发布包的检验。我在这个过程中碰到一个signature validation失败的问题。


# 失败原因:No public key in hkp://keyserver.ubuntu.com:11371,是因为同步key可能会花些时间。这里我们可以手动发布我们的key到相应的服务器上
> gpg --keyserver hkp://keyserver.ubuntu.com:11371 --send-keys signing.keyId

第二步:确保你填入的信息是满足要求之后,Release按钮就会被激活。点击Release,接下来就是等待时间了,不出意外的话。30分钟你可以在nexus repository manager找到,但是在mvnrepository.com/找到的话得花更长的时间。


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

Android系统编译优化:使用Ninja加快编译

背景Android系统模块代码的编译实在是太耗时了,即使寥寥几行代码的修改,也能让一台具有足够性能的编译服务器工作十几分钟以上(模块单编),只为编出一些几兆大小的jar和dex。这里探究的是系统完成过一次整编后进行的模块单编,即m、mm、mmm等命令。除此之外...
继续阅读 »

背景

Android系统模块代码的编译实在是太耗时了,即使寥寥几行代码的修改,也能让一台具有足够性能的编译服务器工作十几分钟以上(模块单编),只为编出一些几兆大小的jar和dex。

这里探究的是系统完成过一次整编后进行的模块单编,即m、mm、mmm等命令。

除此之外,一些不会更新源码、编译配置等文件的内容的操作,如touch、git操作等,会被Android系统编译工具识别为有差异,从而在编译时重新生成编译配置,重新编译并没有更新的源码、重新生成没有差异的中间文件等一系列严重耗时操作。

本文介绍关于编译过程中的几个阶段,以及这些阶段的耗时点/耗时原因,并最后给出一个覆盖一定应用场景的基于ninja的加快编译的方法(实际上是裁剪掉冗余的编译工作)

环境

编译服务器硬件及Android信息:

  • Ubuntu 18.04.4 LTS
  • Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz (28核56超线程)
  • MemTotal: 65856428 kB (62.8GiB)
  • AOSP Android 10.0
  • 仅修改某个Java文件内部的boolean初始化值(true改false)
  • 不修改其他任何内容,包括源码、mk、bp的情况下,使用m单编模块(在清理后,使用对比的ninja进行单编)
  • 使用time计时
  • 此前整个系统已经整编过一次
  • 编译时不修改任何编译配置文件如Android.mk

之所以做一个代码修改量微乎其微的case,是因为要分析编译性能瓶颈,代码变更量越小的情况下,瓶颈就越明显,越有利于分析

关键编译阶段和耗时分析

由于Makefile结构复杂、不易调试、难以扩展,因此Android决定将它替换掉。Android在7.0时引入了Soong,它将Android从Makefile的编译架构带入到了ninja的时代。

Soong包含两大模块,其中Kati负责解析Makefile并转换为.ninja,第二个模块Ninja则基于生成的.ninja完成编译。

Kati是对GNU Make的clone,并将编译后端实现切换到ninja。Kati本身不进行编译,仅生成.ninja文件提供给Ninja进行编译。

Makefile/Android.mk -> Kati -> Ninja
Android.bp -> Blueprint -> Soong -> Ninja

因此在执行编译之前(即Ninja真正开动时),还有一些生成.ninja的步骤。关键编译阶段如下:

  1. Soong的自举(Bootstrap),将Soong本身编译出来
    1. 系统代码首次编译会比较耗时,其中一个原因是Soong要全新编译它自己
  2. 遍历源码树,收集所有编译配置文件(Makefile/Android.mk/Android.bp)
    1. 遍历、验证非常耗时,多么强劲配置的机器都将受限于单线程效率和磁盘IO效率
    2. 由于Android系统各模块之间的依赖、引入,因此即使是单编模块,Soong(Kati)也不得不确认目标模块以外的路径是否需要重新跟随编译
  3. 验证编译配置文件的合法性、有效性、时效性、是否应该加入编译,生成.ninja
    1. 如果没有任何更改,.ninja不需要重新生成
    2. 最终生成的.ninja文件很大(In my case,1GB以上),有很明显的IO性能效率问题,显然在查询效率方面也很低下
  4. 最后一步,真正执行编译,调用ninja进入多线程编译
    1. 由于Android加入了大量的代码编译期工作,如API权限控制检查、API列表生成等工作(比如,生成系统API保护名单、插桩工作等等),因此编译过程实际上不是完全投入到编译中
    2. 编译过程穿插“泛打包工作”,如生成odex、art、res资源打包。虽然不同的“泛打包”可以多线程并行进行,但是每个打包本身只能单线程进行

下面将基于模块单编(因开发环境系统全新编译场景频率较低,不予考虑),对这四个关键阶段进行性能分析。

阶段一:Soong bootstrap

在系统已经整编过一次的情况下,Soong已经完成了编译,因此其预热过程占整个编译时间的比例会比较小。

在“环境”下,修改一行Framework代码触发差异进行编译。并且使用下面的命令进行编译。

time m services framework -j57

编译实际耗时22m37s:

 build completed successfully (22:37 (mm:ss)) ####
real 22m37.504s
user 110m25.656s
sys 12m28.056s

对应的分阶段耗时如下图。

  • 可以看到,包括Soong bootstrap流程在内的预热耗时占比非常低,耗时约为11.6s,总耗时约为1357s,预热耗时占比为0.8%

Soong编译耗时占比

  • Kati和ninja,也就是上述编译关键流程的第2步和第3步,分别占了接近60%(820秒,13分钟半)和约35%(521秒,8分钟半)的耗时,合计占比接近95%的耗时。

注:这个耗时是仅小幅度修改Java代码后测试的耗时。如果修改编译配置文件如Android.mk,会有更大的耗时。

小结:看来在完成一次整编后的模块单编,包括Soong bootstrap、执行编译准备脚本、vendorsetup脚本的耗时占比很低,可以完全排除存在性能瓶颈的可能。

阶段二:Kati遍历、mk搜集与ninja生成

从上图可以看到,Kati耗时占比很大,它的任务是遍历源码树,收集所有的编译配置文件,经过验证和筛选后,将它们解析并转化为.ninja

从性能角度来看,它的主要特点如下:

  1. 它要遍历源码树,收集所有mk文件(In my case,有983个mk文件)
  2. 解析mk文件(In my case,framework/base/Android.mk耗费了~6800ms)
  3. 生成并写入对应的.ninja
  4. 单线程

直观展示如下,它是一个单线程的、IO速度敏感、CPU不敏感的过程:

Soong编译-Kati耗时细节.png

Kati串行地处理文件,此时对CPU利用率很低,对IO的压力也不高。

小结:可以确定它的性能瓶颈来源于IO速度,单纯为编译实例分配更多的CPU资源也无益于提升Kati的速度。

阶段三:Ninja编译

SoongClone了一份GNU Make,并将其改造为Kati。即使我们没有修改任何mk文件,前面Kati仍然会花费数分钟到数十分钟的工作耗时,只为了生成一份能够被Ninja.ninja的生成工具能够识别的文件。接下来是调用Ninja真正开始编译工作。

从性能角度来看,它的主要特点如下:

  1. 根据目标target及依赖,读取前面生成的.ninja配置,进行编译
  2. 比较独立,不与前面的组件,如blueprint、kati等耦合,只要.ninja文件中能找到target和build rule就能完成编译
  3. 多线程

直观展示如下,Ninja将会根据传入的并行任务数参数启动对应数量的线程进行编译。Ninja编译阶段会真正的启动多线程。但做不到一直多线程编译,因为部分阶段如部分编译目标(比如生成一个API文档)、泛打包阶段等本身无法多线程并行执行。

Soong编译-ninja耗时.png

可以看到此时CPU利用率应该是可以明显上升的。但是耗时较大的阶段仅启用了几个线程,后面的阶段和最后的图形很细(时间占比很小)的阶段才用起来更多的线程。

其中,一些阶段(图中时间占比较长的几条记录)没能跑满资源的原因是这些编译目标本身不支持并行,且本次编译命令指定的目标已经全部“安排”了,不需要调动更多资源启动其他编译目标的工作。当编译整个系统时就能够跑满了。

最后一个阶段(图中最后的几列很细的记录)虽然跑满了所有线程资源,但是运行时间很短。这是因为本case进行编译分析的过程中,仅修改了一行代码来触发编译。因编译工作量很小,所以这几列很细。

小结:我们看到,Ninja编译启动比较快,这表明Ninja.ninja文件的读取解析并不敏感。整个过程也没有看到显著的耗时点。且最后面编译量很小,表明Ninja能够确保增量编译、未更新不编译。

编译优化

本节完成点题——Android系统编译优化:使用Ninja加快编译。

根据前面分析的小结,可以总结性能瓶颈:

  1. Kati遍历、生成太慢,受限于IO速率
  2. Kati吞吐量太低,单线程
  3. 不论有无更新均重新解析Makefile

利用Ninja进行编译优化的思路是,大多数场景,可以舍弃Kati的工作,仅执行Ninja的工作,以节省掉60%以上的时间。其核心思路,也是制约条件,即在不影响编译正确性的前提下,舍弃不必要的Kati编译工作

  • 使用Ninja直接基于.ninja文件进行编译来改善耗时:

结合前面的分析,容易想到,如果目标被构建前,能够确保mk文件没有更新也不需要重新生成一长串的最终编译目标(即.ninja),那么make命令带来的Soong bootstrap、Kati等工作完全是重复的冗余的——这个性质Soong和Kati自己识别不出来,它们会重复工作一次。

既重新生成.ninja是冗余的,那么直接命令编译系统根据指定的.ninja进行编译显然会节省大量的工作耗时。ninja命令is the key:

使用源码中自带的ninja:

./prebuilts/build-tools/linux-x86/bin/ninja --version
1.8.2.git

对比最上面列出的make命令的编译,这里用ninja编译同样的目标:

 time ./prebuilts/build-tools/linux-x86/bin/ninja -j 57 -v -f out/combined-full_xxxxxx.ninja services framework

ninja自己识别出来CPU平台后,默认使用-j58。这里为了对比上面的m命令,使用-j57编译

-f参数指定.ninja文件。它是编译配置文件,在Android中由Kati生成。这里文件名用'x'替换修改

编译结果,对比上面的m,有三倍的提升:

real    7m57.835s
user 97m12.564s
sys 8m31.756s

编译耗时为8分半,仅make的三分之一。As we can see,当能够确保编译配置没有更新,变更仅存在于源码范围时,使用Ninja直接编译,跳过Kati可以取得很显著的提升

直接使用ninja:

./prebuilts/build-tools/linux-x86/bin/ninja -j $MAKE_JOBS -v -f out/combined-*.ninja <targets...>

对比汇总

这里找了一个其他项目的编译Demo,该Demo的特点是本身代码较简单,编译配置也较简单,整体编译工作较少,通过make编译的大部分耗时来自soong、make等工具自身的消耗,而真正执行编译的ninja耗时占比极其低。由于ninja本身跳过了soong,因此可以跳过这一无用的繁琐的耗时。可以看到下面,ninja编译iperf仅花费10秒。这个时间如果给soong来编译,预热都不够。

$ -> f_ninja_msf iperf
Run ninja with out/combined-full_xxxxxx.ninja to build iperf.
====== ====== ======
Ninja: ./prebuilts/build-tools/linux-x86/bin/ninja@1.8.2.git
Ninja: build with out/combined-full_xxxxxx.ninja
Ninja: build targets iperf
Ninja: j72
====== ====== ======
time /usr/bin/time ./prebuilts/build-tools/linux-x86/bin/ninja -j 72 -f out/combined-full_xxxxxx.ninja iperf

[24/24] Install: out/target/product/xxxxxx/system/bin/iperf
53.62user 11.09system 0:10.17elapsed 636%CPU (0avgtext+0avgdata 5696772maxresident)
4793472inputs+5992outputs (4713major+897026minor)pagefaults 0swaps

real 0m10.174s
user 0m53.624s
sys 0m11.096s

下面给出soong编译的恐怖耗时:

$ -> rm out/target/product/xxxxxx/system/bin/iperf
$ -> time m iperf -j72

...

[100% 993/993] Install: out/target/product/xxxxxx/system/bin/iperf

#### build completed successfully (14:45 (mm:ss)) ####


real 14m45.164s
user 23m40.616s
sys 11m46.248s

As we can see,m和ninja一个是10+ minutes,一个是10+ seconds,比例是88.5倍。


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

收起阅读 »

Kotlin 标准库随处可见的 contract 到底是什么?

Kotlin 的标准库提供了不少方便的实用工具函数,比如 with, let, apply 之流,这些工具函数有一个共同特征:都调用了 contract() 函数。@kotlin.internal.I...
继续阅读 »

Kotlin 的标准库提供了不少方便的实用工具函数,比如 withletapply 之流,这些工具函数有一个共同特征:都调用了 contract() 函数

@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}

@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
contract { callsInPlace(action) }

for (index in 0 until times) {
action(index)
}
}

contract?协议?它到底是起什么作用?

函数协议

contract 其实就是一个顶层函数,所以可以称之为函数协议,因为它就是用于函数约定的协议

@ContractsDsl
@ExperimentalContracts
@InlineOnly
@SinceKotlin("1.3")
@Suppress("UNUSED_PARAMETER")
public inline fun contract(builder: ContractBuilder.() -> Unit) { }

用法上,它有两点要求:

  • 仅用于顶层方法
  • 协议描述须置于方法开头,且至少包含一个「效应」(Effect)

可以看到,contract 的函数体为空,居然没有实现,真是一个神奇的存在。这么一来,此方法的关键点就只在于它的参数了。

ContractBuilder

contract的参数是一个将 ContractBuilder 作为接受者的lambda,而 ContractBuilder 是一个接口:

@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public interface ContractBuilder {
@ContractsDsl public fun returns(): Returns
@ContractsDsl public fun returns(value: Any?): Returns
@ContractsDsl public fun returnsNotNull(): ReturnsNotNull
@ContractsDsl public fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): CallsInPlace
}

其四个方法分别对应了四种协议类型,它们的功能如下:

  • returns:表明所在方法正常返回无异常
  • returns(value: Any?):表明所在方法正常执行,并返回 value(其值只能是 true、false 或者 null)
  • returnsNotNull():表明所在方法正常执行,且返回任意非 null 值
  • callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN):声明 lambada 只在所在方法内执行,所在方法执行完毕后,不会再被其他方法调用;可通过 kind 指定调用次数

前面已经说了,contract 的实现为空,所以作为接受着的 ContractBuilder 类型,根本没有实现类 —— 因为没有地方调用,就不需要啊。它的存在,只是为了声明所谓的协议代编译器使用。

InvocationKind

InvocationKind 是一个枚举类型,用于给 callsInPlace 协议方法指定执行次数的说明

@ContractsDsl
@ExperimentalContracts
@SinceKotlin("1.3")
public enum class InvocationKind {
// 函数参数执行一次或者不执行
@ContractsDsl AT_MOST_ONCE,
// 函数参数至少执行一次
@ContractsDsl AT_LEAST_ONCE,
// 函数参数执行一次
@ContractsDsl EXACTLY_ONCE,
// 函数参数执行次数未知
@ContractsDsl UNKNOWN
}

InvocationKind.UNKNOWN,次数未知,其实就是指任意次数。标准工具函数中,repeat 就指定的此类型,因为其「重复」次数由参数传入,确实未知;而除它外,其余像 letwith 这些,都是用的InvocationKind.EXACTLY_ONCE,即单次执行。

Effect

Effect 接口类型,表示一个方法的执行协议约定,其不同子接口,对应不同的协议类型,前面提到的 ReturnsReturnsNotNullCallsInPlace 均为它的子类型。

public interface Effect

public interface ConditionalEffect : Effect

public interface SimpleEffect : Effect {
public infix fun implies(booleanExpression: Boolean): ConditionalEffect
}

public interface Returns : SimpleEffect

public interface ReturnsNotNull : SimpleEffect

public interface CallsInPlace : Effect

简单明了,全员接口!来看一个官方使用,以便理解下这些接口的意义和使用:

public inline fun Array<*>?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}

return this == null || this.isEmpty()
}

这里涉及到两个 Effect:Returns 和 ConditionalEffect。此方法的功能为:判断数组为 null 或者是无元素空数组。它的 contract 约定是这样的:

  1. 调用 returns(value: Any?) 获得 Returns 协议(当然也就是 SimpleEffect 协议),其传入值是 false
  2. 第1步的 Returns 调用 implies 方法,条件是「本对象非空」,得到了一个 ConditionalEffect
  3. 于是,最终协议的意思是:函数返回 false 意味着 接受者对象非空

isNullOrEmpty() 的功能性代码给出了返回值为 true 的条件。虽然反过来说,不满足该条件,返回值就是 false,但还是通过 contract 协议里首先说明了这一点。

协议的意义

讲到这里,contract 协议涉及到的基本类型及其使用已经清楚了。回过头来,前面说到,contract() 的实现为空,即函数体为空,没有实际逻辑。这说明,这个调用是没有实际执行效果的,纯粹是为编译器服务。

不妨模仿着 let 写一个带自定义 contract 测试一下这个结论:

// 类比于ContractBuilder
interface Bonjour {

// 协议方法
fun <R> parler(f: Function<R>) {
println("parler something")
}
}


// 顶层协议声明工具,类比于contract
inline fun bonjour(b: Bonjour.() -> Unit) {}


// 模仿let
fun<T, R> T.letForTest(block: (T) -> R): R {
println("test before")
bonjour {
println("test in bonjour")
parler<String> {
""
}
}
println("test after")
return block(this)
}

fun main(args: Array<String>) {
"abc".letForTest {
println("main: $it called")
}
}

letForTest() 是类似于 let 的工具方法(其本身功能逻辑不重要)。执行结果:

test before
test after
main: abc called

如预期,bonjour 协议以及 Bonjour 协议构造器中的所有日志都未打印,都未执行。

这再一次印证,contract 协议仅为编译器提供信息。那协议对编码来说到底有什么意义呢?来看看下面的场景:

fun getString(): String? {
TODO()
}

fun String?.isAvailable(): Boolean {
return this != null && this.length > 0
}

getString() 方法返回一个 String 类型,但是有可能为 null。isAvailable 是 String? 类型的扩展,用以判断是否一个字符串非空且长度大于 0。使用如下:

val target = getString()
if (target.isAvailable()) {
val result: String = target
}

按代码的设计初衷,上述调用没问题,target.isAvailable() 为 true,证明 target 是非空且长度大于 0 的字符串,然后内部将它赋给 String 类型 —— 相当于 String? 转换成 String。

可惜,上述代码,编译器不认得,报错了:

Type mismatch.
Required:
String
Found:
String?

编译器果然没你我聪明啊!要解决这个问题,自然就得今天的主角上场了:

fun String?.isAvailable(): Boolean {
contract {
returns(true) implies (this@isAvailable != null)
}
return this != null && this.length > 0
}

使用 contract 协议指定了一个 ConditionalEffect,描述意思为:如果函数返回true,意味着 Receiver 类型非空。然后,编译器终于懂了,前面的错误提示消失。

这就是协议的意义所在:让编译器看不懂的代码更加明确清晰

小结

函数协议可以说是写工具类函数的利器,可以解决很多因为编译器不够智能而带来的尴尬问题。不过需要明白的是,函数协议还是实验性质的,还没有正式发布为 stable 功能,所以是有可能被 Kotlin 官方 去掉的。


作者:王可大虾
链接:https://juejin.cn/post/7128258776376803359
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

来,跟我一起撸Kotlin runBlocking/launch/join/async/delay 原理&使用

之前一些列的文章重点在于分析协程本质原理,了解了协程的内核再来看其它衍生的知识就比较容易了。 接下来这边文章着重分析协程框架提供的一些重要的函数原理,通过本篇文章,你将了解到: runBlocking 使用与原理 launch 使用与原理 join 使用与...
继续阅读 »

之前一些列的文章重点在于分析协程本质原理,了解了协程的内核再来看其它衍生的知识就比较容易了。

接下来这边文章着重分析协程框架提供的一些重要的函数原理,通过本篇文章,你将了解到:




  1. runBlocking 使用与原理

  2. launch 使用与原理

  3. join 使用与原理

  4. async/await 使用与原理

  5. delay 使用与原理



1. runBlocking 使用与原理


默认分发器的runBlocking


使用


老规矩,先上Demo:


    fun testBlock() {
println("before runBlocking thread:${Thread.currentThread()}")
//①
runBlocking {
println("I'm runBlocking start thread:${Thread.currentThread()}")
Thread.sleep(2000)
println("I'm runBlocking end")
}
//②
println("after runBlocking:${Thread.currentThread()}")
}

runBlocking 开启了一个新的协程,它的特点是:



协程执行结束后才会执行runBlocking 后的代码。



也就是① 执行结束后 ② 才会执行。



image.png


可以看出,协程运行在当前线程,因此若是在协程里执行了耗时函数,那么协程之后的代码只能等待,基于这个特性,runBlocking 经常用于一些测试的场景。


runBlocking 可以定义返回值,比如返回一个字符串:


    fun testBlock2() {
var name = runBlocking {
"fish"
}
println("name $name")
}

原理


    #Builders.kt
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
//当前线程
val currentThread = Thread.currentThread()
//先看有没有拦截器
val contextInterceptor = context[ContinuationInterceptor]
val eventLoop: EventLoop?
val newContext: CoroutineContext
//----------①
if (contextInterceptor == null) {
//不特别指定的话没有拦截器,使用loop构建Context
eventLoop = ThreadLocalEventLoop.eventLoop
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
} else {
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
?: ThreadLocalEventLoop.currentOrNull()
newContext = GlobalScope.newCoroutineContext(context)
}
//BlockingCoroutine 顾名思义,阻塞的协程
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
//开启
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
//等待协程执行完成----------②
return coroutine.joinBlocking()
}

重点看①②。


先说①,因为我们没有指定分发器,因此会使用loop,实际创建的是BlockingEventLoop,它继承自EventLoopImplBase,最终继承自CoroutineDispatcher(注意此处是个重点)。

根据我们之前分析的协程知识可知,协程启动后会构造DispatchedContinuation,然后依靠dispatcher将runnable 分发执行,而这个dispatcher 即是BlockingEventLoop。


    #EventLoop.common.kt
//重写dispatch函数
public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block)

public fun enqueue(task: Runnable) {
//将task 加入队列,task = DispatchedContinuation
if (enqueueImpl(task)) {
unpark()
} else {
DefaultExecutor.enqueue(task)
}
}

BlockingEventLoop 的父类EventLoopImplBase 里有个成员变量:_queue,它是个队列,用来存储提交的任务。


再看②:

协程任务已经提交到队列里,就看啥时候取出来执行了。


#Builders.kt
fun joinBlocking(): T {
try {
try {
while (true) {
//当前线程已经中断了,直接退出
if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) }
//如果eventLoop!= null,则从队列里取出task并执行
val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
//协程执行结束,跳出循环
if (isCompleted) break
//挂起线程,parkNanos 指的是挂起时间
parkNanos(this, parkNanos)
//当线程被唤醒后,继续while循环
}
} finally { // paranoia
}
}
//返回结果
return state as T
}

#EventLoop.common.kt
override fun processNextEvent(): Long {
//延迟队列
val delayed = _delayed.value
//延迟队列处理,这里在分析delay时再解释
//从队列里取出task
val task = dequeue()
if (task != null) {
//执行task
task.run()
return 0
}
return nextTime
}

上面代码的任务有两个:




  1. 尝试从队列里取出Task。

  2. 若是没有则挂起线程。



结合①②两点,再来过一下场景:




  1. 先创建协程,包装为DispatchedContinuation,作为task。

  2. 分发task,将task加入到队列里。

  3. 从队列里取出task执行,实际执行的即是协程体。

  4. 当3执行完毕后,runBlocking()函数也就退出了。




image.png


其中虚线箭头表示执行先后顺序。

由此可见,runBlocking()函数需要等待协程执行完毕后才退出。


指定分发器的runBlocking


上个Demo在使用runBlocking 时没有指定其分发器,若是指定了又是怎么样的流程呢?


    fun testBlock3() {
println("before runBlocking thread:${Thread.currentThread()}")
//①
runBlocking(Dispatchers.IO) {
println("I'm runBlocking start thread:${Thread.currentThread()}")
Thread.sleep(2000)
println("I'm runBlocking end")
}
//②
println("after runBlocking:${Thread.currentThread()}")
}

指定在子线程里进行分发。

此处与默认分发器最大的差别在于:



默认分发器加入队列、取出队列都是同一个线程,而指定分发器后task不会加入到队列里,task的调度执行完全由指定的分发器完成。



也就是说,coroutine.joinBlocking()后,当前线程一定会被挂起。等到协程执行完毕后再唤醒当前被挂起的线程。

唤醒之处在于:


#Builders.kt
override fun afterCompletion(state: Any?) {
// wake up blocked thread
if (Thread.currentThread() != blockedThread)
//blockedThread 即为调用coroutine.joinBlocking()后阻塞的线程
//Thread.currentThread() 为线程池的线程
//唤醒线程
unpark(blockedThread)
}


image.png


红色部分比紫色部分先执行,因此红色部分执行的线程会阻塞,等待紫色部分执行完毕后将它唤醒,最后runBlocking()函数执行结束了。


不管是否指定分发器,runBlocking() 都会阻塞等待协程执行完毕。


2. launch 使用与原理


想必大家刚接触协程的时候使用最多的还是launch启动协程吧。

看个Demo:


    fun testLaunch() {
var job = GlobalScope.launch {
println("hello job1 start")//①
Thread.sleep(2000)
println("hello job1 end")//②
}
println("continue...")//③
}

非常简单,启动一个线程,打印结果如下:



image.png


③一定比①②先打印,同时也说明launch()函数并不阻塞当前线程。

关于协程原理,在之前的文章都有深入分析,此处不再赘述,以图示之:



image.png


3. join 使用与原理


虽然launch()函数不阻塞线程,但是我们就想要知道协程执行完毕没,进而根据结果确定是否继续往下执行,这时候该Job.join()出场了。

先看该函数的定义:


#Job.kt
public suspend fun join()

是个suspend 修饰的函数,suspend 是咱们的老朋友了,说明协程执行到该函数会挂起(当前线程不阻塞,另有他用)。

继续看其实现:


    #JobSupport.kt
public final override suspend fun join() {
//快速判断状态,不耗时
if (!joinInternal()) { // fast-path no wait
coroutineContext.ensureActive()
return // do not suspend
}
//挂起的地方
return joinSuspend() // slow-path wait
}

//suspendCancellableCoroutine 典型的挂起操作
//cont 是封装后的协程
private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
//执行完这就挂起
//disposeOnCancellation 是将cont 记录在当前协程的state里,构造为node
cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(cont).asHandler))
}

其中suspendCancellableCoroutine 是挂起的核心所在,关于挂起的详细分析请移步:讲真,Kotlin 协程的挂起没那么神秘(原理篇)


joinSuspend()函数有2个作用:




  1. 将当前协程体存储到Job的state里(作为node)。

  2. 将当前协程挂起。



什么时候恢复呢?当然是协程执行完成后。


#JobSupport.kt
private class ResumeOnCompletion(
private val continuation: Continuation<Unit>
) : JobNode() {
//continuation 为协程的包装体,它里面有我们真正的协程体
//之后重新进行分发
override fun invoke(cause: Throwable?) = continuation.resume(Unit)
}

当协程执行完毕,会例行检查当前的state是否有挂着需要执行的node,刚好我们在joinSuspend()里放了node,于是找到该node,进而找到之前的协程体再次进行分发。根据协程状态机的知识可知,这是第二次执行协程体,因此肯定会执行job.join()之后的代码,于是乎看起来的效果就是:



job.join() 等待协程执行完毕后才会往下执行。



语言比较苍白,来个图:



image.png


注:此处省略了协程挂起等相关知识,如果对此有疑惑请阅读之前的文章。


4. async/await 使用与原理


launch 有2点不足之处:协程执行没有返回值。

这点我们从它的定义很容易获悉:



image.png


然而,在有些场景我们需要返回值,此时轮到async/await 出场了。


    fun testAsync() {
runBlocking {
//启动协程
var job = GlobalScope.async {
println("job1 start")
Thread.sleep(10000)
//返回值
"fish"
}
//等待协程执行结束,并返回协程结果
var result = job.await()
println("result:$result")
}
}

运行结果:



image.png


接着来看实现原理。


    public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
//构造DeferredCoroutine
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
//coroutine == DeferredCoroutine
coroutine.start(start, coroutine, block)
return coroutine
}

与launch 启动方式不同的是,async 的协程定义了返回值,是个泛型。并且async里使用的是DeferredCoroutine,顾名思义:延迟给结果的协程。

后面的流程都是一样的,不再细说。


再来看Job.await(),它与Job.join()类似:




  1. 先判断是否需要挂起,若是协程已经结束/被取消,当然就无需等待直接返回。

  2. 先将当前协程体包装到state里作为node存放,然后挂起协程。

  3. 等待async里的协程执行完毕,再重新调度执行await()之后的代码。

  4. 此时协程的值已经返回。



这里需要重点关注一下返回值是怎么传递过来的。



image.png


将testAsync()反编译:


    public final Object invokeSuspend(@NotNull Object $result) {
//result 为协程执行结果
Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object var10000;
switch(this.label) {
case 0:
//第一次执行这
ResultKt.throwOnFailure($result);
Deferred job = BuildersKt.async$default((CoroutineScope) GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure(var1);
String var2 = "job1 start";
boolean var3 = false;
System.out.println(var2);
Thread.sleep(10000L);
return "fish";
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
}), 3, (Object)null);
this.label = 1;
//挂起
var10000 = job.await(this);
if (var10000 == var6) {
return var6;
}
break;
case 1:
//第二次执行这
ResultKt.throwOnFailure($result);
//result 就是demo里的"fish"
var10000 = $result;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

String result = (String)var10000;
String var4 = "result:" + result;
boolean var5 = false;
System.out.println(var4);
return Unit.INSTANCE;
}

很明显,外层的协程(runBlocking)体会执行2次。

第1次:调用invokeSuspend(xx),此时参数xx=Unit,后遇到await 被挂起。

第2次:子协程执行结束并返回结果"fish",恢复外部协程时再次调用invokeSuspend(xx),此时参数xx="fish",并将参数保存下来,因此result 就有了值。


值得注意的是:

async 方式启动的协程,若是协程发生了异常,不会像launch 那样直接抛出,而是需要等待调用await()时抛出。


5. delay 使用与原理


线程可以被阻塞,协程可以被挂起,挂起后的协程等待时机成熟可以被恢复。


    fun testDelay() {
GlobalScope.launch {
println("before getName")
var name = getUserName()
println("after getName name:$name")
}
}
suspend fun getUserName():String {
return withContext(Dispatchers.IO) {
//模拟网络获取
Thread.sleep(2000)
"fish"
}
}

获取用户名字是在子线程获取的,它是个挂起函数,当协程执行到此时挂起,等待获取名字之后再恢复运行。


有时候我们仅仅只是想要协程挂起一段时间,并不需要去做其它操作,这个时候我们可以选择使用delay(xx)函数:


    fun testDelay2() {
GlobalScope.launch {
println("before delay")
//协程挂起5s
delay(5000)
println("after delay")
}
}

再来看看其原理。


#Delay.kt
public suspend fun delay(timeMillis: Long) {
//没必要延时
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
//封装协程为cont,便于之后恢复
if (timeMillis < Long.MAX_VALUE) {
//核心实现
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}

主要看context.delay 实现:


#DefaultExecutor.kt
internal actual val DefaultDelay: Delay = kotlinx.coroutines.DefaultExecutor

//单例
internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
const val THREAD_NAME = "kotlinx.coroutines.DefaultExecutor"
//...
private fun createThreadSync(): Thread {
return DefaultExecutor._thread ?: Thread(this, DefaultExecutor.THREAD_NAME).apply {
DefaultExecutor._thread = this
isDaemon = true
start()
}
}
//...
override fun run() {
//循环检测队列是否有内容需要处理
//决定是否要挂起线程
}
//...
}

DefaultExecutor 是个单例,它里边开启了线程,并且检测队列里任务的情况来决定是否需要挂起线程等待。


先看队列的出入队情况。


放入队列

我们注意到DefaultExecutor 继承自EventLoopImplBase(),在最开始分析runBlocking()时有提到过它里面有成员变量_queue 存储队列元素,实际上它还有另一个成员变量_delayed:


#EventLoop.common.kt
internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
//存放正常task
private val _queue = atomic<Any?>(null)
//存放延迟task
private val _delayed = atomic<EventLoopImplBase.DelayedTaskQueue?>(null)
}

private inner class DelayedResumeTask(
nanoTime: Long,
private val cont: CancellableContinuation<Unit>
) : EventLoopImplBase.DelayedTask(nanoTime) {
//协程恢复
override fun run() { with(cont) { resumeUndispatched(Unit) } }
override fun toString(): String = super.toString() + cont.toString()
}

delay.scheduleResumeAfterDelay 本质是创建task:DelayedResumeTask,并将该task加入到延迟队列_delayed里。


从队列取出

DefaultExecutor 一开始就会调用processNextEvent()函数检测队列是否有数据,如果没有则将线程挂起一段时间(由processNextEvent()返回值确定)。

那么重点转移到processNextEvent()上。


##EventLoop.common.kt
override fun processNextEvent(): Long {
if (processUnconfinedEvent()) return 0
val delayed = _delayed.value
if (delayed != null && !delayed.isEmpty) {
//调用delay 后会放入
//查看延迟队列是否有任务
val now = nanoTime()
while (true) {
//一直取任务,直到取不到(时间未到)
delayed.removeFirstIf {
//延迟任务时间是否已经到了
if (it.timeToExecute(now)) {
//将延迟任务从延迟队列取出,并加入到正常队列里
enqueueImpl(it)
} else
false
} ?: break // quit loop when nothing more to remove or enqueueImpl returns false on "isComplete"
}
}
// 从正常队列里取出
val task = dequeue()
if (task != null) {
//执行
task.run()
return 0
}
//返回线程需要挂起的时间
return nextTime
}

而执行任务最终就是执行DelayedResumeTask.run()函数,该函数里会对协程进行恢复。


至此,delay 流程就比较清晰了:




  1. 构造task 加入到延迟队列里,此时协程挂起。

  2. 有个单独的线程会检测是否需要取出task并执行,没到时间的话就要挂起等待。

  3. 时间到了从延迟队列里取出并放入正常的队列,并从正常队列里取出执行。

  4. task 执行的过程就是协程恢复的过程。



老规矩,上图:



image.png


图上虚线紫色框部分表明delay 执行到此就结束了,协程挂起(不阻塞当前线程),剩下的就交给单例的DefaultExecutor 调度,等待延迟的时间结束后通知协程恢复即可。


关于协程一些常用的函数分析到此就结束了,下篇开始我们一起探索协程通信(Channel/Flow 等)相关知识。

由于篇幅原因,省略了一些源码的分析,若你对此有疑惑,可评论或私信小鱼人。


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

润了!大龄码农从北京到荷兰的躺平生活

今天在知乎刷到了一篇大龄码农从北京到荷兰的日记,看完后着实令人羡慕不已,国外不仅生活环境和工作强度,都要比国内轻松很多,以下为原文。作者:小李在荷兰 | 编辑:对白的算法屋https://zhuanlan.zhihu.com/p/469261829一. 背景介...
继续阅读 »

今天在知乎刷到了一篇大龄码农从北京到荷兰的日记,看完后着实令人羡慕不已,国外不仅生活环境和工作强度,都要比国内轻松很多,以下为原文。

作者:小李在荷兰 | 编辑:对白的算法屋

https://zhuanlan.zhihu.com/p/469261829

一. 背景介绍

光阴似箭,今年已经41岁的老码农一枚。在家乡生活了19年,然后到北京学习工作了19年。一直希望去北美发展,由于种种原因最终还是没有去成。出国前在朋友的公司干了一年多,本来想着趁没到40岁再拼一把,结果很遗憾,失败离场。一晃眼已经要到孩子上学的年龄,所以决定快刀斩乱麻果断出国。

二. 工作机会

在前同事(我的前Manager)的推荐下,成功通过荷兰http://Booking.com的面试。

拿到offer签完合同后,Booking会非常贴心地安排几个专员协助后续的流程,包括:搬家,签证,机票,接机,抵达荷兰后入住的酒店,租房中介服务,银行账户,市政厅档案提交等等一些列大大小的事务,整个流程专业且流畅。

三. 搬家

决定使用Booking提供的员工搬家服务后,对接国际搬家服务的公司和我联系并上门初步估算需要搬运的家具等讲占据的空间,Booking最多提供一个集装箱的空间帮助入职员工搬家。搬家相关的一切事务由搬家公司负责,从时间规划,空间大小估算,上门打包,装车,报关等。老实说当时我很忐忑,这么多东西得搬到什么时候啊!但是令我很吃惊的是,专业公司就是不一样,只用了2个小时(6个人),差不多就把我北京的家搬空了。一句话概括:这次搬家是我有史以来感觉到最满意的搬家服务。

另外,Booking还支持将车运到荷兰,以及支持宠物搬家(有限报销)。

四. 签证

Booking提供HSM工作签证,签证会有专业的移民公司跟进。公司协助:递交给大使馆的签证材料清单,提交给荷兰市政厅的材料清单,获得 30% ruling材料清单。签证材料其实是比较麻烦的,其中最麻烦的是出生证明,但是这个方面每个人的情况不同,此处不表。另外,所有材料都需要经过公证(含翻译),然后中国大使馆,荷兰大使馆双认证。我本人怕麻烦,花钱找的中介办理的,一家三口的签证材料也花了不少钱。

当年9月份,在北京亮马河的荷兰大使馆提交完材料后,整个人都轻松了,出国前最麻烦的一个阶段终于结束。


提交完签证去了蓝色港湾走一走

五. 出发前的准备

  1. 打点好北京的房子、车子

  1. 去银行取了前期在荷兰生活用的现金(人生头一次拥有那么多欧元现钞)

  1. 办理小孩用的国际疫苗本,给小孩办理幼儿园的退学手续(仅仅上了一个礼拜)

  1. 注销不用的手机卡

  1. 将自己很多重要的文件档案扫描备用

  1. 吃了好多好多小龙虾

  1. 带着孩子在北京周边尽情地玩耍

  1. 和好友,同事们道别

  1. 和荷兰的租房中介公司沟通,此时他们已经开始进入协助租房的流程了

  1. 开始了解荷兰的幼儿园和小学情况,Booking有相应的服务协助家属融入荷兰社会

  1. 与此同时,Booking开始为入职做准备工作

六. 出发

按照预定好的出发日期,家人将我们一家三口送到了机场。本来一切都很平静,家属之前也去过荷兰。只是登机前,女儿问了我一句:爸爸我们要去荷兰了嘛?我们还回北京吗?瞬间有点要飙泪的感觉。


七. 抵达

10个小时后,我们一家人顺利抵达了荷兰阿姆斯特丹斯基浦机场。天气很好,心情愉悦。

海关一听到是Booking的员工,直接就盖戳了,家属也没有问问题。出关后,去公司指定的柜台等待接机的司机到来,然后带着行李前往酒店。


八. 入住酒店

阿姆斯特丹很小,我们很快就抵达了酒店。

Booking很贴心地根据我们的人数给提供了两室一厅的公寓酒店,可以自己做饭。我们去超市买了水果牛奶面包和三文鱼,吃完就早早休息倒时差了。





九. 开始工作

正式入职后,第一个月公司将新入职员工分配到了Bootcamp,进行入职培训。

当时第一个月基本每天就是早上来喝喝咖啡,参加一下入职培训和各种手续:包括完善员工资料、工作技能培训、公司文化培训、保险知识、租房买房知识、税务问题等。与此同时,需要去和中介公司去看房(下面会提到),争取早日租到房搬离酒店公寓。用当时他们(我当时接触过的几个Manager)的话来说就是:这个阶段你最重要的任务是全家安顿下来。

入职培训差不多结束时,我被现在所在组的TL领了过去,换了办公室,正式开始工作。

说一下WLB的问题,我所在公司这方面非常好,大部分同事都是早上9点-10点陆陆续续到公司,吃早饭接水接咖啡开会,中午12:00-1:00去食堂吃午饭,下午4:30-5:30就走了。我被TL找过两次非正式谈话,说我工作太卖力了,5点就走吧。其实我不是想做奋斗逼,只是有时进入状态了实在不想放下手中的活。试过几次呆到8点多,然后被保安轰走:先生,我要下班了。

疫情前,每天下班感觉都特别好:

  1. 下了班就不用管了,所以一下班就特别轻松,没有OnCall因为有SRE,没有人会给你发消息(特别极端的情况会,但是目前还没有遇到过)。

  1. 每天都特别有仪式感,特别是下班出门:把桌上的水果皮扔了,把喝咖啡的杯子放回去,把笔记本放入包中,带上围巾,带上耳机,穿上外套,背上背包,根据GoogleMap计算好的时间听着音乐准时走到公交车站...


收起阅读 »

Elasticsearch 为什么会产生文档版本冲突?如何避免?

先让大家直观的看到 Elasticsearch 文档版本冲突。模拟脚本1:循环写入数据 index.sh。模拟脚本2:循环update_by_query 批量更新数据 update.sh。由于:写入脚本 index.sh 比更新脚本 update.sh (执行...
继续阅读 »

1、Elasticsearch 版本冲突复现

先让大家直观的看到 Elasticsearch 文档版本冲突。

1.1 场景1:create 场景

DELETE my-index-000001
# 执行创建并写入
PUT my-index-000001/_create/1
{
"@timestamp": "2099-11-15T13:12:00",
"message": "GET /search HTTP/1.1 200 1070000",
"user": {
  "id": "kimchy"
}
}

# 再次执行会报版本冲突错误。
# 报错信息:[1]: version conflict, document already exists (current version [1])
PUT my-index-000001/_create/1
{
"@timestamp": "2099-11-15T13:12:00",
"message": "GET /search HTTP/1.1 200 1070000",
"user": {
  "id": "kimchy"
}
}


1.2 场景2:批量更新场景模拟

模拟脚本1:循环写入数据 index.sh。


模拟脚本2:循环update_by_query 批量更新数据 update.sh。


由于:写入脚本 index.sh 比更新脚本 update.sh (执行一次,休眠1秒)执行要快,所以更新获取的版本较写入的最新版本要低,会导致版本冲突如下图所示:


1.3 场景3:批量删除场景模拟

写入脚本 index.sh 不变。

删除脚本 delete.sh 如下:


和更新原因一致,由于:写入脚本 index.sh 比删除脚本 delete.sh (执行一次,休眠1秒)执行要快,所以删除获取的版本较写入的最新版本要低,会导致版本冲突如下图所示:


2、Elasticsearch 文档版本定义

执行:

GET test/_doc/1

召回结果如下:


这里的 version 代表文档的版本。

  • 当我们在 Elasticsearch 中创建一个新文档时,它会为该文档分配一个_version: 1。

  • 当我们对该文档进行任何后续更新(更新 update、索引 index 或删除 delete)时,_version都会增加 1。

一句话:Elasticsearch 使用_version来鉴别文档是否已更改。

3、Elasticsearch 文档版本产生背景

试想一下,如果没有文档版本?当有并发访问会怎么办?

前置条件:Elasticsearch 从写入到被检索的时间间隔是由刷新频率 refresh_interval 设定的,该值可以更新,但默认最快是 1 秒。


如上图所示,假设我们有一个人们用来评价 T 恤设计的网站。网站很简单,仅列出了T恤设计,允许用户给T恤投票。如果顺序投票,没有并发请求,直接发起update更新没有问题。

但是,在999累计投票数后,碰巧小明同学和小红同学两位同时(并发)发起投票请求,这时候,如果没有版本控制,将导致最终结果不是预期的1001,而是1000。

所以,为了处理上述场景以及比上述更复杂的并发场景,Elasticsearch 亟需一个内置的文档版本控制系统。这就是 _version 的产生背景。

https://kb.objectrocket.com/elasticsearch/elasticsearch-version-history-what-it-does-and-doesnt-do-501

https://www.elastic.co/cn/blog/elasticsearch-versioning-support

4、常见的并发控制策略

并发控制可以简记为:“防止两个或多个用户同时编辑同一记录而导致最终结果和预期不一致”。

常见的并发控制策略:悲观锁、乐观锁。

4.1 悲观锁

悲观锁,又名:悲观并发控制,英文全称:"Pessimistic Concurrency Control",缩写“PCC”,是一种并发控制的方法。

  • 悲观锁本质:在修改数据之前先锁定,再修改。

  • 悲观锁优点:采用先锁定后修改的保守策略,为数据处理的安全提供了保证。

  • 悲观锁缺点:加锁会有额外的开销,还会增加产生死锁的机会。

  • 悲观锁应用场景:比较适合写入操作比较频繁的场景。

4.2 乐观锁

乐观锁,又名:乐观并发控制,英文全称:“Optimistic Concurrency Control”,缩写OCC”,也是一种并发控制的方法。

  • 乐观锁本质:假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

  • 乐观锁优点:“胆子足够大,足够乐观”,直到提交的时候才去锁定,不会产生任何锁和死锁。

  • 乐观锁缺点:并发写入会有问题,需要有冲突避免策略补救。

  • 乐观锁应用场景:数据竞争(data race)不大、冲突较少的场景、比较适合读取操作比较频繁的场景,确保比其他并发控制方法(如悲观锁)更高的吞吐量。

这里要强调的是,Elasticsearch 采用的乐观锁的机制来处理并发问题。

Elasticsearch 乐观锁本质是:没有给数据加锁,而是基于 version 文档版本实现。每次更新或删除数据的时候,都需要对比版本号。

5、Elasticsearch 文档版本冲突的本质

一句话,Elasticsearch 文档冲突的本质——老版本覆盖掉了新版本。

6、如何解决或者避免 Elasticsearch 文档版本冲突?

6.1 external 外部控制版本号

“external”——我的理解就是“简政放权”,交由外部的数据库或者更确切的说,是写入的数据库或其他第三方库来做控制。

版本号可以设置为外部值(例如,如果在数据库中维护)。要启用此功能,version_type应设置为 external。

使用外部版本类型 external 时,系统会检查传递给索引请求的版本号是否大于当前存储文档的版本。

  • 如果为真,也就是新版本大于已有版本,则文档将被索引并使用新的版本号。

  • 如果提供的值小于或等于存储文档的版本号,则会发生版本冲突,索引操作将失败。

好处:不论何时,ES 中只有最新版本的数据,借助 external 相对有效的解决版本冲突问题。

实战一把:

如果没有 external,执行如下命令:

PUT my-index-000001/_doc/1?version=2
{
"user": {
  "id": "elkbee"
}
}

报错如下:

{
"error" : {
  "root_cause" : [
    {
      "type" : "action_request_validation_exception",
      "reason" : "Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use `if_seq_no` and `if_primary_term` instead;"
    }
  ],
.......省略2行......
"status" : 400
}

啥意思呢?内部版本控制(internal)不能用于乐观锁,也就是直接使用 version 是不可以的。需要使用:if_seq_noif_primary_term,它俩的用法,后文会有专门解读。

如果用 external,执行如下命令:

PUT my-index-000001/_doc/1?version=2&version_type=external
{
"user": {
  "id": "elkbee"
}
}

执行结果如下:

{
"_index" : "my-index-000001",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
  "total" : 2,
  "successful" : 1,
  "failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}

相比于之间没有加 external,加上 external 后,可以实现基于version的文档更新操作。

external_gt 和 external_gte的用法见官方文档,本文不展开,原理同 external。

https://www.elastic.co/guide/en/elasticsearch/reference/8.1/docs-index_.html#index-versioning

6.2 通过if_seq_no 和 if_primary_term 唯一标识避免冲突

索引操作(Index,动词)是有条件的,并且只有在对文档的最后修改分配了由 if_seq_no 和 if_primary_term 参数指定的序列号和 primary term specified(翻译起来拗口,索性用英文)才执行。

如果检测到不匹配,该操作将产生一个 VersionConflictException 409 的状态码。

Step1:写入数据

DELETE products_001
PUT products_001/_doc/1567
{
"product" : "r2d2",
"details" : "A resourceful astromech droid"
}


# 查看ifseqno 和 ifprimaryterm
GET products_001/_doc/1567

返回:

{
"_index" : "products_001",
"_type" : "_doc",
"_id" : "1567",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
  "product" : "r2d2",
  "details" : "A resourceful astromech droid"
}
}

Step2:以这种方式更新,前提是先拿到 if_seq_no 和 if_primary_term

# 模拟数据打tag 过程
PUT products_001/_doc/1567?if_seq_no=0&if_primary_term=1
{
"product": "r2d2",
"details": "A resourceful astromech droid",
"tags": [ "droid" ]
}


# 再获取数据
GET products_001/_doc/1567

返回:

{
"_index" : "products_001",
"_type" : "_doc",
"_id" : "1567",
"_version" : 2,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
  "product" : "r2d2",
  "details" : "A resourceful astromech droid",
  "tags" : [
    "droid"
  ]
}
}

step2 更新数据的时候,是在 step1 的获取已写入文档的 if_seq_no=0 和 if_primary_term=1 基础上完成的。

这样能有效避免冲突。

6.3 批量更新和批量删除忽略冲突实现

如下是在开篇的基础上加了:conflicts=proceed。

conflicts 默认值是终止,而 proceed 代表继续。

POST test/_update_by_query?conflicts=proceed
{
"query": {
  "match": {
    "name": "update"
  }
},
"script": {
  "source": "ctx._source['foo'] = '123ss'",
  "lang": "painless"
}
}

conflicts=proceed 的本质——告诉进程忽略冲突并继续更新其他文档。

开篇不会报 409 错误了,但依然会有版本冲突。但,某些企业级场景是可以用的。


同理,delete_by_query 参数及返回结果均和 update_by_query 一致。


扩展:单个更新 update (区别于批量更新:update_by_query)有 retry_on_conflict 参数,可以设置冲突后重试次数。

7、关于频繁更新带来的性能问题

正如文章开篇演示的,并发更新或者并发删除可能会导致版本冲突。

除了并发性和正确性之外,请注意,非常频繁地更新文档可能会导致性能下降。

如果更新了尚未写入段(segment)的文档,将会导致刷新操作。而刷新频率越小(企业级咨询我见过设置小于1s的,不推荐),势必会导致写入低效。

更多探讨推荐阅读:

https://discuss.elastic.co/t/handling-conflicts/135240/2

8、小结

从实际问题抽象出模拟脚本,让大家看到文档版本冲突是如何产生的。而后,定义了版本冲突并指出了其产生的背景。

接着,详细讲解了解决冲突的两种机制:乐观锁、悲观锁。探讨、验证了解决文档版本冲突的几种方案。

你有没有遇到过本文提及的问题,如何解决的呢?欢迎留言交流。

参考

[1] https://www.anycodings.com/1questions/160352/why-bulk-update-never-conflicts-with-update-by-query-requests-in-elasticsearch

[2] https://learnku.com/articles/43867

[3] https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html

[4] https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#optimistic-concurrency-control-index

[5] https://www.elastic.co/guide/en/elasticsearch/reference/8.1/docs-index_.html#index-versioning

来源:mp.weixin.qq.com/s/9XzOogqfz4tavXqDt1pxgA

收起阅读 »

封装一个有趣的 Loading 组件

前言在上一篇普通的加载千篇一律,有趣的 loading 万里挑一 中,我们介绍了使用Path类的PathMetrics属性来控制绘制点在路径上运动来实现比较有趣的loading效果。有评论说因为是黑色背景,所以看着好看。黑色背景确实显得高端一点,但是...
继续阅读 »

前言

在上一篇普通的加载千篇一律,有趣的 loading 万里挑一 中,我们介绍了使用Path类的PathMetrics属性来控制绘制点在路径上运动来实现比较有趣的loading效果。有评论说因为是黑色背景,所以看着好看。黑色背景确实显得高端一点,但是并不是其他配色也不行,本篇我们来封装一个可以自定义配置前景色和背景色的Loading组件。

组件定义

loading组件共定义4个入口参数:

  • 前景色:绘制图形的前景色;
  • 背景色:绘制图形的背景色;
  • 图形尺寸:绘制图形的尺寸;
  • 加载文字:可选,如果有文字就显示,没有就不显示。

得到的Loading组件类如下所示:

class LoadingAnimations extends StatefulWidget {
final Color bgColor;
final Color foregroundColor;
String? loadingText;
final double size;
LoadingAnimations(
{required this.foregroundColor,
required this.bgColor,
this.loadingText,
this.size = 100.0,
Key? key})
: super(key: key);

@override
_LoadingAnimationsState createState() => _LoadingAnimationsState();
}

圆形Loading

我们先来实现一个圆形的loading,效果如下所示。 circle_loading.gif 这里绘制了两组沿着一个大圆运动的轴对称的实心圆,半径依次减小,圆心间距随着动画时间逐步拉大。实际上实现的核心还是基于PathPathMetrics。具体实现代码如下:

_drawCircleLoadingAnimaion(
Canvas canvas, Size size, Offset center, Paint paint) {
final radius = boxSize / 2;
final ballCount = 6;
final ballRadius = boxSize / 15;

var circlePath = Path()
..addOval(Rect.fromCircle(center: center, radius: radius));

var circleMetrics = circlePath.computeMetrics();
for (var pathMetric in circleMetrics) {
for (var i = 0; i < ballCount; ++i) {
var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);

var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(
Offset(size.width - tangent.position.dx,
size.height - tangent.position.dy),
ballRadius / (1 + i),
paint);
}
}
}

其中路径比例为lengthRatio,通过animationValue乘以一个系数使得实心圆的间距越来越大 ,同时通过Offset(size.width - tangent.position.dx, size.height - tangent.position.dy)绘制了一组对对称的实心圆,这样整体就有一个圆形的效果了,动起来也会更有趣一点。

椭圆运动Loading

椭圆和圆形没什么区别,这里我们搞个渐变的效果看看,利用之前介绍过的Paintshader可以实现渐变色绘制效果。

oval_loading.gif

实现代码如下所示。

final ballCount = 6;
final ballRadius = boxSize / 15;

var ovalPath = Path()
..addOval(Rect.fromCenter(
center: center, width: boxSize, height: boxSize / 1.5));
paint.shader = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [this.foregroundColor, this.bgColor],
).createShader(Offset.zero & size);
var ovalMetrics = ovalPath.computeMetrics();
for (var pathMetric in ovalMetrics) {
for (var i = 0; i < ballCount; ++i) {
var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);

var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(
Offset(size.width - tangent.position.dx,
size.height - tangent.position.dy),
ballRadius / (1 + i),
paint);
}
}

当然,如果渐变色的颜色更丰富一点会更有趣些。

colorful_loading.gif

贝塞尔曲线Loading

通过贝塞尔曲线构建一条Path,让一组圆形沿着贝塞尔曲线运动的Loading效果也很有趣。

bezier_loading.gif

原理和圆形的一样,首先是构建贝塞尔曲线Path,代码如下。

var bezierPath = Path()
..moveTo(size.width / 2 - boxSize / 2, center.dy)
..quadraticBezierTo(size.width / 2 - boxSize / 4, center.dy - boxSize / 4,
size.width / 2, center.dy)
..quadraticBezierTo(size.width / 2 + boxSize / 4, center.dy + boxSize / 4,
size.width / 2 + boxSize / 2, center.dy)
..quadraticBezierTo(size.width / 2 + boxSize / 4, center.dy - boxSize / 4,
size.width / 2, center.dy)
..quadraticBezierTo(size.width / 2 - boxSize / 4, center.dy + boxSize / 4,
size.width / 2 - boxSize / 2, center.dy);

这里实际是构建了两条贝塞尔曲线,先从左边到右边,然后再折回来。之后就是运动的实心圆了,这个只是数量上多了,ballCount30,这样效果看着就有一种拖影的效果。

var ovalMetrics = bezierPath.computeMetrics();
for (var pathMetric in ovalMetrics) {
for (var i = 0; i < ballCount; ++i) {
var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);

var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(
Offset(size.width - tangent.position.dx,
size.height - tangent.position.dy),
ballRadius / (1 + i),
paint);
}
}

这里还可以改变运动方向,实现一些其他的效果,例如下面的效果,第二组圆球的绘制位置实际上是第一组圆球的x、y坐标的互换。

bezier_loading_transform.gif

var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);

var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(Offset(tangent.position.dy, tangent.position.dx),
ballRadius / (1 + i), paint);

组件使用

我们来看如何使用我们定义的这个组件,使用代码如下,我们用Future延迟模拟了一个加载效果,在加载过程中使用loading指示加载过程,加载完成后显示图片。

class _LoadingDemoState extends State<LoadingDemo> {
var loaded = false;

@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 5), () {
setState(() {
loaded = true;
});
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('Loading 使用'),
),
body: Center(
child: loaded
? Image.asset(
'images/beauty.jpeg',
width: 100.0,
)
: LoadingAnimations(
foregroundColor: Colors.blue,
bgColor: Colors.white,
size: 100.0,
),
),
);
}

最终运行的效果如下,源码已提交至:绘图相关源码,文件名为loading_animations.dart

loading_usage.gif

总结

本篇介绍了Loading组件的封装方法,核心要点还是利用Path和动画控制绘制元素的运动轨迹来实现更有趣的效果。在实际应用过程中,也可以根据交互设计的需要,做一些其他有趣的加载动效,提高等待过程的趣味性。


作者:岛上码农
链接:https://juejin.cn/post/7129071400509243423
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

关于mmap不为人知的秘密

mmap初入 我们常说的mmap,其实是一种内存映射文件的方法,mmap将一个文件或者其它对象映射进内存。但是更加确切的来说,其实是linux中的线性区提供的可以和基于磁盘文件系统的普通文件的某一个部分相关联的操作。线性区其实是由进程中连续的一块虚拟文件区域,...
继续阅读 »

mmap初入


我们常说的mmap,其实是一种内存映射文件的方法,mmap将一个文件或者其它对象映射进内存。但是更加确切的来说,其实是linux中的线性区提供的可以和基于磁盘文件系统的普通文件的某一个部分相关联的操作。线性区其实是由进程中连续的一块虚拟文件区域,由struct vm_area_struct结构体表示,我们的mmap操作,其实就是最本质的,就是通过其进行的内存操作。


按照归类的思想,其实mmap主要用到的分为两类(还有其他标识不讨论)



  1. 共享的:即对该线性区中的页(注意是以页为单位)的任何写操作,都会修改磁盘上的文件,并且如果一个进程对进行了mmap的页进行了写操作,那么对于其他进程(同样也通过mmap进行了映射),同样也是可见的

  2. 私有的:对于私有映射页的任何写操作,都会使linux内核停止映射该文件的页(注意,假如有进程a,b同时映射了该页,a对页进行了修改,此时这个页就相当于复制出来了一份,a以后的操作就在复制的该页进行操作,b还是引用原来的页,原来的页就可以继续参与内存映射,而复制出来的页,就被停止映射了),因此,在私有情况下,写操作是不会改变磁盘上的文件,同时所做的修改对于其他进程来说,就是不可见的。


我们看一下图片
共享模式下


image.png
私有模式下


image.png


mmap分析


概念我们有了,我们看一下代码mmap定义


#if defined(__USE_FILE_OFFSET64)
void* mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset) __RENAME(mmap64);
#else
void* mmap(void* __addr, size_t __size, int __prot, int __flags, int __fd, off_t __offset);
#endif

#if __ANDROID_API__ >= 21
/**
* mmap64() is a variant of mmap() that takes a 64-bit offset even on LP32.
*
* See https://android.googlesource.com/platform/bionic/+/master/docs/32-bit-abi.md
*
* mmap64 wasn't really around until L, but we added an inline for it since it
* allows a lot more code to compile with _FILE_OFFSET_BITS=64.
*/
void* mmap64(void* __addr, size_t __size, int __prot, int __flags, int __fd, off64_t __offset) __INTRODUCED_IN(21);
#endif

mmap分为好多个版本,但是其他的也是根据不同的Android版本或者abi进行部分的区分,我们来看一下具体含义:



  1. addr:参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。当然,我们也可以设定一个自己的地址,但是如果flag中设定起来MAP_FIXED标志,且内核也没办法从我们指定的线性地址开始分配新线性区的话,就会产生调用失败

  2. size:映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起

  3. prot:指定对线性区的一组权限,比如读权限(PROT_READ),写权限(PROT_WRITE),执行权限(PROT_EXEC)

  4. flags:一组标志,比如我们上面说的共享模式(MAP_SHARED)或者私有模式(MAP_PRIVATE)等

  5. fd:文件描述符,要映射的文件

  6. offset:要映射的文件的偏移量(比如我想要从文件的某部分开始映射)


使用例子


下面是demo例子



#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define TRUE 1
#define FALSE -1
#define FILE_SIZE 100

#define MMAP_FILE_PATH "./mmap.txt"


int main(int argc, char **argv)
{
int fd = -1;
//char buff[100] = {0};
void *result;
int lseek_result = -1;
int file_length = -1;

// 1. open the file
fd = open(MMAP_FILE_PATH, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
if (-1 == fd) {
printf("open failed\n");
printf("%s\n", strerror(errno));
return FALSE;
}

//2. call mmap 这里可以尝试一下其他的flag,比如MAP_PRIVATE
result = mmap(0, 100(这里是demo文件长度), \
PROT_READ|PROT_WRITE, \
MAP_SHARED, \
fd, 0);
if (result == (void *)-1) {
printf("mmap failed\n");
printf("%s\n", strerror(errno));
return FALSE;
}

//3. release the file descriptor
close(fd);

//4. write something to mmap addr,
strncpy(result, "test balabala...", file_length);

//5. call munmap
munmap(0, file_length);

return 0;
}

深入了解mmap


不得不说,mmap的函数实现非常复杂,首先会调用到do_mmap_pgoff进行平台无关的代码分支,然后会调用到do_mmap里面,这个函数非常长!!!


我们挑几个关键的步骤:



  1. 先检查要映射的文件是否定义了mmap文件操作,(如果是目录就不存在mmap文件操作了)如果没有的话就直接调用失败

  2. 检查一系列的一致性检查,同时根据mmap的参数与打开文件的权限标志进行对比:比如检查当前文件是否有文件锁,如果是以MAP_SHARED模式打开进行写入的话,也要检查文件是否有可写的权限,正所谓权限要一致

  3. 设置线性区的vm_flag字段,因为我们mmap返回的地址肯定也是要给到当前进程的,这个线性区的权限所以也要设置,比如VM_READ,VM_WRITE等标识,表明了这块内存的权限

  4. 增加文件的引用计数,因为我们进程mmap用到了该文件对吧,所以计数器也要加一,这样系统才会知道当前文件被多少个进程引用了

  5. 对文件进行映射操作,并且初始化该线性区的页,注意这个时候并没有对页进行操作,因为有可能申请了这个页而没有操作,所以这个时候是以no_page标识的,这里不得不感慨linux的写时复制思想用到了各处!只有真正访问的时候,就会产生一个缺页异常,由内核调度请求完成对该页的填充


unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, vm_flags_t vm_flags,
unsigned long pgoff, unsigned long *populate)
{
struct mm_struct *mm = current->mm; //获取该进程的memory descriptor
int pkey = 0;

*populate = 0;
//函数对传入的参数进行一系列检查, 假如任一参数出错,都会返回一个errno
if (!len)
return -EINVAL;

/*
* Does the application expect PROT_READ to imply PROT_EXEC?
*
* (the exception is when the underlying filesystem is noexec
* mounted, in which case we dont add PROT_EXEC.)
*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && path_noexec(&file->f_path)))
prot |= PROT_EXEC;
//假如没有设置MAP_FIXED标志,且addr小于mmap_min_addr, 因为可以修改addr, 所以就需要将addr设为mmap_min_addr的页对齐后的地址
if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr);

/* Careful about overflows.. */
len = PAGE_ALIGN(len); //进行Page大小的对齐
if (!len)
return -ENOMEM;

/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;

/* Too many mappings? */
if (mm->map_count > sysctl_max_map_count) //判断该进程的地址空间的虚拟区间数量是否超过了限制
return -ENOMEM;

//get_unmapped_area从当前进程的用户空间获取一个未被映射区间的起始地址
addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (offset_in_page(addr)) //检查addr是否有效
return addr;

if (prot == PROT_EXEC) {
pkey = execute_only_pkey(mm);
if (pkey < 0)
pkey = 0;
}

/* Do simple checking here so the lower-level routines won't have
* to. we assume access permissions have been handled by the open
* of the memory object, so we don't do any here.
*/
vm_flags |= calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
//假如flags设置MAP_LOCKED,即类似于mlock()将申请的地址空间锁定在内存中, 检查是否可以进行lock
if (flags & MAP_LOCKED)
if (!can_do_mlock())
return -EPERM;

if (mlock_future_check(mm, vm_flags, len))
return -EAGAIN;

if (file) { // file指针不为nullptr, 即从文件到虚拟空间的映射
struct inode *inode = file_inode(file); //获取文件的inode

switch (flags & MAP_TYPE) { //根据标志指定的map种类,把为文件设置的访问权考虑进去
case MAP_SHARED:
if ((prot&PROT_WRITE) && !(file->f_mode&FMODE_WRITE))
return -EACCES;

/*
* Make sure we don't allow writing to an append-only
* file..
*/
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES;

/*
* Make sure there are no mandatory locks on the file.
*/
if (locks_verify_locked(file))
return -EAGAIN;

vm_flags |= VM_SHARED | VM_MAYSHARE;
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);

/* fall through */
case MAP_PRIVATE:
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (path_noexec(&file->f_path)) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}

if (!file->f_op->mmap)
return -ENODEV;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
break;

default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
/*
* Ignore pgoff.
*/
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
/*
* Set pgoff according to addr for anon_vma.
*/
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}

/*
* Set 'VM_NORESERVE' if we should not account for the
* memory use of this mapping.
*/
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE;

/* hugetlb applies strict overcommit unless MAP_NORESERVE */
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}
//一顿检查和配置,调用核心的代码mmap_region
addr = mmap_region(file, addr, len, vm_flags, pgoff);
if (!IS_ERR_VALUE(addr) &&
((vm_flags & VM_LOCKED) ||
(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
*populate = len;
return addr;
}
do_mmap() 根据用户传入的参数做了一系列的检查,然后根据参数初始化 vm_area_struct 的标志 vm_flags,vma->vm_file = get_file(file) 建立文件与vma的映射, mmap_region() 负责创建虚拟内存区域:

unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff)
{
struct mm_struct *mm = current->mm; //获取该进程的memory descriptor
struct vm_area_struct *vma, *prev;
int error;
struct rb_node **rb_link, *rb_parent;
unsigned long charged = 0;

/* 检查申请的虚拟内存空间是否超过了限制 */
if (!may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)) {
unsigned long nr_pages;

/*
* MAP_FIXED may remove pages of mappings that intersects with
* requested mapping. Account for the pages it would unmap.
*/
nr_pages = count_vma_pages_range(mm, addr, addr + len);

if (!may_expand_vm(mm, vm_flags,
(len >> PAGE_SHIFT) - nr_pages))
return -ENOMEM;
}

/* 检查[addr, addr+len)的区间是否存在映射空间,假如存在重合的映射空间需要munmap */
while (find_vma_links(mm, addr, addr + len, &prev, &rb_link,
&rb_parent)) {
if (do_munmap(mm, addr, len))
return -ENOMEM;
}

/*
* Private writable mapping: check memory availability
*/
if (accountable_mapping(file, vm_flags)) {
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory_mm(mm, charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}

//检查是否可以合并[addr, addr+len)区间内的虚拟地址空间vma
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
if (vma) //假如合并成功,即使用合并后的vma, 并跳转至out
goto out;
//如果不能和已有的虚拟内存区域合并,通过Memory Descriptor来申请一个vma
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}
//初始化vma
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
INIT_LIST_HEAD(&vma->anon_vma_chain);

if (file) { //假如指定了文件映射
if (vm_flags & VM_DENYWRITE) { //映射的文件不允许写入,调用deny_write_accsess(file)排斥常规的文件操作
error = deny_write_access(file);
if (error)
goto free_vma;
}
if (vm_flags & VM_SHARED) { //映射的文件允许其他进程可见, 标记文件为可写
error = mapping_map_writable(file->f_mapping);
if (error)
goto allow_write_and_free_vma;
}

//递增File的引用次数,返回File赋给vma
vma->vm_file = get_file(file);
error = file->f_op->mmap(file, vma); //调用文件系统指定的mmap函数
if (error)
goto unmap_and_free_vma;

/* Can addr have changed??
*
* Answer: Yes, several device drivers can do it in their
* f_op->mmap method. -DaveM
* Bug: If addr is changed, prev, rb_link, rb_parent should
* be updated for vma_link()
*/
WARN_ON_ONCE(addr != vma->vm_start);

addr = vma->vm_start;
vm_flags = vma->vm_flags;
} else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma); //假如标志为VM_SHARED,但没有指定映射文件,需要调用shmem_zero_setup(),实际映射的文件是dev/zero
if (error)
goto free_vma;
}
//将申请的新vma加入mm中的vma链表
vma_link(mm, vma, prev, rb_link, rb_parent);
/* Once vma denies write, undo our temporary denial count */
if (file) {
if (vm_flags & VM_SHARED)
mapping_unmap_writable(file->f_mapping);
if (vm_flags & VM_DENYWRITE)
allow_write_access(file);
}
file = vma->vm_file;
out:
perf_event_mmap(vma);
//更新进程的虚拟地址空间mm
vm_stat_account(mm, vm_flags, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
if (!((vm_flags & VM_SPECIAL) || is_vm_hugetlb_page(vma) ||
vma == get_gate_vma(current->mm)))
mm->locked_vm += (len >> PAGE_SHIFT);
else
vma->vm_flags &= VM_LOCKED_CLEAR_MASK;
}

if (file)
uprobe_mmap(vma);

/*
* New (or expanded) vma always get soft dirty status.
* Otherwise user-space soft-dirty page tracker won't
* be able to distinguish situation when vma area unmapped,
* then new mapped in-place (which must be aimed as
* a completely new data area).
*/
vma->vm_flags |= VM_SOFTDIRTY;

vma_set_page_prot(vma);

return addr;

unmap_and_free_vma:
vma->vm_file = NULL;
fput(file);

/* Undo any partial mapping done by a device driver. */
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
charged = 0;
if (vm_flags & VM_SHARED)
mapping_unmap_writable(file->f_mapping);
allow_write_and_free_vma:
if (vm_flags & VM_DENYWRITE)
allow_write_access(file);
free_vma:
kmem_cache_free(vm_area_cachep, vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
return error;
}

munmap


当进程准备清除一个mmap映射时,就可以调用munmap函数


int munmap(void* __addr, size_t __size);

如demo例子所示,第一个函数是要删除的线性地址的第一个单元的地址,第二个函数是线性区的长度,由于这个函数比较简单,就是系统调用do_munmap函数,这里就不详细分析啦!


数据写回磁盘


经过上面的mmap操作,我们也知道了mmap的使用了,不知道大家有没有留意一个细节,就是上面图中我们内存写入的下一步是页高速缓存,这个时候真正的数据保存,其实还是没有真正写入到磁盘里面的,真正写入其实是靠系统调用写入,即msync函数


int msync(void* __addr, size_t __size, int __flags);

mmap的数据写入依靠着这个系统调用保证,即当前进程被异常销毁了,也可以通过这个系统级别的调用,把属于内存映射的脏页数据写回去磁盘。参数1跟2需要写回去的头地址跟大小,我们重点看一下最后一个参数flags,它有以下几个参数选择:



  • MS_SYNC:要求系统挂起调用进程,直到I/O操作完成

  • MS_ASYNC:可以系统调用立即返回,不用挂起调用进程(大多数使用到mmap库选择)

  • MS_INVALLDATE:要求系统调用从进程地址空间删除mmap映射的所有页
    这个函数主要功能是进行脏页标记,并进行真正的磁盘写入,大概的路径就是通过调用flush_tlb_page把缓冲器标记为脏页标识,同时获取文件对应的索引节点i_sem信号量,进行写入时的上锁,然后刷新到磁盘!


这里可以看到,mmap依靠系统调用,把数据刷新回到磁盘!虽然这个刷新动作是由linux系统进行刷入的,保证了进程出问题的时候,也能够在系统级别刷入数据,比如MMKV的设计就采用了这点,但是这个也不是百分百可靠的,因为这个刷入操作是没有备份操作的/异常容灾处理,如果系统异常或者断电的情况,就会出现错误数据或者没有完全刷入磁盘的数据,造成数据异常,我们也可以看到mmkv介绍


image.png


mmap不足


mmap最大的不足,就是被映射的线性区只能是按照页的整数倍进行计算,如果说我们要映射的内存少于这个数,也是按照一个页进行映射的,当然,一个页长度会根据不同架构而不同,比如常见的4kb等,同时也要时刻注意映射区的生命周期,不然无脑映射也很容易造成oom


扩展


当然,说到mmap,肯定也离不开对mmkv的介绍,虽然mmkv很优秀,但是也不能无脑就使用,这方面可以参考朱凯大佬发布的文章Android 的键值对存储有没有最优解?,这里就不再赘述,按照自己所需的使用,当然,jetpack的DataStore也是很香啊!有机会的话也出一篇解析文!


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

Kotlin协程-协程的暂停与恢复 & suspendCancellableCoroutine的使用

前言 之前在网上看到有人问协程能不能像线程一样 wait(暂停) 和 notify(恢复) 。 应用场景是开启一个线程然后执行一段逻辑,得到了某一个数据,然后需要拿到这个数据去处理一些别的事情,需要把线程先暂停,然后等逻辑处理完成之后再把线程 notify。 ...
继续阅读 »

前言


之前在网上看到有人问协程能不能像线程一样 wait(暂停) 和 notify(恢复) 。


应用场景是开启一个线程然后执行一段逻辑,得到了某一个数据,然后需要拿到这个数据去处理一些别的事情,需要把线程先暂停,然后等逻辑处理完成之后再把线程 notify。


首先我们不说有没有其他的方式实现,我当然知道有其他多种其他实现的方式。单说这一种逻辑来看,我们使用协程能不能达到同样的效果?


那么问题来了,协程能像线程那样暂停与恢复吗?


协程默认是不能暂停与恢复的,不管是协程内部还是返回的Job对象,都不能暂停与恢复,最多只能delay延时一下,无法精准控制暂停与恢复。


但是我们可以通过 suspendCancellableCoroutine 来间接的实现这个功能。


那问题又来了,suspendCancellableCoroutine 是个什么东西?怎么用?


一、suspendCancellableCoroutine的用法


很多人不了解这个类,不知道它是干嘛的,其实我们点击去看源码就知道,源码已经给出了很清晰的注释,并且还附带了使用场景



简单的说就是把Java/Kotlin 的一些回调方法,兼容改造成 suspend 的函数,让它可以运行在协程中。


以一个非常经典的例子,网络请求我们可以通过 Retrofit+suspend 的方式,也可以直接使用 OkHttp 的方式,我们很早之前都是自己封装 OkHttpUtils 的,然后以回调的方式返回正确结果和异常处理。


我们就可以通过 suspendCancellableCoroutine 把 OkHttpUtils 的回调方式进行封装,像普通的 suspend 方法一样使用了。


    fun suspendSth() {

viewModelScope.launch {

val school = mRepository.getSchool() //一个是使用Retrofit + suspend

try {
val industry = getIndustry() //一个是OkHttpUtils回调的方式
} catch (e: Exception) {
e.printStackTrace() //捕获OkHttpUtils返回的异常信息
}

}

}

private suspend fun getIndustry(): String? {
return suspendCancellableCoroutine { cancellableContinuation ->

OkhttpUtil.okHttpGet("http://www.baidu.com/api/industry", object : CallBackUtil.CallBackString() {

override fun onFailure(call: Call, e: Exception) {
cancellableContinuation.resumeWithException(e)
}

override fun onResponse(call: Call, response: String?) {
cancellableContinuation.resume(response)
}

})
}
}

感觉使用起来真是方便呢,那除了 suspendCancellableCoroutine 有没有其他的方式转换回调?有,suspendCoroutine,那它们之间的区别是什么?


suspendCancellableCoroutine 和 suspendCoroutine 区别


SuspendCancellableCoroutine 返回一个 CancellableContinuation, 它可以用 resume、resumeWithException 来处理回调和抛出 CancellationException 异常。


它与 suspendCoroutine的唯一区别就是 SuspendCancellableCoroutine 可以通过 cancel() 方法手动取消协程的执行,而 suspendCoroutine 没有该方法。


所以尽可能使用 suspendCancellableCoroutine 而不是 suspendCoroutine ,因为协程的取消是可控的。


那我们不使用回调直接用行不行?当然可以,例如:


  fun suspendSth() {

viewModelScope.launch {

val school = mRepository.getSchool()

if (school is OkResult.Success) {

val lastSchool = handleSchoolData(school.data)

YYLogUtils.w("处理过后的School:" + lastSchool)
}

}


}

private suspend fun handleSchoolData(data: List<SchoolBean>?): SchoolBean? {

return suspendCancellableCoroutine {

YYLogUtils.w("通过开启一个线程延时5秒再返回")
thread {
Thread.sleep(5000)

it?.resume(mSchoolList?.last(), null)
}

}
}

那怎么能达到协程的暂停与恢复那种效果呢?我们把参数接收一下,变成成员变量不就行了吗?想什么时候resume就什么时候resume。


二、实现协程的暂停与恢复


我们定义一个方法开启一个协程,内部使用一个 suspendCancellableCoroutine 函数包裹我们的逻辑(暂停),再定义另一个方法内部使用 suspendCancellableCoroutine 的 resume 来返回给协程(恢复)。


  fun suspendSth() {

viewModelScope.launch {

val school = mRepository.getSchool() //网络获取数据

if (school is OkResult.Success) {

val lastSchool = handleSchoolData(school.data)

//下面的不会执行的,除非 suspendCancellableCoroutine 的 resume 来恢复协程,才会继续走下去

YYLogUtils.w("处理过后的School:" + lastSchool)
}

}


}

private var mCancellableContinuation: CancellableContinuation<SchoolBean?>? = null
private var mSchoolList: List<SchoolBean>? = null

private suspend fun handleSchoolData(data: List<SchoolBean>?): SchoolBean? {

mSchoolList = data

return suspendCancellableCoroutine {

mCancellableContinuation = it

YYLogUtils.w("开启线程睡眠5秒再说")
thread {
Thread.sleep(5000)

YYLogUtils.w("就是不返回,哎,就是玩...")

}

}
}

//我想什么时候返回就什么时候返回
fun resumeCoroutine() {

YYLogUtils.w("点击恢复协程-返回数据")

if (mCancellableContinuation?.isCancelled == true) {
return
}

mCancellableContinuation?.resume(mSchoolList?.last(), null)

}

使用: 点击开启协程暂停了,再点击下面的按钮即恢复协程


fun testflow() {
mViewModel.suspendSth()
}

fun resumeScope() {
mViewModel.resumeCoroutine()
}

效果是点击开启协程之后我等了20秒恢复了协程,打印如下:



总结


协程虽然默认是不支持暂停与恢复,但是我们可以通过 suspendCancellableCoroutine 来间接的实现。


虽然如此,但实例开发上我还是不太推荐这么用,这样的场景我们有多种实现方式。可以用其他很好的方法实现,比如用一个协程不就好了吗串行执行,或者并发协程然后使用协程的通信来传递,或者用线程+队列也能做等等。真的一定要暂停住协程吗?不是不能实现,只是感觉不是太优雅。


(注:不好意思,这里有点主观意识了,大家不一定就要参考,毕竟它也只是一种场景需求实现的方式而已,只要性能没问题,所有的方案都是可行,大家按需选择即可)


当然关于 suspendCancellableCoroutine 谷歌的本意是让回调也能兼容协程,这也是它最大的应用场景。


本期内容如讲的不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



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

ConstraintLayout 中的 Barrier 和 Chains

1. Barrier是一个准则,可以说是对其的规则,这样说还不够名义,我们可以列表一些比较常见的场景; 官网 Barrier。具体看图 “第二行的label”和“第二行value”是一个整体,他们距离上面是 100dp ...
继续阅读 »

1. Barrier

是一个准则,可以说是对其的规则,这样说还不够名义,我们可以列表一些比较常见的场景; 官网 Barrier

  1. 具体看图

1883633-62653bd01cb70813.webp “第二行的label”和“第二行value”是一个整体,他们距离上面是 100dp ,但是有可能“第二行value”的值为空或者是空,也需要“第二行label”距离上面的距离是 100dp ,由于我们知道“第二行value”的高度高于第一个,所以采用的是“第二行label”跟“第二行value”对其,“第二行value”距离上边 100dp 的距离,但是由于“第二行value”有可能为空,所以当“第二行value”为空的时候就会出现下面的效果:

1883633-043d00e43ff22557.webp 我们发现达不到预期,现在能想到的办法有,首先在代码控制的时候随便把“第二行label”的 marginTop 也添加进去;还有就是换布局,将“第二行label”和“第二行value”放到一个布局中,比如 LinearLayout ,这样上边的 marginTop 由 LinearLayout 控制;这样的话即便“第二行value”消失了也会保持上边的效果。

除了上边的方法还能使用其他的嘛,比如我们不使用代码控制,我们不使用其他的布局,因为我们知道布局嵌套太多性能也会相应的下降,所以在编写的时候能减少嵌套的情况下尽可能的减少,当然也不能为了减少嵌套让代码变得格外的复杂。

为了满足上面的需求, Barrier 出现了,它能做到隐藏的也能依靠它,并且与它的距离保持不变对于隐藏的“第二行value”来说,虽然消失了,但保留了 marginTop 的数值。下面看看布局:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="头部"
android:textSize="36sp"
app:layout_constraintBottom_toTopOf="@id/barrier3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
android:layout_marginTop="100dp"
app:constraint_referenced_ids="textView2,textView3" />

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第二行label"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView3"
app:layout_constraintVertical_bias="0.538" />

<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_marginStart="12dp"
android:layout_marginTop="100dp"
android:text="第二行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/barrier3" />

</androidx.constraintlayout.widget.ConstraintLayout>

这样即便将“第二行value”消失,那么总体的布局仍然达到预期,并且也没有添加很多布局内容。在代码中:

<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
android:layout_marginTop="100dp"
app:constraint_referenced_ids="textView2,textView3" />

这里主要有两个属性 app:barrierDirection 和 app:constraint_referenced_ids :

  • app:barrierDirection 是代表位置,也就是在包含内容的哪一个位置,我这里写的是 top ,是在顶部,还有其他的属性 top,bottom,left,right,start 和 end 这几个属性,看意思就很明白了。
  • app:constraint_referenced_ids 上面说的内容就是包含在这里面的,这里面填写的是 id 的名称,如果有多个,那么使用逗号隔开;这里面的到 Barrier 的距离不会改变,即便隐藏了也不会变。

这里可能会有疑惑,为啥我写的 id 为 textView4 的也依赖于 Barrier ,这是因为本身 Barrier 只是规则不是实体,它的存在只能依附于实体,不能单独存在于具体的位置,如果我们只有“第二行value”依赖于它,但是本身“第二行value”没有上依赖,也相当于没有依赖,这样只会导致“第二行label”和“第二行value”都消失,如果 textView4 依赖于 Barrier ,由于 textView4 的位置是确定的,所以 Barrier 的位置也就确定了。

  1. 类似表格的效果。看布局效果:

1883633-c4b862a2df57fb96.webp 我要做成上面的样子。也就是右边永远与左边最长的保持距离。下面是我的代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">


<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="头部"
android:textSize="36sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第二行"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView3" />

<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:text="第二行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/barrier4"
app:layout_constraintTop_toBottomOf="@+id/textView4" />


<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第三次测试"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView6"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView6" />

<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:text="第三行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/barrier4"
app:layout_constraintTop_toBottomOf="@+id/textView3" />

<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="textView2,textView5"
tools:layout_editor_absoluteX="411dp" />


</androidx.constraintlayout.widget.ConstraintLayout>

添加好了,记得让右边的约束指向 Barrier 。这里的 Barrier ,我们看到包含 textView2 和 textView5 ,这个时候就能做到谁长听谁的,如果此时 textView2 变长了,那么就会将就 textView2 。

2. Chains

我们特别喜欢使用线性布局,因为我们发现 UI 图上的效果使用线性布局都可以使用,当然也有可能跟大部分人的思维方式有关系。比如我们非常喜欢的,水平居中,每部分的空间分布等等都非常的顺手。既然线性布局这么好用,那为啥还有约束布局呢,因为线性布局很容易写出嵌套很深的布局,但约束布局不会,甚至大部分情况都可以不需要嵌套就能实现,那是不是代表线性布局有的约束布局也有,答案是肯定的。

使用普通的约束关系就很容易实现水平居中等常用效果,其他的如水平方向平均分布空间,使用一般的约束是实现不了的,于是就要使用 Chains ,这个就很容易实现下面的效果:

1883633-78aa31c23dcb4c4f.webp 其实上一篇中我已经把官网的教程贴上去了,这里主要写双向约束怎么做,一旦双向约束形成,那么就自然进入到 Chains 模式。

1)在视图模式中操作

1883633-618f9b2eb563a637.webp

如果直接操作,那么只能单向约束,如果要形成这样的约束,需要选择相关的的节点,比如我这里就是同时选择 A 和 B ,然后点击鼠标右键,就可以看到 Chains → Create Horizontal Chain 。

对应的操作

选择图中的选项即可完成从 A 指向 B ,修改的示意图为:

1883633-cf3984e22df83c7c.webp

我们发现已经实现了水平方向的排列效果了。至于怎么实现上面的效果,主要是改变 layout_constraintVertical_chainStyle 和 layout_constraintHorizontal_chainStyle 属性。至于权重则是属性 layout_constraintHorizontal_weight 。

layout_constraintHorizontal_chainStyle 属性说明:

  • spread 默认选项,效果就是上面的那种,也就是平均分配剩余空间;
  • spread_inside 两边的紧挨着非 Chains 的视图,中间的平均分配;

1883633-49c52026c6797e51.webp

  • packed 所有的都在中间

1883633-714e58d28eaab99c.webp 注意了, layout_constraintHorizontal_weight 这个属性只有在 A 身上设置才可以,也就是首节点上设置才可行,同时 layout_constraintHorizontal_weight 是代表水平方向,只能在水平方向才发生作用,如果水平的设置了垂直则不生效。

layout_constraintHorizontal_weight 这个属性只有在当前视图的宽或者高是 0dp 。至于这个的取值跟线性布局相同。

1883633-072f1f968528ef1a.webp

2)代码的方式 跟上面的差别就是在做双向绑定,用代码就很容易实现双向绑定,可平时添加约束相同。


作者:吴敬悦
链接:https://juejin.cn/post/6994385157306351653
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

❤️Android 12 高斯模糊-RenderEffect❤️

 Android 12 高斯模糊 新功能:更易用的模糊、彩色滤镜等特效 。 新的 API 让你能更轻松地将常见图形效果应用到视图和渲染结构上。 使用 RenderEffect 将模糊、色彩滤镜等效果应用于 RenderNode 或 View。 ...
继续阅读 »

 Android 12 高斯模糊


新功能:更易用的模糊、彩色滤镜等特效 。


新的 API 让你能更轻松地将常见图形效果应用到视图和渲染结构上。




  • 使用 RenderEffect 将模糊、色彩滤镜等效果应用于 RenderNode 或 View




  • 使用新的 Window.setBackgroundBlurRadius() API 为窗口背景创建雾面玻璃效果,




  • 使用 blurBehindRadius 来模糊窗口后面的所有内容。




咱们一个一个玩。


🔥 RenderEffect


💥 实现效果


    private void setBlur(){
View.setRenderEffect(RenderEffect.createBlurEffect(3, 3, Shader.TileMode.REPEAT));
...
}

使用特别简单,走你。


🌀 X 轴的模糊效果图



咱再看看代码


    private void setBlur(){
agb.iv1.setRenderEffect(RenderEffect.createBlurEffect(3, 0, Shader.TileMode.CLAMP));
agb.iv2.setRenderEffect(RenderEffect.createBlurEffect(8, 0, Shader.TileMode.REPEAT));
agb.iv3.setRenderEffect(RenderEffect.createBlurEffect(18, 0 ,Shader.TileMode.MIRROR));
agb.iv4.setRenderEffect(RenderEffect.createBlurEffect(36, 0,Shader.TileMode.DECAL));
}

RenderEffect.createBlurEffect()的四个参数:




  • radiusX 沿 X 轴的模糊半径




  • radiusY 沿 Y 轴的模糊半径




  • inputEffect 模糊一次(传入 RenderEffect)




  • edgeTreatment 用于如何模糊模糊内核边缘附近的内容




下面两种仅看效果图。就不做代码设置了。


🌀 Y 轴的模糊效果图



🌀 XY同时模糊效果图



第四个参数对边缘模糊,效果图如下:



Shader.TileMode 提供了四个选项恕我没看出来。。


这里还有一堆方法等你玩。




注意:注意如此完美的画面只能在 Android 12(SDK31)及以上的设备上使用,其他版本的设备使用会导致崩溃,谨记谨记。
效果有了,下面咱们一起看看源码。



💥 源码


🌀 View.setRenderEffect()


    public void setRenderEffect(@Nullable RenderEffect renderEffect) {
...
}

这个方法就是:renderEffect 应用于 View。 传入 null清除之前配置的RenderEffect 。这里咱们先看传入的 RenderEffect。


🌀 RenderEffect.createBlurEffect()


    public static RenderEffect createBlurEffect(
float radiusX,
float radiusY,
@NonNull RenderEffect inputEffect,
@NonNull TileMode edgeTreatment
) {
long nativeInputEffect = inputEffect != null ? inputEffect.mNativeRenderEffect : 0;
return new RenderEffect(
nativeCreateBlurEffect(
radiusX,
radiusY,
nativeInputEffect,
edgeTreatment.nativeInt
)
);
}

两个 createBlurEffect() 方法,分别为三参(模糊一次)和四参(模糊两次)。inputEffect 先进行了一次模糊。


看效果图:



模糊程度一样,但是实现方式不同:


    private void setBlur() {
RenderEffect radiusXRenderEffect = RenderEffect.createBlurEffect(10, 0, Shader.TileMode.MIRROR);
RenderEffect radiusYRenderEffect = RenderEffect.createBlurEffect(0, 10, Shader.TileMode.MIRROR);
agb.iv1.setRenderEffect(RenderEffect.createBlurEffect(10, 10, Shader.TileMode.CLAMP));
agb.iv2.setRenderEffect(RenderEffect.createBlurEffect(10, 10, Shader.TileMode.REPEAT));
//自身radiusY 为 0 ,传入的radiusYRenderEffect设置的radiusY为10;
agb.iv3.setRenderEffect(RenderEffect.createBlurEffect(10, 0, radiusYRenderEffect, Shader.TileMode.MIRROR));
//自身radiusX 为 0 ,传入的radiusXRenderEffect设置的radiusX为10;
agb.iv4.setRenderEffect(RenderEffect.createBlurEffect(0, 10, radiusXRenderEffect, Shader.TileMode.DECAL));
}

这个方法返回一个 new RenderEffect(nativeCreateBlurEffect(...)。


那咱们去看看 nativeCreateBlurEffect()


🌀 nativeCreateBlurEffect()


frameworks/base/libs/hwui/jni/RenderEffect.cpp


static const JNINativeMethod gRenderEffectMethods[] = {
...
{"nativeCreateBlurEffect", "(FFJI)J", (void*)createBlurEffect},
...
};

static jlong createBlurEffect(JNIEnv* env , jobject, jfloat radiusX,
jfloat radiusY, jlong inputFilterHandle, jint edgeTreatment) {
auto* inputImageFilter = reinterpret_cast<SkImageFilter*>(inputFilterHandle);
sk_sp<SkImageFilter> blurFilter =
SkImageFilters::Blur(
Blur::convertRadiusToSigma(radiusX),
Blur::convertRadiusToSigma(radiusY),
static_cast<SkTileMode>(edgeTreatment),
sk_ref_sp(inputImageFilter),
nullptr);
return reinterpret_cast<jlong>(blurFilter.release());
}

这里有两个函数来处理我们传过来的模糊的值,咱进去看看。


🌀 convertRadiusToSigma(convertSigmaToRadius)


//该常数近似于在SkBlurMask::Blur()(1/sqrt(3)中,在软件路径的"高质量"模式下进行的缩放。
static const float BLUR_SIGMA_SCALE = 0.57735f;

float Blur::convertRadiusToSigma(float radius) {
return radius > 0 ? BLUR_SIGMA_SCALE * radius + 0.5f : 0.0f;
}

float Blur::convertSigmaToRadius(float sigma) {
return sigma > 0.5f ? (sigma - 0.5f) / BLUR_SIGMA_SCALE : 0.0f;
}

🌀 sk_ref_sp(inputImageFilter)


external/skia/include/core/SkRefCnt.h


/*
* 返回包装提供的 ptr 的 sk_sp 并对其调用 ref (如果不为空)
*/
template <typename T> sk_sp<T> sk_ref_sp(T* obj) {
//sk_sp<SkImageFilter> :
return sk_sp<T>(SkSafeRef(obj));
}

//SkSafeRef:检查参数是否为非空,如果是,则调用 obj->ref() 并返回 obj。
template <typename T> static inline T* SkSafeRef(T* obj) {
if (obj) {
obj->ref();
}
return obj;
}

再往下走


🌀 SkImageFilters::Blur()



#define SK_Scalar1 1.0f
#define SK_ScalarNearlyZero (SK_Scalar1 / (1 << 12))

sk_sp<SkImageFilter> SkImageFilters::Blur(
SkScalar sigmaX, SkScalar sigmaY, SkTileMode tileMode, sk_sp<SkImageFilter> input,
const CropRect& cropRect) {
if (sigmaX < SK_ScalarNearlyZero && sigmaY < SK_ScalarNearlyZero && !cropRect) {
return input;
}
return sk_sp<SkImageFilter>(
new SkBlurImageFilter(sigmaX, sigmaY, tileMode, input, cropRect));
}

附上最后的倔强


    constexpr sk_sp() : fPtr(nullptr) {}
constexpr sk_sp(std::nullptr_t) : fPtr(nullptr) {}

/**
* Shares the underlying object by calling ref(), so that both the argument and the newly
* created sk_sp both have a reference to it.
*/
sk_sp(const sk_sp<T>& that) : fPtr(SkSafeRef(that.get())) {}
template <typename U,
typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
sk_sp(const sk_sp<U>& that) : fPtr(SkSafeRef(that.get())) {}

/**
* Move the underlying object from the argument to the newly created sk_sp. Afterwards only
* the new sk_sp will have a reference to the object, and the argument will point to null.
* No call to ref() or unref() will be made.
*/
sk_sp(sk_sp<T>&& that) : fPtr(that.release()) {}
template <typename U,
typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
sk_sp(sk_sp<U>&& that) : fPtr(that.release()) {}

/**
* Adopt the bare pointer into the newly created sk_sp.
* No call to ref() or unref() will be made.
*/
explicit sk_sp(T* obj) : fPtr(obj) {}

createBlurEffect() 得到 long 类型的 native 分配的的非零地址, 传入 new RenderEffect()


🌀 new RenderEffect()


    /* 构造方法:仅从静态工厂方法构造 */
private RenderEffect(long nativeRenderEffect) {
mNativeRenderEffect = nativeRenderEffect;
RenderEffectHolder.RENDER_EFFECT_REGISTRY.registerNativeAllocation(
this, mNativeRenderEffect);
}

继续



/**
* @param classLoader ClassLoader 类加载器。
* @param freeFunction 类型为 nativePtr 的本机函数的地址,用于释放这种本机分配
* @return 由系统内存分配器分配的本机内存的 NativeAllocationRegistry。此版本更适合较小的对象(通常小于几百 KB)。
*/
private static class RenderEffectHolder {
public static final NativeAllocationRegistry RENDER_EFFECT_REGISTRY =
NativeAllocationRegistry.createMalloced(
RenderEffect.class.getClassLoader(), nativeGetFinalizer());
}

🌀 NativeAllocationRegistry.createMalloced()


libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java


    @SystemApi(client = MODULE_LIBRARIES)
public static NativeAllocationRegistry createMalloced(
@NonNull ClassLoader classLoader, long freeFunction, long size) {
return new NativeAllocationRegistry(classLoader, freeFunction, size, true);
}

🌀 NativeAllocationRegistry()


libcore/luni/src/main/java/libcore/util/NativeAllocationRegistry.java


    private NativeAllocationRegistry(ClassLoader classLoader, long freeFunction, long size,
boolean mallocAllocation) {
if (size < 0) {
throw new IllegalArgumentException("Invalid native allocation size: " + size);
}
this.classLoader = classLoader;
this.freeFunction = freeFunction;
this.size = mallocAllocation ? (size | IS_MALLOCED) : (size & ~IS_MALLOCED);
}

既然拿到 NativeAllocationRegistry 那就继续调用其
registerNativeAllocation() 方法。


🌀 registerNativeAllocation ()


    @SystemApi(client = MODULE_LIBRARIES)
@libcore.api.IntraCoreApi
public @NonNull Runnable registerNativeAllocation(@NonNull Object referent, long nativePtr) {
//当 referent 或nativePtr 为空
...
CleanerThunk thunk;
CleanerRunner result;
try {
thunk = new CleanerThunk();
Cleaner cleaner = Cleaner.create(referent, thunk);
result = new CleanerRunner(cleaner);
registerNativeAllocation(this.size);
} catch (VirtualMachineError vme /* probably OutOfMemoryError */) {
applyFreeFunction(freeFunction, nativePtr);
throw vme;
}
// Enable the cleaner only after we can no longer throw anything, including OOME.
thunk.setNativePtr(nativePtr);
// Ensure that cleaner doesn't get invoked before we enable it.
Reference.reachabilityFence(referent);
return result;
}

向 ART 注册新的 NativePtr 和关联的 Java 对象(也就是咱们设置的模糊类)。


返回的 Runnable 可用于在引用变得无法访问之前释放本机分配。如果运行时或使用 runnable 已经释放了本机分配,则 runnable 将不起作用。


RenderEffect 算是搞完了,咱们回到View.setRenderEffect()


🌀 View.setRenderEffect()


    public void setRenderEffect(@Nullable RenderEffect renderEffect) {
if (mRenderNode.setRenderEffect(renderEffect)) {
//视图属性更改(alpha、translationXY 等)的快速失效。
invalidateViewProperty(true, true);
}
}

这里有个 mRenderNode.setRenderEffect(renderEffect)。咱们近距离观望一番。


🌀 mRenderNode 的创建


咱们先找找他是在什么地方创建的。


    public View(Context context) {
...
//在View的构造方法中创建
mRenderNode = RenderNode.create(getClass().getName(), new ViewAnimationHostBridge(this));
...
}

🌀 RenderNode.create()


    /** @hide */
public static RenderNode create(String name, @Nullable AnimationHost animationHost) {
return new RenderNode(name, animationHost);
}

private RenderNode(String name, AnimationHost animationHost) {
mNativeRenderNode = nCreate(name);
//注册 Native Allocation。
NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativeRenderNode);
mAnimationHost = animationHost;
}

再往下感觉也看不到啥了 跟上面类似,看.cpp动态分配类的地址还是有点懵。让我缓缓~以后补充。


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

不要把公司当成家,被通知裁员时会变得不幸...

公司对于员工来说,究竟是个什么地方?不要把公司当家,公司只是你出卖劳动力,换取报酬的地方。人在社会上混,不要讲感情,也不要被别人洗脑,去傻乎乎地讲感情,跟你讲感情的老板,基本上都是没更多钱给你的老板。老板开公司就是来盈利的,不是为了什么社会责任感,也不是为了造...
继续阅读 »


公司对于员工来说,究竟是个什么地方?

不要把公司当家,公司只是你出卖劳动力,换取报酬的地方。

人在社会上混,不要讲感情,也不要被别人洗脑,去傻乎乎地讲感情,跟你讲感情的老板,基本上都是没更多钱给你的老板。

老板开公司就是来盈利的,不是为了什么社会责任感,也不是为了造福社会,更不是为了降低社会失业率开的。

老板只会去找一些能够适合他公司发展,能够帮他赚钱的员工过来上班。

员工对于公司来说,只是一个可有可无的螺丝钉,随时都可以替代,你有时会觉得自己在公司好像挺忙,为公司做了很大贡献,在公司不可或缺。但是都是你的错觉,要么就是老板或者公司强加给你的意识,实际上公司除了老板,哪个员工走了,公司都可以照样转。

员工跟老板的本质,就是员工出卖自己的劳动力和时间,然后跟老板换取报酬,大家都是交易而已。

如果哪天员工觉得他可以在别的公司拿到更高的薪水,他就会选择跳槽。老板觉得员工不能给公司带来与其薪水相匹配的利益,老板就会让这个员工卷铺盖走人。

对于一个员工来说,最好的老板不是每天讲情怀,讲社会责任感,也不是每天对他态度很和蔼,笑眯眯地关心他的生活过得好不好。老板的素质再高也没有什么鸟用,一个合格的老板,就是能够按时发工资

并且这个老板有本事能够让公司越做越大,让他在公司里面打工的员工,能拿到的薪水越来越多,并且能够提升的空间也越来越大。哪怕这个老板每天板着个脸,不近人情,也是一个很好的老板。

对于一个老板来说,最好的员工就是拿最低的薪水干最多的事情,能够为他带来最大的利益,这就是一个好的员工。

所以老板不会看你是什么211、双一流、清北毕业的,也不会看你考了多少证书,这些东西虽然有时候是个敲门砖,只是因为能够为老板寻找合适的人节省时间,但是真正入职之后,老板还是要看你是否能够为他创造相应的价值。

对于绝大部分员工来说,老板跟他的关系的重要性,远远超过了他跟他所谓的亲戚朋友,同事同学等等之间的关系。

因为那些人虽然看起来有血缘关系,还沾亲带故的,但是这些人跟他都没有利益上的往来,但是老板却是他的衣食父母。

说白了,老板就是相当于是他的客户,他提供劳动,并出卖自己的时间,老板是购买他劳动和时间的人,跟他是有经济利益关系的。

很多时候,员工应该多琢磨自己跟老板之间的关系,而不要去琢磨跟自己没有经济利益往来的一些人的关系。哪怕这些人跟你很熟,还是亲戚。

所以不要把公司当家,放任自己在里面任性、软弱、懒惰,那对公司不是损失。因为它可以随时换人,而自己损失的时间和机会永远回不来了。

要把公司当成球队或船,抓住在里面的一切机会锻炼自己增加技能,让自己强大,强大到离开它也不会怕,它离开你需要掂量掂量...。

-

小编我:趁着中午吃饭,“偷拍”公司同事工位,第一张是我的...,有财神那张是财务的,嘿嘿。










作者:思齐大神

来源丨蚂蚁大喇叭

收起阅读 »

多行文本下的文字渐隐消失术

web
本文将探讨一下,在多行文本情形下的一些有意思的文字动效。多行文本,相对于单行文本,场景会复杂一些,但是在实际业务中,多行文本也是非常之多的,但是其效果处理比起单行文本会更困难。单行与多行文本的渐隐首先,我们来看这样一个例子,我们要实现这样一个单行文本的渐隐:使...
继续阅读 »

本文将探讨一下,在多行文本情形下的一些有意思的文字动效。

多行文本,相对于单行文本,场景会复杂一些,但是在实际业务中,多行文本也是非常之多的,但是其效果处理比起单行文本会更困难。

单行与多行文本的渐隐

首先,我们来看这样一个例子,我们要实现这样一个单行文本的渐隐:


使用 mask,可以轻松实现这样的效果,只需要:

<p>Lorem ipsum dolor sit amet consectetur.</p>
p {
  mask: linear-gradient(90deg, #fff, transparent);
}

但是,如果,场景变成了多行呢?我们需要将多行文本最后一行,实现渐隐消失,并且适配不同的多行场景:


这个就会稍微复杂一点点,但是也是有多种方式可以实现的。

首先我们来看一下使用 background 的方式。

使用 background 实现

这里会运用到一个技巧,就是 display: inline 内联元素的 background 展现形式与 display: block 块级元素(或者 inline-blockflexgrid)不一致。

简单看个例子:

<p>Lorem .....</p>
<a>Lorem .....</a>

这里需要注意,<p> 元素是块级元素,而 <a>内联元素

我们给它们统一添加上一个从绿色到蓝色的渐变背景色:

p, a {
background: linear-gradient(90deg, blue, green);
}

看看效果:


什么意思呢?区别很明显,块级元素的背景整体是一个渐变整体,而内联元素的每一行都是会有不一样的效果,整体连起来串联成一个整体。

基于这个特性,我们可以构造这样一种布局:

<p><a>Mollitia nostrum placeat consequatur deserunt velit ducimus possimus commodi temporibus debitis quam</a></p>
p {
  position: relative;
  width: 400px;
}

a {
  background: linear-gradient(90deg, transparent, transparent 70%, #fff);
  background-repeat: no-repeat;
  cursor: pointer;
  color: transparent;
   
  &::before {
      content: "Mollitia nostrum placeat consequatur deserunt velit ducimus possimus commodi temporibus debitis quam";
      position: absolute;
      top: 0;
      left: 0;
      color: #000;
      z-index: -1;
  }
}

这里需要解释一下:

  1. 为了利用到实际的内联元素的 background 的特性,我们需要将实际的文本包裹在内联元素 <a>

  2. 实际的文本,利用了 opacity: 0 进行隐藏,实际展示的文本使用了 <a> 元素的伪元素,并且将它的层级设置为 -1,目的是让父元素的背景可以盖过它

  3. <a> 元素的渐变为从透明到白色,利用它去遮住下面的实际用伪元素展示的文字,实现文字的渐隐

这样,我们就能得到这样一种效果:


这里,<a> 元素的渐变为从透明到白色,利用后面的白色逐渐遮住文字。

如果我将渐变改为从黑色到白色(为了方便理解,渐变的黑色和白色都带上了一些透明),你能很快的明白这是怎么回事:

a {
  background: linear-gradient(90deg, rgba(0,0,0, .8), rgba(0,0,0, .9) 70%, rgba(255, 255, 255, .9));
}


完整的代码,你可以戳这里:CodePen Demo -- Text fades away[1]

当然,这个方案有很多问题,譬如利用了 z-index: -1,如果父容器设置了背景色,则会失效,同时不容易准确定位最后一行。因此,更好的方式是使用 mask 来解决。

使用 mask 实现

那么,如果使用 mask 的话,问题,就会变得简单一些,我们只需要在一个 mask 中,实现两块 mask 区域,一块用于准确控制最后一行,一块用于控制剩余部分的透明。

也不需要特殊构造 HTML:

<p>Lorem ipsum dolor sit amet ....</p>
p {
  width: 300px;
  padding: 10px;
  line-height: 36px;
  mask:
      linear-gradient(270deg, transparent, transparent 30%, #000),
      linear-gradient(270deg, #000, #000);
  mask-size: 100% 46px, 100% calc(100% - 46px);
  mask-position: bottom, top;
  mask-repeat: no-repeat;
}

效果如下:


核心在于整个 mask 相关的代码,正如上面而言的,mask 将整个区域分成了两块进行控制:


在下部分这块,我们利用 mask 做了从右向左的渐隐效果。并且利用了 mask-position 定位,以及 calc 的计算,无论文本都多少行,都是适用的!需要说明的是,这里的 46px 的意思是单行文本的行高加上 padding-bottom 的距离。可以适配任意行数的文本:


完整的代码,你可以戳这里:CodePen Demo -- Text fades away 2[2]

添加动画效果

好,看完静态的,我们再来实现一种**动态的文字渐隐消失。

整体的效果是当鼠标 Hover 到文字的时候,整个文本逐行逐渐消失。像是这样:

图片

这里的核心在于,需要去适配不同的行数,不同的宽度,而且文字是一行一行的进行消失。

这里核心还是会运用上内联元素 background 的特性。在 妙用 background 实现花式文字效果[3] 这篇文章中,我们介绍了这样一种技巧。

实现整段文字的渐现,从一种颜色到另外一种颜色

<div>Button</div>
<p><a>Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia nostrum placeat consequatur deserunt velit ducimus possimus commodi temporibus debitis quam, molestiae laboriosam sit repellendus sed sapiente quidem quod accusantium vero.</a></p>
a {    
  background:
      linear-gradient(90deg, #999, #999),
      linear-gradient(90deg, #fc0, #fc0);
  background-size: 100% 100%, 0 100px;
  background-repeat: no-repeat;
  background-position: 100% 100%, 0 100%;
  color: transparent;
  background-clip: text;
}
.button:hover ~ p a {
  transition: .8s all linear;
  background-size: 0 100px, 100% 100%;
}

这里需要解释一下,虽然设置了 color: transparent,但是文字默认还是有颜色的,默认的文字颜色,是由第一层渐变赋予的 background: linear-gradient(90deg, #999, #999), linear-gradient(90deg, #fc0, #fc0),也就是这一层:linear-gradient(90deg, #999, #999)

图片

当 hover 触发时,linear-gradient(90deg, #999, #999) 这一层渐变逐渐消失,而另外一层 linear-gradient(90deg, #fc0, #fc0)` 逐渐出现,借此实现上述效果。

CodePen -- background-clip 文字渐现效果[4]

好,我们可以借鉴这个技巧,去实现文字的渐隐消失。一层为实际的文本,而另外一层是进行动画的遮罩,进行动画的这一层,本身的文字设置为 color: transparent,这样,我们就只能看到背景颜色的变化。

大致的代码如下:

<p>
  <a>Mollitia nostrum placeat consequatur deserunt.</a>
  <a>Mollitia nostrum placeat consequatur deserunt.</a>
</p>
p {
  width: 500px;
}
.word {
  position: absolute;
  top: 0;
  left: 0;
  color: transparent;
  color: #000;
}
.pesudo {    
  position: relative;
  background: linear-gradient(90deg, transparent, #fff 20%, #fff);
  background-size: 0 100%;
  background-repeat: no-repeat;
  background-position: 100% 100%;
  transition: all 3s linear;
  color: transparent;
}
p:hover .pesudo,
p:active .pesudo{
  background-size: 500% 100%;
}

其中,.word 为实际在底部,展示的文字层,而 pesudo 为叠在上方的背景层,hover 的时候,触发上方元素的背景变化,逐渐遮挡住下方的文字,并且,能适用于不同长度的文本。

图片

当然,上述方案会有一点瑕疵,我们无法让不同长度的文本整体的动画时间一致。当文案数量相差不大时,整体可以接受,文案相差数量较大时,需要分别设定下 transition-duration 的时长。

完整的 DEMO,你可以戳:CodePen -- Text fades away Animation[5]

最后

好了,本文到此结束,希望对你有帮助 :)

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

参考资料

[1]CodePen Demo -- Text fades away: https://codepen.io/Chokcoco/pen/xxWPZmz

[2]CodePen Demo -- Text fades away 2: https://codepen.io/Chokcoco/pen/MWVvoyW

[3]妙用 background 实现花式文字效果: https://github.com/chokcoco/iCSS/issues/138

[4]CodePen -- background-clip 文字渐现效果: https://codepen.io/Chokcoco/pen/XWgpyqz

[5]CodePen -- Text fades away Animation: https://codepen.io/Chokcoco/pen/wvmqqWa

[6]Github -- iCSS: https://github.com/chokcoco/iCSS

来源:mp.weixin.qq.com/s/qADnUx3G2tKyMT7iv6qFwg


收起阅读 »

Flutter 中使用Chip 小部件

概述 典型的chip是一个圆角的小盒子。它有一个文本标签,并以一种有意义且紧凑的方式显示信息。chip可以在同一区域同时显示多个交互元素。一些流行的chip用例是: 发布标签(您可以在许多 WordPress ,VuePress,知乎,掘金,公众号或 Git...
继续阅读 »

概述


典型的chip是一个圆角的小盒子。它有一个文本标签,并以一种有意义且紧凑的方式显示信息。chip可以在同一区域同时显示多个交互元素。一些流行的chip用例是:



  • 发布标签(您可以在许多 WordPress ,VuePress,知乎,掘金,公众号或 GitHub等大型平台上看到它们)。

  • 可删除的内容列表(一系列电子邮件联系人、最喜欢的音乐类型列表等)。


img


在 Flutter 中,您可以使用以下构造函数来实现 Chip 小部件:


Chip({
 Key? key,
 Widget? avatar,
 required Widget label,
 TextStyle? labelStyle,
 EdgeInsetsGeometry? labelPadding,
 Widget? deleteIcon,
 VoidCallback? onDeleted,
 Color? deleteIconColor,
 bool useDeleteButtonTooltip = true,
 String? deleteButtonTooltipMessage,
 BorderSide? side,
 OutlinedBorder? shape,
 Clip clipBehavior = Clip.none,
 FocusNode? focusNode,
 bool autofocus = false,
 Color? backgroundColor,
 EdgeInsetsGeometry? padding,
 VisualDensity? visualDensity,
 MaterialTapTargetSize? materialTapTargetSize,
 double? elevation,
 Color? shadowColor
})

只有label属性是必需的,其他是可选的。一些常用的有:



  • avatar:在标签前显示一个图标或小图像。

  • backgroundColor : chip的背景颜色。

  • padding:chip内容周围的填充。

  • deleteIcon:让用户删除chip的小部件。

  • onDeleted:点击deleteIcon时调用的函数。


您可以在官方文档中找到有关其他属性的更多详细信息。但是,对于大多数应用程序,我们不需要超过一半。


简单示例


这个小例子向您展示了一种同时显示多个chip的简单使用的方法。我们将使用Wrap小部件作为chip列表的父级。当当前行的可用空间用完时,筹码会自动下行。由于Wrap 小部件的间距属性,我们还可以方便地设置chip之间的距离。


截屏:


image-20220125100331474


代码:


Scaffold(
     appBar: AppBar(
       title: const Text('大前端之旅'),
    ),
     body: Padding(
       padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 10),
       child: Wrap(
           // space between chips
           spacing: 10,
           // list of chips
           children: const [
             Chip(
               label: Text('Working'),
               avatar: Icon(
                 Icons.work,
                 color: Colors.red,
              ),
               backgroundColor: Colors.amberAccent,
               padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
            ),
             Chip(
               label: Text('Music'),
               avatar: Icon(Icons.headphones),
               backgroundColor: Colors.lightBlueAccent,
               padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
            ),
             Chip(
               label: Text('Gaming'),
               avatar: Icon(
                 Icons.gamepad,
                 color: Colors.white,
              ),
               backgroundColor: Colors.pinkAccent,
               padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
            ),
             Chip(
               label: Text('Cooking & Eating'),
               avatar: Icon(
                 Icons.restaurant,
                 color: Colors.pink,
              ),
               backgroundColor: Colors.greenAccent,
               padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
            )
          ]),
    ),
  );

在这个例子中,chip只呈现信息。在下一个示例中,chip是可交互的。


复杂示例:动态添加和移除筹码


应用预览


chip


我们要构建的应用程序包含一个浮动操作按钮。按下此按钮时,将显示一个对话框,让我们添加一个新chip。可以通过点击与其关联的删除图标来删除每个chip。


以下是应用程序的工作方式:


完整代码


main.dart中的最终代码和解释:


// main.dart
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
  return MaterialApp(
    debugShowCheckedModeBanner: false,
    title: '大前端之旅',
    theme: ThemeData(
      primarySwatch: Colors.green,
    ),
    home: const HomePage(),
  );
}
}

// Data model for a chip
class ChipData {
// an id is useful when deleting chip
final String id;
final String name;
ChipData({required this.id, required this.name});
}

class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);

@override
_HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
// list of chips
final List<ChipData> _allChips = [];

// Text controller (that will be used for the TextField shown in the dialog)
final TextEditingController _textController = TextEditingController();
// This function will be triggered when the floating actiong button gets pressed
void _addNewChip() async {
  await showDialog(
      context: context,
      builder: (_) {
        return AlertDialog(
          title: const Text('添加'),
          content: TextField(
            controller: _textController,
          ),
          actions: [
            ElevatedButton(
                onPressed: () {
                  setState(() {
                    _allChips.add(ChipData(
                        id: DateTime.now().toString(),
                        name: _textController.text));
                  });

                  // reset the TextField
                  _textController.text = '';

                  // Close the dialog
                  Navigator.of(context).pop();
                },
                child: const Text('提交'))
          ],
        );
      });
}

// This function will be called when a delete icon associated with a chip is tapped
void _deleteChip(String id) {
  setState(() {
    _allChips.removeWhere((element) => element.id == id);
  });
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('大前端之旅'),
    ),
    body: Padding(
      padding: const EdgeInsets.all(15),
      child: Wrap(
        spacing: 10,
        children: _allChips
            .map((chip) => Chip(
                  key: ValueKey(chip.id),
                  label: Text(chip.name),
                  backgroundColor: Colors.amber.shade200,
                  padding:
                      const EdgeInsets.symmetric(vertical: 7, horizontal: 10),
                  deleteIconColor: Colors.red,
                  onDeleted: () => _deleteChip(chip.id),
                ))
            .toList(),
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _addNewChip,
      child: const Icon(Icons.add),
    ),
  );
}
}


结论


我们已经探索了 Chip 小部件的许多方面,并经历了不止一个使用该小部件的示例。


大家喜欢的话,点赞支持一下坚果


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

DeepLink在转转的实践

1. DeepLink 简介 DeepLink:“深度链接”技术,这个名词初看比较抽象,不过在我们身边却有不少应用,比如以下场景: 刷抖音看到转转的广告,点击视频下方的下载链接,如果没有安装转转则下载转转,并在打开转转后跳转到相应活动页面 在微信看到朋友分享...
继续阅读 »

1. DeepLink 简介


DeepLink:“深度链接”技术,这个名词初看比较抽象,不过在我们身边却有不少应用,比如以下场景:



  • 刷抖音看到转转的广告,点击视频下方的下载链接,如果没有安装转转则下载转转,并在打开转转后跳转到相应活动页面

  • 在微信看到朋友分享的转转商品,点击后如果没有安装转转则下载转转,并在打开转转跳转到相应商品的详情页

  • 看到转转发送的订单提醒短信,点击链接后如果没有安装转转则下载转转, 并在打开转转跳转到相应订单详情页


DeepLink 使用户能够在目标 APP 之外,比如广告(抖音)/社交媒体(微信)/短信中通过点击链接,直接跳转到目标 APP 特定的页面(对于已经安装了 APP 会直接进行跳转,未安装 APP 会引导下载,下载安装完成之后跳转)。DeepLink 技术可以实现场景的快速还原,缩短用户使用路径,更重要的是能够用于 APP 拉新推广场景,降低用户流失率。


随着短视频的风靡,通过短视频投放广告获客的方式也流行起来, 本文主要介绍在新媒体拉新推广场景中 DeepLink 的应用以及服务端的搭建。


2 .应用场景


我在刷抖音时刷到一个转转回收的广告视频,而家里刚好有闲置的手机,我就抱着试一试的态度点击视频下方的下载链接下载转转,看看能不能在这个平台上处理掉手中的闲置,当下载安装成功之后打开跳转到了回收页面,我可能会眉头一挑,嗯~这个体验还挺好,然后测了下闲置手机值多少钱,回收价又刚好满足我的心理预期,而且还能为碳中和贡献一份自己的力量,何乐而不为之。


新媒体获客场景


以上是比较常见的一种场景,通过在抖音、快手或者其他渠道来投放广告来吸引一些有需求的用户来到转转,并通过 DeepLink 技术在下载完成打开转转后直接跳转到用户感兴趣的页面。


对于上述场景安卓和 IOS 的实现是有所区别的,包括下载策略以及 APP 内部跳转到用户感兴趣页面的策略。


2.1 IOS 应用场景


由于 IOS 下载APP只能通过 AppStore,所以 DeepLink 服务针对 IOS 会重定向到一个 H5 中间页,在 H5 中间页将服务端返回的 DeepLink 跳转链接复制到剪切板中,并拉起 AppStore 引导用户下载转转 APP,安装打开后 IOS 从剪切板中获取跳转链接进行跳转,到达用户感兴趣的页面。


IOS下载


2.2 安卓应用场景


安卓可以直接通过 DeepLink 服务下载转转 APP,而 DeepLink 跳转链接以 APK Signature Scheme v2 方式打入 apk 包中,安装打开后解析跳转链接跳转到用户感兴趣的页面。


安卓下载


APK Signature Scheme v2 是 Android 7.0 引入的一项新的应用签名方案 ,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。


下图是新的签名方案和旧的签名方案的一个对比:


新旧签名方案对比


APK Signing Block 结构
































偏移字节数描述
@+08这个 Block 的长度(本字段的长度不计算在内)
@+8n一组 ID-value
@-248这个 Block 的长度(和第一个字段一样值)
@-1616魔数 “APK Sig Block 42”

APK Signing Block 中的 ID-value 是可扩展的,由于 APK Signing Block 的数据块不会参与签名校验,也就是说我们可以自定义一组 ID-value,用于存储额外的信息,我们通过自定义ID-value将跳转信息打入APK包中。


3. DeepLink 服务


在 IOS DeepLink 方案中服务端只是负责重定向到一个 H5 中间页,因此不再赘述,下面我们主要介绍下安卓的 DeepLink 方案。


概要设计


3.1 投放链接设计


投放链接是投放到各个渠道的下载链接,需要考虑以下几点:




  1. 各个渠道链接规则不一样,保证我们链接规则能够覆盖所有渠道


    通过我们的调研有些渠道只支持 Get 请求,有些渠道不允许带参数,有些渠道必须以.apk 进行结尾




  2. 投放方便,链接投放出去之后不需要再改动


    由于投放链接是给到一些自媒体创作者,在给出链接之后能够保证从始至终都能下到最新的APP




  3. 充分利用 CDN


    转转 APP、找靓机 APP 的包百兆左右,为了保证服务的稳定性同样为了节约带宽,尽量发挥 CDN 的作用把绝大多数请求让 CDN 服务器来进行处理返回




3.1.1 兼容版本1.0


考虑到兼容各个渠道,某些渠道必须以 apk 结尾、某些渠道不支持Get请求带参数,采用什么方式?


既然不能带参数,那我们的参数信息可以直接拼到path中,参数以某种规则组装,服务端解析,需要的信息包括 APP 类型、渠道信息、DeepLink 链接信息、版本号等,简要设计出的投放链接 1.0 大致如下:


apk.zhuanstatic.com/deeplink/**…



  • appType: APP 类型,目前支持转转和找靓机,可扩展,如 zhuanzhuan

  • channel:渠道类型,根据每个投放渠道单独设置渠道 id,如 douyin666

  • version:APP 版本号,如 9.0.0

  • deepLink:deepLink 信息,目前传输 deepLinkId,deepLinkId 和端内跳转链接的映射关系由后台维护,服务端通过映射关系拿到跳转链接打入 apk 包中,如 huishou


3.1.2 升级版本2.0


1.0的版本号是直接写到 path 中的,这会造成很多隐患



  1. 可以通过修改版本号恶意下载 APP 的任意版本

  2. 保证用户一直下到最新的包需要版本更新之后更新所有投放链接


这显然是不合理的,针对以上两点我们必然需要删掉 version,替代方案可以让服务端在处理下载请求的时候通过其他方式拿到版本信息,修正后的投放链接 2.0 如下:


apk.zhuanstatic.com/deeplink/**…


3.1.3 最终版本3.0


2.0中没有了版本信息进而导致相同的渠道投放链接是一致的,只要 CDN 中有老版本APP的缓存,下载的是缓存的老版本APP,无法获取最新APP


因此我们考虑中间做一次重定向,通过一个不接入 CDN 的固定链接去重定向到一个接入 CDN 的带版本号的链接,这样问题就迎刃而解了,因此投放链接 3.0 应运而生:


apk.zhuanzhuan.com/deeplink/**…

apk.zhuanstatic.com/deeplink/**…


apk.zhuanzhuan.com 不走 CDN,只是将链接中的版本号补全并重定向到走 CDN 的 apk.zhuanstatic.com ,这样在投放链接不变的情况下能保证用户下载到最新的包。


3.2 打包&下载


投放链接设计好之后,通过投放链接可以解析到一些参数信息,比如:
apk.zhuanstatic.com/deeplink/zh…


我们知道用户下载的是douyin666渠道转转9.0.0版本的包,并且APP打开后需要跳转回收的页面。


下载渠道包服务端逻辑主要分为两大块,第一部分是拿到相应版本的原始包,然后通过 APK Signature Scheme v2 方式将渠道号和 DeepLink 跳转链接打入原始包中获得渠道包,将渠道包提供给用户进行下载。


为了能应对 APP 升级和渠道投放带来的流量,尤其是 CDN 中还没有缓存的时候,避免大量请求将我们服务打垮,所以需要引入本地缓存,如何引入?


首先我们分析下服务端的主要逻辑找出不可变的数据,第一原始包肯定是不变的,第二在原始包相同的情况下如果 channel 和 deepLink 跳转链接是一致的,那我们打包出来的渠道包也相应是不可变的,因此我们可以针对这两部分来进行缓存。


接下来我们分析缓存选型以及缓存策略,本地缓存的组件有好多可选的,比如 Caffeine Cache、Guava Cache 等,网上关于他们的测评如下:


读场景性能对比


可以看到在读场景下,caffeine cache 是当之无愧的王者,而且我们的场景基本是接近 100%的读,所以我们优先选择了 Caffeine Cache。


以下是两个本地缓存策略介绍:


3.2.1 一级缓存(渠道包)


     /**
* 缓存高频渠道包文件
*/
private static final Cache<String, byte[]> channelFinalAppCache = CacheBuilder
.newBuilder()
.expireAfterAccess(1, TimeUnit.DAYS)
.maximumSize(15)
.build();

渠道包的缓存 key 是 appType+version+channel+deepLink,由于 channel 和 deepLink 组合的众多,通过分析之前的下载数据缓存最高频的 15 个渠道包就基本满足 90%以上的请求而且不至于占用太多的内存,而为了获取最高频的 15 个渠道,我们通过大数据平台以 T+1 的方式将渠道数据更新到数据库中,DeepLink服务通过定时任务读取数据库中的渠道数据刷新缓存。


3.2.2 二级缓存(原始包)


    /**
* 缓存原始包文件
*/
private static final Cache<String, byte[]> channelAppCache = CacheBuilder
.newBuilder()
.expireAfterAccess(2, TimeUnit.DAYS)
.maximumSize(10)
.build();

原始包的缓存 key 是 appType+version,由于我们只下载最新版本的包, APP 类型暂时只有转转和找靓机,所有我们设置最大数量 10 是足够的,在我们应用启动的时候会对这个缓存进行初始化,以避免第一次用户下载速度过慢,并在之后监听APP的发版信息,新版本更新后刷新缓存。


4. 总结


DeepLink 服务支撑了新媒体投放以及 APP 内置更新的下载能力,为了保证服务稳定性和性能,除上述缓存策略外,还有其他策略来协同,比如 APP 发新版本时会进行 CDN 预热,将下载量高的渠道包缓存到 CDN 中,以使大部分流量能够在 CDN 服务器被消化,即使有突发流量打过来也会有限流规则过滤流量以保证服务的稳定性。


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

Retrofit解密:接口请求是如何适配suspend协程?

最初的retrofit请求 我们先看下原来如何通过retrofit发起一个网络请求的,这里我们直接以官网的例子举例: 动态代理创建请求服务 interface GitHubService { //创建get请求方法 @GET("users/{u...
继续阅读 »

最初的retrofit请求


我们先看下原来如何通过retrofit发起一个网络请求的,这里我们直接以官网的例子举例:


动态代理创建请求服务


interface GitHubService {
//创建get请求方法
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String?): Call<Response>
}

//动态代理创建GitHubService
fun createService(): GitHubService {
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()

return retrofit.create(GitHubService::class.java)
}



  • retrofit.create底层是通过动态代理创建的GitHubService的一个子实现类;




  • 创建的这个GitHubService一般作为单例进行使用,这里只是简单举例没有实现单例;




发起网络请求


fun main() {
//异步执行网络请求
createService().listRepos("").enqueue(object : Callback<Response> {
override fun onResponse(call: Call<Response>, response: retrofit2.Response<Response>) {
//主线程网络请求成功回调
}

override fun onFailure(call: Call<Response>, t: Throwable) {
//主线程网络请求失败回调
}
})
}

这种调用enqueue()异步方法并执行callback的方式是不是感觉很麻烦,如果有下一个请求依赖上一个请求的执行结果,那就将会形成回调地狱这种可怕场景。


协程suspend本就有着以同步代码编写执行异步操作的能力,所以天然是解决回调地狱好帮手。接下来我们看下如何使用协程suspend


借助suspend发起网络请求


suspend声明接口方法


interface GitHubService {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String?): Response<String>
}

可以看到就是在listRepos方法声明前加了个suspend关键字就完了。


创建协程执行网络请求


fun main() {
//1.创建协程作用域,需要保证协程的调度器是分发到主线程执行
val scope = MainScope()
scope.launch(CoroutineExceptionHandler { _, _ ->
//2.捕捉请求异常
}) {
//3.异步执行网络请求
val result = createService().listRepos("")

val content = result.body()?
}
}



  1. 首先创建一个协程作用域,需要保证协程调度器类型为Dispatchers.Main,这样整个协程的代码块都会默认在主线程中执行,我们就可以直接在里面执行UI相关操作




  2. 创建一个CoroutineExceptionHandler捕捉协程执行过程中出现的异常,这个捕捉异常的粒度比较大,是捕捉整个协程块的异常,可以考虑使用try-catch专门捕获网络请求执行的异常:


    //异步执行网络请求
    try {
    val result = createService().listRepos("")
    } catch (e: Exception) {
    //可以考虑执行重连等逻辑或者释放资源
    }



  3. 直接调用listRepos()方法即可,不需要传入任何回调,并直接返回方法结果。这样我们就实现了以同步的代码实现了异步网络请求。




接下来我们就看下如何retrofit源码是如何实现这一效果的。


retrofit如何适配suspend


直接定位到HttpServiceMethod.parseAnnotations()方法:


static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
//1.判断是否为suspend挂起方法
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;

//省略一堆和当前分析主题不想关的代码

if (!isKotlinSuspendFunction) {
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else if (continuationWantsResponse) {
//挂起执行
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>();
} else {
//挂起执行
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>();
}
}

1.判断是否为suspend挂起方法


看下requestFactory.isKotlinSuspendFunction赋值的地方,经过一番查找(省略...),最终方法在RequestFactoryparseParameter间接赋值:


private @Nullable ParameterHandler<?> parseParameter() {
//...
//1.是否是方法最后一个参数
if (allowContinuation) {
try {
if (Utils.getRawType(parameterType) == Continuation.class) {
//2.标识为suspend挂起方法
isKotlinSuspendFunction = true;
return null;
}
} catch (NoClassDefFoundError ignored) {
}
}
}

如果一个方法被声明为suspend,该方法翻译成java代码就会给该方法添加一个Continuation类型的参数,并且放到方法参数的最后一个位置,比如:


private suspend fun test66(name: String) {  
}

会被翻译成:


private final Object test66(String name, Continuation $completion) {
return Unit.INSTANCE;
}

所以上面的代码就可以判断出请求的接口方法是否被suspend声明,是isKotlinSuspendFunction将会被置为true。


2.挂起则创建SuspendForResponseSuspendForBody


这个地方我们以SuspendForBody进行分析,最终会执行到其adapt()方法:


@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
//1.获取参数
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];

try {
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
//2.调用真正的挂起方法
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}



  1. 获取调用的suspend声明的接口方法中获取最后一个Continuation类型参数




  2. 调用await方法,由于这是一个kotlin定义的接收者为Call的挂起方法,如果在java中调用,首先第一个参数要传入接收者,也就是call,其实await()是一个挂起方法,翻译成java还会增加一个Continuation类型参数,所以调用await()还要传入第一步获取的Continuation类型参数。




3.核心调用await()方法探究


await()就是retrofit适配suspend实现同步代码写异步请求的关键,也是消除回调地狱的关键:


suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
//关键
continuation.resumeWithException(KotlinNullPointerException())
} else {
//关键
continuation.resume(body)
}
} else {
//关键
continuation.resumeWithException(HttpException(response))
}
}

override fun onFailure(call: Call<T>, t: Throwable) {
//关键
continuation.resumeWithException(t)
}
})
}
}

使用到了协程的一个非常关键的方法suspendCancellableCoroutine{},该方法就是用来捕获传入的Continuation并决定什么恢复挂起的协程执行的,比如官方的delay()方法也是借助该方法实现的。


所以当我们执行调用enqueue()方法时在网络请求没有响应(成功或失败)前,协程一直处于挂起的状态,之后收到网络响应后,才会调用resume()resumeWithException()恢复挂起协程的执行,这样我们就实现了同步代码实现异步请求的操作,而不需要任何的callback嵌套地狱


总结


本篇文章详细分析retrofit如何适配suspend协程的,并且不用编写任何的callback回调,直接以同步代码编写实现异步请求的操作。


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

【Android】一键登录 - 三大运营商

业务背景: 在条件允许的情况下(无 SIM 卡的手机,无法触发一键登录),通过运行商提供的服务,进行【一键登录】。简化用户的登录操作,提高 App 的登录注册率以及使用率。 本方案采用的是阿里云中【一键登录】方案。 效果图: 前提知识: 整个流程如图所 ...
继续阅读 »

业务背景:


在条件允许的情况下(无 SIM 卡的手机,无法触发一键登录),通过运行商提供的服务,进行【一键登录】。简化用户的登录操作,提高 App 的登录注册率以及使用率。


本方案采用的是阿里云中【一键登录】方案。


效果图:



前提知识:



  • 整个流程如图所



(图源自网络[掘金大佬-NanBox],侵删)



  • 该方案下,不允许使用完全自定义的授权页。但是可以通过属性配置,进行一定的修改。可修改的属性如下图所示



Android 接入流程:


1.浅析 Demo


通常第一步都是下载官方 Demo 后,进行一番调试,盘点功能列表,是否符合自身需求。


链接:pan.baidu.com/s/1RX5yGp06… 提取码:qbx0


接下来,简单分析 Demo 项目架构,帮助大家尽快上手这个项目。


首先,我们要知道这个 Demo,是包括【一键登录】和【本机号码校验】两个功能。根据自己的需求分析对应的代码即可。这次我们只使用到前者,所以后者内容不在这里讲述。



主要看到下列三个模块:


Config - 就是上面预告知识中说到的配置项,主要是授权页的一些配置项


OneKeyLoginActivity - 登录页面


MessageActivity - 模拟【其他登录方式】页面


那具体的实现,就可以直接看对应模块的内容即可。可以在原 Demo,进行调试。


2.接入思路分析


基于判断是否支持【一键登录】的时机 提供两种接入思路


第一种:启动登录功能前判断



判断的方式可以通过


mPhoneNumberAuthHelper.checkEnvAvailable(PhoneNumberAuthHelper.SERVICE_TYPE_LOGIN)
复制代码

是否支持【一键登录】。该流程未经检验,大家可以执行验证。


第二种:直接唤起【一键登录】,失败后再唤起【其他登录方式】



Demo 也是第二种方式。这种方式需要用到一个壳 Activity 。但这个壳主要的作用是初始化SDK,以及做逻辑判断和处理(即并无实际内容展示)。


这里引发一个思考:


既然用不到 Activity 的内容,那能不能换种方式呢呢?对于单例,我思考后,一开始觉得是没问题的,但是等写完后,发现我写成了一个 OneKeyLoginHelper 的单例,发现相应逻辑处理需要传入 activity 或者 fragment 的引用。那么我们知道单例中是不能持有这样的引用的(这里可以考虑使用弱引用),这会导致内容泄漏。不知道是否还有其他的方法?


3.代码接入流程


//STEP 1.初始化监听器(这里根据业务自己做处理)


//STEP 2.初始化SDK实例


//STEP 3.设置SDK秘钥


//STEP 4.唤起一键登录页


4.避坑


接着,讲一下接入过程中,遇到的一些问题。帮大家避免无效劳动,可以有更多的时间学(hua )习(shui)。


问题描述: 因为选择了第二种思路,那么会有个壳 Activity 的问题。这个壳,我们不处理的话,是不透明的,这样当我们进到这个壳的时候,再跳转到别的页面就会有个空白页。


解决方案: 将壳的主题改为透明色,经过实验,下述代码可以实现。(壳Activity 需要继承 AppCompatActivity)


<style name="Theme.Transparent" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">false</item>
</style>

问题描述: 发现从【授权页】跳到【其他方式登录】的时候,授权页会逐渐变透明,会看到下一层页面的内容。如动图中,粉红色的箭头所示。



解决方案: 可以直接忽略,这个是 SDK 本身的问题。因为阿里那边给的回复是:(是否有最新解决方案,会及时更新,或者可以以你们当时咨询的为准)


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

记录 Kotlin 实践的一些好建议

目录 注释 函数式接口 高阶函数 扩展函数 注释 Java:    /**     * @see AdVideoUserInfoContainerData#type     */ &nbs...
继续阅读 »

目录



  1. 注释

  2. 函数式接口

  3. 高阶函数

  4. 扩展函数


注释


Java:


    /**
    * @see AdVideoUserInfoContainerData#type
    */
   public Builder type(int type) {
       userInfoData.type = type;
       return this;
  }
   /** 事件配置, 对应于 {@link FeedAdLottieRepoInfo#name 属性} */
   public String lottieConfig;

Kotlin:


/**
* 由[CountDownType.type] mapTo [CountDownType]
* 避免了使用 when(type) 写 else 了
*/
private fun type2Enum(type: () -> String): CountDownType {
   return CountDownType.values().firstOrNull {
       it.type == type()
  } ?: CountDownType.CIRCLE
}

Kotlin 可以使用内联标记来引用类、方法、属性等,这比 Java 中的 @see、@link 更加易用。


文档:kotlinlang.org/docs/kotlin…


函数式接口


非函数式接口:


internal interface ICountDownCallback {
   /**
    * 倒计时完成时回调
    */
   fun finish()
}

internal fun setCountDownCallback(callback: ICountDownCallback) {
   // ignore
}

internal fun show() {
   setCountDownCallback(object : ICountDownCallback {
       override fun finish() {
           TODO("Not yet implemented")
      }
  })
}

函数式接口:


internal fun interface ICountDownCallback {
   /**
    * 倒计时完成时回调
    */
   fun finish()
}

internal fun setCountDownCallback(callback: ICountDownCallback) {
   // ignore
}

internal fun show() {
   setCountDownCallback {
       TODO("Not yet implemented")
  }
}

函数式接口也被称为单一抽象方法(SAM)接口,使用函数式接口可以使代码更加简洁,富有表现力。


对于 Java 的接口,比如 View.OnClickListener,它在使用的时候可以直接转 lambda 使用的,只有是 kotlin 的单一抽象方法,需要加 fun 关键字标示它为函数式接口。


文档:kotlinlang.org/docs/fun-in…


高阶函数


如果对象的初始化比较麻烦,可以使用高阶函数,让代码更加流畅:


    // 定义
open fun attachToViewGroup(
       viewGroup: ViewGroup,
       index: Int = -1,
       lp: () -> MarginLayoutParams = {
           MarginLayoutParams(
               LayoutParams.WRAP_CONTENT,
               LayoutParams.WRAP_CONTENT
          )
      }
  ) {
      (this.parent as? ViewGroup)?.removeView(this)
       viewGroup.addView(this, lp.invoke())
  }

// 使用
   override fun attachToViewGroup(viewGroup: ViewGroup, index: Int, lp: () -> MarginLayoutParams) {
       super.attachToViewGroup(viewGroup, index) {
           MarginLayoutParams(
               ViewGroup.LayoutParams.WRAP_CONTENT,
               ViewGroup.LayoutParams.WRAP_CONTENT
          ).apply {
               leftMargin = 14.px(context)
               topMargin = 44.px(context)
          }
      }
  }

如果参数的获取比较复杂,代码比较长,有不少判断逻辑,也可以使用高阶函数:


// 定义
fun getCountDownViewByType(context: Context, type: () -> String = { "0" }) {
// ignore
}
// 使用
countDownView = CountDownType.getCountDownViewByType(this) {
rewardVideoCmdData.cmdPolicyData?.countDownType ?: ""
}

如果方法的返回值是一个状态值,然后根据状态值去做相关逻辑处理。这种情况下,其实我们想要的是一个行为,比如代码中充斥着大量的数据解析、校验等逻辑,我们也可以是使用高阶函数重构:


// 重构之前
/**
* 校验数据有效(校验标题和按钮有一个不为空,就可以展示 Dialog)
*/
fun checkValid(): Boolean {
   return !dialogTitle.isNullOrEmpty() || !buttonList.isNullOrEmpty()
}

private fun bindData() {
   rewardData = RewardDialogData(arguments?.getString(EXTRA_REWARD_DATA) ?: "")
   // 弹窗数据不合法,就不需要展示 dialog 了
   if (rewardData == null || !rewardData!!.checkValid()) {
       dismiss()
       return
  }
   // 更新字体颜色等
   updateSkin()
}


// 重构之后
/**
* 数据校验失败,执行 [fail] 函数
*/
internal inline fun RewardDialogData?.checkFailed(fail: () -> Unit) {
   this?.let {
       if (dialogTitle.isNullOrEmpty() && buttonList.isNullOrEmpty()) {
           fail()
      }
  } ?: fail()
}


private fun bindData() {
   rewardData = RewardDialogData(arguments?.getString(EXTRA_REWARD_DATA) ?: "")
   // 弹窗数据不合法,就不需要展示 dialog 了
   rewardData?.checkFailed {
       dismiss()
       return
  }
   // 更新字体颜色等
   updateSkin()
}

kotlin 标准库里面也是有非常多的高阶函数的,比如作用域函数(let、apply、run等等),除此之外,还有一些集合类的标准库函数:


// filter
fun showCharge() {
   adMonitorUrl?.filter {
       !it.showUrl.isNullOrEmpty()
  }?.forEach {
       ParallelCharge.charge(it.showUrl)
  }
}
// forEachIndexed
list.forEachIndexed { index, i ->
// ignore
}

文档:kotlinlang.org/docs/lambda…


扩展函数


// 比较不流畅的写法
val topImgUrl = rewardData?.topImg
if (topImgUrl.isNullOrBlank()) {
   topImg.visibility = View.GONE
} else {
   topImg.hierarchy?.useGlobalColorFilter = false
   topImg.visibility = View.VISIBLE
   topImg.setImageURI(topImgUrl)
}

// 使用局部返回标签
topImg.apply {
   if (topImgUrl.isNullOrEmpty()) {
       visibility = View.GONE
       return@apply
  }
   hierarchy?.useGlobalColorFilter = false
   setImageURI(topImgUrl)
   visibility = View.VISIBLE
}

/**
* 校验 View 可见性
*
* @return [predicate] false: GONE;true: VISIBLE
*/
internal inline fun <reified T : View> T.checkVisible(predicate: () -> Boolean): T? {
   return if (predicate()) {
       visibility = View.VISIBLE
       this
  } else {
       visibility = View.GONE
       null
  }
}

// 使用扩展函数
topImg.checkVisible {
   !topImgUrl.isNullOrEmpty()
}?.run {
   hierarchy?.useGlobalColorFilter = false
   setImageURI(topImgUrl)
}

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

我们需要从单体转到微服务吗?

起源martinfowler.com/articles/microservices.html。和微服务相对应的是单体架构,先来看看单体架构是怎样的。大多数人做软件开发应该都是从单体架构开始的,以 .NET 程序员来说,从最早的 WebForm、到后来的 MVC...
继续阅读 »

微服务或许你没有真正实践过,但一定听说过,虽然已经到了 2022 年,这个词依然很热,可以通过搜索 google 指数看得到。

起源

“微服务”一词源于 2011 年 5 月在威尼斯附近的一次软件架构师研讨会上进行的架构风格的讨论。2012 年 5 月 讨论小组决定将这种架构风格命名为“微服务”。Fred George 同年在一次技术大会上进行自己的微服务实践分享,并说微服务是一种细粒度的 SOA ,但最终将其发扬光大的是 Martin Fowler 2014 年写的博文《 Microservices 》,原文链接如下:

martinfowler.com/articles/microservices.html

自此以后,微服务就家喻户晓了,Microservices 的 Google 指数也是在 2014 年后就一路飙升。

和微服务相对应的是单体架构,先来看看单体架构是怎样的。

单体架构

大多数人做软件开发应该都是从单体架构开始的,以 .NET 程序员来说,从最早的 WebForm、到后来的 MVC、再到现在的前后端分离,后端使用 .NET 的 WebAPI ,都是整个项目的代码放到一个解决方案中,发布要么直接整个目录进行替换,或者更新有变更的 dll 文件。

包括到现在,这种单体架构的模该还占着很大的比重,凡是存在,必有道理,单体架构有着他的可取之处:

  • 开发方便,.NET 程序员只需只需使用宇宙最强 IDE VS 就可以。

  • 调试方便,在开发阶段,所有的项目都在一个解决方案下,项目之间是可以直接引用,断点可以到达你想要的任何地方。

  • 运行方便,编码完成,只需一个 F5 搞定。

  • 部署方便,无论是之前部署 IIS ,还是现在的容器部署,都只涉及到一个发布目录。

不过,随着产品的功能越来越复杂,代码也会变得越来越复杂,团队的人数也会越来越多,这时单体架构就会带来一些问题:

  • 因为代码库非常的臃肿,从编译、构建、运行到测试这个时间会越来越长。

  • 技术栈几乎是受限的,比如一个 .NET 的工程,基本就是 C# 来开发了,不太可能混杂其他的语言。

  • 不方便横向扩展,只能整套程序进行扩,满足所有模块的需求,对资源的利用率非常差。

  • 不够敏捷,团队成员越来越多多时,都在同一个代码上进行修改、提交、合并,容易引发冲突和其他问题。

  • 一个很小的改动点,容易引发全身问题,导致系统崩溃,因为影响点多,测试成本也会很高。

  • 缺乏可靠性,我们就碰到过因为一个序列化的问题导致 CPU 占用很高,结果整个系统瘫痪了。

微服务架构

上面提到的单体架构存在的问题,采用微服务架构可以很好地解决。微服务的核心是为了解耦,构建成一个松耦合的分布式系统。

一个庞大的单体系统拆分成若干个小的服务,每个服务可以由一个小的团队来维护,团队会更加敏捷,构建发布的时间更短,代码也容易维护。

不同的微服务团队可以采用不同的技术栈,比如工作流引擎使用 .NET ,规则引擎可以使用 Java ,一些全新的模块更容易采用新的技术,人员流动和补充上也更加灵活。

每个服务通常采用独立的数据库,代码或者数据库层面的问题不会导致整个系统的崩溃。

扩展方便,这个很重要,如果监控发现流程引擎的压力很大,可以只针对这个服务进行横向扩展,服务器资源可以得到更好的利用。

上面说的都是好处,但没有任何一种技术是银弹,微服务解决问题的同时,也会带来更多的问题。

1、开发调试变得困难了,需要通过日志的方式或者借助一些远程调试工具。

2、单体架构中,模块之间的调用都是进程内,添加类库的引用后,就是本地方法的调用,微服务各自独立部署,就会涉及到进程间的通信。

3、线上问题往往需要多个服务团队一起来协作解决,会存在互相甩锅的问题。

4、在分布式系统中,事务、数据一致性、联合查询等相比较单体更加复杂。

5、持续集成、部署、运维的复杂度也显著提升。

6、随着服务越来越多,客户端怎样去找这些服务呢?

7、进程内的访问不存在网络的问题,拆分后的服务可能在同个机器的不同进程,更多的时候是不同机器的不同进程,网络问题导致服务不稳定怎么办?

为了解决这些问题,各种中间件和框架就应运而生,又会带来更多的学习成本。

在 .NET 技术栈中,会用到下面这些中间件:

  • 服务注册与发现:Consul。

  • 网关:Ocelot。

  • 熔断降级:Polly。

  • 服务链路追踪:SkyWalking 或 Twitter 的 Zipkin。

  • 配置中心:Apllo。

  • 鉴权中心:IdentityServer4。

在 Java 中也有 Spring Cloud 和 Spring Cloud Alibaba 这种全家桶套件可以使用。

要不要转微服务呢?

从单体到微服务是一个权衡和取舍的问题,切记不要跟风。以我的经验来看,可以分为两类:

  1. 做企业级系统。

  2. 做互联网系统。

做企业级应用大多都是项目交付型的,客户关系维系的好,后面可以做二期、三期,当然也有一锤子买卖的。这其中一个关键点是要快,单从快速来看,采用单体架构,开发、调试、部署都是最快的。

从客户角度来说,只要能满足业务,是单体还是微服务其实不太关心。

做互联网应用,也就是我们常说的 SaaS,也分为两种情况:

1、将现有的私有化部署的系统(单体架构)改造成支持 SaaS 的模式。

这种我也不建议一上来就大刀阔斧地进行微服务改造,可以在代码的结构上做一些调整,比如按照领域去拆分目录,不同领域之间的调用可以再进行一层抽象,目的是为了未来向微服务架构转化。

当团队的技术栈变得丰富了,比如原先只有 .NET ,现在有些模块采用的是 Java ,这时已然是朝着微服务架构发展了,只是粒度比较大而已,相应的一些中间件也需要引入,比如服务网关、服务发现、服务间通信等。

2、从零开始做一个 SaaS 系统。

互联网系统和企业级系统有很大的差别,如果说企业级系统更多关注功能性需求,那么互联网系统除了功能性需求,还需要关注非功能性需求,比如:横向扩展、限流降级、日志追踪、预警、灰度发布等。

即便因为时间关系,一开始是单体架构,我觉得也应该是微服务架构的单体,随着持续迭代和发展,根据实际情况逐步进行拆分。

如果时间上比较充裕,可以一开始就按照微服务架构进行分离,但粒度不要太小。

总结

  1. 解决常说的的三高问题(高并发、高性能、高可用),一个核心的思路就是拆,分而治之,所以说微服务肯定是能解决掉我们的很多问题,也是发展方向。

  2. 实践微服务需要根据当前的实际情况,如果单体运行的很好,也没什么问题,也不要为了炫技进行微服务改造。

  3. 如果决定要实践微服务,先做好单体架构的设计,让代码遵循面向对象的设计原则,否则即便形式上变成了微服务,也不能尝到微服务的甜头。

作者:不止dotNET

收起阅读 »

牛逼,一款 996 代码分析工具

程序员是一个创作型的职业,频繁的加班并不能增加产出,而国内 996 的公司文化,真的一言难尽。但是如果你进到一家公司,你能从哪些观察来判断这家公司的工作强度的(加班文化)?是看大家走得早不早吗?有一定的参考意义,但是如果走得晚呢,可能是大家不敢提前走而在公司耗...
继续阅读 »

程序员是一个创作型的职业,频繁的加班并不能增加产出,而国内 996 的公司文化,真的一言难尽。但是如果你进到一家公司,你能从哪些观察来判断这家公司的工作强度的(加班文化)?是看大家走得早不早吗?有一定的参考意义,但是如果走得晚呢,可能是大家不敢提前走而在公司耗时间。

今天要推荐一个代码分析工具 code996,它可以统计 Git 项目的 commit 时间分布,进而推导出这个项目的编码工作强度。这算是一种对项目更了解的方式,杜绝 996 从了解数据开始。

我们先来看 code996 分析出来的结果示例,以下是分析项目的基本情况:


通过图表查看 commit 提交分布:


对比项目工作时间类型:


如果你对 code996 是如何工作的,以下是作者的说明:


因为代码是公司的很重要的资产,泄露是肯定不行的,为了解决大家的后顾之忧,该项目是完全安全的。


code996 除了能够分析项目的实际工作强度,也能用来分析我们代码编写的情况,对自身了解自己代码编写效率的时段、最近的工作强度等都是非常好的一个输入。

更多项目详情请查看如下链接。

开源项目地址:https://github.com/hellodigua/code996

开源项目作者:hellodigua

收起阅读 »

七夕,程序员到底该送什么礼物给女朋友?参与讨论有奖励哦

据统计,程序员只有30%是有女朋友的,还有15%是有男朋友的,而已经结婚的占50%。我想,你总不至于是剩下的5%吧!马上情人节了,想好送什么礼物没?柳天明、美国队长、Jiayun、李全喜、conanma柳天明、美国队长、Jiayun、李全喜、conanma
据统计,程序员只有30%是有女朋友的,还有15%是有男朋友的,而已经结婚的占50%。
我想,你总不至于是剩下的5%吧!
马上情人节了,想好送什么礼物没?

获奖名单:

柳天明、美国队长、Jiayun、李全喜、conanma

请以上获奖同学8月13日24:00前私信我邮寄信息!

马上就要情人节了又到了着急买什么送给对象的时候了!

作为一名程序员,如何快乐简单不掉头发的为各种节日安排好女朋友的礼物可能是个难题。

当网络上在讨论程序员的时候,脸谱化的礼物往往就是 鼠标/键盘/降噪耳机,但我知道,每一个程序员,除了程序人生之外,都有自己的精彩而美丽的生活。对你的女朋友来说,也是同样。

先看看各路大牛脑洞大开:

  • 高德地图实现“爱心”轨迹


(来源:juejin.cn/post/7126576400441540621)

  • 浪漫邂逅小动画


(源码:https://github.com/alexwjj/qixi)

  • 情诗表白墙


(来源:juejin.cn/post/7127210046840111117,预览

  • 无法拒绝的表白


(源码:https://github.com/andyngojs/crush-love)

  • 浪漫专属chrome插件


(作者:蜡笔小心_)

  • 代码+谜语系列




所有的相遇,都是命中注定:

致橡树(舒婷):





不得不说,这些礼物都很有特色,

但是,别人需要的是这些吗?

别人需要的是口红,是包包,是各种首饰

你整那些,就好比看到她打扫卫生辛苦了,

自己吃个冰镇瓜、自己打把游戏、自己找朋友K歌,

把自己的享受作为对她的奖励,这合理吗?

5%的群体正在向你招手欢迎!


各位情场得意的码农届高质量群体请分享下,你在七夕送了或得到了什么礼物?

我们这些直男最擅长复制粘贴和clone了。

参与回复的5人有机会获得imgeek准备的小礼物~


获奖名单:

柳天明、美国队长、Jiayun、李全喜、conanma

请以上获奖同学8月13日24:00前私信我邮寄信息!

一天高中的女同桌突然问我是不是程序猿

背景 昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”, 先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人...
继续阅读 »

背景


昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”,


image-20211015101843733.png


先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人生苦短,我用python),即使如此,
为了大家的面子,为了程序猿们的脸,不就简单的小Python嘛,必须答应!


梳理需求


现有excel表格记录着 有效图片的名字,如:


image-20211015103418631.png


要从一个文件夹里把excel表格里记录名字的图片筛选出来;


需求也不是很难,代码思路就有了:



  1. 读取Excel表格第一列的信息并放入A集合

  2. 遍历文件夹下所有的文件,判断文件名字是否存在A集合

  3. 存在A集合则拷贝到目标文件夹


实现(Python 2.7)


读取Excel表格

加载Excel表格的方法有很多种,例如pandasxlrdopenpyxl,我这里选择openpyxl库,
先安装库



pip install openpyxl



代码如下:


from openpyxl import load_workbook

def handler_excel(filename=r'C:/Users/xxx/Desktop/haha.xlsx'):
   # 根据文件路径加载一个excel表格,这里包含所有的sheet
   excel = load_workbook(filename)
   # 根据sheet名称加载对应的table
   table = excel.get_sheet_by_name('Sheet1')
   imgnames = []
   # 读取所有列
   for column in table.columns:
       for cell in column:
           imgnames.append(cell.value+".png")
# 选择图片
   pickImg(imgnames)

遍历文件夹读取文件名,找到target并拷贝

使用os.listdir 方法遍历文件,这里注意windows环境下拿到的unicode编码,需要GBK重新解码


def pickImg(pickImageNames):
   # 遍历所有图片集的文件名
   for image in os.listdir(
           r"C:\Users\xxx\Desktop\work\img"):
       # 使用gbk解码,不然中文乱码
       u_file = image.decode('gbk')
       print(u_file)
       if u_file in pickImageNames:
           oldname = r"C:\Users\xxx\Desktop\work\img/" + image
           newname = r"C:\Users\xxx\Desktop\work\target/" + image
           # 文件拷贝
           shutil.copyfile(oldname, newname)

简单搞定!没有砸程序猿的招牌,豪横的把成果发给女同桌,结果:


image-20211015112550343.png


换来有机会请你吃饭,微信都不带回的,哎 ,xdm,小丑竟是我自己!
小丑竟是我自己什么梗-小丑竟是我自己是什么意思出自什么-55手游网


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

Kotlin协程之Dispatchers原理

Kotlin协程不是什么空中阁楼,Kotlin源代码会被编译成class字节码文件,最终会运行到虚拟机中。所以从本质上讲,Kotlin和Java是类似的,都是可以编译产生class的语言,但最终还是会受到虚拟机的限制,它们的代码最终会在虚拟机上的某个线程上被执...
继续阅读 »

Kotlin协程不是什么空中阁楼,Kotlin源代码会被编译成class字节码文件,最终会运行到虚拟机中。所以从本质上讲,Kotlin和Java是类似的,都是可以编译产生class的语言,但最终还是会受到虚拟机的限制,它们的代码最终会在虚拟机上的某个线程上被执行。


之前我们分析了launch的原理,但当时我们没有去分析协程创建出来后是如何与线程产生关联的,怎么被分发到具体的线程上执行的,本篇文章就带大家分析一下。


前置知识


要想搞懂Dispatchers,我们先来看一下Dispatchers、CoroutineDispatcher、ContinuationInterceptor、CoroutineContext之间的关系


public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler

@JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher

@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined

@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
}

public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
}

public interface ContinuationInterceptor : CoroutineContext.Element {}

public interface Element : CoroutineContext {}

Dispatchers中存放的是协程调度器(它本身是一个单例),有我们平时常用的IO、Default、Main等。这些协程调度器都是CoroutineDispatcher的子类,这些协程调度器其实都是CoroutineContext


demo


我们先来看一个关于launch的demo:


fun main() {
val coroutineScope = CoroutineScope(Job())
coroutineScope.launch {
println("Thread : ${Thread.currentThread().name}")
}
Thread.sleep(5000L)
}

在生成CoroutineScope时,demo中没有传入相关的协程调度器,也就是Dispatchers。那这个launch会运行到哪个线程之上?


运行试一下:


Thread : DefaultDispatcher-worker-1

居然运行到了DefaultDispatcher-worker-1线程上,这看起来明显是Dispatchers.Default协程调度器里面的线程。我明明没传Dispatchers相关的context,居然会运行到子线程上。说明运行到default线程是launch默认的。


它是怎么与default线程产生关联的?打开源码一探究竟:


public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//代码1
val newContext = newCoroutineContext(context)

//代码2
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)

//代码3
coroutine.start(start, coroutine, block)
return coroutine
}


  1. 将传入的CoroutineContext构造出新的context

  2. 启动模式,判断是否为懒加载,如果是懒加载则构建懒加载协程对象,否则就是标准的

  3. 启动协程


我们重点关注代码1,这是与CoroutineContext相关的。


public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
//从父协程那里继承过来的context+这次的context
val combined = coroutineContext.foldCopiesForChildCoroutine() + context
val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
//combined可以简单的把它看成是一个map,它是CoroutineContext类型的
//如果当前context不等于Dispatchers.Default,而且从map里面取ContinuationInterceptor(用于拦截之后分发线程的)值为空,说明没有传入协程应该在哪个线程上运行的相关参数
return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null)
debug + Dispatchers.Default else debug
}

调用launch的时候,我们没有传入context,默认参数是EmptyCoroutineContext。这里的combined,它其实是CoroutineContext类型的,可以简单的看成是map(其实不是,只是类似)。通过combined[ContinuationInterceptor]可以将传入的线程调度相关的参数给取出来,这里如果取出来为空,是给该context添加了一个Dispatchers.Default,然后把新的context返回出去了。所以launch默认情况下,会走到default线程去执行。


补充一点:CoroutineContext能够通过+连接是因为它内部有个public operator fun plus函数。能够通过combined[ContinuationInterceptor]这种方式访问元素是因为有个public operator fun get函数。


public interface CoroutineContext {
/**
* Returns the element with the given [key] from this context or `null`.
*/
public operator fun <E : Element> get(key: Key<E>): E?

/**
* Returns a context containing elements from this context and elements from other [context].
* The elements from this context with the same key as in the other one are dropped.
*/
public operator fun plus(context: CoroutineContext): CoroutineContext {
......
}
}

startCoroutineCancellable


上面我们分析了launch默认情况下,context中会增加Dispatchers.Default的这个协程调度器,到时launch的Lambda会在default线程上执行,其中具体流程是怎么样的,我们分析一下。


在之前的文章 Kotlin协程之launch原理 中我们分析过,launch默认情况下会最终执行到startCoroutineCancellable函数。


public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
//构建ContinuationImpl
createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation<T>
): Continuation<Unit> {
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
//走这里
create(probeCompletion)
else
createCoroutineFromSuspendFunction(probeCompletion) {
(this as Function1<Continuation<T>, Any?>).invoke(it)
}
}

Kotlin协程之launch原理 文章中,咱们分析过create(probeCompletion)这里创建出来的是launch的那个Lambda,编译器会产生一个匿名内部类,它继承自SuspendLambda,而SuspendLambda是继承自ContinuationImpl。所以 createCoroutineUnintercepted(completion)一开始构建出来的是一个ContinuationImpl,接下来需要去看它的intercepted()函数。


internal abstract class ContinuationImpl(
completion: Continuation<Any?>?,
private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
constructor(completion: Continuation<Any?>?) : this(completion, completion?.context)

public override val context: CoroutineContext
get() = _context!!

@Transient
private var intercepted: Continuation<Any?>? = null

public fun intercepted(): Continuation<Any?> =
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
}

第一次走到intercepted()函数时,intercepted肯定是为null的,还没初始化。此时会通过context[ContinuationInterceptor]取出Dispatcher对象,然后调用该Dispatcher对象的interceptContinuation()函数。这个Dispatcher对象在demo这里其实就是Dispatchers.Default。


public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
}

可以看到,Dispatchers.Default是一个CoroutineDispatcher对象,interceptContinuation()函数就在CoroutineDispatcher中。


public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
}

public fun <T> (suspend () -> T).startCoroutineCancellable(completion: Continuation<T>): Unit = runSafely(completion) {
createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))
}

这个方法非常简单,就是新建并且返回了一个DispatchedContinuation对象,将this和continuation给传入进去。这里的this是Dispatchers.Default。


所以,最终我们发现走完startCoroutineCancellable的前2步之后,也就是走完intercepted()之后,创建的是DispatchedContinuation对象,最后是调用的DispatchedContinuation的resumeCancellableWith函数。最后这步比较关键,这是真正将协程的具体执行逻辑放到线程上执行的部分。


internal class DispatchedContinuation<in T>(
//这里传入的dispatcher在demo中是Dispatchers.Default
@JvmField val dispatcher: CoroutineDispatcher,
@JvmField val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {

inline fun resumeCancellableWith(
result: Result<T>,
noinline onCancellation: ((cause: Throwable) -> Unit)?
) {
val state = result.toState(onCancellation)
//代码1
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_CANCELLABLE
//代码2
dispatcher.dispatch(context, this)
} else {
//代码3
executeUnconfined(state, MODE_CANCELLABLE) {
if (!resumeCancelled(state)) {
resumeUndispatchedWith(result)
}
}
}
}
}

internal abstract class DispatchedTask<in T>(
@JvmField public var resumeMode: Int
) : SchedulerTask() {
......
}

internal actual typealias SchedulerTask = Task

internal abstract class Task(
@JvmField var submissionTime: Long,
@JvmField var taskContext: TaskContext
) : Runnable {
......
}

public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {

public abstract fun dispatch(context: CoroutineContext, block: Runnable)

public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true

}

从DispatchedContinuation的继承结构来看,它既是一个Continuation(通过委托给传入的continuation参数),也是一个Runnable。



  • 首先看代码1:这个dispatcher在demo中其实是Dispatchers.Default ,然后调用它的isDispatchNeeded(),这个函数定义在CoroutineDispatcher中,默认就是返回true,只有Dispatchers.Unconfined返回false

  • 代码2:调用Dispatchers.Default的dispatch函数,将context和自己(DispatchedContinuation,也就是Runnable)传过去了

  • 代码3:对应Dispatchers.Unconfined的情况,它的isDispatchNeeded()返回false


现在我们要分析代码2之后的执行逻辑,也就是将context和Runnable传入到dispatch函数之后是怎么执行的。按道理,看到Runnable,那可能这个与线程执行相关,应该离我们想要的答案不远了。回到Dispatchers,我们发现Dispatchers.Default是DefaultScheduler类型的,那我们就去DefaultScheduler中或者其父类中去找dispatch函数。


public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
}

internal object DefaultScheduler : SchedulerCoroutineDispatcher(
CORE_POOL_SIZE, MAX_POOL_SIZE,
IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME
) {
......
}

internal open class SchedulerCoroutineDispatcher(
private val corePoolSize: Int = CORE_POOL_SIZE,
private val maxPoolSize: Int = MAX_POOL_SIZE,
private val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS,
private val schedulerName: String = "CoroutineScheduler",
) : ExecutorCoroutineDispatcher() {

private var coroutineScheduler = createScheduler()

private fun createScheduler() =
CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)

override fun dispatch(context: CoroutineContext, block: Runnable): Unit = coroutineScheduler.dispatch(block)
}

最后发现dispatch函数在其父类SchedulerCoroutineDispatcher中,在这里构建了一个CoroutineScheduler,直接调用了CoroutineScheduler对象的dispatch,然后将Runnable(也就是上面的DispatchedContinuation对象)传入。


internal class CoroutineScheduler(
@JvmField val corePoolSize: Int,
@JvmField val maxPoolSize: Int,
@JvmField val idleWorkerKeepAliveNs: Long = IDLE_WORKER_KEEP_ALIVE_NS,
@JvmField val schedulerName: String = DEFAULT_SCHEDULER_NAME
) : Executor, Closeable {
override fun execute(command: Runnable) = dispatch(command)

fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
trackTask() // this is needed for virtual time support
//代码1:构建Task,Task实现了Runnable接口
val task = createTask(block, taskContext)
//代码2:取当前线程转为Worker对象,Worker是一个继承自Thread的类
val currentWorker = currentWorker()
//代码3:尝试将Task提交到本地队列并根据结果执行相应的操作
val notAdded = currentWorker.submitToLocalQueue(task, tailDispatch)
if (notAdded != null) {
//代码4:notAdded不为null,则再将notAdded(Task)添加到全局队列中
if (!addToGlobalQueue(notAdded)) {
throw RejectedExecutionException("$schedulerName was terminated")
}
}
val skipUnpark = tailDispatch && currentWorker != null
// Checking 'task' instead of 'notAdded' is completely okay
if (task.mode == TASK_NON_BLOCKING) {
if (skipUnpark) return
//代码5: 创建Worker并开始执行该线程
signalCpuWork()
} else {
// Increment blocking tasks anyway
signalBlockingWork(skipUnpark = skipUnpark)
}
}

private fun currentWorker(): Worker? = (Thread.currentThread() as? Worker)?.takeIf { it.scheduler == this }

internal inner class Worker private constructor() : Thread() {
.....
}
}

观察发现,原来CoroutineScheduler类实现了java.util.concurrent.Executor接口,同时实现了它的execute方法,这个方法也会调用dispatch()。



  • 代码1:首先是通过Runnable构建了一个Task,这个Task其实也是实现了Runnable接口,只是把传入的Runnable包装了一下

  • 代码2:将当前线程取出来转换成Worker,当然第一次时,这个转换不会成功,这个Worker是继承自Thread的一个类

  • 代码3:将task提交到本地队列中,这个本地队列待会儿会在Worker这个线程执行时取出Task,并执行Task

  • 代码4:如果task提交到本地队列的过程中没有成功,那么会添加到全局队列中,待会儿也会被Worker取出来Task并执行

  • 代码5:创建Worker线程,并开始执行


开始执行Worker线程之后,我们需要看一下这个线程的run方法执行的是啥,也就是它的具体执行逻辑。


internal inner class Worker private constructor() : Thread() {
override fun run() = runWorker()
private fun runWorker() {
var rescanned = false
while (!isTerminated && state != WorkerState.TERMINATED) {
//代码1
val task = findTask(mayHaveLocalTasks)
if (task != null) {
rescanned = false
minDelayUntilStealableTaskNs = 0L
//代码2
executeTask(task)
continue
} else {
mayHaveLocalTasks = false
}
if (minDelayUntilStealableTaskNs != 0L) {
if (!rescanned) {
rescanned = true
} else {
rescanned = false
tryReleaseCpu(WorkerState.PARKING)
interrupted()
LockSupport.parkNanos(minDelayUntilStealableTaskNs)
minDelayUntilStealableTaskNs = 0L
}
continue
}
tryPark()
}
tryReleaseCpu(WorkerState.TERMINATED)
}

fun findTask(scanLocalQueue: Boolean): Task? {
if (tryAcquireCpuPermit()) return findAnyTask(scanLocalQueue)
// If we can't acquire a CPU permit -- attempt to find blocking task
val task = if (scanLocalQueue) {
localQueue.poll() ?: globalBlockingQueue.removeFirstOrNull()
} else {
globalBlockingQueue.removeFirstOrNull()
}
return task ?: trySteal(blockingOnly = true)
}

private fun executeTask(task: Task) {
val taskMode = task.mode
idleReset(taskMode)
beforeTask(taskMode)
runSafely(task)
afterTask(taskMode)
}

fun runSafely(task: Task) {
try {
task.run()
} catch (e: Throwable) {
val thread = Thread.currentThread()
thread.uncaughtExceptionHandler.uncaughtException(thread, e)
} finally {
unTrackTask()
}
}

}

run方法直接调用的runWorker(),在里面是一个while循环,不断从队列中取Task来执行。



  • 代码1:从本地队列或者全局队列中取出Task

  • 代码2:执行这个task,最终其实就是调用这个Runnable的run方法。


也就是说,在Worker这个线程中,执行了这个Runnable的run方法。还记得这个Runnable是谁么?它就是上面我们看过的DispatchedContinuation,这里的run方法执行的就是协程任务,那这块具体的run方法的实现逻辑,我们应该到DispatchedContinuation中去找。



internal class DispatchedContinuation<in T>(
@JvmField val dispatcher: CoroutineDispatcher,
@JvmField val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
......
}

internal abstract class DispatchedTask<in T>(
@JvmField public var resumeMode: Int
) : SchedulerTask() {
public final override fun run() {
assert { resumeMode != MODE_UNINITIALIZED } // should have been set before dispatching
val taskContext = this.taskContext
var fatalException: Throwable? = null
try {
val delegate = delegate as DispatchedContinuation<T>
val continuation = delegate.continuation
withContinuationContext(continuation, delegate.countOrElement) {
val context = continuation.context
val state = takeState() // NOTE: Must take state in any case, even if cancelled
val exception = getExceptionalResult(state)
/*
* Check whether continuation was originally resumed with an exception.
* If so, it dominates cancellation, otherwise the original exception
* will be silently lost.
*/
val job = if (exception == null && resumeMode.isCancellableMode) context[Job] else null

//非空,且未处于active状态
if (job != null && !job.isActive) {
//开始之前,协程已经被取消,将具体的Exception传出去
val cause = job.getCancellationException()
cancelCompletedResult(state, cause)
continuation.resumeWithStackTrace(cause)
} else {
//有异常,传递异常
if (exception != null) {
continuation.resumeWithException(exception)
} else {
//代码1
continuation.resume(getSuccessfulResult(state))
}
}
}
} catch (e: Throwable) {
// This instead of runCatching to have nicer stacktrace and debug experience
fatalException = e
} finally {
val result = runCatching { taskContext.afterTask() }
handleFatalException(fatalException, result.exceptionOrNull())
}
}
}

我们主要看一下代码1处,调用了resume开启协程。前面没有异常,才开始启动协程,这里才是真正的开始启动协程,开始执行launch传入的Lambda表达式。这个时候,协程的逻辑是在Worker这个线程上执行的了,切到某个线程上执行的逻辑已经完成了。



ps: rusume会走到BaseContinuationImpl的rusumeWith,然后走到launch传入的Lambda匿名内部类的invokeSuspend方法,开始执行状态机逻辑。前面的文章 Kotlin协程createCoroutine和startCoroutine原理 我们分析过这里,这里就只是简单提一下。



到这里,Dispatchers的执行流程就算完了,前后都串起来了。


小结


Dispatchers是协程框架中与线程交互的关键。底层会有不同的线程池,Dispatchers.Default、IO,协程任务来了的时候会封装成一个个的Runnable,丢到线程中执行,这些Runnable的run方法中执行的其实就是continuation.resume,也就是launch的Lambda生成的SuspendLambda匿名内部类,也就是开启协程状态机,开始协程的真正执行。


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

Flutter 使用 json_serializable 解析 JSON 支持泛型

一般情况下,服务端接口都会有一套数据结构规范,比如 { "items": [], "success": true, "msg": "" } 不同的接口,items 中返回的数据结构一般都是不一样的,这时使用泛型,可以简化代码 本文将以 ...
继续阅读 »

一般情况下,服务端接口都会有一套数据结构规范,比如


{
"items": [],
"success": true,
"msg": ""
}

不同的接口,items 中返回的数据结构一般都是不一样的,这时使用泛型,可以简化代码


本文将以 wanAndroid 提供的开放 API 为例,介绍如何通过泛型类接解析 JSON 数据,简化代码。另外,对 wanAndroid 提供开放 API 的行为表示感谢。


本文解析 JSON 使用的方案,是官方推荐的 json_serializable,至于为什么选择 json_serializable,可以参考我之前写的一篇文章:Flutter 使用 json_serializable 解析 JSON 最佳方案


下面开始进入正文


使用 json_serializable 支持泛型


json_serializable 在大概两年前发布的 v3.5.0 版本开始支持泛型,只需要在 @JsonSerializable() 注解中设置 genericArgumentFactories 为 true,同时需要对 fromJson 和 toJson 方法进行调整,即可支持泛型解析,如下所示:


@JsonSerializable(genericArgumentFactories: true)
class Response<T> {
int status;
T value;

factory Response.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$ResponseFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ResponseToJson<T>(this, toJsonT);
}

和正常实体类相比,fromJson 方法多了一个函数参数 T Function(dynamic json) fromJsonT;toJson 方法也多了一个函数参数:Object? Function(T value) toJsonT


分析数据结构


下面使用 wanAndroid 开放 API 接口数据,进行代码实践,我们先看一下服务端接口返回的数据结构


一般接口返回数据结构如下:


{
"data": [
{
"desc": "一起来做个App吧",
"id": 10,
"imagePath": "https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png",
"isVisible": 1,
"order": 1,
"title": "一起来做个App吧",
"type": 1,
"url": "https://www.wanandroid.com/blog/show/2"
}
],
"errorCode": 0,
"errorMsg": ""
}

带分页信息的列表接口,返回数据结构如下:


{
"data": {
"curPage": 1,
"datas": [
{
"id": 23300,
"link": "https://juejin.cn/post/7114142706557075487",
"niceDate": "2022-06-28 15:30",
"niceShareDate": "2022-06-28 15:30",
"publishTime": 1656401449000,
"realSuperChapterId": 493,
"shareDate": 1656401449000,
"shareUser": "灰尘",
"superChapterId": 494,
"superChapterName": "广场Tab",
"title": "Flutter 使用 json_serializable 解析 JSON 最佳方案"
}
],
"offset": 0,
"over": false,
"pageCount": 3,
"size": 20,
"total": 46
},
"errorCode": 0,
"errorMsg": ""
}

通过上面的接口示例,我们可以发现,返回的数据结构有以下两种情况:


在一般情况下 data 是一个数组


{
"data": [],
"errorCode": 0,
"errorMsg": ""
}

在分页相关接口,data 是一个对象


{
"data": {},
"errorCode": 0,
"errorMsg": ""
}

复杂方案


如果想定义一个模型类,同时处理上述两种情况,可以把整个 data 都定义为泛型,代码如下:


import 'package:json_annotation/json_annotation.dart';

part 'base_response.g.dart';

@JsonSerializable(genericArgumentFactories: true)
class BaseResponse<T> {
T data;
int errorCode;
String errorMsg;

BaseResponse({
required this.data,
required this.errorCode,
required this.errorMsg,
});

factory BaseResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$BaseResponseFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$BaseResponseToJson<T>(this, toJsonT);
}



@JsonSerializable(genericArgumentFactories: true)
class ListData<T> {
int? curPage;
List<T> datas;
int? offset;
bool? over;
int? pageCount;
int? size;
int? total;

ListData({
this.curPage,
required this.datas,
this.offset,
this.over,
this.pageCount,
this.size,
this.total,
});

factory ListData.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$ListDataFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ListDataToJson<T>(this, toJsonT);
}

测试代码如下:


void main() {
test("json", () {
String str =
'{"data": [{"category": "设计","icon": "","id": 31,"link": "https://tool.gifhome.com/compress/","name": "gif压缩","order": 4444,"visible": 1}],"errorCode": 0,"errorMsg": ""}';

Map<String, dynamic> json = jsonDecode(str);

BaseResponse<List<CategoryModel>> result =
BaseResponse.fromJson(json, (json) {
return (json as List<dynamic>)
.map((e) => CategoryModel.fromJson(e as Map<String, dynamic>))
.toList();
});

List<CategoryModel> list = result.data;

CategoryModel model = list[0];

print(model.toJson());

expect("category:设计", "category:${model.category}");
});

test("json list", () {
String str =
'{"data": {"curPage": 1,"datas": [{"id": 23300,"link": "https://juejin.cn/post/7114142706557075487","niceDate": "2022-06-28 15:30","niceShareDate": "2022-06-28 15:30","publishTime": 1656401449000,"realSuperChapterId": 493,"shareDate": 1656401449000,"shareUser": "灰尘","superChapterId": 494,"superChapterName": "广场Tab","title": "Flutter 使用 json_serializable 解析 JSON 最佳方案"}],"offset": 0,"over": false,"pageCount": 3,"size": 20,"total": 46},"errorCode": 0,"errorMsg": ""}';

Map<String, dynamic> json = jsonDecode(str);

BaseResponse<ListData<ArticleModel>> result =
BaseResponse.fromJson(json, (json) {
return ListData.fromJson(json, (json) => ArticleModel.fromJson(json));
});

ListData<ArticleModel> listData = result.data;
List<ArticleModel> datas = listData.datas;
ArticleModel model = datas[0];

print(model.toJson());

expect("id:23300", "id:${model.id}");
});
}

虽然一个 BaseResponse 解决了两种数据结构,但使用时的代码会有些复杂,很容易出错。


一般接口:


    BaseResponse<List<CategoryModel>> result =
BaseResponse.fromJson(json, (json) {
return (json as List<dynamic>)
.map((e) => CategoryModel.fromJson(e as Map<String, dynamic>))
.toList();
});

分页接口:


    BaseResponse<ListData<ArticleModel>> result =
BaseResponse.fromJson(json, (json) {
return ListData.fromJson(json, (json) => ArticleModel.fromJson(json));
});

简化方案


可以对一般接口和列表分页接口进行单独处理,


处理一般接口的泛型类,命名为 BaseCommonResponse,代码如下:


import 'package:json_annotation/json_annotation.dart';

part 'base_common_response.g.dart';

@JsonSerializable(genericArgumentFactories: true)
class BaseCommonResponse<T> {
List<T> data;
int errorCode;
String errorMsg;

BaseCommonResponse({
required this.data,
required this.errorCode,
required this.errorMsg,
});

factory BaseCommonResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$BaseCommonResponseFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$BaseCommonResponseToJson<T>(this, toJsonT);
}

处理分页列表接口的泛型类,命令为 BaseListResponse


import 'package:json_annotation/json_annotation.dart';

part 'base_list_response.g.dart';

@JsonSerializable(genericArgumentFactories: true)
class BaseListResponse<T> {
ListData<T> data;
int errorCode;
String errorMsg;

BaseListResponse({
required this.data,
required this.errorCode,
required this.errorMsg,
});

factory BaseListResponse.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$BaseListResponseFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$BaseListResponseToJson<T>(this, toJsonT);
}



@JsonSerializable(genericArgumentFactories: true)
class ListData<T> {
int? curPage;
List<T> datas;
int? offset;
bool? over;
int? pageCount;
int? size;
int? total;

ListData({
this.curPage,
required this.datas,
this.offset,
this.over,
this.pageCount,
this.size,
this.total,
});

factory ListData.fromJson(
Map<String, dynamic> json,
T Function(dynamic json) fromJsonT,
) =>
_$ListDataFromJson<T>(json, fromJsonT);

Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
_$ListDataToJson<T>(this, toJsonT);
}

测试代码如下:


void main() {
test("json", () {
String str =
'{"data": [{"category": "设计","icon": "","id": 31,"link": "https://tool.gifhome.com/compress/","name": "gif压缩","order": 4444,"visible": 1}],"errorCode": 0,"errorMsg": ""}';

Map<String, dynamic> json = jsonDecode(str);

BaseCommonResponse<CategoryModel> result = BaseCommonResponse.fromJson(
json, (json) => CategoryModel.fromJson(json));

List<CategoryModel> list = result.data;

CategoryModel model = list[0];

print(model.toJson());

expect("category:设计", "category:${model.category}");
});

test("json list", () {
String str =
'{"data": {"curPage": 1,"datas": [{"id": 23300,"link": "https://juejin.cn/post/7114142706557075487","niceDate": "2022-06-28 15:30","niceShareDate": "2022-06-28 15:30","publishTime": 1656401449000,"realSuperChapterId": 493,"shareDate": 1656401449000,"shareUser": "灰尘","superChapterId": 494,"superChapterName": "广场Tab","title": "Flutter 使用 json_serializable 解析 JSON 最佳方案"}],"offset": 0,"over": false,"pageCount": 3,"size": 20,"total": 46},"errorCode": 0,"errorMsg": ""}';

Map<String, dynamic> json = jsonDecode(str);

BaseListResponse<ArticleModel> result =
BaseListResponse.fromJson(json, (json) => ArticleModel.fromJson(json));

ListData<ArticleModel> listData = result.data;
List<ArticleModel> datas = listData.datas;
ArticleModel model = datas[0];

print(model.toJson());

expect("id:23300", "id:${model.id}");
});
}

这时使用时的代码,就比较简单了,代码如下:


一般接口,使用 BaseCommonResponse


    BaseCommonResponse<CategoryModel> result = BaseCommonResponse.fromJson(
json, (json) => CategoryModel.fromJson(json));

列表分页接口,使用 BaseListResponse


    BaseListResponse<ArticleModel> result = BaseListResponse.fromJson(
json, (json) => ArticleModel.fromJson(json));

以上就是我在 Flutter 中解析 JSON 数据时处理泛型的实践经验,如果对你有所帮助,欢迎一键三连,👍👍👍


如果大家有相关问题,欢迎评论留言。


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

研发同学应该如何负责好一个项目

引言时间回到6年前,我在京东第一次独立负责项目的前端开发。当时的我可谓雄心壮志,希望表现和证明自己,结果第一次上线就造成了生产事故。后来半年里,我几乎把所有可能的错误都犯了个遍:老板让我搞一个技术方案,我闷头搞了两周也没有给出任何结论;本来安排好的开发计划,被...
继续阅读 »

引言

时间回到6年前,我在京东第一次独立负责项目的前端开发。当时的我可谓雄心壮志,希望表现和证明自己,结果第一次上线就造成了生产事故。

后来半年里,我几乎把所有可能的错误都犯了个遍:老板让我搞一个技术方案,我闷头搞了两周也没有给出任何结论;本来安排好的开发计划,被各种插进来的需求搞得手忙脚乱;参加需求评审没有充分的准备全程提不出问题……那段时间我很苦恼,明明很累很辛苦,但依然拿不到想要的结果。

一晃6年,自己已经从一个小白成长为技术Leader。当我站在更高的视角,我发现身边很多同学不停地重复犯着自己当年类似的错误。经过一段时间的观察与思考,我得出的结论是:他们在工作中缺少方法论的沉淀和指导

“方法论”这个看似虚无缥缈的东西,却犹如指引行动的灯塔,连接着我们的价值观与行动。对于很多研发同学来说,“ 如何负责好一个项目” 是一门只可意会不可言传的玄学,其实不然。

我将我个人经历以及对身边同事的观察,做了一些总结,希望能够给大家带来一些启发,更好地在项目推进过程中指导我们的行动。

一 负责人的定位

这里的负责人不是指的项目经理,而是研发侧牵头和统一对外的那个人,往往是虚线Lead一个项目。

初阶的负责人通常只是做一些需求拆解、任务分配、问题收集和反馈这样一些基础的工作。因为缺乏方法论的指引和对业务的理解,过程中毫无章法、顾此失彼,表现就是像一个救火队长,永远都被事情推着走,很忙很累却拿不到结果。

在我看来,优秀的负责人是这样的:把自己负责的项目当作是一次创业,自己是这个初创团队的CTO,职责是带领这些同学打胜仗(打胜仗是指在产品和技术方案上少走弯路、做出来的东西真的有人用、帮助团队提高效率降低成本、帮助业务带来更多收入和利润)。

难点在于,上面这个表述虽然有了画面感,但具体应该怎么做呢?

二 技术负责人的三大能力

我常和团队的同学讲:技术人的三大支柱是专业技术、项目管理和业务理解。对于负责人而言,则一定是这三项均衡发展。

1、专业技术能力

负责人不见得技术很强,但有深度、有广度、有影响力会走更远。深度很容易理解,广度是指什么呢?


技术负责人需要有广度。广度一方面是 “跨领域” 的部分;比如:大多数情况负责人由后端同学担任,那么最好对前端、质量、算法也了解些,至少跟在讨论方案的时候要有得聊;另一方面是抬高自身视野:在方案评估和决策的时候,能不能看到公司内部其他团队的方案或者行业内部的方案;在做一个决策之前,都了解了哪些方案、不同方案间的对比维度和选择逻辑是什么,现成方案不满足需求时如何处理等。


技术负责人需要有影响力。影响力会让你有足够的自信去把控技术方案:如果自己提出的方案频频被挑战,或者面对别人方案的时候提不出问题和建议,则是削弱技术影响力的行为。在项目中,技术影响力主要体现在三个方面:

(1)内容和技术方案输出的专业性。我见过很多同学在沟通技术方案时都是“口述”,这样的方式既低效又不专业;

(2)工作中要有技术沉淀的意识。比如:效率和质量的提升、稳定性建设等;力争量化结果,拿数据说话;

(3)对技术的场景转化能力。专业性不在于用了哪些高大上的技术,而是用合适的工具解决实际问题,同时能反向推导这一类技术其他的适用场景。

2、项目管理能力

研发项目管理是个很专业的事,我对此的理解是:在有限资源限定的条件下,协同好上下游(包括运营、产品、设计、研发、质量),综合运用专业技能、方法和工具达成项目目标。整个研发项目管理的内容很多,通过观察,我总结出一些负责人在工作中常见问题及注意事项:

(1) 要不卑不亢,对结果负责。既不是高高在上的存在,又不是老好人,在项目中团结好大家,倾听并尊重每一位成员的意见和建议。

负责人在这个过程中要保持空杯心态。初入职场的小白,可能会非常谦虚,但是工作几年之后,专业技能逐步提升,可能还取得了一些小成就,人就会越来越自信。如果不能始终保持“空杯心态”,这种自信就会逐步演变为自满,往往表现为:工作中把别人的建议当成是批评、不喜欢听反对的声音。这样一来,团队里的声音就少了,缺少了交流碰撞,负责人就会成为团队的瓶颈。

(2) 要有敏锐的问题意识以及全局观。可以识别出项目过程中存在的问题和风险、看问题的视角要更高、也要更客观。当遇到问题,不是简单指责哪一方的问题,而是把事情经过还原,弄明白真实原因是什么。


是流程问题、依赖的问题、还是个人能力和态度的问题?在问题归因上干系人是否也这么认为、在解决问题的同时,思考这类问题今后如何规避。当项目遇到风险,不是简单的报备有风险存在,而是如何管理这个风险、过程中做什么努力。

研发面对的绝大多数都是项目延期风险,作为负责人不能单一维度的思考问题。 “上线要delay了通过加班赶工” 这种事情没有任何技术含量,即使刚入职场的实习生都知道。问题和风险不分家,作为负责人不要只看表面因,要多思考过渡原因、根本因是什么,在项目中对症下药。

(3) 要懂“外交”,要学会沟通。自己搞不定的事情,要学会向上沟通和对等沟通,跟什么样的人,说什么样的话。


对等沟通不是职级的对等,而是角色的对等,比如:负责前端的同学在沟通跟测试相关的问题时,最高效的方式是找测试中负责的同学,而不是负责执行的那个同学。

受限于客观条件,项目团队的人员配比不见得是合理的。比如:有的项目或阶段重前端、有的阶段重质量,当某一方相对弱势,除了申请追加资源外,要有补位意识,或者通过技术手段寻找出路。当有一方长期弱势,严重拖累项目进度,要及时识别出来并上升给管理者。

3、业务理解能力

作为负责人,就是要想办法让业务能更好,能让技术的价值有更大的体现。对业务有深刻理解,才知道业务更需要什么,也会更有使命感去推进。

需要特别说明的一点是,理解需求并不意味着理解业务,需求是业务经过产品消化后的产物,可能已经经过演绎,或者是其中某个拆解环节,因此需求并不是业务本身**。当然了解的需求越多,可以让你更清楚业务的全貌。


要理解业务,先要理解用户:他们在干什么、为何而来、到何处去、获得何种收益;然后,了解这里面的商业模式:流量如何来的、内容如何来的、生态情况怎么样、如何商业化的;再站在宏观角度去了解:行业情况怎么样、竞争对手怎么样;最后回到产品和技术:这个业务什么产品在承载、主要对技术的依赖和诉求又是什么。

以上信息了解过后,接下来最好还能够有一些洞察和思考,比如:现在业务发展遇到了什么瓶颈?打算如何破局?基本上把这些摸清楚了,你对这个业务就有个比较清楚的脉络了。

负责人要达到这种程度是需要下苦功夫的,搞清楚上面的问题,接下来要指导自己的行动,比如:要在过程中识别真伪需求、并控制好节奏,要判断哪些功能是一定要做的、哪些是现在这个阶段没必要做但将来可以做的、哪些是完全没必要做的。这里面第二个情况最难识别的。

有了前面这些认知,负责人要做好技术上的规划。优秀的负责人会抬高视野,从思考眼前的事,变成思考未来的事,预判业务未来发展对技术的挑战在哪里。

规划不是空想,是基于对业务理解,预测业务未来发展对技术的挑战,比如:可以通过一系列技术储备,做一些业务方原以为技术不能干、干不好的那些能够直接促进业务发展的事情;还可以发掘业务痛点或机会,然后用技术力量去改善。这里的技术可以是有很大厚度的,比如算法与机器学习、区块链,也可以是不需要技术厚度,但是需要产品设计和链接的,哪怕是简单的技术解决了业务问题。

作为负责人,要有经营意识:通过对数据的洞察寻找问题的答案。在项目中资源永远都是有限的,重要的不是做了多少功能,而是做的东西有多少人用。所以要学会用数据说话,大到交易规模、小到UV/PV等行为埋点,要定期的看,用它来指导你的行为。

一个需求评审前,你是否了解过这个功能的用户量是什么规模?上线后你是否有分析过是否符合最初的假设?这次产品功能上线给业务带来的实际结果是什么?当真实情况和假设间存在偏差,你是否有跟产品了解过背后的原因?当这种问题频频出现,你是否会质疑当前的产品路线和建设节奏有问题?有了这些思考和行动,才会保证项目往正确的方向推进。

三 总结

大家做业务,都有很大的业务压力,但对于技术人的要求,是除了完成技术实现外最大化的体现业务价值,这就需要我们做事情之前有充分的思考,在做事的过程中有正确的章法。

在负责一个项目的时候,要想清楚3个问题:

· 业务的目标是什么;

· 技术团队的策略是什么;

· 我们在里面的价值是什么。

如果3个问题都想明白了,前后的衔接也对了,这事情才靠谱。

回顾自己这7年的成长历程,我总结出技术人的成长诀窍,那便是:不混日子,有自驱;不求安逸,爱折腾

最后,希望大家还是能像最初的时候一样,能多折腾,保留这种折腾劲,甚至是孩子气,如果你还有的话。

来源:李志阳-京东云

原文:mp.weixin.qq.com/s/83qFIDTNCAGxzRzmcm4m_Q

收起阅读 »

抖音 Android 性能优化系列:Java 锁优化

背景Java 多线程开发中为了保证数据的一致性,引入了同步锁(synchronized)。但是,对锁的过度使用,可能导致卡顿问题,甚至 ANR:Systrace 中的主线程因为等锁阻塞了绘制,导致卡顿 Slardar 平台(字节跳动内部 APM 平台,以下简称...
继续阅读 »

背景

Java 多线程开发中为了保证数据的一致性,引入了同步锁(synchronized)。但是,对锁的过度使用,可能导致卡顿问题,甚至 ANR:

  • Systrace 中的主线程因为等锁阻塞了绘制,导致卡顿

  • Slardar 平台(字节跳动内部 APM 平台,以下简称 Slardar)中搜索 waiting to lock 关键字发现很多锁导致的 ANR,仅 Java 锁异常占到总 ANR 的 3.9%


本文将着重向大家介绍 Slardar 线上锁监控方案的原理与使用方法,以及我们在抖音上发现的锁的经典案例与优化实践。

监控方案

获取运行时锁信息的方法有以下几种

方案应用范围特点
systrace线下可以发现锁导致的耗时没有调用栈
定制 ROM线下可以支持调用栈修改 ROM 门槛较高,仅支持特定机型
JVMTI线下只支持 Android8+ 设备不支持 release 包,且性能开销较大

考虑到,很多锁问题需要一定规模的线上用户才能暴露出来,另外没有调用栈难以从根本上定位和解决线上用户的锁问题。最终我们自研了一套线上锁监控系统,它需要满足以下要求:

  • 线上监控方案

  • 丰富的锁信息,包括 Java 调用栈

  • 数据分析平台,包括聚合能力,设备和版本信息等

  • 可纳入开发和合码流程,防止不良代码上线

这样的锁监控系统,能够帮助我们高效定位和解决线上问题,并实现防劣化。

锁监控原理

我们先从 Systrace 入手,有一类常见的耗时叫做 monitor contention,其实是 Android ART 虚拟机输出的锁信息。


简单介绍一下里面的信息

monitor contention with owner work_thread (27176) at android.content.res.Resources android.app.ResourcesManager.getOrCreateResources(android.os.IBinder, android.content.res.ResourcesKey, java.lang.ClassLoader)(ResourcesManager.java:901) waiters=1 blocking from java.util.ArrayList android.app.ActivityThread.collectComponentCallbacks(boolean, android.content.res.Configuration)(ActivityThread.java:5836)
  • 持锁线程:work_thread

  • 持锁线程方法:android.app.ResourcesManager.getOrCreateResources(…)

  • 等待线程 1 个

  • 等锁方法:android.app.ActivityThread.collectComponentCallbacks(…)

Java 锁,无论是同步方法还是同步块,虚拟机最终都会到 MonitorEnter。我们关注的 trace 是 Android 6 引入的, 在锁的开始和结束时分别调用ATRACE_BEGIN(...)ATRACE_END()

线上方案

默认情况下 atrace 是关闭的,开关在 ATRACE_ENABLED() 中。我们通过设置 atrace_enabled_tags 为 ATRACE_TAG_DALVIK 可以开启当前进程的 ART 虚拟机的 atrace。

再看 ATRACE_BEGIN(...)ATRACE_END() 的实现,其实是用 write 将字符串写入一个特殊的 atrace_marker_fd (/sys/kernel/debug/tracing/trace_marker)。

因此通过 hook libcutils.so 的 write 方法,并按 atrace_marker_fd 过滤,就实现了对 ATRACE_BEGIN(...)ATRACE_END() 的拦截。有了 BEGIN 和 END 后可以计算出阻塞时长,解析 monitor contention with owner... 日志可以得到我们关注的 Java 锁信息。

获取堆栈

到目前为止,我们已经可以监控到线上用户的锁问题。但是还不够,为了能够优化锁的性能,我们想需要知道等锁的具体原因,也就是 Java 调用栈。

获取 Java 调用栈,可以使用Thread.getStackTrace()方法。由于我们 hook 住了虚拟机的等锁线程,此时线程处于一种特殊状态,不可以直接通过 JNI 调用 Java 方法,否则导致线上 crash 问题。


解决方案是异步获取堆栈,在 MonitorBegin 的时候通知子线程 5ms 之后抓取堆栈,MonitorEnd 计算阻塞时长,并结合堆栈数据一起放入队列,等待上报 Slardar。如果 MonitorEnd 时不满足 5ms 则取消抓栈和上报


数据平台

由于方案本身有一定性能开销,我们仅对灰度测试中的部分用户开启了锁监控。配置线上采样后,命中的用户将自动开启锁监控,数据上报 Slardar 平台后就可以消费了。


具体 case 可以看到设备信息、阻塞时长、调用堆栈


根据调用栈查找源码,可以定位到是哪一个锁,说明上报数据是准确的。


稳定性方面,10 万灰度用户开启锁监控后,无新增稳定性问题。

优化实践

经过多轮锁收集和治理,我们取得了一些不错的收益,这里简单介绍下锁治理的几个典型案例。

典型案例

inflate 锁:

先解析一下什么是 inflate:Android 中解析 xml 生成 View 树的过程就叫做 inflate 过程。inflate 是一个耗时过程,常规的手段就是通过异步来减少其在主线程的耗时,这样大大的减少了卡顿、页面打开和启动时长;但此方式也会带来新的问题,比如 LayoutInflater 的 inflate 方法中有加锁保护的代码块,并行构建会造成锁等待,可能反而增加主线程耗时,针对这个问题有三种解决方案:

  • 克隆 LayoutInflater

    • 把线程分为三类别:Main、工作线程和其它线程(野线程),Context(Activity 和 App)为每个类别提供专有 LayoutInflater,这样能有效的规避 inflate 锁。

    • 优点:实现简单、兼容性好

    • 缺点:LayoutInflater 中非安全的静态属性在并发情况下有概率产生稳定性问题

  • code 构造替代 xml 构造

    • 这种方式完美的绕开了 inflate 操作,极大提高了 View 构造速度。

    • 优点:复杂度高、性能好

    • 缺点:影响编译速度、View 自定义属性需要做转换、存在兼容性问题(比如厂商改属性)

  • 定制 LayoutInflater

    • 自定义 FastInflater(继承自 LayoutInflater)替换系统的 PhoneLayoutInflater,重写 inflate 操作,去掉锁保护;从统计数据看,在并发时快了约 4%。

    • 优点:复杂度高、性能好

    • 缺点:存在兼容性,比如华为的 Inflater 为 HwPhoneLayoutInflater,无法直接替换。

文件目录锁:

ContextImpl 中获取目录(cache、files、DB 和 preferenceDir)的实现有两个关键耗时点:1. 存在 IPC(IStorageManager.mkdir)和文件 check;2. 加锁“nSync”保护;所以 ipc 变长和并发存在,都可能导致 App 卡顿,如图为 Anr 数据:

相关的常用 Api 有 getExternalCacheDir、getCacheDir、getFilesDir、getTheme 等,考虑到系统的部分目录一般不会发生变化,所以我们可以对一些不会变化的目录进行 cache 处理,减少带 锁方法块的执行,从而有效的绕过锁等待。

MessageQueue:

Android 子线程与主线程通讯的通用方式是向主线程 MessageQueue 中插入一个任务(message),等此任务(message)被主线程 Looper 调度执行;所以 MessageQueue 中会对消息链表的修改加锁保护,主要实现在 enqueueMessage 和 next 两个方法中。

利用 Slardar 采集线上锁信息,根据这些信息,我们可以轻松追踪锁的执有线程和 owner,最后根据情况将请求(message)移到子线程,这样就可以极大的减轻主线程压力和等锁的可能性。此问题的修改方式并不复杂,重点在于如何监控到这些执锁线程。


序列化和反序列化:

抖音中有一些常用数据对象使用 Json 格式存储。为保证这些数据的完整性,在读取和存储时加了锁保护,从而导致锁等待比较常见,这种情况在启动场景特别明显;所以要想减少锁等待,就必段加快序列化和反序列化,针对这个问题,我们做了三个优化方案:

  • Gson 反序列化的耗时集中在 TypeAdapter 的构建,此过程利用反射创建 Filed 和 name(key)的映射表;所以我们在编译时针对数据类创建对应的 TypeAdapter,大大减少反序列化的时耗。

  • 部分类使用 parcel 序列化和反序列化,大大提高了速度,约减少 90%的时耗。

  • 大对像根据情况拆分成多个小对像,这样可以减少锁粒度,也就减少了锁等待。以上方案在抖音项目中都有使用,取得了很不错的收益。

AssetManager 锁:

获取 string、size、color 或 xml 等资源的最终实现基本都封装在 AssertManager 中,为了保证数据的正确性,加了锁(对象 AssetManager)保护,大致的调用关系如图:

常用的调用点有:

  • View 构造方法中调用 context.obtainStyledAttributes(…)获取 TypedArray,最后都会调用 AssetManager 的带锁方法。

  • View 的 toString 也调用了 AssetManager 的带锁方法。

随着 xml 异步 inflate 的增加,这些方法并发调用也增加,造成主线程的锁等待也日渐突出,最终导致卡顿,针对这个问题,目前我们的优化方案主要有:

  • 去掉多余的调用,比如 View 的 toString,这个常见于日志打印。

  • 一个 Context 根据线程名提供不同的 AssetManager,绕过 AssetManager 对象锁;此方法可能带来一些内存消耗。

So 加载锁优化:

Android 提供的加载 so 的接口实现都在封装在 Runtime 中,比如常用的 loadLibrary0 和 load0,如图 1 和 l 图 2 所示,此方法是加了锁的,如果并发加载 so 就会造成锁等待。通过 Slardar 的监控数据,我们验证了这个问题,同时也有一些意外收获,比如平台可能有自己的 so 需要加:

我们根据 so 的不同情况,主要有以下优化思路:

  • 对于 cinit 加载的 so,我们可以提前在子线程中加载一下 cinit 的宿主类。

  • 业务层面的 so, 可以统一在子线程中进行提前加载。

  • 使用 load0 替代 loadLibrary0,可以减少锁中拼接 so 路径的时耗。

  • so 文件加载优化,比如 JNI_OnLoad。

ActivityThread:

在收集的的数据中我们也发现了一些系统层的框架锁,比如下图这个:


这个问题主要集中在启动阶段,ams 会发 trim 通知给 ActivityThread 中的 ApplicationThread,收到通知后会向 Choreographer 的 commit 列表(此任务列表不作展开)中添加一个 trim 任务,也就是在下个 vsync 到达时被执行;

trim 过程主要包括收集 Applicatioin、Activity、Service、Provider 和向它们发送 trim 消息,也是系统提供给业务清理自身内存的一个时机;收集过程是加锁(ResourcesManager)保护的,如图:

考虑到启动阶段并不太关心内存的释放,所以可以尝试在启动阶段,比如 40 秒内,不执行 trim 操作;具体的实现是这样,首先替换 Choreographer 的 FrameHandler, 这样就能接管 vsync 的 doFrame 操作,在启动 40 秒内的每次 vsync 主动 check 或删除 commint 任务列表中的 trim 操作。


收益

在抖音中我们除了优化前面列出的这些典型锁外,还优化了一些业务本身的锁,部分已经通过线上实验验证了收益,也有一些还在尝试实验中;通过对实验中各指标的分析,也证实了锁优化能带来启动和流畅度等技术收益,间接带来了不错的业务收益,这也坚定了我们在这个方向上的继续探索和深化。

小结

前面列出的只是有代表性的一些通用 Java 锁,在实际开发中遇到的远比这多,但不管什么样的锁,都可以根据进程和代码归属分为以下四类:业务锁、依赖库锁、框架锁和系统锁;

不同类型的锁优化思路也会不一样,部分方案可以复用,部分只能 case-by-case 解决,具体的优化方案有:减少调用、绕过调用、使用读写锁和无锁等。


分类描述进程代码优化方案
业务锁源码可见,可以直接修改;比如前面的序列化优化。App 进程包含直接优化;静态 aop
依赖库锁包含编译产物,可以修改产物App 进程包含直接优化;静态 aop
框架锁运行时加载,同时存在兼容性;比如前面提到的 inflate 锁、AssetManager 锁和 MessageQueue 锁App 进程不包含减少调用;动态 aop
系统锁系统为 App 提供的服务和资源,App 间存在竞争,所以服务层需要加锁保护,比如 IPC、文件系统和数据库等服务进程不包含减少调用

总结

经过了长达半年的探索和优化,此方案已在线上使用,作为我们日常防劣化和主动优化的输入工具,我们评判的点主要有以下四个:

  • 稳定性:线上开启后,ANR、Crash 和 OOM 和大盘一致。

  • 准确性:从目前线上的消费数据来看,这个值达到了 99%。

  • 扩展性:业务可以根据场景开启和关闭采集功能,也可以收集指定时间内的锁,比如启动阶段可以收集 32ms 的锁,其它阶段收集 16ms 的锁。

  • 劣化影响:从线上实验数据看,一定量(UV)的情况下,业务和性能(丢帧和启动)无显著劣化。

此方案虽然只能监控 synchronized 锁,像 CAS、Native 锁、sleep 和 wait 都无法监控,但在我们日常开发中synchronized 锁占比非常大, 所以基本满足了我们绝大部分的需求,当然,我们也在持续探索其它锁的监控和验证其价值。

————————————————
来源: 字节跳动技术团队
原文:blog.csdn.net/ByteDanceTech/article/details/125863436

收起阅读 »

倍投模型模拟:1w块搏10w,靠谱吗?

前两天刷影视解说看到一个短片,甲乙两个人打赌,甲每次输了后,都会加倍。一群人围观,甲连输8次,在最后一次赌上全部身家后,一把梭哈赢了,直接走上人生巅峰。 当然,这只是爽剧,让我们用代码模拟下真实情况是怎么的,超刺激哦! 让我们先看下代码(用JS简单写的): /...
继续阅读 »

前两天刷影视解说看到一个短片,甲乙两个人打赌,甲每次输了后,都会加倍。一群人围观,甲连输8次,在最后一次赌上全部身家后,一把梭哈赢了,直接走上人生巅峰。


当然,这只是爽剧,让我们用代码模拟下真实情况是怎么的,超刺激哦!


让我们先看下代码(用JS简单写的):


// 家底
var all = 10000;
// 第一次投注
var first = 1000;
// 假设玩1000次
for(var i=0;i<1000;i++){
// 输赢概率都是50%
var check = Math.random() >= 0.5;
console.log("第"+(i+1)+"次");
if(check){
// 赢了初始化投注
console.log("赚了"+first);
all+= first;
first=1000;
}else{
all -= first;
if(first*2>all){
// 输光了,梭哈
first=all
}else{
// 还有家底,加倍投注。
first=first*2;
}
console.log("输了"+first);
}
if(all<=0){
console.log("输光了,拜拜");
break;
}
console.log("现在有:"+all);
}

第一次模拟:


家底:10000,初始投注:1000。拿出十分之一去搏一搏,合理。


image.png


image.png


顶峰时第46轮:36000,在59轮时被一波带走。


再来一次:


image.png


image.png


这次就比较惨了,第27次顶峰:21000,在32轮时被一波带走。


上面这个有点离谱,我们在保守点,拿出家底的1%去搏一搏,把初始值设为100。


第二次模拟


家底:10000,初始投注:100,稳重求胜。


果然稳健才是硬道理,这一把运气绝对爆棚。


image.png
在第843轮,家底来到了45800,这收益率逆天啊,然而天道有轮回,仅仅到第852轮,我就输光了全部家底。果然人生得意莫嘚瑟。
image.png


再来一次:


image.png
image.png
这次运气一般,在第276轮,才14800。在340轮时被一波带走。


总结


投资有风险,入市需谨慎啊。


在资金有限的情况的下,倍投绝对不是一个好的选项,你们可以试下在当家底是100000时,初始值设为1,有意想不到的惊喜,虽然赚的少,但家底只要够厚,就不会赔。


如果你的资产是有限的,玩下去一定会输,毕竟运气总会用尽,50%概率赢在现实中也几乎不存在。


挺有意思的,欢迎大家试试,调整家底和初始值即可。



逆天了逆天了,我必须分享给大家。


初始我设定的家底1000,初始投注200,在1000轮后,家底来到了惊人的100200,简直运气爆棚,要上天。


image.png


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

Kotlin函数声明与闭包【Kotlin从拒绝到真香】

前言本文介绍闭包。闭包其实不算是新东西了。 其实 Kotlin 就基本没有多少新东西,甚至可以说新型编程语言基本都没有新东西。是把先前编程语言好用的特性组装起来,再加一部分拓展。本文大纲1. 闭包介绍首次接触 闭包 应该...
继续阅读 »

前言

本文介绍闭包。闭包其实不算是新东西了。 其实 Kotlin 就基本没有多少新东西,甚至可以说新型编程语言基本都没有新东西。是把先前编程语言好用的特性组装起来,再加一部分拓展。

本文大纲

Kotlin 函数声明与闭包.png

1. 闭包介绍

首次接触 闭包 应该是在 JavaScript 上,有函数为“一等公民”特性的编程语言都有这个概念。 函数是“一等公民”的意思是,函数跟变量一样,是某种类型的实例,可以被赋值,可以被引用。函数还可以被调用。变量类型是某个声明的类,函数类型就是规定了入参个数,类型和返回值类型(不规定名字。函数名就和变量名一样,任意定义但要符合规则)。

如要声明 Kotlin 一个函数类型,入参是两个整数,出参是一个整数,那应该这样写: val add: (Int, Int) -> Int。箭头左边括号内表示入参,括号不可省略。箭头右边表示返回值。

wiki上闭包的概念是:引用了自由变量的函数,这个被引用的自由变量将和这个函数一同存在。从定义来说,对闭包的理解,是基于普通函数之上的。一般的函数,能处理的只有入参和全局变量,然后返回一个结果。闭包比普通函数功能更强,可以获取当前上下文的局部变量。当然了,捕获局部变量的前提是可以在局部环境里声明一个函数,这只有把函数当作“一等公民”才可以做到。

2. 闭包与匿名类比较

在 Java 中,匿名类其实就是代替闭包而存在的。不过 Java 严格要求所有函数都需要在类里面,所以巧妙的把“声明一个函数”这样的行为变成了“声明一个接口”或“重写一个方法”。匿名类也可以获取当前上下文的 final 局部变量。和闭包不一样的是,匿名类无法修改获取的局部变量final 不可修改

匿名类能引用 final 局部变量,是因为在编译阶段,会把该局部变量作为匿名类的构造参数传入。

Java8 lambda 是进一步接近闭包的特性,lambda 的 JVM 实现是类似函数指针的东西。 但 Java7 中的 lambda 语法糖兼容不算是真正的 lambda,只是简化了匿名类的书写。

3. 闭包使用

来看一个闭包的例子:

fun returnFun(): () -> Int {
var count = 0
return { count++ }
}

fun main() {
val function = returnFun()
val function2 = returnFun()
println(function()) // 0
println(function()) // 1
println(function()) // 2

println(function2()) // 0
println(function2()) // 1
println(function2()) // 2
}

分析上面的代码,returnFun返回了一个函数,这个函数没有入参,返回值是Int。可以用变量接收它,还可以调用它。functionfunction2分别是创建的两个函数实例。

可以看到,每调用一次function()count都会加一,说明count 被function持有了而且可以被修改。而function2functioncount是独立的,不是共享的。

而通过 jadx 反编译可以看到:

public final class ClosureKt {
@NotNull
public static final Function0<Integer> returnFun() {
IntRef intRef = new IntRef();
intRef.element = 0;
return (Function0) new 1<>(intRef);
}

public static final void main() {
Function0 function = returnFun();
Function0 function2 = returnFun();
System.out.println(((Number) function.invoke()).intValue());
System.out.println(((Number) function.invoke()).intValue());
System.out.println(((Number) function2.invoke()).intValue());
System.out.println(((Number) function2.invoke()).intValue());
}
}

被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRefFloatRef 等,如果是非基础类型,就统一用 ObjectRef 即可。Ref 家族源码:github.com/JetBrains/k…

在 Java 中,如果想要匿名类来操作外部变量,一般做法是把这个变量放入一个 final 数组中。这和 Kotlin 的做法本质上是一样的,即通过持有该变量的引用来使得两个类可以修改同一个变量。

4. 总结

根据示例上面分析,可以总结出:

  • 闭包不算是新东西,是把函数作为“一等公民”的编程语言的特性;
  • 匿名类是 Java 世界里的闭包,但有局限性,即只能读 final 变量,不能写任何变量;
  • Kotlin 的闭包可以获取上下文的局部变量,并可以修改它。实现办法是 Kotlin 编译器给引用的局部变量封装了一层引用。


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

收起阅读 »

二次元恋爱社交开源项目---mua【附客户端、服务端源码】

Mua是由环信MVP开发者精心打造的开源项目,提供Demo体验和示例源码,支持开发者结合业务需要灵活自定义产品形态。Mua是一个二次元恋爱互动社交APP,有类似项目需求的创业者或想拥有甜蜜恋爱过程的开发者们都可以来44,下面介绍下这个恋爱升温神器的功能 打开A...
继续阅读 »

Mua是由环信MVP开发者精心打造的开源项目,提供Demo体验和示例源码,支持开发者结合业务需要灵活自定义产品形态。

Mua是一个二次元恋爱互动社交APP,有类似项目需求的创业者或想拥有甜蜜恋爱过程的开发者们都可以来44,下面介绍下这个恋爱升温神器的功能

APP----码,码   ,


Mua 

线

Mua

1MuaIM

 

 

 



2IM
Mua使IM


3
5Appdddd



4

tips.




5

AppApp+100




6





7

& 便&&





8Mua

Mua3

饿



9

cmd

 




MuaAPP,


Mua

⬇️Demo  


Android

mua端、服务端
https://github.com/easemob/mua


收起阅读 »

Kotlin-Flow常用封装类StateFlow的使用

Kotlin中StateFlow的使用 StateFlow 是 Flow 的实现,是一个特殊的流,默认的 Flow 是冷流,而StateFlow 是热流,和 LiveData 比较类似。关于冷热流后面一期 SharedFlow 会详细说明。 使用 StateF...
继续阅读 »

Kotlin中StateFlow的使用


StateFlow 是 Flow 的实现,是一个特殊的流,默认的 Flow 是冷流,而StateFlow 是热流,和 LiveData 比较类似。关于冷热流后面一期 SharedFlow 会详细说明。


使用 StateFlow 替代 LiveData 应该是目前很多开发者的呼吁了,确实 LiveData 的功能 StateFlow 都能实现,可以说是 LiveData 的升级版。


StateFlow的特点



  • 它始终是有值的。

  • 它的值是唯一的。

  • 它允许被多个观察者共用 (因此是共享的数据流)。

  • 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。


官方推荐当暴露 UI 的状态给视图时,应该使用 StateFlow。这是一种安全和高效的观察者,专门用于容纳 UI 状态。


一、StateFlow的使用


方式一,我们自己 new 出来


一般我们再ViewModel中定义读写分类的StateFlow


@HiltViewModel
class Demo4ViewModel @Inject constructor(
val savedState: SavedStateHandle
) : BaseViewModel() {

private val _searchFlow = MutableStateFlow("")
val searchFlow: StateFlow<String> = _searchFlow

fun changeSearch(keyword: String) {
_searchFlow.value = keyword
}
}

在Activity中我们就可以像类似 LiveData 一样的使用 StateFlow



private fun testflow() {
mViewModel.changeSearch("key")
}

override fun startObserve() {
lifecycleScope.launchWhenCreated {
mViewModel.searchFlow.collect {
YYLogUtils.w("value $it")
}
}
}

方式二,通过一个 冷流 Flow 转换为 StateFlow


    val stateFlow = flowOf(1, 2, 3).stateIn(
scope = lifecycleScope,
// started = WhileSubscribed(5000, 1000),
// started = Eagerly,
started = Lazily,
initialValue = 1
)

lifecycleScope.launch {
stateFlow.collect {

}
}

几个重要参数的说明如下



  • scope 共享开始时所在的协程作用域范围

  • started 控制共享的开始和结束的策略

  • Lazily: 当首个订阅者出现时开始,在 scope 指定的作用域被结束时终止。

  • Eagerly: 立即开始,而在 scope 指定的作用域被结束时终止。

  • WhileSubscribed能够指定当前不有订阅者后,多少时间取消上游数据和能够指定多少时间后,缓存中的数据被丢失,回复称initialValue的值。

  • initialValue 初始值


二、替代LiveData


不管是普通的 ViewModel 观察订阅模式,在Activity中订阅,还是DataBinding的模式,我们都可以使用StateFlow来代替ViewModel


    val withdrawMethod = MutableStateFlow(0)

<ImageView
android:id="@+id/iv_giro_checked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="@dimen/d_15dp"
android:src="@drawable/pay_method_checked"
android:visibility="gone"
binding:isVisibleGone="@{viewModel.withdrawMethod == 1}" />

为什么我们需要用StateFlow来代替LiveData,或者说LiveData有什么缺点?


LiveData vs Flow


先上代码,看看它们的用法与差异


ViewModel的代码


@HiltViewModel
class Demo4ViewModel @Inject constructor(
val savedState: SavedStateHandle
) : BaseViewModel() {

private val _searchLD = MutableLiveData<String>()
val searchLD: LiveData<String> = _searchLD

private val _searchFlow = MutableStateFlow("")
val searchFlow: StateFlow<String> = _searchFlow

fun changeSearch(keyword: String) {
_searchFlow.value = keyword
_searchLD.value = keyword
}
}

Activity中触发与接收事件



private fun testflow() {
mViewModel.changeSearch("key")
}

override fun startObserve() {
mViewModel.searchLD.observe(this){
YYLogUtils.w("value $it")
}

lifecycleScope.launchWhenCreated {
mViewModel.searchFlow.collect {
YYLogUtils.w("value $it")
}
}
}

可以看到基本的使用几乎是没有差异,在DataBinding中同样的是都能使用。那么它们有哪些差异呢?


它们相同的地方:



  1. 仅持有单个且最新的数据

  2. 自动取消订阅

  3. 提供「可读可写」和「仅可读」两个版本收缩权限

  4. 配合 DataBinding 实现「双向绑定」


相比StateFlow ,LiveData的确定:



  1. LiveData在某些特定的场景下会丢失数据

  2. LiveData 只能在主线程不能方便地支持异步化

  3. LiveData 的数据变换能力远远不如 Flow

  4. LiveData 粘性问题解决需要额外扩展

  5. LiveData 多数据源的合流能力远远不如 Flow

  6. LiveData 默认不支持防抖,值没有变化也会通知


这么惨,那我们开发是不是要放弃LiveData了?



恰恰不是!


如果大家全部是Koltin代码开发,那么是可以用Flow,这是基于Kotlin代码,基于协程实现的,但是现在很多项目还是 Java 语言开发的。那么LiveData还是很香的。


其二是LiveData的学习成本与 协程、Flow 的学习成本不可同日而语,开发项目是整个团队的事情,不能说你一个人会一个人用,目前LiveData的简单学习成本是很有优势的。


只是我们需要在一些特定的场景慎重使用postValue,比如数据比较秘籍的场景,我们尽量使用setValue方法。


总结


如果大家的项目的语言是 Kotlin ,并且小组成员都会 Flow 。那么我推荐你们使用StateFlow 替代LiveData 。如果不是,那么 LiveData 是你最好的选择。


谷歌也只是推荐使用Flow替代LiveData。但是并没有说打算放弃 LiveData 。并且 LiveData 与 StateFlow 都有各自的使用场景,不需要担心 LiveData的 使用。


本文我们只是简单的对比,关于StateFlow 与 SharedFlow 和LiveData 三者的差异与选择,后面等SharedFlow那一期详细的讲解。


为什么很多东西都要等SharedFlow,是因为 SharedFlow 是 StateFlow 的基础,StateFlow 像是 SharedFlow 的‘青春版’。很多东西需要讲完 SharedFlow 才能把知识点串起来,期待一下。


好了,本期内容如讲的不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



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

如何从0到1构建一个稳定、高性能的Redis集群

这篇文章我想和你聊一聊 Redis 的架构演化之路。现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是如何稳定、高性能地提供服务的?你也可以尝试回答一下以下这些问题:我使用 Redi...
继续阅读 »

这篇文章我想和你聊一聊 Redis 的架构演化之路。

现如今 Redis 变得越来越流行,几乎在很多项目中都要被用到,不知道你在使用 Redis 时,有没有思考过,Redis 到底是如何稳定、高性能地提供服务的?

你也可以尝试回答一下以下这些问题:

  • 我使用 Redis 的场景很简单,只使用单机版 Redis 会有什么问题吗?
  • 我的 Redis 故障宕机了,数据丢失了怎么办?如何能保证我的业务应用不受影响?
  • 为什么需要主从集群?它有什么优势?
  • 什么是分片集群?我真的需要分片集群吗?

如果你对 Redis 已经有些了解,肯定也听说过数据持久化、主从复制、哨兵这些概念,它们之间又有什么区别和联系呢?

如果你存在这样的疑惑,这篇文章,我会从 0 到 1,再从 1 到 N,带你一步步构建出一个稳定、高性能的 Redis 集群。

在这个过程中,你可以了解到 Redis 为了做到稳定、高性能,都采取了哪些优化方案,以及为什么要这么做?

掌握了这些原理,这样平时你在使用 Redis 时,就能够做到「游刃有余」。

这篇文章干货很多,希望你可以耐心读完。

从最简单的开始:单机版 Redis

首先,我们从最简单的场景开始。

假设现在你有一个业务应用,需要引入 Redis 来提高应用的性能,此时你可以选择部署一个单机版的 Redis 来使用,就像这样:

这个架构非常简单,你的业务应用可以把 Redis 当做缓存来使用,从 MySQL 中查询数据,然后写入到 Redis 中,之后业务应用再从 Redis 中读取这些数据,由于 Redis 的数据都存储在内存中,所以这个速度飞快。

如果你的业务体量并不大,那这样的架构模型基本可以满足你的需求。是不是很简单?

随着时间的推移,你的业务体量逐渐发展起来了,Redis 中存储的数据也越来越多,此时你的业务应用对 Redis 的依赖也越来越重。

但是,突然有一天,你的 Redis 因为某些原因宕机了,这时你的所有业务流量,都会打到后端 MySQL 上,这会导致你的 MySQL 压力剧增,严重的话甚至会压垮 MySQL。

这时你应该怎么办?

我猜你的方案肯定是,赶紧重启 Redis,让它可以继续提供服务。

但是,因为之前 Redis 中的数据都在内存中,尽管你现在把 Redis 重启了,之前的数据也都丢失了。重启后的 Redis 虽然可以正常工作,但是由于 Redis 中没有任何数据,业务流量还是都会打到后端 MySQL 上,MySQL 的压力还是很大。

这可怎么办?你陷入了沉思。

有没有什么好的办法解决这个问题?

既然 Redis 只把数据存储在内存中,那是否可以把这些数据也写一份到磁盘上呢?

如果采用这种方式,当 Redis 重启时,我们把磁盘中的数据快速恢复到内存中,这样它就可以继续正常提供服务了。

是的,这是一个很好的解决方案,这个把内存数据写到磁盘上的过程,就是「数据持久化」。

数据持久化:有备无患

现在,你设想的 Redis 数据持久化是这样的:

但是,数据持久化具体应该怎么做呢?

我猜你最容易想到的一个方案是,Redis 每一次执行写操作,除了写内存之外,同时也写一份到磁盘上,就像这样:

没错,这是最简单直接的方案。

但仔细想一下,这个方案有个问题:客户端的每次写操作,既需要写内存,又需要写磁盘,而写磁盘的耗时相比于写内存来说,肯定要慢很多!这势必会影响到 Redis 的性能。

如何规避这个问题?

我们可以这样优化:Redis 写内存由主线程来做,写内存完成后就给客户端返回结果,然后 Redis 用另一个线程去写磁盘,这样就可以避免主线程写磁盘对性能的影响。

这确实是一个好方案。除此之外,我们可以换个角度,思考一下还有什么方式可以持久化数据?

这时你就要结合 Redis 的使用场景来考虑了。

回忆一下,我们在使用 Redis 时,通常把它用作什么场景?

是的,缓存。

把 Redis 当做缓存来用,意味着尽管 Redis 中没有保存全量数据,对于不在缓存中的数据,我们的业务应用依旧可以通过查询后端数据库得到结果,只不过查询后端数据的速度会慢一点而已,但对业务结果其实是没有影响的。

基于这个特点,我们的 Redis 数据持久化还可以用「数据快照」的方式来做。

那什么是数据快照呢?

简单来讲,你可以这么理解:

  1. 你把 Redis 想象成一个水杯,向 Redis 写入数据,就相当于往这个杯子里倒水
  2. 此时你拿一个相机给这个水杯拍一张照片,拍照的这一瞬间,照片中记录到这个水杯中水的容量,就是水杯的数据快照

也就是说,Redis 的数据快照,是记录某一时刻下 Redis 中的数据,然后只需要把这个数据快照写到磁盘上就可以了。

它的优势在于,只在需要持久化时,把数据「一次性」写入磁盘,其它时间都不需要操作磁盘。

基于这个方案,我们可以定时给 Redis 做数据快照,把数据持久化到磁盘上。

其实,上面说的这些持久化方案,就是 Redis 的「RDB」和「AOF」:

  • RDB:只持久化某一时刻的数据快照到磁盘上(创建一个子进程来做)
  • AOF:每一次写操作都持久到磁盘(主线程写内存,根据策略可以配置由主线程还是子线程进行数据持久化)

它们的区别除了上面讲到的,还有以下特点:

  1. RDB 采用二进制 + 数据压缩的方式写磁盘,这样文件体积小,数据恢复速度也快
  2. AOF 记录的是每一次写命令,数据最全,但文件体积大,数据恢复速度慢

如果让你来选择持久化方案,你可以这样选择:

  1. 如果你的业务对于数据丢失不敏感,采用 RDB 方案持久化数据
  2. 如果你的业务对数据完整性要求比较高,采用 AOF 方案持久化数据

假设你的业务对 Redis 数据完整性要求比较高,选择了 AOF 方案,那此时你又会遇到这些问题:

  1. AOF 记录每一次写操作,随着时间增长,AOF 文件体积会越来越大
  2. 这么大的 AOF 文件,在数据恢复时变得非常慢

这怎么办?数据完整性要求变高了,恢复数据也变困难了?有没有什么方法,可以缩小文件体积?提升恢复速度呢?

我们继续来分析 AOF 的特点。

由于 AOF 文件中记录的都是每一次写操作,但对于同一个 key 可能会发生多次修改,我们只保留最后一次被修改的值,是不是也可以?

是的,这就是我们经常听到的「AOF rewrite」,你也可以把它理解为 AOF 「瘦身」。

我们可以对 AOF 文件定时 rewrite,避免这个文件体积持续膨胀,这样在恢复时就可以缩短恢复时间了。

再进一步思考一下,还有没有办法继续缩小 AOF 文件?

回顾一下我们前面讲到的,RDB 和 AOF 各自的特点:

  1. RDB 以二进制 + 数据压缩方式存储,文件体积小
  2. AOF 记录每一次写命令,数据最全

我们可否利用它们各自的优势呢?

当然可以,这就是 Redis 的「混合持久化」。

具体来说,当 AOF rewrite 时,Redis 先以 RDB 格式在 AOF 文件中写入一个数据快照,再把在这期间产生的每一个写命令,追加到 AOF 文件中。因为 RDB 是二进制压缩写入的,这样 AOF 文件体积就变得更小了。

此时,你在使用 AOF 文件恢复数据时,这个恢复时间就会更短了!

Redis 4.0 以上版本才支持混合持久化。

这么一番优化,你的 Redis 再也不用担心实例宕机了,当发生宕机时,你就可以用持久化文件快速恢复 Redis 中的数据。

但这样就没问题了吗?

仔细想一下,虽然我们已经把持久化的文件优化到最小了,但在恢复数据时依旧是需要时间的,在这期间你的业务应用还是会受到影响,这怎么办?

我们来分析有没有更好的方案。

一个实例宕机,只能用恢复数据来解决,那我们是否可以部署多个 Redis 实例,然后让这些实例数据保持实时同步,这样当一个实例宕机时,我们在剩下的实例中选择一个继续提供服务就好了。

没错,这个方案就是接下来要讲的「主从复制:多副本」。

主从复制:多副本

此时,你可以部署多个 Redis 实例,架构模型就变成了这样:

我们这里把实时读写的节点叫做 master,另一个实时同步数据的节点叫做 slave。

采用多副本的方案,它的优势是:

  1. 缩短不可用时间:master 发生宕机,我们可以手动把 slave 提升为 master 继续提供服务
  2. 提升读性能:让 slave 分担一部分读请求,提升应用的整体性能

这个方案不错,不仅节省了数据恢复的时间,还能提升性能,那它有什么问题吗?

你可以思考一下。

其实,它的问题在于:当 master 宕机时,我们需要「手动」把 slave 提升为 master,这个过程也是需要花费时间的。

虽然比恢复数据要快得多,但还是需要人工介入处理。一旦需要人工介入,就必须要算上人的反应时间、操作时间,所以,在这期间你的业务应用依旧会受到影响。

怎么解决这个问题?我们是否可以把这个切换的过程,变成自动化呢?

对于这种情况,我们需要一个「故障自动切换」机制,这就是我们经常听到的「哨兵」所具备的能力。

哨兵:故障自动切换

现在,我们可以引入一个「观察者」,让这个观察者去实时监测 master 的健康状态,这个观察者就是「哨兵」。

具体如何做?

  1. 哨兵每间隔一段时间,询问 master 是否正常
  2. master 正常回复,表示状态正常,回复超时表示异常
  3. 哨兵发现异常,发起主从切换

有了这个方案,就不需要人去介入处理了,一切就变得自动化了,是不是很爽?

但这里还有一个问题,如果 master 状态正常,但这个哨兵在询问 master 时,它们之间的网络发生了问题,那这个哨兵可能会误判。

这个问题怎么解决?

答案是,我们可以部署多个哨兵,让它们分布在不同的机器上,它们一起监测 master 的状态,流程就变成了这样:

  1. 多个哨兵每间隔一段时间,询问 master 是否正常
  2. master 正常回复,表示状态正常,回复超时表示异常
  3. 一旦有一个哨兵判定 master 异常(不管是否是网络问题),就询问其它哨兵,如果多个哨兵(设置一个阈值)都认为 master 异常了,这才判定 master 确实发生了故障
  4. 多个哨兵经过协商后,判定 master 故障,则发起主从切换

所以,我们用多个哨兵互相协商来判定 master 的状态,这样一来,就可以大大降低误判的概率。

哨兵协商判定 master 异常后,这里还有一个问题:由哪个哨兵来发起主从切换呢?

答案是,选出一个哨兵「领导者」,由这个领导者进行主从切换。

问题又来了,这个领导者怎么选?

想象一下,在现实生活中,选举是怎么做的?

是的,投票。

在选举哨兵领导者时,我们可以制定这样一个选举规则:

  1. 每个哨兵都询问其它哨兵,请求对方为自己投票
  2. 每个哨兵只投票给第一个请求投票的哨兵,且只能投票一次
  3. 首先拿到超过半数投票的哨兵,当选为领导者,发起主从切换

其实,这个选举的过程就是我们经常听到的:分布式系统领域中的「共识算法」。

什么是共识算法?

我们在多个机器部署哨兵,它们需要共同协作完成一项任务,所以它们就组成了一个「分布式系统」。

在分布式系统领域,多个节点如何就一个问题达成共识的算法,就叫共识算法。

在这个场景下,多个哨兵共同协商,选举出一个都认可的领导者,就是使用共识算法完成的。

这个算法还规定节点的数量必须是奇数个,这样可以保证系统中即使有节点发生了故障,剩余超过「半数」的节点状态正常,依旧可以提供正确的结果,也就是说,这个算法还兼容了存在故障节点的情况。

共识算法在分布式系统领域有很多,例如 Paxos、Raft,哨兵选举领导者这个场景,使用的是 Raft 共识算法,因为它足够简单,且易于实现。

现在,我们用多个哨兵共同监测 Redis 的状态,这样一来,就可以避免误判的问题了,架构模型就变成了这样:

好了,到这里我们先小结一下。

你的 Redis 从最简单的单机版,经过数据持久化、主从多副本、哨兵集群,这一路优化下来,你的 Redis 不管是性能还是稳定性,都越来越高,就算节点发生故障,也不用担心了。

你的 Redis 以这样的架构模式部署,基本上就可以稳定运行很长时间了。

随着时间的发展,你的业务体量开始迎来了爆炸性增长,此时你的架构模型,还能够承担这么大的流量吗?

我们一起来分析一下:

  1. 稳定性:Redis 故障宕机,我们有哨兵 + 副本,可以自动完成主从切换
  2. 性能:读请求量增长,我们可以再部署多个 slave,读写分离,分担读压力
  3. 性能:写请求量增长,但我们只有一个 master 实例,这个实例达到瓶颈怎么办?

看到了么,当你的写请求量越来越大时,一个 master 实例可能就无法承担这么大的写流量了。

要想完美解决这个问题,此时你就需要考虑使用「分片集群」了。

分片集群:横向扩展

什么是「分片集群」?

简单来讲,一个实例扛不住写压力,那我们是否可以部署多个实例,然后把这些实例按照一定规则组织起来,把它们当成一个整体,对外提供服务,这样不就可以解决集中写一个实例的瓶颈问题吗?

所以,现在的架构模型就变成了这样:

现在问题又来了,这么多实例如何组织呢?

我们制定规则如下:

  1. 每个节点各自存储一部分数据,所有节点数据之和才是全量数据
  2. 制定一个路由规则,对于不同的 key,把它路由到固定一个实例上进行读写

而分片集群根据路由规则所在位置的不同,还可以分为两大类:

  1. 客户端分片
  2. 服务端分片

客户端分片指的是,key 的路由规则放在客户端来做,就是下面这样:

这个方案的缺点是,客户端需要维护这个路由规则,也就是说,你需要把路由规则写到你的业务代码中。

如何做到不把路由规则耦合在业务代码中呢?

你可以这样优化,把这个路由规则封装成一个模块,当需要使用时,集成这个模块就可以了。

这就是 Redis Cluster 的采用的方案。

Redis Cluster 内置了哨兵逻辑,无需再部署哨兵。

当你使用 Redis Cluster 时,你的业务应用需要使用配套的 Redis SDK,这个 SDK 内就集成好了路由规则,不需要你自己编写了。

再来看服务端分片。

这种方案指的是,路由规则不放在客户端来做,而是在客户端和服务端之间增加一个「中间代理层」,这个代理就是我们经常听到的 Proxy。

而数据的路由规则,就放在这个 Proxy 层来维护。

这样一来,你就无需关心服务端有多少个 Redis 节点了,只需要和这个 Proxy 交互即可。

Proxy 会把你的请求根据路由规则,转发到对应的 Redis 节点上,而且,当集群实例不足以支撑更大的流量请求时,还可以横向扩容,添加新的 Redis 实例提升性能,这一切对于你的客户端来说,都是透明无感知的。

业界开源的 Redis 分片集群方案,例如 Twemproxy、Codis 就是采用的这种方案。

分片集群在数据扩容时,还涉及到了很多细节,这块内容不是本文章重点,所以暂不详述。

至此,当你使用分片集群后,对于未来更大的流量压力,都可以从容面对了!

总结

好了,我们来总结一下,我们是如何一步步构建一个稳定、高性能的 Redis 集群的。

首先,在使用最简单的单机版 Redis 时,我们发现当 Redis 故障宕机后,数据无法恢复的问题,因此我们想到了「数据持久化」,把内存中的数据也持久化到磁盘上一份,这样 Redis 重启后就可以从磁盘上快速恢复数据。

在进行数据持久化时,我们又面临如何更高效地将数据持久化到磁盘的问题。之后我们发现 Redis 提供了 RDB 和 AOF 两种方案,分别对应了数据快照和实时的命令记录。当我们对数据完整性要求不高时,可以选择 RDB 持久化方案。如果对于数据完整性要求较高,那么可以选择 AOF 持久化方案。

但是我们又发现,AOF 文件体积会随着时间增长变得越来越大,此时我们想到的优化方案是,使用 AOF rewrite 的方式对其进行瘦身,减小文件体积,再后来,我们发现可以结合 RDB 和 AOF 各自的优势,在 AOF rewrite 时使用两者结合的「混合持久化」方式,又进一步减小了 AOF 文件体积。

之后,我们发现尽管可以通过数据恢复的方式还原数据,但恢复数据也是需要花费时间的,这意味着业务应用还是会受到影响。我们进一步优化,采用「多副本」的方案,让多个实例保持实时同步,当一个实例故障时,可以手动把其它实例提升上来继续提供服务。

但是这样也有问题,手动提升实例上来,需要人工介入,人工介入操作也需要时间,我们开始想办法把这个流程变得自动化,所以我们又引入了「哨兵」集群,哨兵集群通过互相协商的方式,发现故障节点,并可以自动完成切换,这样就大幅降低了对业务应用的影响。

最后,我们把关注点聚焦在如何支撑更大的写流量上,所以,我们又引入了「分片集群」来解决这个问题,让多个 Redis 实例分摊写压力,未来面对更大的流量,我们还可以添加新的实例,横向扩展,进一步提升集群的性能。

至此,我们的 Redis 集群才得以长期稳定、高性能的为我们的业务提供服务。

这里我画了一个思维导图,方便你更好地去理解它们之间的关系,以及演化的过程。

后记

看到这里,我想你对如何构建一个稳定、高性能的 Redis 集群问题时,应该会有自己的见解了。

其实,这篇文章所讲的优化思路,围绕的主题就是「架构设计」的核心思想:

  • 高性能:读写分离、分片集群
  • 高可用:数据持久化、多副本、故障自动切换
  • 易扩展:分片集群、横向扩展

当我们讲到哨兵集群、分片集群时,这还涉及到了「分布式系统」相关的知识:

  • 分布式共识:哨兵领导者选举
  • 负载均衡:分片集群数据分片、数据路由

当然,除了 Redis 之外,对于构建任何一个数据集群,你都可以沿用这个思路去思考、去优化,看看它们到底是如何做的。

例如当你在使用 MySQL 时,你可以思考一下 MySQL 与 Redis 有哪些不同?MySQL 为了做到高性能、高可用,又是如何做的?其实思路都是类似的。

我们现在到处可见分布式系统、数据集群,我希望通过这篇文章,你可以理解这些软件是如何一步步演化过来的,在演化过程中,它们遇到了哪些问题,为了解决这些问题,这些软件的设计者设计了怎样的方案,做了哪些取舍?

你只有了解了其中的原理,掌握了分析问题、解决问题的能力,这样在以后的开发过程中,或是学习其它优秀软件时,就能快速地找到「重点」,在最短的时间掌握它,并能在实际应用中发挥它们的优势。

其实这个思考过程,也是做「架构设计」的思路。在做软件架构设计时,你面临的场景就是发现问题、分析问题、解决问题,一步步去演化、升级你的架构,最后在性能、可靠性方面达到一个平衡。虽然各种软件层出不穷,但架构设计的思想不会变,我希望你真正吸收的是这些思想,这样才可以做到以不变应万变。

来源:mp.weixin.qq.com/s/q79ji-cgfUMo7H0p254QRg

收起阅读 »

浅谈程序的数字签名

理论基础数字签名它是基于非对称密钥加密技术与数字摘要算法技术的应用,它是一个包含电子文件信息以及发送者身份,并能够鉴别发送者身份以及发送信息是否被篡改的一段数字串。一段数字签名数字串,它包含电子文件经过Hash编码后产生的数字摘要,即一个Hash函数值以及发送...
继续阅读 »

理论基础

数字签名它是基于非对称密钥加密技术与数字摘要算法技术的应用,它是一个包含电子文件信息以及发送者身份,并能够鉴别发送者身份以及发送信息是否被篡改的一段数字串。

一段数字签名数字串,它包含电子文件经过Hash编码后产生的数字摘要,即一个Hash函数值以及发送者的公钥和私钥三部分内容。发送方通过私钥加密后发送给接收方,接收方使用公钥解密,通过对比解密后的Hash函数值确定数据电文是否被篡改。

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。

它是一种类似写在纸上的普通的物理签名,但是在使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。


数字签名方案是一种以电子形式存储消息签名的方法。一个完整的数字签名方案应该由两部分组成:签名算法和验证算法。


android数字签名

在android的APP应用程序安装过程中,系统首先会检验APP的签名信息,如果发现签名文件不存在或者校验签名失败,系统则会拒绝安装,所以APP应用程序在发布到市场之前一定要进行签名。

OTA升级中也必须使用到数字签名进行校验,在应用版本迭代必须使用相同的证书签名,不然会生成一个新的应用,导致更新失败。在更新过程中使用相同的证书签名的应用可以共享代码和功能

App安装过程中签名检验的流程:

1、检查 APP中包含的所有文件,对应的摘要值与 MANIFEST.MF 文件中记录的值一致。

2、使用证书文件(RSA 文件)检验签名文件(SF文件)是否被修改过。

3、使用签名文件(SF 文件)检验 MF 文件没有被修改过。


CERT.RSA包含数字签名以及开发者的数字证书。CERT.RSA里的数字签名是指对CERT.SF的摘要采用私钥加密后的数据;

MANIFEST.MF文件中是APP中每个文件名称和摘要SHA256;

CERT.SF则是对MANIFEST.MF的摘要

android中的数字签名有2个主要作用:

1、能定位消息确实是由发送方签名并发出来的,其他假冒不了发送方的签名。

2、确定消息的完整性,签名它代表文件的特征,文件发生变化,数字签名的数值也会发送变化。

Anroid中的签名证书不需要权威机构认证,一般是开发者的自签名证书。所以签名信息中会包含有开发者信息,在一定程度上可以防止应用被破解二次打包成山寨的APP应用,所以签名信息也是用于对APP包防二次打包的一个校验功能点。


(上图是android studio中自创建签名的界面)

在 Android Studio中通过上图创建签名信息后,最终会生成一个 .jks 的文件,它是用作证书和私钥的二进制文件。


(上图是反编译工具直接查看app的签名信息),也可以通过jarsigner,jadx,jeb等工具查看app的签名信息。

从上图中可以看到这个APP采用了V1和V2签名信息,Android中的签名目前主要由V1、V2、V3、V4组成的。

v1签名方案:基于 JAR 签名,签名完之后是META-INF 目录下的三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。通过这三个文件校验来确保APP中的每个文件都不被改动。

APK v1的缺点就是META-INF目录下的文件并不在校验范围内,所以之前多渠道打包等都是通过在这个目录下添加文件来实现的。

V2签名方案:它是在Android 7.0系统中引入,为了使 APP可以在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APP 进行签名,然后再使用 v2 方案对其进行签名。它是一个全文件的签名方案,它能够发现对 APP的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。

V2签名,它会在 APP文件中插入一个APP签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APP签名分块”内,v2 签名和签名者身份信息会存储在 APK 签名方案 v2 分块中。


V3签名方案:它是Android 9.0系统中引入,基于 v2签名的升级,Android 9 支持 APK密钥轮替,这使应用能够在 APK 更新过程中更改其签名密钥。为了实现轮替,APK 必须指示新旧签名密钥之间的信任级别。v3 在 APK 签名分块中添加了有关受支持的 SDK 版本和 proof-of-rotation 结构的信息。

下面链接官方对V3签名相关的说明

https://source.android.google.cn/security/apksigning/v3

APK 密钥轮替功能可以参考:

https://developer.android.google.cn/about/versions/pie/android-9.0

V4签名方案:它是在Android 11.0 引入,用来支持 ADB 增量 APK 安装。通过 APK 签名方案 v4 支持与流式传输兼容的签名方案。v4 签名基于根据 APK 的所有字节计算得出的 Merkle 哈希树。

Android 11 将签名存储在单独的 .apk.idsig 文件中。

下面2个链接是官方对V4签名的相关说明

https://source.android.google.cn/security/apksigning/v4

https://developer.android.google.cn/about/versions/11/features

从上面的签名信息截图中,也可以看到android的签名采用的是X.509V3国际标准。

这个标准下约定了签名证书必须包含以下的内容。

1、证书的序列号

2、证书所使用的签名算法

3、证书的发行机构名称,命名规则一般采用X.500格式

4、证书的有效期

5、证书的所有人的名称

6、证书所有人的公开密钥

7、证书发行者对证书的签名

从上图APP的签名信息中数字签名要包含摘要加密算法:MD5、SHA-1、SHA-256

MD5是一种不可逆的加密算法。

SHA1:它是由NISTNSA设计为同DSA一起使用的,它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。

SHA-256 是 SHA-1 的升级版,现在 Android 签名使用的默认算法都已经升级到 SHA-256 了。

摘要算法中又涉及到对称加密和非对加密

对称加密就是在加密和解密过程中需要使用同一个密钥

非对称加密使用公钥/私钥中的公钥来加密明文,然后使用对应的私钥来解密密文。

APP中如果没采用加固保护,容易出现二次打包重新签名的山寨APP。

APP中二次打包流程:破解者需要对APK文件做反编译分析,反编译为smali代码,并对某些关键函数或者资源进行修改,再回编译为apk文件并重签名。

常见的对抗二次打包的方案:

1、签名校验

原理:二次打包会篡改签名,通过签名前后的变化可以检测是否被二次打包;但是这种很容易被hook掉。

2、文件校验

原理:二次打包前后apk关键文件hash值比较,判断是否被修改;但是这种很容易被hook掉。

3、核心函数转为jni层实现

原理:java层代码转为jni层实现,jni层代码相对而言篡改难度更大;写大量反射代码降低了开发效率。

window数字签名

Window的数字签名是微软的一种安全保障机制。

Window数字签名中的签名证书用于验证开发者身份真实性、保护代码的完整性。用户下载软件时,能通过数字签名验证软件来源可信,确认软件、代码没有被非法篡改或植入病毒。所以,软件开发者会在软件发行前使用代码签名证书为软件代码添加数字签名。

对于一个Windows的可执行应用程序,签发数字签名的时候需要计算的数据摘要并不会是程序文件的全部数据,而是要排除一些特定区域的数据。而这些区域当然和PE文件结构有关,具体地,不管是签发时还是校验时计算的hash都会排除一个checksum字段、一个Security数据目录字段以及数字签名证书部分的数据。


Window签名的RSA算法:通过公钥与私钥来判断私钥的合法。

公钥与私钥具有对称性,既可以通过私钥加密,公钥解密,以此来论证私钥持有者的合法身份。也可以通过公钥加密,私钥解密,来对私钥持有者发信息而不被泄露。

由于在交换公钥时免不了遭遇中间人劫持,因此window程序的签名证书,都需要第三方权威机构的认证,并不像android程序一样开发者可以对自己程序签发证书。


(查看某程序的数字签名信息)

从上面截图中看到了摘要算法用到sha1和sha256。

由于SHA-256更强的安全性,现在SHA-256已经作为代码签名证书的行业标准签名算法。

从上图中看到程序拥有2个签名信息,也就是双签名机制。

双签名就是对一个软件做两次签名,先进行SHA1签名,之后再进行SHA2签名的做法就叫做双签名。双签名需要一张支持SHA1和SHA2算法的代码签名证书,利用具备双签名功能的工具导入申请的代码签名证书对软件或应用程序进行双签名,签发后的软件或应用程序就支持SHA1和SHA2签名算法。

Windows10要求使用SHA2算法签名,而Windows7(未更新补丁的)因其兼容性只能使用SHA1算法签名,那么使用一张支持双签SHA1和SHA2算法的代码签名证书就可以实现。

软件签名校验的流程图


Windows系统验证签名流程

1、系统UAC功能开启(用户账户控制功能,默认开启);

2、程序启动时,进行CA校验程序签名信息;

2.1、使用同样算法对软件产生Hash表

2.2、使用公钥产生一个Hash表认证摘要

2.3、比较程序的Hash表认证摘要 与 自己生成的Hash表认证摘要是否一致。

3、程序在window系统执行功能。

数字签名的验证过程本质:

1、通过对要验证的软件创建hash数据;

2、使用发布者的公共密匙来解密被加密的hash数据;

3、最后比较解密的hash和新获得的hash,如果匹配说明签名是正确的,软件没有被修改过。

代码实现校验程序是否有签名,它本质上就是被加密的hash和发布者的数字证书被插入到要签名的软件,最后在进行校验签名信息。


(实现判断程序是否有签名功能)

代码实现可以通过映射文件方式,然后去安装PE文件结构去读取,读取到可选头中的数据目录表,通过判断数据目录表中

IMAGE_DIRECTORY_ENTRY_SECURITY的虚拟地址和大小不为空,那么就表示改应用程序有签名,因为数据签名都是存在在这个字段中。



同样如果要将某个应用程序的签名信息给抹除了,也是一样的思路,将数据目录表中的IMAGE_DIRECTORY_ENTRY_SECURITY的大小和地址都设置为0即可。


下图通过PE工具,可以查看这个字段Security的虚拟地址和大小不为空那么表示应用程序经过签名的。


小结

数字签名不管是在android端还是window端,它都是一种应用程序的身份标志,在安全领域中对应用程序的数字签名校验是一个很常见的鉴别真伪的一个手段。

现在很多杀毒的厂商也都是通过这个数字签名维度,作为一个该应用程序是否可信程序的校验,虽然一些安全杀毒厂商签完名后还是误报毒,那这只能找厂商开白名单了。

来源:mp.weixin.qq.com/s/gC1sqVlLdPQcJg6OkgwzZg

收起阅读 »

从val跟var了解虚拟机世界

val 跟 var val本意就是一个不可变的变量,即赋初始值后不可改变,想较于val,var其实就简单的多,就是可变变量。为什么说val是不可变的变量呢?这不就是矛盾了嘛,其实不矛盾,我们在字节码的角度出发,比如有 val a = Test() var b...
继续阅读 »

val 跟 var


val本意就是一个不可变的变量,即赋初始值后不可改变,想较于val,var其实就简单的多,就是可变变量。为什么说val是不可变的变量呢?这不就是矛盾了嘛,其实不矛盾,我们在字节码的角度出发,比如有


val a  = Test()
var b = Test()

变成的字节码是


  private final Lcom/example/newtestproject/Test; a

private Lcom/example/newtestproject/Test; b

其实val 本质就是用final修饰的变量罢了,而var,就是一个很普通的变量。两者默认都赋予private作用域,这个其实是kotlin世界赋予的额外操作,并不影响我们的理解。从这里出发,我们再继续深入进去!


一个有趣的实验


companion object{
val c = Test()
const val d = "1"
const val e = "1"
val r = "1"
val v = d
}

如果我们把val变量放在companion object里面,这个时候就会被赋予静态的特性,我们看下上面这段代码生成后的字节码



private final static Lcom/example/newtestproject/Test; c


public final static Ljava/lang/String; d = "1"


public final static Ljava/lang/String; e = "1"


private final static Ljava/lang/String; r


private final static Ljava/lang/String; v

我们可以看到,无论是普通对象还是基本数据类型,都被赋予了static的前缀,但是又有稍微不同??我们再来仔细观察一下。


对于String类型,可以用const关键字进行修饰,表示当前的String可用于字符串常量进行替换,这个就是完全的替换,直接进行了初始化!而没有const修饰的字符串r,可以看到,只是生成了一个r变量,并没有直接初始化。而r被初始化的阶段,是在clinit阶段


static void <clinit>() {
ldc "1"
putstatic 'com/example/newtestproject/ValClass.r','Ljava/lang/String;'
...

假如说我们用java代码去写的话,比如


public class JavaStaticClass {
static final String s = "123";
...
}

所生成的字节码是


  final static Ljava/lang/String; s = "123"

跟我们kotlin用const修饰的string变量一致,都是直接初始化的!(留到后面解释)我们继续深入一点,为什么有的变量直接就初始化了,有的却在clinit阶段被初始化?那就要从我们的类加载过程说起了!


类加载过程


虽然类加载有很多细分版本,但是这里笔者引用以下细分版本


image.png
由于类加载过程不是本篇的重点,这里我们稍微解释一下各阶段的主要任务即可



  1. 加载:载入类的过程 :主要是把类的二进制文件,转化为运行时内存的数据,包括静态的存储结构转为方法区等操作,在内存中生成一个代表这个类的java.lang.Class对象

  2. 验证:验证class文件等是否合法:确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

  3. 准备:准备初始数据 :准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

  4. 解析:解析常量池,函数符号等 :解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,这个阶段就把我们普通的符号转化为对内存运行数据地址。

  5. 初始化:真正的初始化,调用clinit:在初始化阶段,则会根据代码去初始化类变量和其他资源,这个时候,就走到了我们clinit阶段了,上面的阶段都是由虚拟机操控,这个阶段过去后就正在把控制权给我们程序了


准备阶段对static数据的影响


我们主要看到准备阶段:准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段,即在这个阶段过后,所有的static数据被赋予“零值”,以下是零值表


image.png
但是也有例外,就是如果类的属性表中存在ConstantValue这个特殊的属性值时,就会在准备阶段把真正的常量直接替换给当前的static变量,比如上述代码中的


省略companion object
const val d = "1"
public final static Ljava/lang/String; d = "1"

此时,只要对d的操作,就会被转化为以下字节码,比如


val v = d

字节码是
ldc "1"
putstatic 'com/example/newtestproject/ValClass.v','Ljava/lang/String;'

变成了ldc指令,即押入了一个字符串“1”进了操作数栈上,而原本的d变量盒子,已经彻底被虚拟机抛弃了。对于属性表中没有ConstantValue的变量,就会在初始化阶段,即调用clinti时,就会把数值赋给相关的变量,以替换“零值”(ps:这里就是各大字节码精简方案的核心,即删除把零值赋予零值的相关操作,比如static int xx = 0这种,就可以在Clint阶段把相关的赋值字节码删除掉也不影响其原本数值,参考框架bytex)。


当然,我们看到上面的对象c,也是在clinit阶段被赋值的,这其实就是ConstantValue生成机制的限制,ConstantValue只会对String跟基本数据类型进行生成,因为我们要替换的常量在常量池里面!对象肯定是不存在的对不对!


回归主题


看到这里,我们再回来看上面的问题,我们就知道了,kotlin中companion object里面的字符串变量,如果不用const修饰的话,其实对应的字符串String类型是不会以ConstantValue生成的,而是以静态对象相同的方式,在clinit进行!


说了半天!那么这个又有什么用呢!?其实这里主要是为了说明虚拟机背后生成的原理,同时也是为了提醒!如果以后有做指令优化的需求的时候,就要非常小心kotlin companion object里面的非const 修饰的String变量,我们就不能在Clinit的时候把这个赋值指令给清除掉!或者说不能跳过Clinit阶段就去用这个数值,因为它还是处于未初始化的状态!


最后


我们从val跟var的角度出发,分析了其背后隐含的故事,当然,看完之后你肯定就彻底懂得了这部分知识啦!无论是以后字节码插桩还是面试,相信可以很从容面对啦!


笔者说:如果你看过这篇文章 黑科技!让Native Crash 与ANR无处发泄!,就会了解到Signal的今生前世,同时我们也发布了beta版本到maven啦!快来用起来!


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

Kotlin协程:协程上下文与上下文元素

一.EmptyCoroutineContext    EmptyCoroutineContext代表空上下文,由于自身为空,因此get方法的返回值是空的,fold方法直接返回传入的初始值,plus方法也是直接返回传入的c...
继续阅读 »

一.EmptyCoroutineContext

    EmptyCoroutineContext代表空上下文,由于自身为空,因此get方法的返回值是空的,fold方法直接返回传入的初始值,plus方法也是直接返回传入的context,minusKey方法返回自身,代码如下:

public object EmptyCoroutineContext : CoroutineContext, Serializable {
private const val serialVersionUID: Long = 0
private fun readResolve(): Any = EmptyCoroutineContext

public override fun <E : Element> get(key: Key<E>): E? = null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = initial
public override fun plus(context: CoroutineContext): CoroutineContext = context
public override fun minusKey(key: Key<*>): CoroutineContext = this
public override fun hashCode(): Int = 0
public override fun toString(): String = "EmptyCoroutineContext"
}

二.CombinedContext

    CombinedContext是组合上下文,是存储Element的重要的数据结构。内部存储的组织结构如下图所示:
image.png

    可以看出CombinedContext是一种左偏(从左向右计算)的列表,这么设计的目的是为了让CoroutineContext中的plus方法工作起来更加自然。

    由于采用这种数据结构,CombinedContext类中的很多方法都是通过循环实现的,代码如下:

internal class CombinedContext(
// 数据结构左边可能为一个Element对象或者还是一个CombinedContext对象
private val left: CoroutineContext,
// 数据结构右边只能为一个Element对象
private val element: Element
) : CoroutineContext, Serializable {

override fun <E : Element> get(key: Key<E>): E? {
var cur = this
while (true) {
// 进行get操作,如果当前CombinedContext对象中存在,则返回
cur.element[key]?.let { return it }
// 获取左边的上下文对象
val next = cur.left
// 如果是CombinedContext对象
if (next is CombinedContext) {
// 赋值,继续循环
cur = next
} else { // 如果不是CombinedContext对象
// 进行get操作,返回
return next[key]
}
}
}
// 数据结构左右分开操作,从左到右进行fold运算
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(left.fold(initial, operation), element)

public override fun minusKey(key: Key<*>): CoroutineContext {
// 如果右边是指定的Element对象,则返回左边
element[key]?.let { return left }
// 调用左边的minusKey方法
val newLeft = left.minusKey(key)
return when {
// 这种情况,说明左边部分已经是去掉指定的Element对象的,右边也是如此,因此返回当前对象,不需要在进行包裹
newLeft === left -> this
// 这种情况,说明左边部分包含指定的Element对象,因此返回只右边
newLeft === EmptyCoroutineContext -> element
// 这种情况,返回的左边部分是新的,因此需要和右边部分一起包裹后,再返回
else -> CombinedContext(newLeft, element)
}
}

private fun size(): Int {
var cur = this
//左右各一个
var size = 2
while (true) {
cur = cur.left as? CombinedContext ?: return size
size++
}
}

// 通过get方法实现
private fun contains(element: Element): Boolean =
get(element.key) == element

private fun containsAll(context: CombinedContext): Boolean {
var cur = context
// 循环展开每一个CombinedContext对象,每个CombinedContext对象中的Element对象都要包含
while (true) {
if (!contains(cur.element)) return false
val next = cur.left
if (next is CombinedContext) {
cur = next
} else {
return contains(next as Element)
}
}
}
...
}

三.Key与Element

    Key接口与Element接口定义在CoroutineContext接口中,代码如下:

public interface Key<E : Element>

public interface Element : CoroutineContext {
// 一个Key对应着一个Element对象
public val key: Key<*>
// 相等则强制转换并返回,否则则返回空
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
// 自身与初始值进行fold操作
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
// 如果要去除的是当前的Element对象,则返回空的上下文,否则返回自身
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}

四.CoroutineContext

    CoroutineContext接口定义了协程上下文的基本行为以及Key和Element接口。同时,重载了"+"操作,相关代码如下:

public interface CoroutineContext {

public operator fun <E : Element> get(key: Key<E>): E?

public fun <R> fold(initial: R, operation: (R, Element) -> R): R

public operator fun plus(context: CoroutineContext): CoroutineContext =
// 如果要与空上下文相加,则直接但会当前对象,
if (context === EmptyCoroutineContext) this else
// 当前Element作为初始值
context.fold(this) { acc, element ->
// acc:已经加完的CoroutineContext对象
// element:当前要加的CoroutineContext对象

// 获取从acc中去掉element后的上下文removed,这步是为了确保添加重复的Element时,移动到最右侧
val removed = acc.minusKey(element.key)
// 去除掉element后为空上下文(说明acc中只有一个Element对象),则返回element
if (removed === EmptyCoroutineContext) element else {
// ContinuationInterceptor代表拦截器,也是一个Element对象
// 下面的操作是为了把拦截器移动到上下文的最右端,为了方便快速获取
// 从removed中获取拦截器
val interceptor = removed[ContinuationInterceptor]
// 若上下文中没有拦截器,则进行累加(包裹成CombinedContext对象),返回
if (interceptor == null) CombinedContext(removed, element) else {
// 若上下文中有拦截器
// 获取上下文中移除到掉拦截器后的上下文left
val left = removed.minusKey(ContinuationInterceptor)
// 若移除到掉拦截器后的上下文为空上下文,说明上下文left中只有一个拦截器,
// 则进行累加(包裹成CombinedContext对象),返回
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
// 否则,现对当前要加的element和left进行累加,然后在和拦截器进行累加
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}

public fun minusKey(key: Key<*>): CoroutineContext

... // (Key和Element接口)
}

1.plus方法图解

    假设我们有一个上下文顺序为A、B、C,现在要按顺序加上D、C、A。

1)初始值A、B、C
27ee3db5-ba83-4f8b-b155-de7974e76e4a.png
2)加上D
335ec6b6-b12f-4367-a274-5f65b4330517.png
3)加上C
6c36e62f-f050-47ca-b769-c29a91ef6f07.png
4)加上A
de380c56-5377-4fcc-a8c3-e6a579bf6609.png

2.为什么要将ContinuationInterceptor放到协程上下文的最右端?

    在协程中有大量的场景需要获取ContinuationInterceptor。根据之前分析的CombinedContext的minusKey方法,ContinuationInterceptor放在上下文的最右端,可以直接获取,不需要经过多次的循环。

五.AbstractCoroutineContextKey与AbstractCoroutineContextElement

    AbstractCoroutineContextElement实现了Element接口,将Key对象作为构造方法必要的参数。

public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element

    AbstractCoroutineContextKey用于实现Element的多态。什么是Element的多态呢?假设类A实现了Element接口,Key为A。类B继承自类A,Key为B。这时将类B的对象添加到上下文中,通过指定不同的Key(A或B),可以得到不同类型对象。具体代码如下:

// baseKey为衍生类的基类的Key
// safeCast用于对基类进行转换
// B为基类,E为衍生类
public abstract class AbstractCoroutineContextKey<B : Element, E : B>(
baseKey: Key<B>,
private val safeCast: (element: Element) -> E?
) : Key<E> {
// 顶置Key,如果baseKey是AbstractCoroutineContextKey,则获取baseKey的顶置Key
private val topmostKey: Key<*> = if (baseKey is AbstractCoroutineContextKey<*, *>) baseKey.topmostKey else baseKey

// 用于类型转换
internal fun tryCast(element: Element): E? = safeCast(element)
// 用于判断当前key是否是指定key的子key
// 逻辑为与当前key相同,或者与当前key的顶置key相同
internal fun isSubKey(key: Key<*>): Boolean = key === this || topmostKey === key
}

1.getPolymorphicElement方法与minusPolymorphicKey方法

    如果衍生类使用了AbstractCoroutineContextKey,那么基类在实现Element接口中的get方法时,就需要通过getPolymorphicElement方法,实现minusKey方法时,就需要通过minusPolymorphicKey方法,代码如下:

public fun <E : Element> Element.getPolymorphicElement(key: Key<E>): E? {
// 如果key是AbstractCoroutineContextKey
if (key is AbstractCoroutineContextKey<*, *>) {
// 如果key是当前key的子key,则基类强制转换成衍生类,并返回
@Suppress("UNCHECKED_CAST")
return if (key.isSubKey(this.key)) key.tryCast(this) as? E else null
}
// 如果key不是AbstractCoroutineContextKey
// 如果key相等,则强制转换,并返回
@Suppress("UNCHECKED_CAST")
return if (this.key === key) this as E else null
}
public fun Element.minusPolymorphicKey(key: Key<*>): CoroutineContext {
// 如果key是AbstractCoroutineContextKey
if (key is AbstractCoroutineContextKey<*, *>) {
// 如果key是当前key的子key,基类强制转换后不为空,说明当前Element需要去掉,因此返回空上下文,否则返回自身
return if (key.isSubKey(this.key) && key.tryCast(this) != null) EmptyCoroutineContext else this
}
// 如果key不是AbstractCoroutineContextKey
// 如果key相等,说明当前Element需要去掉,因此返回空上下文,否则返回自身
return if (this.key === key) EmptyCoroutineContext else this
}


作者:李萧蝶
链接:https://juejin.cn/post/7126161787392622623
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

有趣的 Kotlin 0x0D: IntArray vs Array<Int>

介绍 IntArray 整数数组。在 JVM 平台上,对应 int[]。 Array Array<T> 表示 T 类型数组。在 JVM 平台上,Array<Int> 对应 Integer[]。 验证 fun main() { &nbs...
继续阅读 »

介绍


IntArray


整数数组。在 JVM 平台上,对应 int[]


Array


Array<T> 表示 T 类型数组。在 JVM 平台上,Array<Int> 对应 Integer[]


验证


fun main() {
   val one = IntArray(10) { it }
   val two = Array<Int>(10) { it }
}

Decompile


Java Code


综上,JVM 平台上,IntArrayArray<Int> 的区别在于对应的类型不同,一个是基础类型 int 数组,另外一个是封装类型 Integer 数组,有装箱开销


开销差距



一般情况下,看不出差距,只能用放大镜看一下了。



@OptIn(ExperimentalTime::class)
fun main() {

   val duration1 = measureTime {
       case1()
  }
   println(duration1)

   val duration2 = measureTime {
       case2()
  }
   println(duration2)
}

private fun case1() {
   val t = IntArray(10_000_000)
}

private fun case2() {
   val t = Array<Int>(10_000_000) { it }
}

运行结果


使用场景



  • 默认使用 IntArray,基础类型因无装箱开销而性能好,且每个元素都有默认值 0

  • 如果数组需要使用 null 值,使用 Array<Int>


StackOverflow



高赞回答,一言以蔽之。



StackOverflow Issues


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

Flutter实现微信朋友圈高斯模糊效果

1. 背景 最近一个需求改版UI视觉觉得微信朋友圈的边缘高斯模糊挺好看,然后就苦逼吭哧的尝试在Flutter实现了,来看微信朋友圈点击展开的大图效果图: 微信朋友圈高斯模糊效果大概分4部分区域实现,如下图: 居中图片为原始图,然后背景模糊全图是原始图放大c...
继续阅读 »

1. 背景


最近一个需求改版UI视觉觉得微信朋友圈的边缘高斯模糊挺好看,然后就苦逼吭哧的尝试在Flutter实现了,来看微信朋友圈点击展开的大图效果图:


image.png|400


微信朋友圈高斯模糊效果大概分4部分区域实现,如下图:
image.png


居中图片为原始图,然后背景模糊全图是原始图放大cover模式的高斯模糊,在上下两个区域分别是两层单独处理边界的高斯模糊效果特殊处理,因此有时候可以看到微信朋友圈在上下两侧有明显分界线;


2. 实践


在Flutter侧实现高斯模糊比较简单,可以直接使用系统的BackdropFilter函数实现,需要传入一个filter方式,然后对child区域进行模糊过滤;


  const BackdropFilter({
Key? key,
required this.filter,
Widget? child,
this.blendMode = BlendMode.srcOver,
}) : assert(filter != null),
super(key: key, child: child);

Flutter提供了简化ImageFiltered实现高斯模糊,代码如下:


ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Image.network(url,fit: BoxFit.cover, height: expandedHeight, width: width),
),

通过此方式,可以非常简约实现全屏高斯模糊~,现在难点是上下边界区域的边界模糊处理,这里需要使用一个ShaderMask组件,在Flutter侧ShaderMask主要是实现渐变过渡能力的;


  const ShaderMask({
Key? key,
required this.shaderCallback,
this.blendMode = BlendMode.modulate,
Widget? child,
}) : assert(shaderCallback != null),
assert(blendMode != null),
super(key: key, child: child);

其需要shaderCallback回调渐变Shader,共提供3种渐变模式:



  • RadialGradient:放射状渐变

  • LinearGradient:线性渐变

  • SweepGradient:扇形渐变


这里我们需要使用线性渐变LinearGradient从上到下的渐变过渡,代码如下:


             ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.white,
Colors.transparent
],
).createShader(bounds);
},
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
)


就这样实现了?当我运行时候出现如下效果,效果还挺好的:


image.png


但是当我把封面图url替换了一个浅色图片,却出现如下效果,中间区域变成了黑色的,看来是我想的简单了:


image.png


分析了下Flutter线性过度源码,其将颜色进行过渡,
Color transparent = Color(0x00000000) , 而
Color white = Color(0xFFFFFFFF),可以看到除了透明度之外,需要保证颜色不要发生大变化,其实我们诉求只是需要将透明度发生渐变即可,因此将Colors.white改为Colors.black,


             ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.transparent
],
).createShader(bounds);
},
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
)


出现如下效果:


image.png


这里颜色貌似符合预期,但是混合模式出现了问题,学过Android开发的一定属性如下这张BlendMode混合模式图片:


image.png


ShaderMaster默认的混合模式是BlendMode.modulate,这个我也解释不清楚:这里有一篇相关文章juejin.cn/post/684490…


这里我们将混合模式替换为BlendMode.dstIn:只显示src和dst重合部分,且src的重合部分只有不透明度有用,经过这些操作后,整体效果最后如下所示:


image.png


最后奉上完整demo的相关代码:


  Widget buildCover(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double expandedHeight = 600;
double closeHeight = 300;
const String url =
'https://img.alicdn.com/imgextra/i2/O1CN01YWcPh81fbUvpcjUXp_!!6000000004025-2-tps-842-350.png';
return Container(
height: expandedHeight,
alignment: Alignment.center,
child: Stack(
children: [
ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Image.network(url,
fit: BoxFit.cover, height: expandedHeight, width: width),
),
Container(
height: expandedHeight,
alignment: Alignment.center,
child: ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.transparent
],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
),
)
],
),
);
}

3. 总结


通过实践,发现Flutter实现高斯模糊BackdropFilter/ImageFiltered组件,渐变实现方式ShaderMask,此外还需要掌握图形学的BlendMode混合模式,以后在碰到类似需求时候建议直接砍了UI视觉吧~~费劲~~~~


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

年薪达到多少才适合留在北京?

07年大学毕业,最初没敢来北京,在三线城市哈尔滨找了份工作,也和我的小学同学在2008年5月12日之前确定了恋爱关系,她当时还在读本科,09年9月女朋友考研来到了北京,我们面临第一次异地,10年5月我辞掉哈尔滨的工作来到了北京,12年4月在媳妇毕业前在北京把证...
继续阅读 »

07年大学毕业,最初没敢来北京,在三线城市哈尔滨找了份工作,也和我的小学同学在2008年5月12日之前确定了恋爱关系,她当时还在读本科,09年9月女朋友考研来到了北京,我们面临第一次异地,10年5月我辞掉哈尔滨的工作来到了北京,12年4月在媳妇毕业前在北京把证领了,12年7月她研究生毕业,顺利的解决了北京的户口,2012年7月21日我们从北京回东北办婚礼。

2012年底我们攒了20万,结婚父母给了小10万,都拼西凑借了十几万,凑够首付44万,在南城的价格洼地买了套86平两居现房,13年10月5日搬进新居,10月26日摇号中标,10月29日喜提二手宝来,10月31日大宝降生。

2014年4月换了份待遇不错还不出差的工作,就是加班太狠,抓紧还债。15年想着换把房换到媳妇单位附近,媳妇上班方便一些,15年12月敲定为海淀某学区房,媳妇单位对面,一举多得。16年元旦后房子签约,发现钱不够,然后把南城的房子卖掉付了首付,手头还有点结余。此时出现了问题老人还想继续住在郊区不想进城,媳妇说不想租房,只想住自己的房子,几天几夜没怎么睡觉后我决定在南城原小区又贷款买了一套而且更大一点的。2017年初换了辆30多万的车,同年点电标又排到了,买了辆300公里的电车。2020年9月疫情后首批孩子开学,我们也搬进了海淀,2022年2月22日二宝也来了。

收入嘛,10年刚来北京时1万多点,每年都在涨,14年年薪30万+,16年套了点期权,19年离职,现在薪资又回到每月1万多点。轻轻松松的活着,媳妇工资一直都是1万多。

我想说的是,一定要在年轻的时候拼一拼,学到自己吃饭的本领,不要拿着6千的工资干着6千的活,那真是在浪费生命,因为再出来找工作可能也就能到8千。不论拿着多少钱都要全力投入工作,老板不给涨你也有跳槽的资本。还有我想说的就是车子是消耗品,代步工具,别追太高,够用就行,最好不要贷款买车,压力会变大。

作者:神的小屋
来源:http://www.zhihu.com/question/430567574/answer/2479008231


说说我本人的情况:老家河南农村,2005年本科毕业,在三线城市工作五年,2010年到北京读研,在学校认识了来自山东农村的老婆。2013年毕业后留京工作,2014年老婆博士毕业也留京工作。2014年底领证,2015年初结婚。2015年底买了一套小两居(当时北京出台了公积金可以最高贷120万的政策,我和老婆工作两年左右攒了30多万,又借了30多万,在南三环这个房价洼地买了套55平的两居室)。2016年7月儿子出生。2017年初获得新能源车指标,买了辆占号车。2019年底换了一辆续航里程更长的。2021年,孩子转年要上小学了,两居室满五年了,就换了套学校稍好一点的三居室(五年多时间,买第一套房借的钱还完了,又攒了三十多万。又借了几十万,还是在南三环,买了套68平的三居室)。

我的月工资收入,最初是6000多,陆续涨到1万多点。老婆的工资,最初是1万多,现在加上公积金有3万左右。

因此,年薪多少不重要。找到另一半最重要,即使家庭经济条件很差,只要工作稳定,人品好就可以,两个人一起慢慢奋斗 ,也挺好的。不要被太多焦虑误导。买不起大房子,可以买小房子;买不起海淀朝阳的,可以买丰台。良好的心态最重要。夫妻两个携手并肩最重要。

作者:坐看北二环
来源:http://www.zhihu.com/question/430567574/answer/2377968907

收起阅读 »

巧用摩斯密码作为调试工具的入口|vConsole 在线上的2种使用方式

web
前言在做手机端项目的时候,我们经常在测试环境使用 vConsole 作为调试工具,它大概可以做这么多事情:查看 console 日志查看网络请求查看页面 element 结构查看 Cookies、localStorage 和 SessionStorage手动执...
继续阅读 »

前言

在做手机端项目的时候,我们经常在测试环境使用 vConsole 作为调试工具,它大概可以做这么多事情:

  • 查看 console 日志

  • 查看网络请求

  • 查看页面 element 结构

  • 查看 Cookies、localStorage 和 SessionStorage

  • 手动执行 JS 命令

  • 自定义插件

除了开发人员,vConsole 对于,测试人员也很有用,测试 bug 的时候,如果测试人员能拿到 console 信息和网络请求,无疑对于帮助开发快速定位问题是很有帮助的。

那问题来了,这么好用的工具,貌似大家都是在测试环境使用的,线上就没有引入,是不想让这个大大的调试按钮影响用户的使用体验么?这个理由显然站不住脚啊,谁能保证线上不出问题呢,如果线上可以用 vConsole,也许就能帮助我们快速定位问题,鉴于此,我给大家提供 2 种比较好的方式来解决这个问题。

速点触发

防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时

这种方法的原理是利用了 函数防抖的概念,我们设置每次 600 ms 的间隔,在此间隔内的重复点击将计数总和,当达到 10或者10的倍数时,启用 vconsole 显示状态的改变;

若某次点击间隔超过 600 ms,则计数归零,从新开始;

实现代码如下:

import VConsole from "vconsole";

function handleVconsole() {
 new VConsole()
 let count = 0
 let lastClickTime = 0
 const VconsoleDom = document.getElementById("__vconsole")
 VconsoleDom.style.display = "none"

 window.addEventListener("click", function () {
   console.log(`连续点击数:${count}`)
   const nowTime = new Date().getTime()
   nowTime - lastClickTime < 600 ? count++ : (count = 0);
   lastClickTime = nowTime

   if (count > 0 && count % 10 === 0) {
     if (!VconsoleDom) return false
     const currentStatus = VconsoleDom.style.display
     VconsoleDom.style.display = currentStatus === "block" ? "none" : "block";
     count = 0
  }
});
}

实际效果


使用摩斯密码

摩尔斯电码(英語:Morse code)是一种时通时断的信号代码,通过不同的排列顺序来表达不同的英文字母数字标点符号。是由美國發明家萨缪尔·摩尔斯及其助手艾爾菲德·維爾在1836年发明。--维基百科

第一种方法虽然好用,不过貌似太简单了,可能会误触,有没有一种可以通过 click 模拟实现的复杂指令呢?没错,我想到了摩斯密码; 简单来说,我们可以通过两种「符号」用来表示字符:点(·)和划(-),或叫「滴」(dit)和「嗒」(dah),下面是常见字符、数字、标点符号的摩斯密码公式标识:


假设,我们用 SOS 这个单词来表示 vconsole 启用的指令,那么通过查询其标识映射表,可以得出 SOS 的 摩斯密码表示为 ...---...,只要执行这个指令我么就改变 vconsole 按钮的显示状态就好了;那么问题又来了,怎么表示点(·)和划(-)呢,本来我想还是用点击间隔的长短来表示,比如 600ms 内属于短间隔,表示点(·),600ms - 2000ms 内属于长间隔,表示划(-);

但是实现后发现效果不太好,实际操作这个间隔不太好控制,容易输错; 后来我想到可以了双击 dblclick 事件,我们用 click 表示点(·),dblclick表示划(-),让我们实现下看看。

function handleVconsole() {
 new VConsole();
 let sos = [];
 let lastClickTime = 0;
 let timeId;
 const VconsoleDom = document.getElementById("__vconsole");
 VconsoleDom.style.display = "none";

 window.addEventListener("click", function () {
   clearTimeout(timeId);
   const nowTime = new Date().getTime();
   const interval = nowTime - lastClickTime;
   timeId = setTimeout(() => {
     console.log("click");
     
     if (interval < 3000) {
       sos.push(".");
    }

     if (interval > 3000) {
       sos = [];
       lastClickTime = 0;
    }

     console.log(sos);
     lastClickTime = nowTime;

     if (sos.join("") === "...---...") {
       if (!VconsoleDom) return;
       const currentStatus = VconsoleDom.style.display;
       VconsoleDom.style.display =
         currentStatus === "block" ? "none" : "block";
       sos = [];
    }
  }, 300);
});

 window.addEventListener("dblclick", function () {
   console.log("dbclick");
   clearTimeout(timeId);
   const nowTime = new Date().getTime();
   const interval = nowTime - lastClickTime;

   if (interval < 3000) {
     sos.push("-");
  }

   if (interval > 3000) {
     sos = [];
     lastClickTime = 0;
  }

   console.log(sos);
   lastClickTime = nowTime;

   if (sos.join("") === "...---...") {
     if (!VconsoleDom) return;
     const currentStatus = VconsoleDom.style.display;
     VconsoleDom.style.display = currentStatus === "block" ? "none" : "block";
     sos = [];
  }
});
}

实际效果如下所示,感觉还不错,除了 SOS, 还可以用其他的单词或者数字什么的,这就大大增加了误触的难度,实现了完全的定制化。


总结

本文针对移动端线上调试问题,提出了 2 种解决方案,特别是通过摩斯密码这种方式,据我所知,实为首创,如果各位觉得有帮助和启发,请不要吝啬给个一件三连哦,这次一定~~~。

作者:Ethan_Zhou
来源:juejin.cn/post/7126434333442703367

收起阅读 »

Android抓包从未如此简单

一、情景再现: 有一天你正在王者团战里杀的热火朝天,忽然公司测试人员打你电话问为什么某个功能数据展示不出来了,昨天还好好的纳,是不是你又偷偷写bug了。。。WTF!,你会说:把你手机给我,我连上电脑看看打印的请求日志是不是接口有问题。然后吭哧吭哧搞半天看到接...
继续阅读 »

一、情景再现:



有一天你正在王者团战里杀的热火朝天,忽然公司测试人员打你电话问为什么某个功能数据展示不出来了,昨天还好好的纳,是不是你又偷偷写bug了。。。WTF!,你会说:把你手机给我,我连上电脑看看打印的请求日志是不是接口有问题。然后吭哧吭哧搞半天看到接口数据返回的格式确实不对,然后去群里丢了几句服务端人员看一下这个接口,数据有问题。然后有回去打游戏,可惜游戏早已结束,以失败告终,自己还被无情的举报禁赛了。。。人生最痛苦的事莫过于此。假如你的项目已经集成了抓包助手,并且也给其他人员介绍过如何使用,那么像这类问题根本就不需要你再来处理了,遇到数据问题他们第一时间会自己看请求数据,而你就可以安心上王者了。



二、Android抓包现状


目前常见的抓包工具有Charles、Fiddler、Wireshark等,这些或多或少都需要一些配置,略显麻烦,只适合开发及测试人员玩,如果产品也想看数据怎么办纳,别急,本文的主角登场了,你可以在项目中集成AndroidMonitor,只需两步简单配置即可实现抓包数据可视化功能,随时随地,人人都可以方便快捷的查看请求数据了。


三、效果展示



俗话说无图无真相



111.jpg


222.jpg


333.jpg


抓包pc.png


四、如何使用



抓包工具有两个依赖需要添加:monito和monitor-plugin



Demo下载体验


源码地址


1、monitor接入


添加依赖


   debugImplementation 'io.github.lygttpod:monitor:0.0.4'
复制代码

-备注: 使用debugImplementation是为了只在测试环境中引入


2、monitor-plugin接入



  1. 根目录build.gradle下添加如下依赖


    buildscript {
dependencies {
......
//monitor-plugin需要
classpath 'io.github.lygttpod:monitor-plugin:0.0.1'
}
}

复制代码

2.添加插件


    在APP的build.gradle中添加:

//插件内部会自动判断debug模式下hook到okhttp
apply plugin: 'monitor-plugin'

复制代码


原则上完成以上两步你的APP就成功集成了抓包工具,很简单有没有,如需定制化服务请看下边的个性化配置



3、 个性化配置


1、修改桌面抓包工具入口名字:在主项目string.xml中添加 monitor_app_name即可,例如:
```
<string name="monitor_app_name">XXX-抓包</string>
```
2、定制抓包入口logo图标:
```
添加 monitor_logo.png 即可
```
3、单个项目使用的话,添加依赖后可直接使用,无需初始化,库里会通过ContentProvider方式自动初始化

默认端口8080(端口号要唯一)

4、多个项目都集成抓包工具,需要对不同项目设置不同的端口和数据库名字,用来做区分

在主项目assets目录下新建 monitor.properties 文件,文件内如如下:对需要变更的参数修改即可
```
# 抓包助手参数配置
# Default port = 8080
# Default dbName = monitor_db
# ContentTypes白名单,默认application/json,application/xml,text/html,text/plain,text/xml
# Default whiteContentTypes = application/json,application/xml,text/html,text/plain,text/xml
# Host白名单,默认全部是白名单
# Default whiteHosts =
# Host黑名单,默认没有黑名单
# Default blackHosts =
# 如何多个项目都集成抓包工具,可以设置不同的端口进行访问
monitor.port=8080
monitor.dbName=app_name_monitor_db
```
复制代码

4、 proguard(默认已经添加混淆,如遇到问题可以添加如下混淆代码)


```
# monitor
-keep class com.lygttpod.monitor.** { *; }
```
复制代码

5、 温馨提示


    虽然monitor-plugin只会在debug环境hook代码,
但是release版编译的时候还是会走一遍Transform操作(空操作),
为了保险起见建议生产包禁掉此插件。

在jenkins打包机器的《生产环境》的local.properties中添加monitor.enablePlugin=false,全面禁用monitor插件
复制代码

6、如何使用



  • 集成之后编译运行项目即可在手机上自动生成一个抓包入口的图标,点击即可打开可视化页面查看网络请求数据,这样就可以随时随地的查看我们的请求数据了。

  • 虽然可以很方便的查看请求数据了但是手机屏幕太小,看起来不方便怎么办呐,那就去寻找在PC上展示的方法,首先想到的是能不能直接在浏览器里边直接看呐,这样不用安装任何程序在浏览输入一个地址就可以直接查看数据

  • PC和手机在同一局域网的前提下:直接在任意浏览器输入 手机ip地址+抓包工具设置的端口号即可(地址可以在抓包app首页TitleBar上可以看到)


五、原理介绍


①、 拦截APP的OKHTTP请求(添加拦截器处理抓包请求,使用ASM字节码插装技术实现)



  • 写一个Interceptor拦截器,获取请求及响应的数据,转化为需要的数据结构


override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (!MonitorHelper.isOpenMonitor) {
return chain.proceed(request)
}
val monitorData = MonitorData()
monitorData.method = request.method
val url = request.url.toString()
monitorData.url = url
if (url.isNotBlank()) {
val uri = Uri.parse(url)
monitorData.host = uri.host
monitorData.path = uri.path + if (uri.query != null) "?" + uri.query else ""
monitorData.scheme = uri.scheme
}
......以上为部分代码展示
}
复制代码


  • 有了拦截器就可以通过字节码插桩技术在编译期自动为OKHTTP添加拦截器了,避免了使用者自己添加拦截器的操作


        mv?.let {
it.visitVarInsn(ALOAD, 0)
it.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient\$Builder", "interceptors", "Ljava/util/List;")
it.visitFieldInsn(GETSTATIC, "com/lygttpod/monitor/MonitorHelper", "INSTANCE", "Lcom/lygttpod/monitor/MonitorHelper;")
it.visitMethodInsn(INVOKEVIRTUAL, "com/lygttpod/monitor/MonitorHelper", "getHookInterceptors", "()Ljava/util/List;", false)
it.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true)
it.visitInsn(POP)
}
复制代码

②、 数据保存到本地数据库(room)



  • 数据库选择官方推荐Room进行数据操作


@Dao
interface MonitorDao {
@Query("SELECT * FROM monitor WHERE id > :lastId ORDER BY id DESC")
fun queryByLastIdForAndroid(lastId: Long): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor ORDER BY id DESC LIMIT :limit OFFSET :offset")
fun queryByOffsetForAndroid(limit: Int, offset: Int): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor")
fun queryAllForAndroid(): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor WHERE id > :lastId ORDER BY id DESC")
fun queryByLastId(lastId: Long): MutableList<MonitorData>

@Query("SELECT * FROM monitor ORDER BY id DESC LIMIT :limit OFFSET :offset")
fun queryByOffset(limit: Int, offset: Int): MutableList<MonitorData>

@Query("SELECT * FROM monitor")
fun queryAll(): MutableList<MonitorData>

@Insert
fun insert(data: MonitorData)

@Update
fun update(data: MonitorData)

@Query("DELETE FROM monitor")
fun deleteAll()
}
复制代码

③、 APP本地开启一个socket服务AndroidLocalService



  • AndroidLocalService基于NanoHttpd实现的一个本地微服务库,底层是通过socket实现,同时使用注解加上javapoet框架自动生成模版代码,这样就可以很方便的创建服务了,下边是创建服务并启动服务示例代码


   //@Service标记这是一个服务,端口号是服务器的端口号,注意端口号唯一
@Service(port = 9527)
abstract class AndroidService {

//@Page标注页面类,打开指定h5页面
@Page("index")
fun getIndexFileName() = "test_page.html"

//@Get注解在方法上边
@Get("query")
fun query(aaa: Boolean, bbb: Double, ccc: Float, ddd: String, eee: Int,): List<String> {
return listOf("$aaa", "$bbb", "$ccc", "$ddd", "$eee")
}

@Get("saveData")
fun saveData(content: String) {
LiveDataHelper.saveDataLiveData.postValue(content + UUID.randomUUID());
}

@Get("queryAppInfo")
fun getAppInfo(): HashMap<String, Any> {
return hashMapOf(
"applicationId" to BuildConfig.APPLICATION_ID,
"versionName" to BuildConfig.VERSION_NAME,
"versionCode" to BuildConfig.VERSION_CODE,
"uuid" to UUID.randomUUID(),
)
}
}

//初始化
ALSHelper.init(this)
//启动服务
ALSHelper.startService(ServiceConfig(AndroidService::class.java))


然后就可以通过 ip地址 + 端口号 访问了,例如:http://172.18.41.157:9527/index

复制代码


使用AndroidLocalService之后创建和启动服务就是这么简单有没有,具体用法及细节请查看其说明文档



④、 与本地socket服务通信



  • 剩下的就是与服务器的通信了,无论使用前端使用aJax还是客户端使用okhttp都可以正常请求数据了


⑤、 UI展示数据(手机端和PC端)



  • 有了接口和数据具体展示就看可以随意定制了,如果你不喜欢默认的UI风格,那就拉源码自己定制UI哦



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

Python办公软件自动化,5分钟掌握openpyxl操作

今天给大家分享一篇用openpyxl操作Excel的文章。各种数据需要导入Excel?多个Excel要合并?目前,Python处理Excel文件有很多库,openpyxl算是其中功能和性能做的比较好的一个。接下来我将为大家介绍各种Excel操作。打开Excel...
继续阅读 »

今天给大家分享一篇用openpyxl操作Excel的文章。

各种数据需要导入Excel?多个Excel要合并?目前,Python处理Excel文件有很多库,openpyxl算是其中功能和性能做的比较好的一个。接下来我将为大家介绍各种Excel操作。

打开Excel文件

新建一个Excel文件


打开现有Excel文件


打开大文件时,根据需求使用只读或只写模式减少内存消耗。


获取、创建工作表

获取当前活动工作表:


创建新的工作表:


使用工作表名字获取工作表:


获取所有的工作表名称:


保存

保存到流中在网络中使用:



单元格
单元格位置作为工作表的键直接读取:


为单元格赋值:


多个单元格 可以使用切片访问单元格区域:


使用数值格式:


使用公式:


合并单元格时,除左上角单元格外,所有单元格都将从工作表中删除:


行、列
可以单独指定行、列、或者行列的范围:


可以使用Worksheet.iter_rows()方法遍历行:


同样的Worksheet.iter_cols()方法将遍历列:


遍历文件的所有行或列,可以使用Worksheet.rows属性:


Worksheet.columns属性:


使用Worksheet.append()或者迭代使用Worksheet.cell()新增一行数据:


插入操作比较麻烦。可以使用Worksheet.insert_rows()插入一行或几行:


Worksheet.insert_cols()操作类似。Worksheet.delete_rows()Worksheet.delete_cols()用来批量删除行和列。

只读取值
使用Worksheet.values属性遍历工作表中的所有行,但只返回单元格值:


Worksheet.iter_rows()Worksheet.iter_cols()可以设置values_only参数来仅返回单元格的值:



作者:Sinchard | 来源:python中文社区

收起阅读 »

Android ViewModelScope 如何自动取消协程

先看一下 ViewModel 中的 ViewModelScope 是何方神圣 val ViewModel.viewModelScope: CoroutineScope get() { val scope: Corouti...
继续阅读 »

先看一下 ViewModel 中的 ViewModelScope 是何方神圣


val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}

可以看到这个是一个扩展方法,


再点击 setTagIfAbsent 方法进去


 <T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);//第一次肯定为null
if (previous == null) {
mBagOfTags.put(key, newValue);//null 存储
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {//判断是否已经clear了
// It is possible that we'll call close() multiple times on the same object, but
// Closeable interface requires close method to be idempotent:
// "if the stream is already closed then invoking this method has no effect." (c)
closeWithRuntimeException(result);
}
return result;
}

可以看到 这边 会把 我们的 ViewModel 存储到 ViewModel 内的 mBagOfTags 中


这个 mBagOfTags 是


    private final Map<String, Object> mBagOfTags = new HashMap<>();

这个时候 我们 viewModel 就会持有 我们 viewModelScope 的协程 作用域了。


那..这也只是 表述了 我们 viewModelScope 存在哪里而已,


什么时候清除呢?


先看一下 ViewModel 的生命周期



可以看到 ViewModel 的生命周期 会在 Activity onDestory 之后会被调用。


那...具体哪里调的?


翻看源码可以追溯到 ComponentActivity 的默认构造器内


 public ComponentActivity() {
/*省略一些*/
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
}

可以看到内部会通对 Lifecycle 添加一个观察者,观察当前 Activity 的生命周期变更事件,如果走到了 Destory ,并且 本次 Destory 并非由于配置变更引起的,才会真正调用 ViewModelStore 的 clear 方法。


跟进 clear 方法看看


public class ViewModelStore {

private final HashMap<String, ViewModel> mMap = new HashMap<>();

/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}

可以看到这个 ViewModelStore 内部实现 用 HashMap 存储 ViewModel


于是在 clear 的时候,会逐个遍历调用 clear方法


再次跟进 ViewModel 的 clear 方法


 @MainThread
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}

可以发现我们最初 存放 viewmodelScope 的 mBagOfTags


这里面的逻辑 就是对 mBagOfTags 存储的数据 挨个提取出来并且调用 closeWithRuntimeException


跟进 closeWithRuntimeException


 private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

该方法内会逐个判断 对象是否实现 Closeable 如果实现就会调用这个接口的 close 方法,


再回到最初 我们 viewModel 的扩展方法那边,看看我们 viewModelScope 的真正面目


internal class CloseableCoroutineScope(context: CoroutineContext) 
: Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context

override fun close() {
coroutineContext.cancel()
}
}

可以明确的看到 我们的 ViewModelScope 实现了 Closeable 并且充写了 close 方法,


close 方法内的实现 会对 协程上下文进行 cancel。


至此我们 可以大致整理一下



  1. viewModelScope 是 ViewModel 的扩展成员,该对象是 CloseableCoroutineScope,并且实现了 Closeable 接口

  2. ViewModelScope 存储在 ViewModel 的 名叫 mBagOfTags 的HashMap中 啊

  3. ViewModel 存储在 Activity 的 ViewModelStore 中,并且会监听 Activity 的 Lifecycle 的状态变更,在ON_DESTROY 且 非配置变更引起的事件中 对 viewModelStore 进行清空

  4. ViewModelStore 清空会对 ViewModelStore 内的所有 ViewModel 逐个调用 clear 方法。

  5. ViewModel的clear方法会对 ViewModel的 mBagOfTags 内存储的对象进行调用 close 方法(该对象需实现Closeable 接口)

  6. 最终会会调用 我们 ViewModelScope 的实现类 CloseableCoroutineScope 的 close 方法中。close 方法会对协程进行 cancel。

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

Android 12新功能:使用SplashScreen优化启动体验

前言由于很多应用在启动时需要进行一些初始化事务,导致在启动应用时有一定的空白延迟,在之前我们一般的做法是通过替换 android:windowBackground 的自定义主题,使应用启动时及时显示一张默认图片来改善启动体验。在Androi...
继续阅读 »

前言

由于很多应用在启动时需要进行一些初始化事务,导致在启动应用时有一定的空白延迟,在之前我们一般的做法是通过替换 android:windowBackground 的自定义主题,使应用启动时及时显示一张默认图片来改善启动体验。

在Android 12中,官方添加了SplashScreen API,它可为所有应用启用新的应用启动界面。新的启动界面是瞬时显示的,所以就不必再自定义android:windowBackground 了。新启动页面的样式默认是正中显示应用图标,但是允许我们自定义,以便应用能够保持其独特的品牌。下面我们来看看如何使用它。

启动画面实现

其实在Android 12上已经默认使用了SplashScreen,如果没有任何配置,会自动使用App图标。

当然也允许自定义启动画面,在value-v31中的style.xml中,可以在App的主Theme中通过如下属性来进行配置:

<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<item name="android:windowSplashScreenBackground">@android:color/white</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/anim_ai_loading</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
<item name="android:windowSplashScreenBrandingImage">@mipmap/brand</item>
</style>
  • windowSplashScreenBackground设置启动画面的背景色

  • windowSplashScreenAnimatedIcon启动图标。就是显示在启动界面中间的图片,也可以是动画

  • windowSplashScreenAnimationDuration设置动画的长度。注意这里最大只能1000ms,如果需要动画时间更长,则需要通过代码的手段让启动画面在屏幕上显示更长时间(下面会讲到)

  • windowSplashScreenIconBackground设置启动图标的背景色

  • windowSplashScreenBrandingImage设置要显示在启动画面底部的图片。官方设计准则建议不要使用品牌图片。

运行启动应用就可以看到新的启动画面了,如下: 屏幕录制2022-01-19 上午10.gif

动画的元素

在Android 12上,显示在启动界面中间的图片会有一个圆形遮罩,所以在设计图片或动画的时候一定要注意,比如上面我的例子,动画其实就没有显示完整。对此官方给了详细的设计指导,如下:

image.png

  • 应用图标 (1) 应该是矢量可绘制对象,它可以是静态或动画形式。虽然动画的时长可以不受限制,但我们建议让其不超过 1000 毫秒。默认情况下,使用启动器图标。
  • 图标背景 (2) 是可选的,在图标与窗口背景之间需要更高的对比度时很有用。如果您使用一个自适应图标,当该图标与窗口背景之间的对比度足够高时,就会显示其背景。
  • 与自适应图标一样,前景的 ⅓ 被遮盖 (3)。
  • 窗口背景 (4) 由不透明的单色组成。如果窗口背景已设置且为纯色,则未设置相应的属性时默认使用该背景。

启动时长

默认当应用绘制第一帧后,启动画面会立即关闭。但是在我们实际使用中,一般在启动时进行一些初始化操作,另外大部分应用会请求启动广告,这样其实需要一些耗时的。通常情况下,这些耗时操作我们会进行异步处理,那么是否可以让启动画面等待这些初始化完成后才关闭?

我们可以使用 ViewTreeObserver.OnPreDrawListener让应用暂停绘制第一帧,直到一切准备就绪才开始,这样就会让启动画面停留更长的时间,如下:

...
var isReady = false
...

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
...

val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
return if (isReady) {
content.viewTreeObserver.removeOnPreDrawListener(this)
true
} else {
false
}
}
}
)
}

这样当初始化等耗时操作完成后,将isReady置为true即可关闭启动画面进入应用。

上面我们提到配置启动动画的时长最多只能是1000ms,但是通过上面的代码可以让启动画面停留更长时间,所以动画的展示时间也就更长了。

关闭动画

启动画面关闭时默认直接消失,当然我们也可以对其进行自定义。

在Activity中可以通过getSplashScreen来获取(注意判断版本,低版本中没有这个函数,会crash),然后通过它的setOnExitAnimationListener来定义关闭动画,如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener { splashScreenView ->
val slideUp = ObjectAnimator.ofFloat(
splashScreenView,
View.TRANSLATION_Y,
0f,
-splashScreenView.height.toFloat()
)
slideUp.interpolator = AnticipateInterpolator()
slideUp.duration = 200L
//这里doOnEnd需要Android KTX库,即androidx.core:core-ktx:1.7.0
slideUp.doOnEnd { splashScreenView.remove() }
slideUp.start()
}
}

加上如上代码后,本来直接消失的启动画面就变成了向上退出了。

这里可以通过splashScreenView可以获取到启动动画的时长和开始时间,如下:

val animationDuration = splashScreenView.iconAnimationDurationMillis
val animationStart = splashScreenView.getIconAnimationStartMillis

这样就可以计算出启动动画的剩余时长。

顺便吐槽一下官网这里代码错了,开始时间也用了iconAnimationDurationMillis来获取,实际上应该是getIconAnimationStartMillis

低版本使用SplashScreen

只能在Android 12上体验官方的启动动画,显然不能够啊!官方提供了Androidx SplashScreen compat库,能够向后兼容,并可在所有 Android 版本上显示外观和风格一致的启动画面(这点我保留意见)。

首先要升级compileSdkVersion,并依赖SplashScreen库,如下:

android {
compileSdkVersion 31
...
}
dependencies {
...
implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
}

然后在style.xml添加代码如下:

<style name="Theme.App.Starting" parent="Theme.SplashScreen">
// Set the splash screen background, animated icon, and animation duration.
<item name="windowSplashScreenBackground">@android:color/white</item>

// Use windowSplashScreenAnimatedIcon to add either a drawable or an
// animated drawable. One of these is required.
<item name="windowSplashScreenAnimatedIcon">@drawable/anim_ai_loading</item>
<item name="windowSplashScreenAnimationDuration">1000</item> # Required for
# animated icons

// Set the theme of the Activity that directly follows your splash screen.
<item name="postSplashScreenTheme">@style/AppTheme</item> # Required.
</style>

前三个我们上面都介绍过了,这里新增了一个postSplashScreenTheme,它应该设置为应用的原主题,这样会将这个主题设置给启动画面之后的Activity,这样就可以保持样式的不变。

注意上面提到的windowSplashScreenIconBackgroundwindowSplashScreenBrandingImage没有,这是与Android12的不同之一。

然后我们将这个style设置给Application或Activity即可:

<manifest>
<application android:theme="@style/Theme.App.Starting">
<!-- or -->
<activity android:theme="@style/Theme.App.Starting">
...

最后需要在启动activity中,先调用installSplashScreen,然后才能调用setContentView,如下

class MainActivity : ComponentActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       val splashScreen = installSplashScreen()
       setContentView(R.layout.activity_main)
...

然后在低版本系统上启动应用就可以看到启动画面了。

installSplashScreen这一步很重要,如果没有这一行代码,postSplashScreenTheme就无法生效,这样启动画面后Activity就无法使用之前的样式,严重的会造成崩溃。比如在Activity中存在AppCompat组件,这就需要使用AppCompat样式,否则就会Crash。

最后注意在Android 12上依然有圆形遮罩,所以需要遵循官方的设计准则;但是在低版本系统上则没发现有这个遮罩,而且在低版本上动画无效,只会显示第一帧的画面,所以我对官方说的风格一致保留意见。

现有启动画面迁移

目前市场上的App基本都自己实现了启动页面,如果直接添加SplashScreen,就会造成重复,所以我们需要对原有启动页面进行处理。具体处理还要根据每个App自己的启动页面的实现逻辑来定,这里官方给出了一些意见,大家可以参考一下:将现有的启动画面实现迁移到 Android 12 及更高版本

总结

官方的SplashScreen有点姗姗来迟,不过效果还是不错的,使用起来也非常简单,但是一定要注意版本。虽然Androidx SplashScreen compat库可以向后兼容,但是与Android 12上还是有一些不同。


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

收起阅读 »

WebView初体验【Android】

每天认真洗脸,多读书,按时睡,少食多餐。变得温柔,大度,继续善良,保持爱心。不在人前矫情,四处诉说以求宽慰,而是学会一个人静静面对,自己把道理想通。这样的你,单身也所谓啊,你在那么虔诚地做更好的自己,一定会遇到最好的,而那个人也一定值得你所有等待。 在We...
继续阅读 »

每天认真洗脸,多读书,按时睡,少食多餐。变得温柔,大度,继续善良,保持爱心。不在人前矫情,四处诉说以求宽慰,而是学会一个人静静面对,自己把道理想通。这样的你,单身也所谓啊,你在那么虔诚地做更好的自己,一定会遇到最好的,而那个人也一定值得你所有等待。



书客创作


在WebView没有出现之前,如果要访问一个网页只能通过打开手机内的浏览器,通过浏览器来加载网页,但是打开浏览器的同时,也脱离了当前的应用软件,这样就大大的降低了网页与应用软件的交互。随着Android SDK的不断升级,官方提供一个WebView控件,专门用于加载网页并实现交互。那么到底WebView是什么?又该如何使用呢?


什么是WebView?

简单来说WebView是移动端用于加载Web页面的控件。


怎么使用WebView?

1、移动端加载网页方式


A、通过打开浏览器访问网页


String weburl ="http://www.baidu.com/";
Uri uri = Uri.parse(weburl);// weburl网址
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);

B、通过WebView打开本地网页


WebView.loadUrl("file:///android_asset/baidu.html");

注意1:本地文件放在assets文件中,assets文件是main的子文件,与res文件同级。
注意2:设置WebView支持加载本地文件。


WebSettings webSettings = webView.getSettings();
// 允许加载Assets和resources文件
webSettings.setAllowFileAccess(true);

本地baidu.html代码


C、通过WebView加载网址


webView.loadUrl("http://www.baidu.com/");

加载网址,需要在清单文件中加上网络请求权限


<uses-permission android:name="android.permission.INTERNET"/>

当WebView加载失败时,可以使用webView.reload();来重新加载。
注意:当加载完网页之后,如果发现网页无法点击,这很可能是WebView没有获取焦点。


webView.requestFocus();// 使页面获取焦点,防止点击无响应

2、WebView基本属性设置


WebView提供很多属性,需要通过WebSettings来进行设置,下面是对一些常用属性进行设置。


// 设置WebView相关属性
WebSettings webSettings = webView.getSettings();
// 是否缓存表单数据
webSettings.setSaveFormData(false);
// 设置WebView 可以加载更多格式页面
webSettings.setLoadWithOverviewMode(true);
// 设置WebView使用广泛的视窗
webSettings.setUseWideViewPort(true);
// 支持2.2以上所有版本
webSettings.setPluginState(WebSettings.PluginState.ON);
// 允许加载Assets和resources文件
webSettings.setAllowFileAccess(true);
// 告诉webview启用应用程序缓存api
webSettings.setAppCacheEnabled(true);
// 排版适应屏幕
webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
// 支持插件
webSettings.setPluginState(WebSettings.PluginState.ON);
// 设置是否启用了DOM storage AP搜索I
webSettings.setDomStorageEnabled(true);
// 设置缓存,默认不使用缓存-有缓存,使用缓存
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
// 是否允许缩放
webSettings.setSupportZoom(false);
// 是否支持通过js打开新的窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 允许加载JS
webSettings.setJavaScriptEnabled(true);

// 隐藏滚动条
webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);

3、WebView默认是通过浏览器打开网页,如何使用WebView打开网页?


WebViewClient是WebView的一个重要属性,它不仅仅能够实现WebView打开网页,而且还能够实现URL重构等功能。


// WebView默认是通过浏览器打开url,使用url在WebView中打开
webView.setWebViewClient(new WebViewClient() {
// // 旧版本
// @Override
// public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 使url在WebView中打开,在这里可以进行重构url
// webView.loadUrl(url);
// return true;
// }

// 新版本
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// 返回false,意味着请求过程中,不管有多少次的跳转请求(即新的请求地址),均交给webView自己处理,这也是此方法的默认处理
// 返回true,说明你自己想根据url,做新的跳转,比如在判断url符合条件的情况下,我想让webView加载http://baidu.com/
// 加载Url,使网页在WebView中打开,在这里可以进行重构url
if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.LOLLIPOP) {
webView.loadUrl(request.getUrl().toString());
}
return true;
}

// WebViewClient帮助WebView去处理页面控制和请求通知
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
}

// 错误代码处理,一般是加载本地Html页面,或者使用TextView显示错误
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
// 当网页加载出错时,加载本地错误文件
// webView.loadUrl("file:///android_asset/error.html");
}

// 页面开始加载-例如在这里开启进度条
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}

// 页面加载结束,一般用来加载或者执行javaScript脚本
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
}
});

4、设置WebView的WebChromeClient属性


WebChromeClient是WebView中一个非常重要的属性,使用它可以监听网页加载的进度,获取网页主题等信息。


// 监听网页加载进度
webView.setWebChromeClient(new WebChromeClient() {
// 网页Title信息
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
}

// 监听网页alert方法
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super(view, url, message, result);
}

// 显示网页加载进度
@Override
public void onProgressChanged(WebView view, int newProgress) {
// newProgress 1-100
}
});

5、WebView中使用JavaScript


WebView与网页的交互大多数是使用JavaScript来实现


//设置WebView支持JavaScript
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);

6、下载文件监听


// 下载文件
webView.setDownloadListener(new DownloadListener() {
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
// url下载文件地址
// 处理下载文件逻辑
}
});

7、后退与前进


// 返回键监听
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (webView.canGoBack())
// 判断WebView是否能够返回,能-返回
webView.canGoBack();
else
finish();
return true;
}
return super.onKeyDown(keyCode, event);
}

8、WebView优化-缓存


//设置缓存,默认不使用缓存
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//有缓存,使用缓存
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);//不使用缓存

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

普通的加载千篇一律,有趣的 loading 万里挑一

前言在网络速度较慢的场景,一个有趣的加载会提高用户的耐心和对 App 的好感,有些 loading 动效甚至会让用户有想弄清楚整个动效过程到底是怎么样的冲动。然而,大部分的 App的 loading 就是下面这种千篇一律...
继续阅读 »

前言

在网络速度较慢的场景,一个有趣的加载会提高用户的耐心和对 App 的好感,有些 loading 动效甚至会让用户有想弄清楚整个动效过程到底是怎么样的冲动。然而,大部分的 App的 loading 就是下面这种千篇一律的效果 —— 俗称“转圈”。

loading-ios.gif

loading-android.gif

本篇我们利用Flutter 的 PathMetric来玩几个有趣的 loading 效果。

效果1:圆环内滚动的球

加载圆形球动画.gif

如上图所示,一个红色的小球在蓝色的圆环内滚动,而且在往上滚动的时候速度慢,往下滚动的时候有个明显的加速过程。这个效果实现的思路如下:

  • 绘制一个蓝色的圆环,在蓝色的圆环内构建一个半径更小一号的圆环路径(Path)。
  • 让红色小球在动画控制下沿着内部的圆环定义的路径运动。
  • 选择一个中间减速(上坡)两边加速的动画曲线。

下面是实现代码:

// 动画控制设置
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.slowMiddle,
))
..addListener(() {
setState(() {});
});

// 绘制和动画控制方法
_drawLoadingCircle(Canvas canvas, Size size) {
var paint = Paint()..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;
var path = Path();
final radius = 40.0;
var center = Offset(size.width / 2, size.height / 2);
path.addOval(Rect.fromCircle(center: center, radius: radius));
canvas.drawPath(path, paint);

var innerPath = Path();
final ballRadius = 4.0;
innerPath.addOval(Rect.fromCircle(center: center, radius: radius - ballRadius));
var metrics = innerPath.computeMetrics();
paint.color = Colors.red;
paint.style = PaintingStyle.fill;
for (var pathMetric in metrics) {
var tangent = pathMetric.getTangentForOffset(pathMetric.length * animationValue);
canvas.drawCircle(tangent!.position, ballRadius, paint);
}
}

效果2:双轨运动

双轨运动.gif

上面的实现效果其实比较简单,就是绘制了一个圆和一个椭圆,然后让两个实心圆沿着路径运动。因为有了这个组合效果,趣味性增加不少,外面的椭圆看起来就像是一条卫星轨道一样。实现的逻辑如下:

  • 绘制一个圆和一个椭圆,二者的中心点重合;
  • 在圆和椭圆的路径上分别绘制一个小的实心圆;
  • 通过动画控制实心圆沿着大圆和椭圆的路径上运动。

具体实现的代码如下所示。

controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutSine,
))
..addListener(() {
setState(() {});
});

_drawTwinsCircle(Canvas canvas, Size size) {
var paint = Paint()
..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;

final radius = 50.0;
final ballRadius = 6.0;
var center = Offset(size.width / 2, size.height / 2);
var circlePath = Path()
..addOval(Rect.fromCircle(center: center, radius: radius));
paint.style = PaintingStyle.stroke;
paint.color = Colors.blue[400]!;
canvas.drawPath(circlePath, paint);

var circleMetrics = circlePath.computeMetrics();
for (var pathMetric in circleMetrics) {
var tangent = pathMetric
.getTangentForOffset(pathMetric.length * animationValue);

paint.style = PaintingStyle.fill;
paint.color = Colors.blue;
canvas.drawCircle(tangent!.position, ballRadius, paint);
}

paint.style = PaintingStyle.stroke;
paint.color = Colors.green[600]!;
var ovalPath = Path()
..addOval(Rect.fromCenter(center: center, width: 3 * radius, height: 40));
canvas.drawPath(ovalPath, paint);
var ovalMetrics = ovalPath.computeMetrics();

for (var pathMetric in ovalMetrics) {
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * animationValue);

paint.style = PaintingStyle.fill;
canvas.drawCircle(tangent!.position, ballRadius, paint);
}
}

效果3:钟摆运动

钟摆球动画.gif 钟摆运动的示意图如下所示,一条绳子系着一个球悬挂某处,把球拉起一定的角度释放后,球就会带动绳子沿着一条圆弧来回运动,这条圆弧的半径就是绳子的长度。 钟摆示意图.png 这个效果通过代码来实现的话,需要做下面的事情:

  • 绘制顶部的横线,代表悬挂的顶点;
  • 绘制运动的圆弧路径,以便让球沿着圆弧运动;
  • 绘制实心圆代表球,并通过动画控制沿着一条圆弧运动;
  • 用一条顶端固定,末端指向球心的直线代表绳子;
  • 当球运动到弧线的终点后,通过动画反转(reverse)控制球 返回;到起点后再正向(forward) 运动就可以实现来回运动的效果了。

具体实现的代码如下,这里在绘制球的时候给 Paint 对象增加了一个 maskFilter 属性,以便让球看起来发光,更加好看点。

controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutQuart,
))
..addListener(() {
setState(() {});
}
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});

_drawPendulum(Canvas canvas, Size size) {
var paint = Paint()
..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;

final ceilWidth = 60.0;
final pendulumHeight = 200.0;
var ceilCenter =
Offset(size.width / 2, size.height / 2 - pendulumHeight / 2);
var ceilPath = Path()
..moveTo(ceilCenter.dx - ceilWidth / 2, ceilCenter.dy)
..lineTo(ceilCenter.dx + ceilWidth / 2, ceilCenter.dy);
canvas.drawPath(ceilPath, paint);

var pendulumArcPath = Path()
..addArc(Rect.fromCircle(center: ceilCenter, radius: pendulumHeight),
3 * pi / 4, -pi / 2);

paint.color = Colors.white70;
var metrics = pendulumArcPath.computeMetrics();

for (var pathMetric in metrics) {
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * animationValue);

canvas.drawLine(ceilCenter, tangent!.position, paint);
paint.style = PaintingStyle.fill;
paint.color = Colors.blue;
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4.0);
canvas.drawCircle(tangent.position, 16.0, paint);
}
}

总结

本篇介绍了三种 Loading 动效的绘制逻辑和实现代码,可以看到利用路径属性进行绘图以及动画控制可以实现很多有趣的动画效果。


作者:岛上码农
链接:https://juejin.cn/post/7125029565625270286
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

女学霸考 692 分想当“程序媛”,网友:快劝劝孩子

当记者问道这个专业男孩子比较多时,女孩女王式发言:也没见男生考得比我好,一时间引起大家热议,下面就来一起了解一下吧。据了解,今年四川理科一本线为 521 分,这名女学霸超出 171 分之多。首先我们从各大厂校招网申数量上看(盘了字节跳动、网易、快手、三七互娱、...
继续阅读 »

近日四川成都一女学霸高考分数 692 分,直言想当程序员。

当记者问道这个专业男孩子比较多时,女孩女王式发言:也没见男生考得比我好,一时间引起大家热议,下面就来一起了解一下吧。

女孩考 692 分想当程序员


6月26日,四川成都。女学霸高考考了 692 分,其中数学成绩为 149 分,最后一道大题一个小细节扣了一分。


坦言想报考复旦大学,学电子信息类工科专业,未来要做一名“程序猿”。





当记者开玩笑提到“掉头发”和“行业里男生较多”时,女孩更是霸气发言,“家里遗传的头发比较好。程序员行业女生没有什么问题,我们学校也没有男生考得比我好”。

据了解,今年四川理科一本线为 521 分,这名女学霸超出 171 分之多。

有网友评论羡慕的同时也想劝劝孩子。


引人注意的是,记者的提问也引起了不少网友反感。毕竟程序员一开始从业者都是女性,而且当今世界上最伟大程序员排名第一位的也是女性。


那么计算机技术哪家高校强呢?

首先我们从各大厂校招网申数量上看(盘了字节跳动、网易、快手、三七互娱、金山云、浪潮集团),以下学校均排在投递数量前列:

华中科技大学、北京邮电大学、西安电子科技大学、电子科技大学、哈尔滨工业大学、东北大学、武汉大学、上海交通大学、南京大学。


当程序员包括的专业类型可以有计算机专业、软件开发专业、电子信息专业、通信专业、软件工程等,程序员的范围很广,主要包括软件设计/开发和程序编码两大类。

来源:mp.weixin.qq.com/s/vxb3c_5C-Ap_9NGRMeGltA

收起阅读 »

uniapp项目优化方式及建议

1.复杂页面数据区域封装成组件例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新...
继续阅读 »

介绍:性能优化自古以来就是重中之重,关于uniapp项目优化方式最全整理,会根据开发情况进行补充

1.复杂页面数据区域封装成组件

场景

例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿

优化方案

对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新这个组件

注:app-nvue和h5不存在此问题;造成差异的原因是小程序目前只提供了组件差量更新的机制,不能自动计算所有页面差量

2.避免使用大图

场景

页面中若大量使用大图资源,会造成页面切换的卡顿,导致系统内存升高,甚至白屏崩溃;对大体积的二进制文件进行 base64 ,也非常耗费资源

优化方案

图片请压缩后使用,避免大图,必要时可以考虑雪碧图或svg,简单代码能实现的就不要图片

3.小程序、APP分包处理pages过多

前往官网手册查看配置

4.图片懒加载

功能描述

此功能只对微信小程序、App、百度小程序、字节跳动小程序有效,默认开启

前往uView手册查看配置

5.禁止滥用本地存储

不要滥用本地存储,局部页面之间的传参用url,如果用本地存储传递数据要命名规范和按需销毁

6.可在外部定义变量

在 uni-app 中,定义在 data 里面的数据每次变化时都会通知视图层重新渲染页面;所以如果不是视图所需要的变量,可以不定义在 data 中,可在外部定义变量或直接挂载在 vue实例 上,以避免造成资源浪费

7.分批加载数据优化页面渲染

场景

页面初始化时,逻辑层一次性向视图层传递很大的数据,使视图层一次性渲染大量节点,可能造成通讯变慢、页面切换卡顿

优化方案

以局部更新页面的方式渲染页面;如:服务端返回 100条数据 ,可进行分批加载,一次加载 50条 , 500ms 后进行下一次加载

8.避免视图层和逻辑层频繁进行通讯

  1. 减少 scroll-view 组件的 scroll 事件监听,当监听 scroll-view 的滚动事件时,视图层会频繁的向逻辑层发送数据

  2. 监听 scroll-view 组件的滚动事件时,不要实时的改变 scroll-top / scroll-left 属性,因为监听滚动时,视图层向逻辑层通讯,改变 scroll-top / scroll-left 时,逻辑层又向视图层通讯,这样就可能造成通讯卡顿

  3. 注意 onPageScroll 的使用, onPageScroll 进行监听时,视图层会频繁的向逻辑层发送数据

  4. 多使用 css动画 ,而不是通过js的定时器操作界面做动画

  5. 如需在 canvas 里做跟手操作, app端 建议使用 renderjs ,小程序端建议使用 web-view 组件; web-view 里的页面没有逻辑层和视图层分离的概念,自然也不会有通信折损

9.CSS优化

要知道哪些属性是有继承效果的,像字体、字体颜色、文字大小都是继承的,禁止没有意义的重复代码

10.善用节流和防抖

防抖

等待n秒后执行某函数,若等待期间再次被触发,则等待时间重新初始化

节流

触发事件n秒内只执行一次,n秒未过,再次触发无效

11.优化页面切换动画

场景

页面初始化时存在大量图片或原生组件渲染和大量数据通讯,会发生新页面渲染和窗体进入动画抢资源,造成页面切换卡顿、掉帧

优化方案

  1. 建议延时 100ms~300ms 渲染图片或复杂原生组件,分批进行数据通讯,以减少一次性渲染的节点数量

  2. App 端动画效果可以自定义; popin/popout 的双窗体联动挤压动画效果对资源的消耗更大,如果动画期间页面里在执行耗时的js,可能会造成动画掉帧;此时可以使用消耗资源更小的动画效果,比如 slide-in-right / slide-out-right

  3. App-nvue 和 H5 ,还支持页面预载,uni.preloadPage,可以提供更好的使用体验

12.优化背景色闪白

场景

进入新页面时背景闪白,如果页面背景是深色,在vue页面中可能会发生新窗体刚开始动画时是灰白色背景,动画结束时才变为深色背景,造成闪屏

优化方案

  1. 将样式写在 App.vue 里,可以加速页面样式渲染速度; App.vue 里面的样式是全局样式,每次新开页面会优先加载 App.vue 里面的样式,然后加载普通 vue 页面的样式

  2. app端 还可以在 pages.json 的页面的 style 里单独配置页面原生背景色,比如在 globalStyle->style->app-plus->background 下配置全局背景色

"style": { "app-plus": { "background":"#000000" } }
  1. nvue页面不存在此问题,也可以更改为nvue页面

13.优化启动速度

  1. 工程代码越多,包括背景图和本地字体文件越大,对小程序启动速度有影响,应注意控制体积

  2. App端的 splash 关闭有白屏检测机制,如果首页一直白屏或首页本身就是一个空的中转页面,可能会造成 splash 10秒才关闭

  3. App端使用v3编译器,首页为 nvue页面 时,并设置为fast启动模式,此时App启动速度最快

  4. App设置为纯 nvue项目 (manifest里设置app-plus下的renderer:"native"),这种项目的启动速度更快,2秒即可完成启动;因为它整个应用都使用原生渲染,不加载基于webview的那套框架

14.优化包体积

  1. uni-app 发行到小程序时,如果使用了 es6 转 es5 、css 对齐的功能,可能会增大代码体积,可以配置这些编译功能是否开启

  2. uni-app 的 H5端,uni-app 提供了摇树优化机制,未摇树优化前的 uni-app 整体包体积约 500k,服务器部署 gzip 后162k。开启摇树优化需在manifest配置

  3. uni-app 的 App端,Android 基础引擎约 9M ,App 还提供了扩展模块,比如地图、蓝牙等,打包时如不需要这些模块,可以裁剪掉,以缩小发行包;体积在 manifest.json-App 模块权限里可以选择

  4. App端支持如果选择纯nvue项目 (manifest里设置app-plus下的renderer:"native"),包体积可以进一步减少2M左右

  5. App端在 HBuilderX 2.7 后,App 端下掉了 非v3 的编译模式,包体积下降了3M

15.禁止滥用外部js插件

描述

有官方API的就不要额外引用js插件增加项目体积

例如

url传参加密直接用 encodeURIComponent() 和 decodeURIComponent()

作者:Panda_HYC
来源:juejin.cn/post/6997224351346982942

收起阅读 »

回村三天,二舅治好了我的精神内耗

这是我的二舅,村子里曾经的天才少年。这是我的姥姥,一个每天都在跳 poping的老太太。他们在这个老屋生活。建它的时候还没美国。老师们三次登门相劝,二舅闭着眼睛横躺在床上,一言不发,像一位断了腿的卧龙先生。第一年,二舅拒绝下床,他不知道从哪找到了一本赤脚医生手...
继续阅读 »

这是我的二舅,村子里曾经的天才少年。这是我的姥姥,一个每天都在跳 poping的老太太。他们在这个老屋生活。建它的时候还没美国。

二舅上小学是全校第一,上了初中还是全校第一,全市统考。从农村一共收上去三份试卷,其中一份就是二舅的。有一天,二舅发高烧请假回家,隔壁村的医生一天在他屁股上打了四针,二舅就成了残疾。十几岁的二舅躺在床上,再也不想回到学校。

老师们三次登门相劝,二舅闭着眼睛横躺在床上,一言不发,像一位断了腿的卧龙先生。第一年,二舅拒绝下床,他不知道从哪找到了一本赤脚医生手册,疯狂地看了一年。但二舅的腿不是伤了,而是废了,所以久病并不能成医。于是第二年,二舅扔掉了手册,从床上爬了下来,呆坐在天井里望天,像一只大号的青蛙。第三年,二舅不看天了,看家里来的一个木匠干活。木匠干了三天走了,二舅跟姥爷说他看会了,求姥爷去铁匠铺给自己打做木工的工具。三年来,二舅第一次走出了院门,去生产队给人做板凳,一天做两个,一个一毛钱,可以养活自己了。

如此几年,有一天,二舅照常拄着拐来到生产队,队长告诉二舅以后不用来了,生产队没了。二舅问为什么?队长说改革开放了,于是二舅就开始改革开放,游走在镇上的各个村子给人做木工。

有天在路上遇到了当年的那个医生,他跟二舅说要是在今天我早被告倒了,得承包你一辈子。二舅笑着骂他一句,一瘸一拐的又给人干活去了。

后来不知道什么手续上的原因,二舅的残疾证怎么都办不下来,他很失望,居然拄着拐辗转去了北京。他想去天安门广场的纪念堂说要去看看他,他就说改革开放很好,他也好。为什么呢?二舅说他公平。

很快,二舅的兜里就没剩几个钱了。他的一个堂弟在北京当兵,二舅作为军人家属住进了部队,没想到居然混得风生水起。因为二舅不爱搭讪交际,只爱干活,他不知道从哪借到了木工工具。在那个部队条件还很艰苦的年代,给士兵们默默地做了很多的柜子和桌子。

哪个士兵会不喜欢这样的homie呢?

有一天,二舅的堂弟去澡堂,看见一个老头和二舅正坐在一块泡澡,二舅的堂弟吓得一句话都说不出来,因为那个老头是他只见过几次的一位首长,此刻正蹲在池子里给二舅搓背。

后来二舅回到村里,大家都问北京怎么样?二舅说北京人搓背搓得很好。

到了两个妹妹出嫁的年纪,二舅心里很不舍。二舅有自己的表达,大姨和我妈结婚时的所有家具,每一张图纸、每一块木板、每一块玻璃、每一根装饰条、每一个螺丝、每一遍漆,都是二舅一个人完成的。

你能想象在 80 年代在一个山村的女孩子结婚的时候,能拥有这样的一套家具,是多么梦幻的事情吗?

姥姥家这么穷,妹妹出嫁有这么一套家具,婆家也会高看一眼,也许就会更好地对待自己的妹妹。你可能说我在吹牛,因为这是上海牌的家具,但你忘了这是我的二舅。二舅总有办法。什么牌子他都能给你贴上,你还要什么牌子,他还有天津牌、北京牌、香港牌,超豪华OK。

再后来,年轻的二舅领养了刚出生的宁宁,二舅拼命地在周边做工赚钱,大部分时间都把宁宁寄养在了大姨家里,很少陪伴他。宁宁小时候经常被人在背后议论,不懂礼貌。

一个被抛弃了两次的小孩,对这个世界还能有什么礼貌呢?十年前,宁宁和男朋友结婚了,20万出头的县城房子啊就出了十几万,真不敢想象他是怎么攒下来的,他就掏光了半辈子积蓄给宁宁买了房子,却开心得要死。这就是中国式的家长,中国式的可敬又可怜的家长卑微地伟大着。

二舅在30岁出头的时候迎来了说媒的高峰期。但二舅跟我说,他一时觉得他这辈子只能顾得住自己,顾不住别人了,所以从来没有动过这方面的心思。

二舅说谎了,当时有一个隔壁村的女人,有老公还有两个孩子,不知道是什么样的契机,二人的关系突然变得非常的熟络,并很快变得过于熟络。她经常来二舅家串门,二舅也经常去找他。即便是她老公在的时候,两个孩子也很喜欢二舅。

再后来他开始作为二舅家的正式一员,出席家族的一切红白喜事,并对二舅体贴入微,把他乱糟糟的小屋收拾得井井有条。二舅做工回来能吃上一碗热饭,顺手把今天结的钱递给他。就这样好多年过去了,她却并没有离婚。

二舅的四个兄妹从一开始的全力支持,转而怀疑这个女人只是图二舅的那一点钱而强烈反对。而还在上小学的宁宁则喊那个女人老狐狸,喊自己班里的她的女儿小狐狸。老实的二舅进退失据,不知所措。再后来这个女人和她的丈夫死在了外地的一个工棚,煤气中毒,二舅也终生未婚。

这段感情的细节我理解不了,大姨也都记不清了,二舅则是不愿意讲,这到底算怎么一回事呢?

既不是今日实行的仙人跳,也不是那个年月的拉帮套。那时候爱情来过没有呢?

几十年过去了,故人故事无疾而终,到现在什么也没剩下,只剩了一笔烂账,烂在了二舅一个人的心里。流了血,又长了痂,不能撕,一撕就会带下皮肉。

就这样又过去了三十年,乏善可陈。是的,普通人的生活就是这样,普通到不快进 1 万倍都没法看。

转眼姥姥已经88岁了,现在农村的人工成本也越来越高。二舅正是挣钱的好时候,他很想为自己多挣一点养老钱,将来就不用拖累宁宁。但是姥姥现在的生活已经不能自理,也不是很想活了,有一次甚至已经把绳子挂到了门框上。

中国人老说生老病死,生死之间何苦还要再隔上个老病呢,这可不是上天的不仁,而是怜悯。不然我们每个人都在七八十岁却还康健力壮之年去世,对这个世界该有多么的留恋呢?那不是更加的痛苦吗?从这个意义上来讲,老病是生死之间的必要演习。所以在几年前二舅出门的时候就开始把姥姥放到车上。去别人家做木工活的时候,就把姥姥放到身边的小板凳上。

66岁老汉随身携带88岁老母,这个6688组合简直是酷得要死。这几年二舅木工活也不做了,全职照顾姥姥,早上给姥姥洗脸,晚上给姥姥洗脚,下午给姥姥锻炼。

每走二十步就是坐下歇10秒,二舅每走20步就会落后姥姥3米,赶上这3米正好需要10秒。接着走。

这么默契的走位配合,我上一次见到还是在乔丹和皮蓬身上。乔丹喜欢给皮蓬送超跑,二舅喜欢给姥姥蒸面条,再浇上点西红柿炒鸡蛋。嗯好吃的。

二舅从小对宁宁没有什么教育可言,今天的宁宁却成为了村里最孝顺的孩子。可见让小孩将来孝顺自己的最好方法就是默默地孝顺自己的父母,小孩是小不是瞎。

其实很难把二舅定义为一个木匠。我在家这三天的时间里,他给村里人修好了一个插线板、一个燃气灶、一盏床头灯、一辆玩具车、一个掘头、一个洗衣机、一个水龙头,回来的路上被另一个婶子拦住,修好了他家的门锁。还没进家门,又被另一个老头叫到家里,说电磁炉坏了。

二舅到他家发现是他插线板的电源忘了打开。

可怜的老头。

回到家,又修好了一个买来的老人机和收音机。

姥姥有胃病,他就给姥姥针灸,人家嫌门楼上光秃秃的,木头不好看,二舅自己设计好了给人画上去,山顶修了座庙,所有的龙都是二舅雕的。村里没有神婆,二舅就成了算命师。

当然了,签子是自己做的,竹筒是自己做的,本子是自己做的,挂是自己抄来的。

他甚至有一天突发奇想,要做一把二胡。木头做弧身,电话线铜芯做弦,竹子做弓杆、钓鱼线做弓卯。我们这没有蟒蛇,他就上山抓了几条双斑锦拼成一张琴皮。

你看二舅总有办法。

很想给你们看看那把有模有样的二胡。可惜十几年前,姥姥让我的傻子弟弟拿二胡当锄头娃给玩坏了。

这个村子里有的一切农具、家具、电器、车辆。二舅不会修的,只有三样,智能手机、汽车和电脑。因为这些东西二舅也没有。不过现在智能手机也有了,宁宁买的,等他拆上几次也就会修了。

夜深了,二舅家的灯还亮着,又给谁家修东西呢?听见锣声和鞭炮声了吗?不是村里有人结婚,而是年轻人都走了之后,野猪回来了。吓唬野猪呢。

村里就剩下几百个老头老太太了,如果有什么东西坏了,送维修店去修,先别说得花钱,如果到镇上是三十里山路,如果坐客车去县城下了车,他们是连北都招不到的。

二舅就总说他能顾得住自己就不错了。他其实顾住了整个村子。村里人开玩笑叫他歪子。但我们每个人都很清楚,我们爱这个歪子,我们离不开这个歪子。

一九七七年恢复高考的时候,二舅正是十八九岁。如果不是当年发烧后轮的 4 针,二舅可能已经考上了大学,成为了一名工程师。单位分的房子,国家发的退休金,悠游自适,颐养天年。隔壁村一个老头就是这样,当年学习还没二舅学习好呢。

如果是这样,那该有多好。二舅一定会成为汪曾祺笔下父亲汪居生那样充满闲情野趣的老顽童。

看着眼前的二舅,总让我想起电影棋王里的台词:他这种奇才啊只不过是生不逢时,他应该受国家的栽培,名扬天下才对,不应该弄得这么落魄。太遗憾了,真的是太遗憾了。

我问二舅有没有这么想过?

他说从来没有。

这样的心态让二舅成为了村里第二快乐的人。第一快乐的人是刚刚——我们村的树先生。

所以你看,这个世界上第一快乐的人是不需要对别人负责的人,第二快乐的人就是从不回头看的人。

遗憾谁没有呢?人往往都是快死的时候才发现,人生最大的遗憾就是一直在遗憾过去的遗憾。遗憾在电影里是主角崛起的前戏,在生活里是让人沉沦的毒药。

我北漂九年,也曾有幸相识过几位人中龙凤,反倒是从二舅这里让我看到了我们这个民族身上所有的平凡美好与强悍。

都说人生最重要的不是胡一把好牌,而是打好一把烂牌。二舅这把烂牌,打的是真好。

他在挣扎与困顿中表现出来的庄敬自强,令我心生敬意。

我四肢健全,上过大学,又生在一个充满机遇的时代,我理应度过一个比二舅更为饱满的人生。今天二舅还在走在自己的人生路,这条长长的路最终会通往何处呢?

二舅的床下有一个几十年前的笔记本。笔记本的第一页是他摘抄的一句话:

下定决心,不怕牺牲,排除万难,去争取胜利。

是的,这条人生路最后通向的一定是胜利。

作者:衣戈猜想 https://www.bilibili.com/video/BV1MN4y177PB

收起阅读 »

作为一名前端工程师,我浪费了时间学习了这些技术

作为一名前端工程师我浪费时间学习了这些技术 不要犯我曾经犯过的错误! 我2015年刚刚开始学习前端开发的时候,我在文档和在线教程上了解到了许多技术,我浪费大量时间去学习这些技术。 在一个技术、库和框架数量不断增长的行业中,高效地学习才是关键。不管你是新的Web...
继续阅读 »

作为一名前端工程师我浪费时间学习了这些技术


不要犯我曾经犯过的错误!


我2015年刚刚开始学习前端开发的时候,我在文档和在线教程上了解到了许多技术,我浪费大量时间去学习这些技术。


在一个技术、库和框架数量不断增长的行业中,高效地学习才是关键。不管你是新的Web开发人员,还是你已经入门前端并有了一些开发经验,都可以了解一下,以下列出的技术,要么是我花费时间学习但从未在我的职业生涯中实际使用过的,要么是2021年不再重要的事情(也就是说,你可以不知道)。



Ruby / Ruby-on-rails


Ruby-on-Rails在本世纪早期非常流行。我花了几个月的时间尝试用Ruby-on-Rails构建应用程序。虽然一些大型科技公司的代码库中仍然会有一些Rails代码,但近年来我很少遇到使用Rails代码的公司。事实上,在我六年的职业生涯中,我一次也没有使用过Rails。更重要的是,我不想这么做。


AngularJS


不要把AngularJS和Angular混淆。AngularJS从版本2开始就被Angular取代了。不要因为这个原因而浪费时间学习AngularJS,你会发现现在很少有公司在使用它。


jQuery


jQuery仍然是最流行的JavaScript库,但这是一个技术上的历史遗留问题,而非真的很流行(只是很多10-15年前的老网站仍然使用它)。近年来,许多大型科技公司的代码都不再使用jQuery,而是使用常规的JavaScript。jQuery过去提供的许多好处已经不像以前那么关键了(比如能编写在所有类型的浏览器上都能工作的代码,在浏览器有非常不同的规范的年代,这是一个大的问题)。


Ember


学习Ember的热火很久以前就熄灭了。如果你需要一个JavaScript库,那就去学习React(或者Vue.js)。


React class components


如果你在工作中使用React,你可能仍然会发现一些React类组件。因此,理解它们是如何工作的以及它们的生命周期方法可能仍然是很好的。但如果你正在编写新的React组件,你应该使用带有React hook的功能性组件。


PHP


坦诚的说,PHP并没有那么糟糕。在我的第一份网页开发工作中(和Laravel一起),我确实需要经常使用它。但是现在,web开发者应该着眼于更有效地学习 Node.js。如果你已经在学习JavaScript,为什么还要在服务器端添加PHP之类的服务器端语言呢?现在你可以在服务器端使用JavaScript了。


Deno


Deno是一家新公司,在未来几年可能会成为一家大公司。然而,不要轻信炒作。现在很少有公司在使用Deno。因此,如果你是Web开发新手,那就继续学习Node.js(又名服务器端JavaScript)。不过,Deno可能是你在未来几年选择学习的东西。


Conclusion


这就是我今天想说的技术。我相信还有很多东西可以添加到技术列表中——请在评论中留下你的想法。我相信对于这里列出的技术也会有一些争论——Ruby开发者更容易破防。你也可以在评论中进行讨论,这些都是宝贵的意见。


链接:https://juejin.cn/post/7086019601372282888

收起阅读 »

API 请求慢?这次锅真不在后端

问题 我们在开发过程中,发现后端 API 请求特别慢,于是跟后端抱怨。 “怎么 API 这么慢啊,请求一个接口要十几秒”。 而且这种情况是偶现的,前端开发同学表示有时候会出现,非必现。 但是后端同学通过一顿操作后发现,接口没有问题,他们是通过 postman ...
继续阅读 »

问题


我们在开发过程中,发现后端 API 请求特别慢,于是跟后端抱怨。


“怎么 API 这么慢啊,请求一个接口要十几秒”。


而且这种情况是偶现的,前端开发同学表示有时候会出现,非必现。


但是后端同学通过一顿操作后发现,接口没有问题,他们是通过 postman 工具以及 test 环境尝试,都发现接口请求速度是没有问题的。


“那感觉是前端问题”?


我们来梳理一下问题,如下:



  • 后端 API 请求特别慢,而且是偶现的。

  • 在 test 环境没有复现。

  • postman 工具请求没有复现。


问题解决过程


时间都去哪了?


第一个问题,API 耗费的时间都用来做什么了?


我们打开 Chrome 调试工具。在 network 中可以看到每个接口的耗时。



hover 到你的耗时接口的 Waterful,就可以看到该接口的具体耗时。



可以看到,其耗时主要是在 Stalled,代表浏览器得到要发出这个请求的指令到请求可以发出的等待时间,一般是代理协商、以及等待可复用的 TCP 连接释放的时间,不包括 DNS 查询、建立 TCP 连接等时间等。


所以 API 一直在等待浏览器给它发出去的指令,以上面截图的为例,整整等待了 23.84S,它请求和响应的时间很快(最多也就几百毫秒,也就是后端所说的接口并不慢)。


所以 API 到底在等待浏览器的什么处理?


什么阻塞了请求?


经过定位,我们发现,我们项目中使用 Server-Sent Events(以下简称 SSE)。它跟 WebSocket 一样,都是服务器向浏览器推送信息。但不同的是,它使用的是 HTTP 协议。


当不通过 HTTP / 2 使用时,SSE 会受到最大连接数的限制,限制为 6 次。此限制是针对每个浏览器 + 域的,因此这意味着您可以跨所有选项卡打开 6 个 SSE 连接到 http://www.example1.com,并打开 6 个 SSE 连接到 http://www.example2.com。这一点可以通过以下这个 demo 复现。


复制问题的步骤:



结果是,第 6 次之后,SSE 请求一直无法响应,打开新的标签到同一个地址的时候,浏览器也无法访问。


效果图如下:



该问题在 ChromeFirefox 中被标记为“无法解决”。


至于偶现,是因为前端开发者有时候用 Chrome 会打开了多个选项卡,每个选项卡都是同一个本地开发地址,就会导致达到 SSE 的最大连接数的限制,而它的执行时间会很长,也就会阻塞其他的请求,一致在等待 SSE 执行完。


所以解决的方法是什么?


解决方案


简单粗暴的两个方法



  • 不要打开太多个选项卡。这样就不会达到它的限制数。(因为我们一个选项卡只请求一个 SSE)。

  • 开发环境下,关闭该功能。


使用 HTTP / 2


使用 HTTP / 2 时,HTTP 同一时间内的最大连接数由服务器和客户端之间协商(默认为 100)


这解释了为什么我们 test 环境没有问题,因为 test 环境用的是 HTTP / 2。而在开发环境中,我们使用的是 HTTP 1.1 就会出现这个问题。


那如何在开发环境中使用 HTTP / 2 呢?


我们现在在开发环境,大部分还是使用 webpack-dev-server 起一个本地服务,快速开发应用程序。在文档中,我们找到 server 选项,允许设置服务器和配置项(默认为 'http')。


只需要加上这一行代码即可。


devServer: {
+ server: 'spdy',
port: PORT,
}

看看效果,是成功了的。



原理使用 spdy 使用自签名证书通过 HTTP/2 提供服务。需要注意的一点是:



该配置项在 Node 15.0.0 及以上的版本会被忽略,因为 spdy 在这些版本中不会正常工作。一旦 Express 支持 Node 内建 HTTP/2,dev server 会进行迁移。



总结归纳


原本这个问题认为跟前端无关,没想到最后吃瓜吃到自己头上。提升相关技能的知识储备以及思考问题的方式,可能会方便我们定位到此类问题。


充分利用好浏览器的调试工具,对一个问题可以从多个角度出发进行思考。比如一开始,没想到本地也可以开启 HTTP / 2。后来偶然间想搜下是否有此类方案,结果还真有!




链接:https://juejin.cn/post/7119074496610304031

收起阅读 »

搞不定移动端性能,全球爆火的 Notion 从 Hybrid 转向了 Native

7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了...
继续阅读 »

7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。

该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了 3 倍。


Notion 发布的这条 Twitter 也得到了广泛的关注,几天之内就有了上千条转发。由于前几年 Notion 的技术栈一直没有公开,开发者对此充满了各种猜测,很多人认为 Notion 使用的是 React Native 或 Electron,因此这次 Notion 宣称切换为原生 iOS 和原生 Android,再一次引发了“框架之争”。

其中有不少人发表了“贬低”跨平台开发的看法,对 React Native 等框架产生了质疑,毕竟现在向跨平台过渡是不可避免的,这些框架是对原生工具包的一个“威胁”,而 Notion 恰恰又切换到了“原生”开发模式。

实际上,在 2020 年之前 Notion 使用的是 React Native,随后切换到了 Hybrid 混合开发模式:使用 Kotlin/Swift + 运行网络应用程序的 Web 视图。但移动端的性能一直是一个问题,2 年之后,Notion 再次切换到了原生开发模式。

有网友认为,像 Notion 这样重 UI 和交互的产品,如果不知道如何掌握 Web 技术,那么对他们的产出速度表示担忧。面对这种吵翻天的状况,Notion 的前端工程师也因此再度出面回应这次切换的原因和一些思考。

Notion 的发展和理念

Notion 是一款将笔记、知识库和任务管理无缝衔接整合的多人协作平台。Notion 打破了传统的笔记软件对于内容的组合方式,将文档的内容分成一个个 Block,并且能够以乐高式的方式拖动组合这些 Block,让它使用起来十分灵活。

Notion 由 Ivan Zhao、Simon Last 于 2013 年在旧金山创立。去年底,Notion 获得了 2.75 亿美元的 C 轮融资。截至 2021 年 10 月,Notion 估值 103 亿美元,在全球拥有超 2000 万用户。Notion 的创始人和 CEO Ivan Zhao 是一位 80 后华人。他出生于中国新疆,曾就读于清华附中,中学时随家人移居加拿大,现在被很多人认为将成为硅谷的下一个袁征(Zoom 的创始人)。Ivan 在大学时期主修认知科学,学习的是人的大脑怎么运作,外加对计算机也很感兴趣。



Ivan 也曾表示“我的很多朋友都是艺术家。我是他们中唯一会编码的人。我想开发一款软件,它不仅可以为人们提供文档或网页。” 因此,在 2012 年大学毕业后,在文档共享初创公司 Inkling 工作期间,他创办了 Notion。原本的目标是构建一个无代码应用构建工具,不过项目很快失败了。随后 Ivan 与 Simon 迁往了日本京都,待了一年左右,小而安静的地方能“让我们专注在写代码”,在相对无压力和与世隔绝的环境下,构思并设计出了现在的 Notion 原型。用 Reddit 论坛上的一条获得高赞的网友总结就是:一个 Notion = Google docs + Evernote + Trello + Confluence + Github + Wiki +……

“工具应该模仿人脑的工作方式。但由于每个人的思维和工作方式都不同,这意味着工具需要非常灵活。”Ivan 解释道。而 Notion 创建的目的,就是将用户从一堆各式各样的生产力工具之中解放出来,给予一个干净清爽、简便易行的 All in One 工作平台。企业用户也可以在 Notion 上基本实现公司的内部管理所需要涉及到的所有功能。包括公司知识库和资料库的创建与管理、项目进度管理、信息共享、工作日志、内部社交、协作办公等等。


有人甚至说,Notion 堪比办公软件届的苹果。在 2016 年发布 1.0 版本后,因其独特的设计、专注于将事情做得更好、对投资人的冷淡态度,外加疫情远程办公潮,多方面因素让 Notion 迅速火遍全球。作为一款 All in one 的概念型工具,Notion 一直被众多企业抄作业,但它目前几乎未逢敌手。

Notion 为什么要两次更换技术栈?

Notion 在 2017 年、2018 年分别发布了 iOS 客户端和 Android 客户端。在发布 2.0 版本之后,该公司于 2019 年以 8 亿美元的估值筹集了 1000 万美元的资金。但也许和创始人的发展理念相关,Notion 的员工数量一直不多。

2019 年 3 月的时候,工程团队总共才 4 个人,当时 Notion 用 React Native 来渲染 web 视图。Notion 在 Twitter 上解释说,这是为了更快地部署新功能和进行一些其他修复。

但如果这个系统适合开发者,那么它对用户来说远非最佳:许多人抱怨移动版本非常缓慢。“即使是新 iPhone 也非常慢 - 大约 6-7 秒后我才能开始输入笔记。到那时我都快忘记了我之前想写什么。它基本上是一个非常重的 web 应用程序视图。”“如果 Notion 不选择改变,那么它将迅速被其它同类产品取代。”......



2020 年,Notion 第一次因这个问题,更改了技术栈,放弃 React Native,切换到了 Hybrid 开发环境。

Notion 前端负责人 Jake Teton‑Landis 表示,“React Native 的优势在于允许 Web 开发人员构建手机应用程序。如果我们已经有了 webview,那么 React Native 不会增加价值。对我们来说,它让一切变得更加困难:性能、代码复杂性、招聘等等。用 React Native 快速完成任务的同时,也在跟复杂性战斗,这让我们感觉束手束脚。”

虽然这次移动端的性能有了一些提升,但也没有根本解决问题,更新之后,Android 端依然是一个相当大的痛点。


Notion 也曾在 2019 年的时候表示不会很快发布本机应用程序,但他们同时强调“原生开发也是一个选择”。

7 月 20 日,Notion 发布了版本更新,并表示将从主页选项卡开始,从 webview 逐步一个个地切换到本机应用程序。

此时 Notion 工程团队也大约只有 100 人, 总共包含 3 位 iOS 工程师、4 位 android 工程师,除主页使用 SwiftUI/Jetpack Compose 进行渲染,其他部分仍然是 webview 进行绘制。

“似乎这还是招聘不足产生的人员问题。”Jake 解释说,“我们的策略是随着团队的壮大逐步本地化我们应用程序的更多部分。我们这个程序必须使用本机性能,如果它是原生的,则更容易达到这个性能要求。

凭借我们拥有的经验,以及对问题的了解,我们因此选择了原生 iOS 和原生 Android 开发。虽然出于复杂性的权衡,在可预见的未来,编辑器可能仍然是一个 webview,毕竟 Google Docs、Quip、Dropbox Paper、Coda 都使用原生 shell、webview 编辑器。”

原生开发才是王道?!

虽然无论是原生开发还是 Hybrid 都可以完成工作,但原生应用程序是按照操作系统技术和用户体验准则开发的,因此具有更快的性能优势,并能轻松访问和利用用户设备的内置功能(例如,GPS、地址簿、相机等)。

Hybrid 开发方式,通常是在面对市场竞争需要尽快构建并发布应用程序时候的一种选择。如果期望的发布时间少于六个月,那么混合可能是一个更好的选择,因为可以构建一套源代码,跨平台发布,与原生开发相比,其开发时间和工作量要少得多,但这也意味着需要做出许多性能和功能上的妥协。

如果有足够时间,那么原生方法最有意义,可以让应用程序具有最佳性能、最高安全性和最佳用户体验。毕竟,用户体验是应用程序成功的关键。互联网正在放缓,人们使用手机的时间越来越长,缓慢的应用程序意味着糟糕的业务。在这种情况下,对 Notion 来说,拥有一个快速应用程序比以往任何时候都更加重要。

参考链接:

https://www.notion.so/releases/2022-07-20

https://twitter.com/jitl/status/1530326516013342723?s=20&t=xT0gfWhFvs0yNvc1GQ3sTQ

收起阅读 »