0%

2020-04-03

现在有个这样的场景,需要你编写一个基础库sdk供上层业务调用,同时考虑引入kotlin,于是你花了3分钟很快就把所有的代码写完了,然后assembleRelease输出aar,再引入aar到主工程中。此时你想在主工程中结合业务调试下刚写完的kt代码,发现没法debug,效果如下所示:

record-kt-aar-source-code-decomplie-example

由于项目时间关系,在我遇到这个问题时由于代码量不大,立马就将kt编写改成java了。但java语法在某些场景下实在太罗嗦,同时为了引入kt的协程特性,如果我要继续在基础库中使用kt,前提条件是需要解决kt的aar包不能debug的问题。

解决问题的过程总是那么曲折不顺,解决问题后的感受总是那么神清气爽。先说结论,这个问题有两种解决方式:

  1. 通过引入子模块的方式,配置一个开关,在你需要调试代码时引入子模块中的源代码,而发布时依赖aar
  2. 通过maven库的方式,不管是本地还是远程maven,在发布代码时附带源码

子模块方式

这种方式在操作上依赖一个开关变量,而我根本不想再多维护一个开关值,所以不推荐。下面还是简单说明下怎么操作,原本是aar方式依赖,现在改成子模块方式,如下图所示:

record-kt-aar-source-code-submodule

1
2
include 'basic-net'
project(':basic-net').projectDir = new File(settingsDir, '../TestKtAarLib/basic-net')

这种方式明目张胆把源码依赖进来了,实在找不到借口不能debug了。说了这么多不好,其实还是有优点的,你可以及时修改源码来佐证自己的想法,但仅仅是佐证而已,如果这套基础库代码不是你维护的,或者你们有明确分工,不建议你修改后commit。

Read more »

示例项目地址:https://github.com/Leeeyou/SampleOfKotlin-InDepth

1. 操作符

1.1 集合操作符

元素相关的

  • + 、 - :往集合中增加或者删除某元素
  • groupBy:按照闭包条件分组
  • slice :按照入参将集合切分
  • take:从前往后拿取集合中的前n个元素,n是入参
  • takeLast:从后往前拿取集合中的前n个元素,n是入参
  • drop:从前往后丢弃集合中的前n个元素,n是入参
  • dropLast:从后往前丢弃集合中的前n个元素,n是入参
  • takeWhile:按照lambda表达式条件,从前往后拿取元素,直到第一个不符合条件的元素出现为止
  • takeLastWhile:按照lambda表达式条件,从后往前拿取元素,直到第一个不符合条件的元素出现为止
  • dropWhile:按照lambda表达式条件,从前往后丢弃元素,直到第一个不符合条件的元素出现为止
  • dropLastWhile:按照lambda表达式条件,从后往前丢弃元素,直到第一个不符合条件的元素出现为止
  • chunked:将集合按照n分块,n是入参,被分块的元素不在参与下一次分块
  • windowed:将集合按照n分块,n是入参,被分块的元素可能继续参与下一次分块,类似滑动窗口
  • zipWithNext:将集合两两一组切分
  • elementAt:返回对应的元素,越界会抛IndexOutOfBoundsException
  • first:返回符合条件的第一个元素,没有不返回任何内容
  • firstOrNull:返回符合条件的第一个元素,没有返回null
  • last:返回符合条件的最后一个元素,没有不返回任何内容
  • lastOrNull:返回符合条件的最后一个元素,没有返回null
  • elementAtOrNull:返回对应的元素,越界返回null
  • elementAtOrElse:返回对应的元素,越界则执行lambda表达式
  • find:同firstOrNull
  • findLast:同lastOrNull
  • contains:判断是否有指定元素
  • containsAll:判断是否包含指定的元素集

排序相关的

  • sortedWith:接受一个Comparator对象
  • sorted:升序
  • sortedDescending:降序
  • sortedBy:自定义顺序排列
  • sortedByDescending:自定义逆序排列
  • asReversed:反序
  • shuffled:随机排序
Read more »

1. 简介

1.1. 历史发展

  • 2011年7月JetBrains推出Kotlin项目,这是一个面向JVM的新语言,它已被开发一年之久。
  • 2012年2月JetBrains以Apache 2许可证开源此项目,Jetbrains希望这个新语言能够推动IntelliJ IDEA的销售。
  • 2016年2月15日发布1.0版,被认为是第一个官方稳定版本,并且JetBrains已准备从该版本开始的长期向后兼容性。
  • 2017年3月,JetBrains发布Kotlin 1.1版本,Kotlin在全球范围内成长显著。
  • 2017年5月18日Google I/O大会上宣布Kotlin正式成为Android的官方一级开发语言,同时AS3.0默认集成Kotlin Plugin。
  • 2017年11月,JetBrains发布了Kotlin 1.2版本,推出了多平台项目特性,可以将原始代码编译成多个平台的目标代码,目前支持JVM和JavaScript。
  • 2018年2月,Google发布了Android KTX扩展库,目前已属于Android Jetpack系列中的一员,简单的说Android KTX旨在让我们利用Kotlin语言功能(例如扩展函数/属性、lambda、命名参数和参数默认值),以更简洁、更愉悦、更惯用的方式使用Kotlin进行Android开发。甚至可以简单理解为Google为Kotlin准备的适配Android的一系列xxxUtils工具。这个库由Jake Wharton负责维护。
  • 2018年10月,JetBrains发布了Kotlin 1.3版本,这个版本最重要的特性就是协程,它使得非阻塞代码易于读写。此外在多平台方面,支持了支持 JVM、Android、JavaScript和Native。这意味着Kotlin能进行前端、移动端以及后端代码的开发了。
  • 目前已有非常多的开源库推出Kotlin版本:RxKotlinkotterknifeleakcanarymaterial-dialogskotlin-web-sitekotlin-dslSwiftKotlinkotlinconf-appDesign-Patterns-In-Kotlinankorecyclerview-animatorsglide-transformationsretrofit2-kotlin-coroutines-adapter

1.2. APP集成现状

使用 未使用
Pinterest    Evernote    Uber    Facebook    twitter    微信读书    豆瓣    钉钉    京东    百度    抖音    今日头条    爱奇艺 YouTube    Instagram    迅雷    微信    快手    手机QQ    手机淘宝
Read more »

RecyclerView的使用场景非常丰富,而本篇的源码分析基于上下滑动一个列表的场景来观察它的复用-回收机制。本文基于27.0.0版本进行分析,如下是Demo展示:

source-code-analysis-recyclerview-demo

RecyclerView继承自ViewGroup,属于系统级别的自定义控件,而它的源码长达12000多行,还不包括抽取出去的其他辅助类、管理类等,可想而知其复杂性,本文的分析思路主要是集中在RecyclerView的缓存机制上,通过滑动事件结合源码分析它的复用-回收机制,而RecyclerView的绘制流程、ItemDecoration、LayoutManager、State、Recycler等会一笔带过。

自定义控件三部曲:onMeasure - onLayout - onDraw,RecyclerView也不例外。查看源码可以看到,RecyclerView测量的一部分逻辑委托给了LayoutManager,源码如下所示,进来判断是否存在LayoutManager实例,不存在则调用defaultOnMeasure进行默认测量。然后就是一个if…else…判断是否为AutoMeasure,LinearLayoutManager和GridLayoutManager使用这种模式,而StaggerLayoutManager在一定条件下会使用自定义测量这种模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
//LinearLayoutManager和GridLayoutManager使用这种模式
if (mLayout.mAutoMeasure) {
...
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
...
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
...
//而StaggerLayoutManager在一定条件下会使用自定义测量这种模式
}
}
Read more »

本文基于EventBus3.1.1进行源码分析,以发送一个正常事件和粘性事件为例,探索EventBus工作的整个过程。你也可以直接下载demo同步运行调试,Gif示例如下:

source-code-analysis-eventbus-demo

gif中首先展示了发送一个LoginSuccessEvent的正常事件,在MainActivity和SecondActivity中都有订阅,这里主要展示一对多的场景;接着分别以正常方式和粘性方式发送了一个RegisterSuccessEvent事件,看看在GoToLoginActivity中有怎样不同的表现。

本文的思路是先分析注册和注销的流程,也就是订阅和解订阅;再分析发布正常事件和发布粘性事件的流程。

1. 注册和注销的流程分析

1
2
EventBus.getDefault().register(this)
EventBus.getDefault().unregister(this)

1.1. register

首先通过EventBus.getDefault()拿到实例对象,源码如下所示,

1
2
3
4
5
6
7
8
9
10
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}

这是一种双重校验的懒汉式单例,双重校验机制只会在第一次创建实例时有锁的介入,一旦实例创建成功,下次再获取实例就不会进入锁块了。还有一点需要注意务必要用volatile关键字修饰defaultInstance变量,保证在操作defaultInstance对象时,都是从内存中加载最新的状态。后续通过getDefault()拿到的都是defaultInstance这个实例对象了。接着看看regitster()干了什么,源码如下所示:

Read more »

本文基于Retrofit2.5.0进行源码分析,以发送一个异步get网络请求为例,直到取回数据再渲染到页面的整个过程。Gif示例如下:

source-code-analysis-retrofit-demo

本文不过多解释Retrofit是怎样使用的,本文假设你已经使用过它并对其有一定程度的了解,那么你应该清楚它有几个可以自由配置的属性分别是:callFactory、converterFactories、callAdapterFactories、callbackExecutor;掌握这几个属性对于理解Retrofit源码起着关键作用。

callFactory:网络请求器,用于发起真正的网络请求。
converterFactories:数据转换器,用于将网络请求的结果转换成你想要的目标数据结构,比如GsonJacksonSimple XML
callAdapterFactories:网络请求适配器,用于将网络请求包装成不同的类型,如默认的Call、RxJava2的ObservableKotlin的coroutines
callbackExecutor:回调执行器,用于将网络请求的结果从子线程拉回到主线程。

使用Retrofit发送一个网络请求的代码如下所示,你也可以直接下载demo运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//step1:创建Retrofit实例
val retrofit = Retrofit.Builder()
.baseUrl("http://gank.io/api/")
.addConverterFactory(GsonConverterFactory.create())
.build()

//step2:创建接口服务类的代理对象,这里是gankService
val gankService = retrofit.create(GankService::class.java)

//step3:访问接口服务类中的具体业务方法,得到一个适配器对象,这里的categoriesCall是Call<ResponseCategory>类型
val categoriesCall = gankService.categories()

//step4:通过categoriesCall发起网络请求,在Callback中解析数据并处理后续业务逻辑。
categoriesCall.enqueue(object : Callback<ResponseCategory> {
override fun onFailure(call: Call<ResponseCategory>, t: Throwable) {
Log.e("MainActivity", "访问失败", t)
}

override fun onResponse(call: Call<ResponseCategory>, response: Response<ResponseCategory>) {
val body = response.body()
body?.takeIf { result -> !result.isError }?.also { category ->
Toast.makeText(this@MainActivity, "访问成功 -> " + Gson().toJson(category), Toast.LENGTH_SHORT).show()
}
}
})

Read more »

这篇文章主要梳理Android中的应用签名是怎样执行的,以及为什么要这样做。

1. 应用签名

给应用签名其实可以理解为给应用加上开发者自己的一套指纹,以便开发者后续可以继续创作和更新其应用。在 Android 平台上运行的每个应用都必须要有开发者的签名。Google Play 或 Android 设备上的软件包安装程序会拒绝没有获得签名就尝试安装的应用。

应用可以由第三方(OEM、运营商、其他应用市场)签名,也可以自行签名。Android 提供了使用自签名证书进行代码签名的功能,而开发者无需外部协助或许可即可生成自签名证书。应用并非必须由核心机构签名。Android 目前不对应用证书进行 CA 认证。

1.1. APK签名方案

Android 支持以下三种应用签名方案:

v1 方案:基于 JAR 签名。
v2 方案:APK 签名方案 v2(在 Android 7.0 中引入)。
v3 方案:APK 签名方案 v3(在 Android 9 中引入)。

为了最大限度地提高兼容性,请按照 v1、v2、v3 的先后顺序采用所有方案对应用进行签名。与只通过 v1 方案签名的应用相比,还通过 v2+ 方案签名的应用能够更快速地安装到 Android 7.0 及更高版本的设备上。更低版本的 Android 平台会忽略 v2+ 签名,这就需要应用包含 v1 签名。

另外在 Android P 中,v2 方案已更新为 v3 方案,以便在签名分块中包含其他信息,但在其他方面保持相同的工作方式。

V1和V2的区别在于:在验证期间,v2+ 方案会将 APK 文件视为 Blob,并对整个文件进行签名检查。对 APK 进行的任何修改(包括对 ZIP 元数据进行的修改)都会使 APK 签名作废。这种形式的 APK 验证不仅速度要快得多,而且能够发现更多种未经授权的修改。而V1方案需要对每个文件来验证完整性,耗时较长。

Read more »

1. 线程安全

“线程安全”这个名称,相信稍有经验的程序员都会听说过,甚至在代码编写和走查的时候可能还会经常挂在嘴边,但是如何找到一个不太拗口的概念来定义线程安全却不是一件容易的事情,笔者尝试在Google中搜索它的概念,找到的是类似于“如果一个对象可以安全地被多个线程同时使用,那它就是线程安全的”这样的定义并不能说它不正确,但是人们无法从中获取到任何有用的信息。

笔者认为《Java Concurrency In Practice》的作者Brian Coca对“线程安全”有一个比较恰当的定义:“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的”。

这个定义比较严谨,它要求线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段(如互斥同步等),令调用者无须关心多线程的问题,更无须自己采取任何措施来保证多线程的正确调用。这点听起来简单,但其实并不容易做到,在大多数场景中,我们都会将这个定义弱化一些,如果把“调用这个对象的行为”限定为“单次调用”, 这个定义的其他描述也能够成立的话,我们就可以称它是线程安全了,为什么要弱化这个定义,现在暂且放下,稍后再详细探讨。

1.1. Java语言中的线程安全

按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

1.1.1. 不可变

在Java语言中(特指JDK 1.5以后,即Java内存模型被修正之后的Java语言),不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施,在我们谈到final关键字带来的可见性时曾经提到过这一点,只要一个不可变的对象被正确地构建出来(没有发生this引用逃逸的情况),那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。“不可变”带来的安全性是最简单和最纯粹的。

Java语言中,如果共享数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行,如果读者还没想明白这句话,不妨想一想java.lang.String类的对象,它是一个典代的不可变对象,我们调用它的substring()、replace()和concat()这些方法都不会影响它原来的值,只会返回一个新构造的字符串对象。

保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final,这样在构造函数结束之后,它就是不可变的,例如如下代码中 java.lang.Integer构造函数所示的,它通过将内部状态变量value定义为final来保障状态不变。

Read more »

1. 线程的实现

我们知道,线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。

主流的操作系统都提供了线程实现,Java语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行start()且还未结束的java.lang.Thread类的实例就代表了一个线程。我们注意到Thread类与大部分的Java API有显著的差别,它的所有关键方法都是声明为Native的。在Java API中,一个Native方法往往意味着这个方法没有使用或无法使用平台无关的手段来实现(当然也可能是为了执行效率而使用Native方法,不过通常最高效率的手段也就是平台相关的手段)。正因为如此,作者把本节的标题定为“线程的实现” 而不是“Java线程的实现”。 实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。

1.1. 使用内核线程实现

内核线程(Kernel-Levek Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,井负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel)。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口————轻量级进程(Light Weight Process, LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型,如下图所示:

jvm-efficient-concurrency-process-thread

由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作,但是轻量级进程具有它的局限性:首先由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。其次每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。

1.2. 使用用户线程实现

从广义上来讲,一个线程只要不是内核线程,就可以认为是用户线程(User Thread, UT),因此从这个定义上来讲,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,效率会受到限制。

而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为1对多的线程模型,如下图所示:

jvm-efficient-concurrency-process-thread-one-to-many

使用用户线程的优势在于并不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。线程的创建、切换和调度都是需要考虑的问题。而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”、“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至不可能完成。因为使用用户线程实现的程序一般都比较复杂,除了以前在不支持多线程的操作系统中(如DOS)的多线程程序与少数有特殊需求的程序外,现在使用用户线程的程序越来越少了,Java、Ruby等语言都曾经使用过用户线程,最终由都放弃使用它。

Read more »

在正式讲解Java虚拟机并发相关的知识之前,我们先花费一点时间去了解一下物理计算机中的并发问题,物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。

“让计算机并发执行若干个运算任务” 与 “更充分地利用计算机处理器的效能” 之间的因果关系,看起来顺理成章,实际上它们之间的关系并没有想象中的那么简单,其中一个重要的复杂性来源是绝大多数的运算任务都不可能只靠处理器 “计算” 就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作是很难消除的(无法仅靠寄存器来完成所有运算任务)。由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引人了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory), 如下图所示。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、 MESI(Illinois ProtocoI)、 MOSI、Synapse、Firefly及Dragon Protocol等。在本章中将会多次提到的 “内存模型” 一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。不同架构的物理机器可以拥有不一样的内存模型,而Java虚拟机也有自己的内存模型,并且这里介绍的内存访问操作与硬件的缓存访问操作具有很高的可比性。

jvm-efficient-concurrency-processor-cache-main-memory

除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输人代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。

1. Java内存模型

Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model, JMM)来 屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。 在此之前,主流程序语言(如C/C++等)直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错,因此在某些场景就必须针对不同的平台来编写程序。 定义Java内存模型并非一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发内存访问操作不会产生歧义;但是也必须定义得足够宽松,使得虚拟机的实现有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存和指令集中某些特有的指令)来获取更好的执行速度。经过长时间的验证和修补,在JDK 1.5(实现了JSR-133)发布后, Java内存模型已经成熟和完善起来了。

Read more »