kross

kross 查看完整档案

成都编辑重庆邮电大学  |  软件工程 编辑腾讯科技有限公司  |  Android工程师 编辑 example.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

kross 发布了文章 · 4月28日

有趣的问题:给你一个生成0到1之间随机数的函数,请求出圆周率π

来源

这是一道我在看 Youtube 时无意间发现的问题,原视频链接:https://www.youtube.com/watch...

问题是:给你一个生成0到1之间随机数的函数,请求出圆周率 π

第一眼看上去仿佛是一个完全没头脑的问题,但是仔细一想,就可以使用一点编程能力和初中的数学知识解决。

思路

思路应该是去寻找两个东西的交汇点,一个随机数,是一个数字,也可以是数轴上的一个点。两个随机数,就是两个数轴上的点,或者是一个坐标轴上的点(r1, r2),如果调用无数次的随机数,就可以获得无数个坐标轴上的点。

圆周率,我们必须要找到一个使用了圆周率的东西,很容易想到圆的面积和周长。

现在我们有了无数个在坐标轴上的点圆的面积圆的周长。emmmm……有想到什么吗?

也许我们应该画一个图。

image.png

也许我们应该把圆画上去,应该会帮助我们思考。

image.png

OK,知道了,所有的点会落在正方形(0,0,1,1) 的范围内,其中有一部分会落在圆的范围内,如果我们统计落在圆内的点的个数,记为 N,那么 N 与总点数 M 的比值,应该是等于圆的面积比上正方形的面积。

列下公式应该是:

$$ r = 1/2 $$
$$\frac{N}{M} = \frac{\pi r^2}{(2r)^2}$$

把公式化简一下,变成:
$$\pi = 4 * N / M$$

为了编程中方便计算任意点距离圆心的距离,可以这样理解这个概率:
image.png

公式中的关系依然成立。

编程实现

fun main() {
    var times = 100L
    for (i in 1..9) {
        println("loop: $times pi: ${getPi(times)}")
        times *= 10
    }
}

fun getPi(count: Long): Double {
    var c = 0
    for (i in 0 until count) {
        val x = Math.random()
        val y = Math.random()

        if (x * x + y * y < 1) {
            c++
        }
    }
    return 4.0 * c / count
}

运行结果:

loop: 100 pi: 3.24
loop: 1000 pi: 3.168
loop: 10000 pi: 3.1528
loop: 100000 pi: 3.14348
loop: 1000000 pi: 3.141964
loop: 10000000 pi: 3.1414304
loop: 100000000 pi: 3.14131604
loop: 1000000000 pi: 3.141585632
loop: 10000000000 pi: 3.1415885784
查看原文

赞 0 收藏 0 评论 0

kross 发布了文章 · 4月15日

electron 如何将任意资源打包

如何打包资源

只想写个图形小工具,本质上还是调用写好的 java 程序,因为我觉得在命令行里面来回切目录,复制路径等操作实在是太麻烦了。

那么我现在已经搞定了如何从 electron 的 js 事件里获得文件路径,我也搞定了如何在 electron 的 main.js 里面创建新的进程执行指令,那么如何使用到打包好的 jar 包或者其他资源呢?

直接看下 packages.json 里面吧。

{
  "name": "....",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "scripts": {
    "start": "export FAVOR=debug && electron .",
    "pack": "electron-packager ./ yourAppName --platform=darwin --arch=x64 --app-version=0.0.1 --app-bundle-id=com.xxxx.yourAppName --out=build --overwrite --extra-resource='./extraResources'"
  },
  "build": {
    "extraResources": [
      "./extraResources/**"
    ]
  },
  
}

需要注意的是 build 里面加了一个 extraResources,另外,通过 electron-packager 打包的参数里面也加上一个 --extra-resource='./extraResources'

然后打包的时候就可以把你想要的任何文件打包进去了,jar 也好,python 脚本也好。

如何引用资源

需要注意的是,debug 运行和 release 运行是不一样的,这里,我们就需要一个东西来在运行时区分,我现在是 debug 还是 release。

注意上面的 json 脚本中,有一个 export FAVOR=debug,这个相当于在 debug 运行的时候加入了一个环境变量。

怎么读取环境变量呢?在 main.js 里面这样读取:

console.log("favor: " + process.env.FAVOR)

接下来就是区分运行时来获取资源路径了。

function getResPath() {
  if (isDebug) {
    return "./extraResources"
  } else {
    return process.resourcesPath + "/extraResources"
  }
}

仅此记录一下,给可能需要的人。

查看原文

赞 1 收藏 0 评论 2

kross 发布了文章 · 4月1日

Android - keep the device awake

备选方案

首先,官方提供了一些功能来满足我们开发一些特定功能的需求。

  • 如果要执行一个长时间的下载任务,使用 DownloadManager
  • 如果要和服务器 sync 数据,使用 Sync adapter
  • 依赖一些 service 做一些后台工作,可以使用 JobScheduler

如果上面的功能满足不了你的需求,才考虑使用 wake lock。

保持屏幕常亮

保持屏幕常亮的方法很简单,在 Activity 中增加 flag 即可。

public class MainActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
  }
}

另一种方式是在 xml 中申明。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true">
    ...
</RelativeLayout>

保持 CPU 工作

首先要加一个权限:

<uses-permission android:name="android.permission.WAKE_LOCK" />

接下来有两种使用方法。

第一种是手动获取 wake lock

PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
        "MyApp::MyWakelockTag");
wakeLock.acquire();

手动获取 wake lock,后执行自己的操作。再通过 wakeLock.release() 释放。

第二种方式是使用WakefulBroadcastReceiver,算了看了下文档,说是已经弃用了,就不介绍了。

查看原文

赞 0 收藏 0 评论 0

kross 发布了文章 · 3月23日

关于 Java 中类的初始化顺序的问题

突然思考到这个问题,就想做点实验理清楚一下。

1. 单个类的初始化

class Foo {
    static {
        // [1]
    }
    
    static Foo2 obj = new Foo2(); // [3]
    
    Foo2 obj2 = new Foo2(); // [4]
    
    public Foo() {
        // [5]
    }
}

class Foo2 {
    static {
        // [2]
    }
}

如上面代码所示,加载顺序如备注标记的一样。

  1. 先加载这个类的 static 代码块
  2. 加载 Foo2 的 static 代码块(因为要初始化 Foo 的静态成员 obj)
  3. 再加载 static 成员变量 obj
  4. 再加载普通成员变量 obj2
  5. 最后执行构造方法

这里需要注意的是,如果对象 obj 初始化为 null,则不会执行 [2] 的位置。

class Foo {
    static {
        // [1]
    }
    
    static Foo2 obj = null;
    
    Foo2 obj2 = null; 
    
    public Foo() {
        // [2]
    }
}

class Foo2 {
    
}

如上面代码所示,如果成员变量都被赋值为 null,则不会调用类的初始化逻辑。

2. 继承关系中类的初始化

在继承关系中,不过是先执行父类的初始化,再执行子类的初始化。

但这也就会发生一个怪事(面试点),在执行父类初始化的时候,读取子类的成员变量,自然就会出现 null 了。

比如下面这个例子:

class Animal {
    private String name = "Animal";
    
    public Animal() {
        print();
    }
    
    public void print() {
        System.out.println(name);
    }
}

class Dog extends Animal {
    private String name = "Dog";
    
    public void print() {
        System.out.println(name);
    }
}

public void static main(String[] args) {
    Animal a = new Dog();
}

请问输出是什么?

答案是:null

为什么呢?

首先记住大原则就好了,先执行父类的初始化,再执行子类的初始化。

我在代码上加上执行标记。

class Animal {
    // [2] 执行 Animal 的 static block,可惜没有

    private String name = "Animal"; // [3] 执行父类的成员变量初始化
    
    public Animal() { 
        // [4] 执行构造函数
        print();
    }
    
    public void print() {
        System.out.println(name);
    }
}

class Dog extends Animal {
    // [3] 执行 Animal 的 static block,可惜没有

    private String name = "Dog";
    
    public void print() {
        // [5] 这个方法 override 了父类的,所以执行这个 print
        // 由于 Dog.name 没有初始化,所以是null
        System.out.println(name);
    }
}

public void static main(String[] args) {
    Animal a = new Dog(); // [1] 先执行这里
}

需要额外注意的地方是:

  • 父类和子类都定义了 name ,它的修饰符是 private,因此这两个是独立的变量,没有任何关联。
  • print 方法是 public 的,因此在集成关系上有 override 的关系(有些情况下不加 @Override 注解被认为不是 override,因此规范起见,最好加上 @Override)
查看原文

赞 0 收藏 0 评论 0

kross 发布了文章 · 3月9日

使用 IDEA 将 Java/Kotliin 工程到处 Jar 包的正确姿势

导出的 Jar 包无法运行?
导出的 Jar 包找不到 Main class?

大概是我对导出 Jar 包的理解不深吧,反正一直不太懂 IDEA 导出 Jar 包的界面和功能到底怎么用。但总算是自己摸索出了正确的方法。

第一步:添加构建

首先要介绍的是 Artifact 这个概念,可以理解为一种构建,比如说 Android Studio 构建出来的 Artifact 就是 Apk 文件。Java 程序当然是可以构建出 Jar 包的。

在一个 Java 或 Kotlin 工程中,在 IDEA 的顶部的工具栏上,找到 Project Structure 图标。
image.png

或者在,File -> Project Structures... 也可以找到。

点击,打开 Project Structure 窗口。

如下图所示,Project Structure 窗口左侧可以选择 Artifacts ,右侧点击加号,选择 Jar -> Empty
image.png

在窗口的右侧,就会出现一个可以编辑的界面。如下图所示:上面的红框是填写 Jar 包的名称,左下的红框表示这个 Jar 包里面包含什么内容,右下的红框表示有什么东西是可以放进去的。
image.png

第二步:添加 Manifest

在 Jar 里面没有添加任何东西的时候,点击 xxx.jar ,底部会显示 添加 Manifest 的操作按钮。
image.png

添加完,并指定 Main class 就可以了。如下图所示:
image.png

第三步:添加 Jar 包内容

接下来就是最关键的操作了。

对于工程中的源码,一般都是显示为 'xxxx' compile output,对它们的操作为,右键,选择 Put into Output Root
image.png

对于工程中依赖的库什么的,就右键,选择 Extract Into Output Root
image.png

最终会得到下面这样的结果。
image.png

第四步:构建

经过上面的步骤,一个 Artifact 的配置就做好了。我们就可以执行它。

在菜单栏,Build -> Build Artifacts...

image.png

点击 Build 即可构建。

生成的东西应该会在 output 或 out 或 build 之类的目录里面。

赶快执行一下 java -jar yourJar.jar 试试看吧!

查看原文

赞 0 收藏 0 评论 1

kross 发布了文章 · 2019-12-03

git merge和rebase的区别2:对远端分支的影响

上一篇中已经说明了 merge 和 rebase 会如何改变 commit 链路的结构,但有一些场景依然很模糊。

比如:如果远端分支有一些提交了,客户端也有一些提交了,客户端 fetch 到数据后,再 merge ,产生了新的 commit 节点,这也是我们知道的,那么客户端将变动 push 到远端,远端的 commit 结构会变成什么样呢?

我做了实验确认了一下,远端的分支就出现了分叉,再合并的情况。
64152516-FD09-43FB-AA16-2B12AE671A5A.png

我想,这是非常不好的。造成上述情况,仅仅是因为两个程序员都基于某个 commit 节点开发,尽管没有冲突,也会形成这样一个分叉的结构。

假如我们一个小组有 10 个成员,那么岂不是这个远程 commit 结构到处都分叉,然后又合并回去,这样看起来是不太友好的。

于是我又实验了一下 rebase
git pull --rebase
或者
git fetch
git rebase

我在远程分支上线 push 了两次提交,然后另一个本地分支,commit 两次提交,后执行git pull --rebase
来看看命令行的输出:

git pull --rebase
First, rewinding head to replay your work on top of it...
Applying: a5
Applying: a6

它apply了a5和a6(这两个就是我最近两次commit提交的备注)

再来看看commit的结构:
6CAA4697-CA89-4114-93A5-C23EAC0EE3D1.png

如上图所示,就是一个线性的结构。

但它有一个小毛病,我们就按着这个图来说。假如 b6 的提交实际上是比 a5 晚的,但它却排在了 a5 前面。也就是 commit 节点的顺序和时间顺序是不一致的。

但至少这样,远端的 commit 历史是线性的了。

至于选择 merge 和 rebase 应该是和团队实际情况考虑并选择。

查看原文

赞 0 收藏 0 评论 0

kross 发布了文章 · 2019-11-28

git merge和rebase的区别

赞 4 收藏 3 评论 0

kross 发布了文章 · 2019-11-28

什么是 URI ?

什么是 URI ?

URI 的定义在 RFC 2396 中有详尽的描述。

URI 是 Uniform Resource Indentifier 的缩写。是用来描述物理的或者抽象的资源的唯一标识符。

这三个字单词也正描述了 URI 的特点:

  • 形式统一(Uniform)

形式统一带来的好处是,对于各种各样不同的资源,都能有相同的表现形式。各种资源不相同,但在形式上统一,因此可以使用相同的语义进行解释和理解。可以在不影响现存的资源的情况下,出现新的资源。

  • 资源(Resource)

任何事情都可以成为资源,可以被标识。

  • 标识符(Indentifier)

标识符即是一个对资源的引用。

URI 与 URL

URL 是 URI 的子集概念,它表示的是网络上的一个位置

URI 的结构与属性

这里将结合 Android 中对应的 Uri 类一起讲。

最基本的结构是:
<scheme>:<scheme-specific-part>
举例说明:
http://google.com
其中 scheme 为 http ,而 scheme-specific-part 就是 //google.com

但是 URI 并没有规定 scheme-specific-part 必须有什么固定的结构或规范,这完全是取决于 scheme 是什么。

但是一般的 URI 结构为:
<scheme>:<authority><path>?<query>#<fragment>

上面除了 scheme 是必须的之外,其他的部分都是可选的

scheme

资源有多种多样类型,也因此对应了多种多样的访问形式(方法),因此 scheme 就是一种描述以何种方式访问何种资源的概念。

常见的有: http, ftp, mailto, file 等等。

scheme 可以通过 Uri.getScheme() 获取。

val uri = Uri.parse("http://google.com")
uri.scheme // "http"

authority

authority是表示用于验证的用户的信息,它的构成形式为
<user>@<host>:<port>

举例说明:
kross@google.com:80

其中 kross 就是 <user> 部分

可以使用 Uri.getAuthority() 获取整个 authority,通过 Uri.getUserInfo() 获取 user 信息。

这里需要注意,有一些方法是带了 encoded 前缀,它的意思是返回 escaped 内容。看下面的例子就懂了。

val uri = Uri.parse("http://kr%20oss@google.com")
uri.authority // "kr oss@google.com"
uri.encodedAuthority // "kr%20oss@google.com"
uri.userInfo // "kr oss"

path

简单理解的话, path 是通过单个斜杠 / 连接的一个路径,形如: google.com/article/comment

query

query 是指在 URI 中,跟在 ? 后面的部分。换句话说,就是 GET 请求时的参数部分。

如:http://google.com?keyword=hello&lang=zh_cn

在上面的示例中,query 为 keyword=hello&lang=zh_cn ,这整个一长串。

android.net.Uri 中,可以使用 Uri.getQuery()Uri.getQueryParameter() 获取参数。

val uri = Uri.parse("http://google.com?keyword=hello&lang=zh_cn")
uri.getQuery() // "keyword=hello&lang=zh_cn"
uri.getQueryParameter("keyword") // "hello"

fragment

fragment 是由一个井号 # 开始,后面跟着的一串,都为 fragment。

如:http://google.com#title1

在上面的示例中 title1 为 fragment。

可以使用 Uri.getFragment() 获取。

val uri = Uri.parse("http://google.com#abcd#title")
uri.getFragment() // "abcd#title"

isHierarchical 与 isOpaque

什么叫 isHierarchical 的,当 scheme-specific-part 的部分是由斜杠 / 开始的,即这个 URI is Hierarchical。

isOpaque 是与 isHierarchical 相反的概念,即 isOpaque = !isHierarchical

举例说明:
HierarchicalURI: http://google.com or http:/google.com
OpaqueURI: http:google.com

isAbsolute 与 isRelative

什么叫 isAbsolute ?当 URI 有明确的 scheme 的时候,这个 URI 即为 absolute,反之为 relative,即 isAbsolute = !isRelative

举例说明:
AbsoluteURI: http://google.com
RelativeURI: google.com

其他

为了性能,Uri 类没有做任何验证逻辑。

为什么有的 URI 有三个斜杠?

在 Android 中用 file:///android_asset/abc.txt 来表明 Assets 目录中的文件。

所以为什么要用三个斜杠呢?

原因依然回归到本质,URI 的形式为 <scheme>://<host>/<path>

如果 <host> 是 localhost,则可以省略。所以,省略掉 <host> 后,就变成了 <scheme>:///<path>,这就是三个斜杠的由来。

参考资料:why-do-file-urls-start-with-3-slashes

URI 示例

ftp://ftp.is.co.za/rfc/rfc1808.txt
-- ftp scheme for File Transfer Protocol services

gopher://spinaltap.micro.umn.edu/00/Weather/California/Los%20Angeles
-- gopher scheme for Gopher and Gopher+ Protocol services

http://www.math.uio.no/faq/compression-faq/part1.html
-- http scheme for Hypertext Transfer Protocol services

mailto:mduerst@ifi.unizh.ch
-- mailto scheme for electronic mail addresses

news:comp.infosystems.www.servers.unix
-- news scheme for USENET news groups and articles

telnet://melvyl.ucop.edu/
-- telnet scheme for interactive services via the TELNET Protocol

更新日志

2019-12-03 11:32 更新了为什么 URI 有三个斜杠的内容
2019-11-28 首次完成

查看原文

赞 0 收藏 0 评论 0

kross 赞了文章 · 2019-09-23

浅析HTTP/2的多路复用

HTTP/2有三大特性:头部压缩、Server Push、多路复用。前两个特性意思比较明确,也好理解,唯有多路复用不太好理解,尤其是和HTTP1.1进行对比的时候,这个问题我想了很长时间,也对比了很长时间,现在把思考的结果分享出来,希望对大家有帮忙。

先来说说Keep-Alive

在没有Keep-Alive前,我们与服务器请求数据的流程是这样:

clipboard.png

  • 浏览器请求//static.mtime.cn/a.js-->解析域名-->HTTP连接-->服务器处理文件-->返回数据-->浏览器解析、渲染文件
  • 浏览器请求//static.mtime.cn/b.js-->解析域名-->HTTP连接-->服务器处理文件-->返回数据-->浏览器解析、渲染文件
  • ...
  • ...
  • ...
  • 这样循环下去,直至全部文件下载完成。

这个流程最大的问题就是:每次请求都会建立一次HTTP连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,而且逻辑上是非必需的,因为不间断的请求数据,第一次建立连接是正常的,以后就占用这个通道,下载其他文件,这样效率多高啊!你猜对了,这就是Keep-Alive

Keep-Alive解决的问题

Keep-Alive解决的核心问题:一定时间内,同一域名多次请求数据,只建立一次HTTP请求,其他请求可复用每一次建立的连接通道,以达到提高请求效率的问题。这里面所说的一定时间是可以配置的,不管你用的是Apache还是nginx

HTTP1.1还是存在效率问题

如上面所说,在HTTP1.1中是默认开启了Keep-Alive,他解决了多次连接的问题,但是依然有两个效率上的问题:

  • 第一个:串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒,那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)
  • 第二个:连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制,浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。

HTTP/2的多路复用

HTTP/2的多路复用就是为了解决上述的两个性能问题,我们来看一下,他是如何解决的。

  • 解决第一个:在HTTP1.1的协议中,我们传输的requestresponse都是基本于文本的,这样就会引发一个问题:所有的数据必须按顺序传输,比如需要传输:hello world,只能从hd一个一个的传输,不能并行传输,因为接收端并不知道这些字符的顺序,所以并行传输在HTTP1.1是不能实现的。

clipboard.png

HTTP/2引入二进制数据帧的概念,其中帧对数据进行顺序标识,如下图所示,这样浏览器收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。同样是因为有了序列,服务器就可以并行的传输数据,这就是所做的事情。

clipboard.png

  • 解决第二个问题:HTTP/2对同一域名下所有请求都是基于,也就是说同一域名不管访问多少文件,也只建立一路连接。同样Apache的最大连接数为300,因为有了这个新特性,最大的并发就可以提升到300,比原来提升了6倍!

以前我们做的性能优化不适用于HTTP/2

  • JS文件的合并。我们现在优化的一个主要方向就是尽量的减少HTTP的请求数, 对我们工程中的代码,研发时分模块开发,上线时我们会把所有的代码进行压缩合并,合并成一个文件,这样不管多少模块,都请求一个文件,减少了HTTP的请求数。但是这样做有一个非常严重的问题:文件的缓存。当我们有100个模块时,有一个模块改了东西,按照之前的方式,整个文件浏览器都需要重新下载,不能被缓存。现在我们有了HTTP/2了,模块就可以单独的压缩上线,而不影响其他没有修改的模块。
  • 多域名提高浏览器的下载速度。之前我们有一个优化就是把css文件和js文件放到2个域名下面,这样浏览器就可以对这两个类型的文件进行同时下载,避免了浏览器6个通道的限制,这样做的缺点也是明显的,1.DNS的解析时间会变长。2.增加了服务器的压力。有了HTTP/2之后,根据上面讲的原理,我们就不用这么搞了,成本会更低。
查看原文

赞 59 收藏 41 评论 8

kross 发布了文章 · 2019-08-29

Kotlin协程教程(3):操控协程

在之前的文章中,已经讲了如何启动协程、协程的作用域是如何组织和工作的以及各种协程构造器(builder)的特性。

本篇将讲解对协程的各种操作,包括挂起、取消、超时、切换上下文等。

挂起

fun main()  {
    runBlocking(Dispatchers.Default) {
        for (i in 0 .. 10) {
            println("aaaaa ${Thread.currentThread().name}")
            delay(1000) // 这是一个挂起函数
            println("bbbbb ${Thread.currentThread().name}")
        }
    }
}

delay就是一个挂起函数,挂起的意思是:非阻塞的暂停,与之对应的就是阻塞(的暂停)。比如线程的方法Thread.sleep就是一个阻塞的方法。关于阻塞还是非阻塞,可以简单的理解为:

  • 阻塞就是cpu不执行后面的代码,需要某种通知告诉线程继续执行。
  • 非阻塞就是cpu依然在执行线程的代码,非阻塞的暂停只是通过用户态的程序逻辑让代码块不执行而已。

用图来表示线程阻塞的情况应该是这样:

clipboard.png

而在协程中,非阻塞的情况应该是这样:

clipboard.png

可以看到,线程的阻塞,那这个线程就真的不去做事情了,必须等到被唤醒了,才会继续执行,在被唤醒之前,这个线程资源可以说就被浪费了,如果我有新的任务,就必须在启动一个新的线程来执行。

但是协程上的挂起,它会去寻找有没有需要执行的代码块,如果有,就拿来跑,这样就能更高效的利用线程资源。如果挂起后,也没有发现任何可以执行的代码块,同样的也会进入阻塞状态,这一点和线程是一样的。

在kotlin中,挂起函数只能在协程环境中使用。

等待与取消

等待一个协程执行完毕,和线程的API一致,使用join方法就可以了。

val job = launch {
    // ....
}

job.join()

如果需要返回值,也可以使用async来启动协程,使用await方法来等待完成,并取得返回值数据。

val job = async {
    // ....
}

job.await()

await和join都是挂起函数。

协程应该被实现为可以被取消的,调用Job的cancel方法可以取消。但是,如果我们写个while(true)的死循环怎么取消呢?

显然是取消不了的。

为了能让我们的协程逻辑能被取消,就需要使用到协程的一个属性isActive。

假设我们有一个协程是下载一个文件,我们想让它能被取消。它可能是这样:

val dlJob = launch {
    var isFinished = false
    while (!isFinished) {
        // download ...
        
        if (dlSize == totalSize) {
            isFinished = true
        }
    }
}

这样的话,这个协程是无法被取消的,它无法被外侧所操控,我们可以使用isActive来改写一下。

val dlJob = launch {
    var isFinished = false
    while (!isFinished && isActive) { // 注意这里
        // download ...

        if (dlSize == totalSize) {
            isFinished = true
        }
    }
}

只需要这样,就可以实现取消逻辑了。

问题也就随之而来,像打开网络连接,读写文件,总是需要去执行一些close的逻辑才是符合规范的,如果协程被取消,就直接退出了,要如何才能回收打开的资源呢?

如何回收资源

可以通过try{...}finally{...}进行回收资源,就像这样:

val dlJob = launch {
    try {
        var isFinished = false
        while (!isFinished && isActive) { // 注意这里
            // download ...

            if (dlSize == totalSize) {
                isFinished = true
            }
        }    
    } finally {
        // close something
    }
}

当job被取消后,finally方法里面依然会在最后被执行,可以在这里进行一些回收的操作。

超时

如果我们期望一个协程最多只能执行多少时间,超过这个时间就要被取消的时候,就可以使用超时逻辑,可以使用withTimeout函数来实现。

runBlocking(Dispatchers.Default) {

    try {
        // 只允许协程执行最多500毫秒
        val job = withTimeout(500) { 
            try {
                println("working 1")
                delay(1000)
                println("working 2")
            } finally {
                println("finally, I will do something")
            }
        }

        println("job $job") // 无法被执行到
    } catch (e: Throwable) {
        println("out coroutine $e")
    }

}

如果超时了,则会抛异常,并且,这个函数与runBlocking是一样的,都会阻塞当前线程。上面的代码中,协程外的print不会被执行到。

如果不想抛异常,可以使用另一个超时函数withTimeoutOrNull。

runBlocking(Dispatchers.Default) {

    try {
        // 只允许协程执行最多500毫秒
        val job = withTimeoutOrNull(500) {
            try {
                println("working 1")
                delay(1000)
                println("working 2")
            } finally {
                println("finally, I will do something")
            }
        }
    
        println("job $job") // 可以被执行到
    } catch (e: Throwable) {
        println("out coroutine $e")
    }

}

最终运行的结果是:

working 1
finally, I will do something
job null

切换上下文

如果我们期望协程的代码在不同的线程中来回跳转,可以使用withContext来实现。(emmmmm,这是什么场景的需求呢?)

newSingleThreadContext("Ctx1").use { ctx1 ->
    newSingleThreadContext("Ctx2").use { ctx2 ->
        runBlocking(ctx1) {
            log("Started in ctx1")
            withContext(ctx2) {
                log("Working in ctx2")
            }
            log("Back to ctx1")
        }
    }
}

这里直接照搬文档中的示例代码,最后输出的结果为:

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1

总结

以上就是操控协程的各种方法了。

挂起函数是协程中定义的概念,只能在协程中使用,挂起的含义是非阻塞的暂停,调度器会寻找需要运行的协程放到线程中去执行,如果找不到任何需要执行的协程,才会将线程阻塞。

协程是可以被取消的,任何系统提供的挂起函数内部都有取消的逻辑,如果自己的协程想要可以被取消,就必须通过isActive变量来编写逻辑。

取消后的协程总是会执行finally代码块,可以在这里进行一些资源回收的操作。

如果希望控制协程的工作时长,可以使用withTimeout来限制协程。

通过withContext函数来将逻辑切换到其他的线程上去。

之前的表格,就可以得到进一步的扩展了

clipboard.png

相关阅读

如果你喜欢这篇文章,欢迎点赞评论打赏
更多干货内容,欢迎关注我的公众号:好奇码农君

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 30 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-12-04
个人主页被 780 人浏览