From 38a114d7e946ee4b0a0a271bcef32cc179fbcf7b Mon Sep 17 00:00:00 2001 From: leavesC <1990724437@qq.com> Date: Mon, 15 Nov 2021 11:20:20 +0800 Subject: [PATCH 01/62] =?UTF-8?q?=E5=8B=98=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- ...\227\250Kotlin\345\215\217\347\250\213.md" | 8 +- ...77\345\235\221\346\214\207\345\215\227.md" | 2 +- ...45\351\227\250\346\225\231\347\250\213.md" | 0 ...\263\242\346\265\252\347\220\203Widget.md" | 397 ++++++++++++++++++ ...3\344\274\232\357\274\2101\357\274\211.md" | 0 ...3\344\274\232\357\274\2102\357\274\211.md" | 2 +- ...3\344\274\232\357\274\2103\357\274\211.md" | 0 ...17\347\250\213\345\237\272\347\241\200.md" | 0 ...10\345\222\214\350\266\205\346\227\266.md" | 0 ...02\350\265\267\345\207\275\346\225\260.md" | 0 ...14\350\260\203\345\272\246\345\231\250.md" | 0 ...5-\345\274\202\346\255\245\346\265\201.md" | 0 .../6-\351\200\232\351\201\223.md" | 0 ...02\345\270\270\345\244\204\347\220\206.md" | 0 ...14\345\271\266\345\217\221\346\200\247.md" | 0 ...51\350\241\250\350\276\276\345\274\217.md" | 0 17 files changed, 405 insertions(+), 8 deletions(-) rename "dart/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" => "flutter_core/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" (100%) create mode 100644 "flutter_core/Flutter\345\256\236\346\210\230-\347\224\250\350\264\235\345\241\236\345\260\224\346\233\262\347\272\277\347\224\273\344\270\200\344\270\252\345\270\246\346\226\207\346\234\254\347\232\204\346\263\242\346\265\252\347\220\203Widget.md" rename "android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" => "java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" (100%) rename "android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" => "java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" (99%) rename "android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" => "java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" (100%) rename "kotlin_coroutine/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" => "kotlin_core/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" (100%) rename "kotlin_coroutine/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" => "kotlin_core/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" (100%) rename "kotlin_coroutine/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" => "kotlin_core/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" (100%) rename "kotlin_coroutine/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" => "kotlin_core/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" (100%) rename "kotlin_coroutine/5-\345\274\202\346\255\245\346\265\201.md" => "kotlin_core/5-\345\274\202\346\255\245\346\265\201.md" (100%) rename "kotlin_coroutine/6-\351\200\232\351\201\223.md" => "kotlin_core/6-\351\200\232\351\201\223.md" (100%) rename "kotlin_coroutine/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" => "kotlin_core/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" (100%) rename "kotlin_coroutine/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" => "kotlin_core/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" (100%) rename "kotlin_coroutine/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" => "kotlin_core/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" (100%) diff --git a/README.md b/README.md index f0e4116..c441c27 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![公众号:字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) 文章链接均导航至掘金:[业志陈](https://juejin.cn/user/923245496518439/posts) 如果发现掘金文章内容不全,请查看此处文件夹中的文章,这里会一直保留最新最全的文章内容 - + * [Android 精讲]() * [Android WebView H5 秒开方案总结](https://juejin.cn/post/7016883220025180191) * [Jetpack LiveData 的设计理念及改进](https://juejin.cn/post/6903096576734920717) @@ -39,9 +39,9 @@ * [学不动也要学,Jetpack Compose 玩一把俄罗斯方块](https://juejin.cn/post/6974585048762679310) * [学不动也要学,Jetpack Compose 自定义 View 你学不学](https://juejin.cn/post/6996568363581308959/) * [Flutter]() + * [Dart 入门教程](https://juejin.cn/post/6844903937342963719) * [FlutterDo - 入门实例教程](https://github.com/leavesC/flutter_do) * [Flutter 实战 - 用贝塞尔曲线画一个带文本的波浪球 Widget](https://juejin.cn/post/6844903981790019592) - * [Dart 入门教程](https://juejin.cn/post/6844903937342963719) * [Jetpack 源码笔记]() * [从源码看 Jetpack(1)- Lifecycle 源码详解](https://juejin.cn/post/6847902220755992589) * [从源码看 Jetpack(2)- Lifecycle 衍生物源码详解](https://juejin.cn/post/6847902220760203277) diff --git "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Kotlin\345\215\217\347\250\213.md" "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Kotlin\345\215\217\347\250\213.md" index b10dac8..825a47e 100644 --- "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Kotlin\345\215\217\347\250\213.md" +++ "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Kotlin\345\215\217\347\250\213.md" @@ -32,8 +32,8 @@ Goggle 官方推荐将 Kotlin 协程作为在 Android 上进行异步编程的 引入依赖: ```kotlin - implementation 'org.jetbrains.Kotlinx:Kotlinx-coroutines-core:1.4.2' - implementation 'org.jetbrains.Kotlinx:Kotlinx-coroutines-android:1.4.2' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' ``` # 二、第一个协程 @@ -83,7 +83,7 @@ public suspend fun delay(timeMillis: Long) 对于这句话我的理解是:`delay()` 函数类似于 Java 中的 `Thread.sleep()`,而之所以说 `delay()` 函数是非阻塞的,是因为它和单纯的线程休眠有着本质的区别。协程是运行于线程上的,一个线程可以运行多个(几千上万个)协程。线程的调度行为是由操作系统来管理的,而协程的调度行为是可以由开发者来指定并由编译器来实现的,协程能够细粒度地控制多个任务的执行时机和执行线程,当某个特定的线程上的所有协程被 suspend 后,该线程便可腾出资源去处理其他任务 -例如,当在 ThreadA 上运行的 CoroutineA 调用了`delay(1000L)`函数指定延迟一秒后再运行,ThreadA 会转而去执行 CoroutineB,等到一秒后再来继续执行 CoroutineA。所以,ThreadA 并不会因为 CoroutineA 的延时而阻塞,而是能继续去执行其它任务,所以挂起函数并不会阻塞其所在线程,这样就极大地提高了线程的并发灵活度,最大化了线程的利用效率。而如果是使用`Thread.sleep()`的话,线程就只能干等着而不会去执行其它任务,降低了线程的利用效率 +例如,当在 ThreadA 上运行的 CoroutineA 调用了`delay(1000L)`函数指定延迟一秒后再运行,ThreadA 会转而去执行 CoroutineB,等到一秒后再来继续执行 CoroutineA。所以,ThreadA 并不会因为 CoroutineA 的延时而阻塞,而是能继续去执行其它任务,所以挂起函数并不会阻塞其所在线程,这样就极大地提高了线程的并发灵活度,最大化了线程的利用效率。而如果是使用`Thread.sleep()`的话,线程就只能干等着而不能去执行其它任务,降低了线程的利用效率 # 四、suspend function 的挂起与恢复 @@ -227,7 +227,7 @@ fun main() { delay(500) log("runBlocking") } - //主动休眠两百毫秒,使得和 runBlocking 加起来的延迟时间少于六百毫秒 + //主动休眠两百毫秒,使得和 runBlocking 加起来的延迟时间多于六百毫秒 Thread.sleep(200) log("after sleep") } diff --git "a/android_core/\346\216\242\347\251\266Kotlin\347\232\204\351\232\220\350\227\217\346\200\247\350\203\275\345\274\200\351\224\200\344\270\216\351\201\277\345\235\221\346\214\207\345\215\227.md" "b/android_core/\346\216\242\347\251\266Kotlin\347\232\204\351\232\220\350\227\217\346\200\247\350\203\275\345\274\200\351\224\200\344\270\216\351\201\277\345\235\221\346\214\207\345\215\227.md" index 3bfe9fb..4ce9cfb 100644 --- "a/android_core/\346\216\242\347\251\266Kotlin\347\232\204\351\232\220\350\227\217\346\200\247\350\203\275\345\274\200\351\224\200\344\270\216\351\201\277\345\235\221\346\214\207\345\215\227.md" +++ "b/android_core/\346\216\242\347\251\266Kotlin\347\232\204\351\232\220\350\227\217\346\200\247\350\203\275\345\274\200\351\224\200\344\270\216\351\201\277\345\235\221\346\214\207\345\215\227.md" @@ -538,7 +538,7 @@ public final class KotlinMode { 所以说,init 块和成员变量之间的声明顺序决定了在构造函数中的初始化顺序,我们应该先声明成员变量再声明 init 块,否则就有可能导致 NPE -# Gosn & data class +# Gson & data class 来看个小例子,猜猜其运行结果会是怎样的 diff --git "a/dart/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" "b/flutter_core/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" similarity index 100% rename from "dart/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" rename to "flutter_core/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" diff --git "a/flutter_core/Flutter\345\256\236\346\210\230-\347\224\250\350\264\235\345\241\236\345\260\224\346\233\262\347\272\277\347\224\273\344\270\200\344\270\252\345\270\246\346\226\207\346\234\254\347\232\204\346\263\242\346\265\252\347\220\203Widget.md" "b/flutter_core/Flutter\345\256\236\346\210\230-\347\224\250\350\264\235\345\241\236\345\260\224\346\233\262\347\272\277\347\224\273\344\270\200\344\270\252\345\270\246\346\226\207\346\234\254\347\232\204\346\263\242\346\265\252\347\220\203Widget.md" new file mode 100644 index 0000000..4df819f --- /dev/null +++ "b/flutter_core/Flutter\345\256\236\346\210\230-\347\224\250\350\264\235\345\241\236\345\260\224\346\233\262\347\272\277\347\224\273\344\270\200\344\270\252\345\270\246\346\226\207\346\234\254\347\232\204\346\263\242\346\265\252\347\220\203Widget.md" @@ -0,0 +1,397 @@ +> 公众号:[字节数组](https://link.juejin.cn/?target=https%3A%2F%2Fp6-juejin.byteimg.com%2Ftos-cn-i-k3u1fbpfcp%2F0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image) 希望对你有所帮助 🤣🤣 + +flutter 中的自定义 Widget 算作是 flutter 体系中比较高阶的知识点之一了,相当于原生开发中的自定义 View,以我个人的感受来说,自定义 widget 的难度要低于自定义 View,不过由于当前 flutter 的开源库还不算多丰富,所以有些效果还是需要开发者自己动手来实现,而本篇文章就来介绍如何用 flutter 来实现一个带文本的波浪球 Widget,实现的的效果如下所示: + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6ecd9c54e5e049708a4b990bca004998~tplv-k3u1fbpfcp-zoom-1.image) + +源代码点击这里下载:https://github.com/leavesC/flutter_do + +先来总结下该 WaveLoadingWidget 的特点,这样才能归纳出实现该效果所需的步骤 + +1. widget 的主体是一个不规则的半圆,顶部以类似于波浪的形式从左往右上下波动运行 +2. 球形波浪可以自定义颜色,此处以 waveColor 命名 +3. 波浪的起伏线将嵌入的文本分为上下两种颜色,上边的文本颜色以 backgroundColor 命名,下边的文本颜色以 foregroundColor 命名,文本的颜色一直在动态变化中 + +虽然波浪是不断运动的,但只要能够绘制出其中一帧的图形,其动态效果就能通过不断改变波浪的位置参数来完成,所以这里先把该 widget 当成静态的,先实现其静态效果即可 + +将绘制步骤拆解为以下几步: + +1. 绘制颜色为 backgroundColor 的文本,将其绘制在 canvas 的最底层 +2. 根据 widget 的宽高信息构建一个不超出范围的最大圆形路径 circlePath +3. 以 circlePath 的水平中间线作为波浪的起伏线,在起伏线的上边和下边分别利用贝塞尔曲线绘制一段连续的波浪 path,将 path 的首尾两端以矩形的形式连接在一起,构成 wavePath,wavePath 的底部会与 circlePath 的底部相交于一点 +4. 取 circlePath 和 wavePath 的交集 targetPath,用 waveColor 填充, 此时就得到了半圆形的球形波浪了 +5. 利用 `canvas.clipPath(targetPath)` 方法裁切画布,再绘制颜色为 foregroundColor 的文本,此时绘制的 foregroundColor 文本只会显示 targetPath 范围内的部分,从而使两次不同时间绘制的文本重叠在了一起,得到了有不同颜色范围的文本 +6. 利用 flutter 动画不断改变 wavePath 的起始点的 X 坐标,同时重新绘制 UI,从而得到波浪不断从左往右前进的效果 + +现在就来一步步实现以上的绘制步骤吧 + +# 一、初始化画笔 + +flutter 通过抽象类 `CustomPainter` 为开发者提供了自绘 UI 的入口,其内部的抽象方法 `void paint(Canvas canvas, Size size)` 提供了**画布对象 canvas** 以及包含 widget **宽高信息的 size 对象** + +此处就来继承 CustomPainter 类,初始化画笔对象以及各个配置参数(要绘制的文本,颜色值等) + +```dart +class WaveLoadingPainter extends CustomPainter { + //如果外部没有指定颜色值,则使用此默认颜色值 + static final Color defaultColor = Colors.lightBlue; + + //画笔对象 + var _paint = Paint(); + + //圆形路径 + Path _circlePath = Path(); + + //波浪路径 + Path _wavePath = Path(); + + //要显示的文本 + final String text; + + //字体大小 + final double fontSize; + + final Color backgroundColor; + + final Color foregroundColor; + + final Color waveColor; + + WaveLoadingPainter( + {this.text, + this.fontSize, + this.backgroundColor, + this.foregroundColor, + this.waveColor}) { + _paint + ..isAntiAlias = true + ..style = PaintingStyle.fill + ..strokeWidth = 3 + ..color = waveColor ?? defaultColor; + } + + @override + void paint(Canvas canvas, Size size) { + + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} + +``` + +# 二、绘制 backgroundColor 文本 + +flutter 的 canvas 对象没有提供直接 `drawText` 的 API,其绘制文本的步骤相对原生的自定义 View 要比较麻烦 + +```dart +@override + void paint(Canvas canvas, Size size) { + double side = min(size.width, size.height); + double radius = side / 2.0; + + _drawText(canvas: canvas, side: side, colors: backgroundColor); + + ··· + } + + void _drawText({Canvas canvas, double side, Color colors}) { + ParagraphBuilder pb = ParagraphBuilder(ParagraphStyle( + textAlign: TextAlign.center, + fontStyle: FontStyle.normal, + fontSize: fontSize ?? 0, + )); + pb.pushStyle(ui.TextStyle(color: colors ?? defaultColor)); + pb.addText(text); + ParagraphConstraints pc = ParagraphConstraints(width: fontSize ?? 0); + Paragraph paragraph = pb.build()..layout(pc); + canvas.drawParagraph( + paragraph, + Offset( + (side - paragraph.width) / 2.0, (side - paragraph.height) / 2.0)); + } +``` + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fd485927e396495aabba9f6bd00eb673~tplv-k3u1fbpfcp-zoom-1.image) + +# 三、构建圆形路径 circlePath + +取 widget 的宽和高的最小值作为圆的直径大小,以此构建出一个不超出 widget 范围的最大圆形路径 + +```dart + @override + void paint(Canvas canvas, Size size) { + double side = min(size.width, size.height); + double radius = side / 2.0; + + _drawText(canvas: canvas, side: side, colors: backgroundColor); + + _circlePath.reset(); + _circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi); + + ··· + } +``` + +# 四、绘制波浪线 + +此处波浪的宽度和高度就根据一个固定的比例值来求值,以 _circlePath 的中间分隔线作为水平线,在水平线上下根据贝塞尔曲线绘制出连续的波浪线 + +```dart + @override + void paint(Canvas canvas, Size size) { + double side = min(size.width, size.height); + double radius = side / 2.0; + + _drawText(canvas: canvas, side: side, colors: backgroundColor); + + _circlePath.reset(); + _circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi); + + double waveWidth = side * 0.8; + double waveHeight = side / 6; + _wavePath.reset(); + _wavePath.moveTo(-waveWidth, radius); + for (double i = -waveWidth; i < side; i += waveWidth) { + _wavePath.relativeQuadraticBezierTo( + waveWidth / 4, -waveHeight, waveWidth / 2, 0); + _wavePath.relativeQuadraticBezierTo( + waveWidth / 4, waveHeight, waveWidth / 2, 0); + } + + //为了方便读者理解,这里把路径绘制出来,实际上不需要 + canvas.drawPath(_wavePath, _paint); + + } +``` + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/36663f142ac640179a8823bf091a4e1c~tplv-k3u1fbpfcp-zoom-1.image) + +此时绘制的曲线还处于非闭合状态,需要将 _wavePath 的首尾两端连接起来,这样才可以和 _circlePath 做交集 + +```dart + _wavePath.relativeLineTo(0, radius); + _wavePath.lineTo(-waveWidth, side); + _wavePath.close(); +``` + +_wavePath 闭合后,此时绘制出来的图形就如下所示 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b6377be8ef3846c58ab291f8d396ea0d~tplv-k3u1fbpfcp-zoom-1.image) + +# 五、取交集 + +_circlePath 和 _wavePath 的交集就是一个半圆形波浪了 + +```dart + var combine = Path.combine(PathOperation.intersect, _circlePath, _wavePath); + canvas.drawPath(combine, _paint); + + //为了方便读者理解,这里把路径绘制出来,实际上不需要 + canvas.drawPath(combine, _paint); +``` + + + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a01e65ed24844720b455ceb69ac0a74e~tplv-k3u1fbpfcp-zoom-1.image) + +# 六、绘制 foregroundColor 文本 + +文本的颜色是分为上下两部分的,foregroundColor 颜色的文本不需要显示上半部分,所以在绘制 foregroundColor 文本的时候需要把上半部分文本给裁切掉,使两次不同时间绘制的文本重叠在了一起,得到了有不同颜色范围的文本 + +```dart + canvas.clipPath(combine); + _drawText(canvas: canvas, side: side, colors: foregroundColor); +``` + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/babf80b896ce4a56bb20bbfc53da6966~tplv-k3u1fbpfcp-zoom-1.image) + +# 七、添加动画 + +现在已经绘制好了单独一帧时的效果图了,可以考虑使 widget 动起来了 + +只要不断改变贝塞尔曲线的起始点坐标,使之不断从左往右移动,就可以营造出波浪从左往右前进的效果了。WaveLoadingPainter 只负责根据外部传入的动画值 **animatedValue** 来绘制 UI,构造 animatedValue 的逻辑则由外部的 _WaveLoadingWidgetState 进行处理,这里规定 animatedValue 的值是从 0 递增到 1,在开始构建 _wavePath 前只需要移动其起始坐标点即可 + +```dart + @override + void paint(Canvas canvas, Size size) { + double side = min(size.width, size.height); + double radius = side / 2.0; + + _drawText(canvas: canvas, side: side, colors: backgroundColor); + + _circlePath.reset(); + _circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi); + + double waveWidth = side * 0.8; + double waveHeight = side / 6; + _wavePath.reset(); + _wavePath.moveTo((animatedValue - 1) * waveWidth, radius); + for (double i = -waveWidth; i < side; i += waveWidth) { + _wavePath.relativeQuadraticBezierTo( + waveWidth / 4, -waveHeight, waveWidth / 2, 0); + _wavePath.relativeQuadraticBezierTo( + waveWidth / 4, waveHeight, waveWidth / 2, 0); + } + _wavePath.relativeLineTo(0, radius); + _wavePath.lineTo(-waveWidth, side); + _wavePath.close(); + + var combine = Path.combine(PathOperation.intersect, _circlePath, _wavePath); + canvas.drawPath(combine, _paint); + + canvas.clipPath(combine); + _drawText(canvas: canvas, side: side, colors: foregroundColor); + } +``` + +```dart +class _WaveLoadingWidgetState extends State + with SingleTickerProviderStateMixin { + final String text; + + final double fontSize; + + final Color backgroundColor; + + final Color foregroundColor; + + final Color waveColor; + + AnimationController controller; + + Animation animation; + + _WaveLoadingWidgetState( + {@required this.text, + @required this.fontSize, + @required this.backgroundColor, + @required this.foregroundColor, + @required this.waveColor}); + + @override + void initState() { + super.initState(); + controller = + AnimationController(duration: const Duration(seconds: 1), vsync: this); + controller.addStatusListener((status) { + switch (status) { + case AnimationStatus.dismissed: + print("dismissed"); + break; + case AnimationStatus.forward: + print("forward"); + break; + case AnimationStatus.reverse: + print("reverse"); + break; + case AnimationStatus.completed: + print("completed"); + break; + } + }); + + animation = Tween( + begin: 0.0, + end: 1.0, + ).animate(controller) + ..addListener(() { + setState(() => {}); + }); + controller.repeat(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: WaveLoadingPainter( + text: text, + fontSize: fontSize, + animatedValue: animation.value, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + waveColor: waveColor, + ), + ); + } +} +``` + +# 八、使用 + +之后只要将 WaveLoadingPainter 包裹到 StatefulWidget 中即可,在 StatefulWidget 中开放可以自定义配置的参数就可以了 + +```dart +class WaveLoadingWidget extends StatefulWidget { + final String text; + + final double fontSize; + + final Color backgroundColor; + + final Color foregroundColor; + + final Color waveColor; + + WaveLoadingWidget( + {@required this.text, + @required this.fontSize, + @required this.backgroundColor, + @required this.foregroundColor, + @required this.waveColor}) { + assert(text != null && text.length == 1); + assert(fontSize != null && fontSize > 0); + } + + @override + _WaveLoadingWidgetState createState() => _WaveLoadingWidgetState( + text: text, + fontSize: fontSize, + backgroundColor: backgroundColor, + foregroundColor: foregroundColor, + waveColor: waveColor, + ); +} +``` + +使用方式就类似于一般的系统 widget + +```dart + Container( + width: 300, + height: 300, + child: WaveLoadingWidget( + text: "锲", + fontSize: 215, + backgroundColor: Colors.lightBlue, + foregroundColor: Colors.white, + waveColor: Colors.lightBlue, + ), + ), + Container( + width: 250, + height: 250, + child: WaveLoadingWidget( + text: "而", + fontSize: 175, + backgroundColor: Colors.indigoAccent, + foregroundColor: Colors.white, + waveColor: Colors.indigoAccent, + ), + ), +``` + + + +源代码点击这里下载:https://github.com/leavesC/flutter_do + +此外该项目也提供了 N 多个常用 Widget 和自定义 Widget 的使用及实现方法,涵盖了系统 Widget 、布局容器、动画、高阶功能、自定义 Widget 等内容,欢迎 star \ No newline at end of file diff --git "a/android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" similarity index 100% rename from "android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" rename to "java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" diff --git "a/android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" similarity index 99% rename from "android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" rename to "java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" index 36939f8..932a27a 100644 --- "a/android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" +++ "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" @@ -561,7 +561,7 @@ public class Key { } ``` -初始化大小从 200 到 200000 之间以 10 倍的倍数递增,向不同 HashMap 存入同等数据量的数据,观察存入数据所需要的时间 +初始化大小从 200 到 20000 之间以 10 倍的倍数递增,向不同 HashMap 存入同等数据量的数据,观察存入数据所需要的时间 ```java public class Test { diff --git "a/android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" similarity index 100% rename from "android_java_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" rename to "java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" diff --git "a/kotlin_coroutine/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" "b/kotlin_core/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" similarity index 100% rename from "kotlin_coroutine/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" rename to "kotlin_core/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" diff --git "a/kotlin_coroutine/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" "b/kotlin_core/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" similarity index 100% rename from "kotlin_coroutine/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" rename to "kotlin_core/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" diff --git "a/kotlin_coroutine/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" "b/kotlin_core/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" similarity index 100% rename from "kotlin_coroutine/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" rename to "kotlin_core/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" diff --git "a/kotlin_coroutine/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" "b/kotlin_core/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" similarity index 100% rename from "kotlin_coroutine/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" rename to "kotlin_core/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" diff --git "a/kotlin_coroutine/5-\345\274\202\346\255\245\346\265\201.md" "b/kotlin_core/5-\345\274\202\346\255\245\346\265\201.md" similarity index 100% rename from "kotlin_coroutine/5-\345\274\202\346\255\245\346\265\201.md" rename to "kotlin_core/5-\345\274\202\346\255\245\346\265\201.md" diff --git "a/kotlin_coroutine/6-\351\200\232\351\201\223.md" "b/kotlin_core/6-\351\200\232\351\201\223.md" similarity index 100% rename from "kotlin_coroutine/6-\351\200\232\351\201\223.md" rename to "kotlin_core/6-\351\200\232\351\201\223.md" diff --git "a/kotlin_coroutine/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" "b/kotlin_core/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" similarity index 100% rename from "kotlin_coroutine/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" rename to "kotlin_core/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" diff --git "a/kotlin_coroutine/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" "b/kotlin_core/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" similarity index 100% rename from "kotlin_coroutine/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" rename to "kotlin_core/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" diff --git "a/kotlin_coroutine/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" "b/kotlin_core/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" similarity index 100% rename from "kotlin_coroutine/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" rename to "kotlin_core/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" From 8632ce00a794d3a3c84ec8357596668ec94a2a00 Mon Sep 17 00:00:00 2001 From: leavesc <1990724437@qq.com> Date: Wed, 15 Dec 2021 17:17:39 +0800 Subject: [PATCH 02/62] =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- ...71\346\241\210\346\200\273\347\273\223.md" | 4 +- ...40\350\276\223\346\226\207\344\273\266.md" | 8 +- ...15\347\224\250\346\234\272\345\210\266.md" | 4 +- ...77\345\235\221\346\214\207\345\215\227.md" | 6 +- ...65\345\217\212\346\224\271\350\277\233.md" | 4 +- ...45\205\245\351\227\250ConstraintLayout.md" | 4 +- ...51\200\237\345\205\245\351\227\250Gson.md" | 4 + ...\227\250Kotlin\345\215\217\347\250\213.md" | 585 +++++++++++------- ...200\237\345\205\245\351\227\250RxJava2.md" | 4 +- ...02\351\205\215\346\226\271\346\241\210.md" | 4 +- ...6\207\350\257\273\346\207\202AsyncTask.md" | 4 +- ...71\346\226\271\351\235\242\351\235\242.md" | 4 + ...207\202Handler\346\234\272\345\210\266.md" | 4 +- ...33\345\236\213\351\232\276\347\202\271.md" | 4 +- ...71\347\202\271\346\200\235\350\200\203.md" | 4 +- ...202View&Window\346\234\272\345\210\266.md" | 4 +- ...06\345\217\212\347\274\272\351\231\267.md" | 4 +- ...06\345\217\221\346\234\272\345\210\266.md" | 4 +- ...sureLayoutDraw\346\265\201\347\250\213.md" | 4 +- ...346\261\202\346\241\206\346\236\2663.0.md" | 4 +- ...77\345\235\221\346\214\207\345\215\227.md" | 4 +- ...57\345\212\250\346\250\241\345\274\217.md" | 4 +- ...33\347\237\245\350\257\206\347\202\271.md" | 4 +- ...33\347\237\245\350\257\206\347\202\271.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...PP\357\274\210\344\270\200\357\274\211.md" | 4 +- ...PP\357\274\210\344\272\214\357\274\211.md" | 2 +- ...27\346\226\257\346\226\271\345\235\227.md" | 4 +- ...40\345\255\246\344\270\215\345\255\246.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...04\347\237\245\350\257\206\347\202\271.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...03\350\257\225\345\210\251\345\231\250.md" | 4 +- ...06\346\236\220\346\226\207\347\253\240.md" | 4 +- ...36\347\216\260\344\270\200\344\270\252.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...36\347\216\260\344\270\200\344\270\252.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...51\345\261\225\351\230\205\350\257\273.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...23\345\220\210\344\275\277\347\224\250.md" | 4 +- ...20\347\240\201\350\257\246\350\247\243.md" | 4 +- ...45\351\227\250\346\225\231\347\250\213.md" | 4 +- ...\263\242\346\265\252\347\220\203Widget.md" | 4 +- ...3\344\274\232\357\274\2101\357\274\211.md" | 4 +- ...3\344\274\232\357\274\2102\357\274\211.md" | 4 +- ...3\344\274\232\357\274\2103\357\274\211.md" | 4 +- ...57\345\244\232\347\272\277\347\250\213.md" | 4 +- ...77\347\250\213\345\220\214\346\255\245.md" | 4 +- ...34\346\234\211\345\223\252\344\272\233.md" | 4 +- ...11\350\277\231\344\271\210\345\244\232.md" | 4 +- ...20\347\240\201\350\247\243\346\236\220.md" | 4 +- ...17\347\250\213\345\237\272\347\241\200.md" | 4 +- ...10\345\222\214\350\266\205\346\227\266.md" | 4 +- ...02\350\265\267\345\207\275\346\225\260.md" | 4 +- ...14\350\260\203\345\272\246\345\231\250.md" | 4 +- ...5-\345\274\202\346\255\245\346\265\201.md" | 4 +- "kotlin_core/6-\351\200\232\351\201\223.md" | 4 +- ...02\345\270\270\345\244\204\347\220\206.md" | 4 +- ...14\345\271\266\345\217\221\346\200\247.md" | 4 +- ...51\350\241\250\350\276\276\345\274\217.md" | 4 +- ...\275\240Kotlin\345\205\245\351\227\250.md" | 4 +- 69 files changed, 569 insertions(+), 290 deletions(-) diff --git a/README.md b/README.md index c441c27..7b47d2d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ 🎁🎁🎁 这是一份给 Android Developer 的进阶不完整指南,喜欢写又长又臭的文章,希望对你有所帮助 🤣🤣🤣 -原创不易,觉得有用的话可以关注下公众号,第一时间接收文章更新通知:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image) +原创不易,觉得有用的话可以关注下公众号,第一时间接收文章更新通知:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) ![公众号:字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) diff --git "a/android_core/AndroidWebViewH5\347\247\222\345\274\200\346\226\271\346\241\210\346\200\273\347\273\223.md" "b/android_core/AndroidWebViewH5\347\247\222\345\274\200\346\226\271\346\241\210\346\200\273\347\273\223.md" index 561b9ef..aa9288a 100644 --- "a/android_core/AndroidWebViewH5\347\247\222\345\274\200\346\226\271\346\241\210\346\200\273\347\273\223.md" +++ "b/android_core/AndroidWebViewH5\347\247\222\345\274\200\346\226\271\346\241\210\346\200\273\347\273\223.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://link.juejin.cn/?target=https%3A%2F%2Fp6-juejin.byteimg.com%2Ftos-cn-i-k3u1fbpfcp%2F0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image) 希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 为了满足跨平台和动态性的要求,如今很多 App 都采用了 Hybrid 这种比较成熟的方案来满足多变的业务需求。Hybrid 也叫混合开发,即半原生半 H5 的方式,通过 WebView 来实现需要高度灵活性的业务,在需要和 Native 做交互或者是调用特定平台能力时再通过 JsBridge 来实现两端交互 diff --git "a/android_core/Android\345\256\236\347\216\260\346\227\240\347\275\221\347\273\234\344\274\240\350\276\223\346\226\207\344\273\266.md" "b/android_core/Android\345\256\236\347\216\260\346\227\240\347\275\221\347\273\234\344\274\240\350\276\223\346\226\207\344\273\266.md" index 77376d9..4c97b84 100644 --- "a/android_core/Android\345\256\236\347\216\260\346\227\240\347\275\221\347\273\234\344\274\240\350\276\223\346\226\207\344\273\266.md" +++ "b/android_core/Android\345\256\236\347\216\260\346\227\240\347\275\221\347\273\234\344\274\240\350\276\223\346\226\207\344\273\266.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 最近的项目需要实现一个 Android 手机之间无网络传输文件的功能,就研究了下 Wifi P2P(Wifi点对点) 这么一个功能,最后也实现了通过 Wifi 隔空传输文件 的功能,这里我也来整理下代码,分享给大家 @@ -8,7 +10,9 @@ Wifi P2P 是在 Android 4.0 以及更高版本系统中加入的功能,通过 实现的效果如下所示: - + + + 开发步骤分为以下几点: diff --git "a/android_core/Coil\345\222\214Glide\347\232\204Bitmap\347\274\223\345\255\230\345\244\215\347\224\250\346\234\272\345\210\266.md" "b/android_core/Coil\345\222\214Glide\347\232\204Bitmap\347\274\223\345\255\230\345\244\215\347\224\250\346\234\272\345\210\266.md" index 0354a6f..1dbf343 100644 --- "a/android_core/Coil\345\222\214Glide\347\232\204Bitmap\347\274\223\345\255\230\345\244\215\347\224\250\346\234\272\345\210\266.md" +++ "b/android_core/Coil\345\222\214Glide\347\232\204Bitmap\347\274\223\345\255\230\345\244\215\347\224\250\346\234\272\345\210\266.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 Android 开发者最熟悉的图片加载框架应该是 Glide 和 Picasso 这两个了,最近两年也出现了一个后起之秀:Coil diff --git "a/android_core/Gson\345\222\214KotlinDataClass\347\232\204\351\201\277\345\235\221\346\214\207\345\215\227.md" "b/android_core/Gson\345\222\214KotlinDataClass\347\232\204\351\201\277\345\235\221\346\214\207\345\215\227.md" index 5c5ac29..88180d2 100644 --- "a/android_core/Gson\345\222\214KotlinDataClass\347\232\204\351\201\277\345\235\221\346\214\207\345\215\227.md" +++ "b/android_core/Gson\345\222\214KotlinDataClass\347\232\204\351\201\277\345\235\221\346\214\207\345\215\227.md" @@ -1,6 +1,8 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 -在蛮久前有同事问过我关于一个 Gson 和 Kotlin dataClass 的问题,当时答不上来也没去细究,但一直都放在心底,今天就认真探究下原因,也输出总结了一下,希望能帮助你避开这个坑😂😂 +在蛮久前有同事问过我关于一个 Gson 和 Kotlin dataClass 的问题,当时答不上来也没去细究,但一直都放在心底,今天就认真探究下原因,也输出总结了一下,希望能帮助你避开这个坑 😂😂 来看个小例子,猜猜其运行结果会是怎样的 diff --git "a/android_core/JetpackLiveData\347\232\204\350\256\276\350\256\241\347\220\206\345\277\265\345\217\212\346\224\271\350\277\233.md" "b/android_core/JetpackLiveData\347\232\204\350\256\276\350\256\241\347\220\206\345\277\265\345\217\212\346\224\271\350\277\233.md" index c2f3b28..dcc0cd3 100644 --- "a/android_core/JetpackLiveData\347\232\204\350\256\276\350\256\241\347\220\206\345\277\265\345\217\212\346\224\271\350\277\233.md" +++ "b/android_core/JetpackLiveData\347\232\204\350\256\276\350\256\241\347\220\206\345\277\265\345\217\212\346\224\271\350\277\233.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 # 一、架构指南 diff --git "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250ConstraintLayout.md" "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250ConstraintLayout.md" index ed0a653..46e05a2 100644 --- "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250ConstraintLayout.md" +++ "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250ConstraintLayout.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 ConstraintLayout 目前是 Android Studio 的默认布局,其优势就是可以使用扁平化的视图层次结构(无嵌套视图组)来创建复杂多变的大型布局,在绘制效率上相对其它布局有很大优势。ConstraintLayout 与 RelativeLayout 相似,其中所有的视图均根据同级视图与父布局之间的关系来进行定位,但其灵活性要高于 RelativeLayout,并且更易于与 Android Studio 的布局编辑器配合使用 diff --git "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Gson.md" "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Gson.md" index 557d64a..73fa58a 100644 --- "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Gson.md" +++ "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Gson.md" @@ -1,3 +1,7 @@ +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 + Json 是一种文本形式的数据交换格式,比 xml 更为轻量。Json 的解析和生成的方式很多,在 Android 平台上最常用的类库有 Gson 和 FastJson 两种,这里要介绍的是 Gson Gson 的 GitHub 主页点击这里:[Gson](https://github.com/google/gson) diff --git "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Kotlin\345\215\217\347\250\213.md" "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Kotlin\345\215\217\347\250\213.md" index 825a47e..0b8bf29 100644 --- "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Kotlin\345\215\217\347\250\213.md" +++ "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250Kotlin\345\215\217\347\250\213.md" @@ -1,8 +1,10 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 -在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种网络请求方式,采用的技术栈各不相同,Java、Kotlin、RxJava、LiveData 各种混搭,技术栈的不统一长远来看肯定是会造成很多不便的,所以当时就打算封装一个新的网络请求框架来作为项目的统一规范(前面的人估计也是这么想的,所以就造成了同个项目中的网络请求方式越来越多😂😂),那么就需要考虑采用什么技术栈来实现了 +在今年的三月份,我因为需要为项目搭建一个新的网络请求框架开始接触 Kotlin 协程。那时我司项目中同时存在着两种网络请求方式,采用的技术栈各不相同,Java、Kotlin、RxJava、LiveData 各种混搭,技术栈的不统一长远来看肯定是会造成很多不便的,所以就打算封装一个新的网络请求框架来作为项目的统一规范(前面的人估计也是这么想的,所以就造成了同个项目中的网络请求方式越来越多 😂😂),那么就需要考虑采用什么技术栈来实现了 -采用 Kotlin 语言来实现必不可少,都这年头了还用 Java 也说不过去。Retrofit 也必不可少,而当时 Retrofit 也已经原生支持 Kotlin 协程了,Google 官方推出的 Jetpack 协程扩展库也越来越多,就最终决定弃用 RxJava 拥抱 Kotlin 协程,将协程作为技术栈之一 +采用 Kotlin 语言来实现必不可少,都这年头了还用 Java 也说不过去。Retrofit 也必不可少,而目前 Retrofit 也已经支持 Kotlin 协程了,Google 官方推出的 Jetpack 协程扩展库也越来越多,就最终决定弃用 RxJava 拥抱 Kotlin 协程,将协程作为技术栈之一 当时我是通过翻译协程官方文档来作为入门手段,到现在也大半年了,回头来看感觉官方文档还是挺晦涩难懂的,就想着再来写一两篇入门或进阶的文章来加深下理解,希望对你有所帮助 @@ -20,16 +22,16 @@ # 一、Kotlin 协程 -Kotlin [协程](https://links.jianshu.com/go?to=https%3A%2F%2FKotlinlang.org%2Fdocs%2Freference%2Fcoroutines-overview.html)提供了一种全新处理并发的方式,你可以在 Android 平台上使用它来简化异步执行的代码。协程从 Kotlin 1.3 版本开始引入,但这一概念在编程世界诞生的黎明之际就有了,最早使用协程的编程语言可以追溯到 1967 年的 [Simula](https://links.jianshu.com/go?to=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FSimula) 语言。在过去几年间,协程这个概念发展势头迅猛,现已经被诸多主流编程语言采用,比如 [Javascript](https://links.jianshu.com/go?to=https%3A%2F%2Fjavascript.info%2Fasync-await)、[C#](https://links.jianshu.com/go?to=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fcsharp%2Fprogramming-guide%2Fconcepts%2Fasync%2F)、[Python](https://links.jianshu.com/go?to=https%3A%2F%2Fdocs.python.org%2F3%2Flibrary%2Fasyncio-task.html)、[Ruby](https://links.jianshu.com/go?to=https%3A%2F%2Fruby-doc.org%2Fcore-2.1.1%2FFiber.html) 以及 [Go](https://links.jianshu.com/go?to=https%3A%2F%2Ftour.golang.org%2Fconcurrency%2F1) 等。Kotlin 协程是基于来自其他语言的既定概念 +Kotlin 协程提供了一种全新处理并发的方式,你可以在 Android 平台上使用它来简化异步执行的代码。协程从 Kotlin 1.3 版本开始引入,但这一概念在编程世界诞生的黎明之际就有了,最早使用协程的编程语言可以追溯到 1967 年的 Simula 语言。在过去几年间,协程这个概念发展势头迅猛,现已经被诸多主流编程语言采用,比如 Javascript、C#、Python、Ruby 以及 Go 等。Kotlin 协程是基于来自其他语言的既定概念 -Goggle 官方推荐将 Kotlin 协程作为在 Android 上进行异步编程的解决方案,值得关注的功能点包括: +Google 官方推荐将 Kotlin 协程作为在 Android 上进行异步编程的解决方案,值得关注的功能点包括: -- **轻量**:你可以在单个线程上运行多个协程,因为协程支持[挂起](https://Kotlinlang.org/docs/reference/coroutines/basics.html),不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作 -- **内存泄露更少**:使用[结构化并发](https://Kotlinlang.org/docs/reference/coroutines/basics.html#structured-concurrency)机制在一个作用域内执行多个操作 -- **内置取消支持**:[取消](https://Kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html)功能会自动通过正在运行的协程层次结构传播 -- **Jetpack 集成**:许多 Jetpack 库都包含提供全面协程支持的[扩展](https://developer.android.google.cn/Kotlin/ktx)。某些库还提供自己的[协程作用域](https://developer.android.google.cn/topic/libraries/architecture/coroutines),可供你用于结构化并发 +- 轻量:可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作 +- 内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作 +- 内置取消支持:取消功能会自动通过正在运行的协程层次结构传播 +- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供你用于结构化并发 -引入依赖: +如果是用于 Android 平台的话,可以只引用以下的 coroutines-android,当中已经包含了 coroutines-core ```kotlin implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' @@ -38,7 +40,7 @@ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2' # 二、第一个协程 -协程可以称为**轻量级线程**。Kotlin 协程在 CoroutineScope 的上下文中通过 launch、async 等**协程构造器**(CoroutineBuilder)来声明并启动 +协程可以称为 **轻量级线程**。Kotlin 协程在 CoroutineScope 的上下文中通过 launch、async 等 **协程构造器**(CoroutineBuilder)来声明并启动 ```kotlin fun main() { @@ -47,7 +49,7 @@ fun main() { delay(1000) log("launch") } - //主动休眠两秒,防止JVM过快退出 + //主动休眠两秒,防止 JVM 过快退出 Thread.sleep(2000) log("end") } @@ -56,20 +58,20 @@ private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg") ``` ```kotlin -[DefaultDispatcher-worker-1 @coroutine#1] launch +[DefaultDispatcher-worker-1] launch [main] end ``` -在上面的例子中,通过 GlobalScope(即**全局作用域**)启动了一个协程,在延迟一秒后输出一行日志。从输出结果可以看出来,启动的协程是运行在协程内部的**线程池**中。虽然从表现结果上来看,启动一个协程类似于我们直接使用 Thread 来执行耗时任务,但实际上协程和线程有着本质上的区别。通过使用协程,可以极大的提高线程的并发效率,避免以往的嵌套回调地狱,极大提高了代码的可读性 +在上面的例子中,通过 GlobalScope(全局作用域)启动了一个协程,在延迟一秒后输出一行日志。从输出结果可以看出来,启动的协程是运行在协程内部的**线程池**中。虽然从表现结果上来看,启动一个协程类似于我们直接使用 Thread 来执行耗时任务,但实际上协程和线程有着本质上的区别。通过使用协程,可以极大的提高线程的并发效率,避免以往的嵌套回调地狱,极大提高了代码的可读性 以上代码就涉及到了协程的四个基础概念: -- suspend function。即挂起函数,delay 函数就是协程库提供的一个用于实现非阻塞式延时的挂起函数 +- suspend function。即挂起函数,delay() 就是协程库提供的一个用于实现非阻塞式延时的挂起函数 - CoroutineScope。即协程作用域,GlobalScope 是 CoroutineScope 的一个实现类,用于指定协程的作用范围,可用于管理多个协程的生命周期,所有协程都需要通过 CoroutineScope 来启动 -- CoroutineContext。即协程上下文,包含多种类型的配置参数。Dispatchers.IO 就是 CoroutineContext 这个抽象概念的一种实现,用于指定协程的运行载体,即用于指定协程要运行在哪类线程上 -- CoroutineBuilder。即协程构建器,协程在 CoroutineScope 的上下文中通过 launch、async 等协程构建器来进行声明并启动。launch、async 等均被声明 CoroutineScope 的扩展方法 +- CoroutineContext。即协程上下文,包含多种类型的配置参数。`Dispatchers.IO` 就是 CoroutineContext 这个抽象概念的一种实现,用于指定协程的运行载体,即用于指定协程要运行在哪类线程上 +- CoroutineBuilder。即协程构建器,协程在 CoroutineScope 的上下文中通过 launch、async 等协程构建器来进行声明并启动。launch、async 均被声明为 CoroutineScope 的扩展方法 -# 三、suspend function +# 三、suspend 如果上述例子试图直接在 GlobalScope 外调用 `delay()` 函数的话,IDE 就会提示一个错误:**Suspend function 'delay' should be called only from a coroutine or another suspend function**。意思是:`delay()` 函数是一个挂起函数,只能由协程或者由其它挂起函数来调用 @@ -79,15 +81,15 @@ private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg") public suspend fun delay(timeMillis: Long) ``` -读者在网上看关于协程的文章的时候,应该经常会看到这么一句话:**挂起函数不会阻塞其所在线程,而是会将协程挂起,在特定的时候才再恢复协程** +读者在网上看关于协程的文章的时候,应该经常会看到这么一句话:**挂起函数不会阻塞其所在线程,而是会将协程挂起,在特定的时候才再恢复执行** -对于这句话我的理解是:`delay()` 函数类似于 Java 中的 `Thread.sleep()`,而之所以说 `delay()` 函数是非阻塞的,是因为它和单纯的线程休眠有着本质的区别。协程是运行于线程上的,一个线程可以运行多个(几千上万个)协程。线程的调度行为是由操作系统来管理的,而协程的调度行为是可以由开发者来指定并由编译器来实现的,协程能够细粒度地控制多个任务的执行时机和执行线程,当某个特定的线程上的所有协程被 suspend 后,该线程便可腾出资源去处理其他任务 +对于这句话我的理解是:`delay()` 函数类似于 Java 中的 `Thread.sleep()`,而之所以说 `delay()` 函数是非阻塞的,是因为它和单纯的线程休眠有着本质的区别。例如,当在 ThreadA 上运行的 CoroutineA 调用了`delay(1000L)`函数指定延迟一秒后再运行,ThreadA 会转而去执行 CoroutineB,等到一秒后再来继续执行 CoroutineA。所以,ThreadA 并不会因为 CoroutineA 的延时而阻塞,而是能继续去执行其它任务,所以挂起函数并不会阻塞其所在线程,这样就极大地提高了线程的并发灵活度,最大化了线程的利用效率。而如果是使用`Thread.sleep()`的话,线程就只能干等着而不能去执行其它任务,降低了线程的利用效率 -例如,当在 ThreadA 上运行的 CoroutineA 调用了`delay(1000L)`函数指定延迟一秒后再运行,ThreadA 会转而去执行 CoroutineB,等到一秒后再来继续执行 CoroutineA。所以,ThreadA 并不会因为 CoroutineA 的延时而阻塞,而是能继续去执行其它任务,所以挂起函数并不会阻塞其所在线程,这样就极大地提高了线程的并发灵活度,最大化了线程的利用效率。而如果是使用`Thread.sleep()`的话,线程就只能干等着而不能去执行其它任务,降低了线程的利用效率 +协程是运行于线程上的,一个线程可以运行多个(几千上万个)协程。线程的调度行为是由操作系统来管理的,而协程的调度行为是可以由开发者来指定并由编译器来实现的,协程能够细粒度地控制多个任务的执行时机和执行线程,当线程所执行的当前协程被 suspend 后,该线程也可以腾出资源去处理其他任务 -# 四、suspend function 的挂起与恢复 +# 四、suspend 挂起与恢复 -协程在常规函数的基础上添加了两项操作用于处理长时间运行的任务。在`invoke`(或 `call`)和`return`之外,协程添加了`suspend`和 `resume`: +协程在常规函数的基础上添加了两项操作用于处理长时间运行的任务,在`invoke`(或 `call`)和`return`之外,协程添加了`suspend`和 `resume`: - `suspend` 用于暂停执行当前协程,并保存所有局部变量 - `resume` 用于让已暂停的协程从暂停处继续执行 @@ -105,30 +107,30 @@ suspend fun fetchDocs() { // Dispatchers.Main suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ } ``` -在上面的示例中,`get()` 仍在主线程上被调用,但它会在启动网络请求之前暂停协程。`get()` 主体内通过调用 `withContext(Dispatchers.IO)` 创建了一个在 IO 线程池中运行的代码块,在该块内的任何代码都始终通过 IO 调度器执行。当网络请求完成后,`get()` 会恢复已暂停的协程,使得主线程协程可以直接拿到网络请求结果而不用使用回调来通知主线程。Retrofit 就是以这种方式来实现对协程的支持的 +在上面的示例中,`get()` 仍在主线程上被调用,但它会在启动网络请求之前暂停协程。`get()` 主体内通过调用 `withContext(Dispatchers.IO)` 创建了一个在 IO 线程池中运行的代码块,在该块内的任何代码都始终通过 IO 调度器执行。当网络请求完成后,`get()` 会恢复已暂停的协程,使得主线程协程可以直接拿到网络请求结果而不用使用回调来通知主线程。Retrofit 就是以这种方式来实现对协程的支持 -Kotlin 使用堆栈帧管理要运行哪个函数以及所有局部变量。暂停协程时,系统会复制并保存当前的堆栈帧以供稍后使用。恢复时,会将堆栈帧从其保存位置复制回来,然后函数再次开始运行。即使代码可能看起来像普通的顺序阻塞请求,协程也能确保网络请求避免阻塞主线程 +Kotlin 使用 **堆栈帧** 来管理要运行哪个函数以及所有局部变量。暂停协程时,系统会复制并保存当前的堆栈帧以供稍后使用。恢复时,会将堆栈帧从其保存的位置复制回来,然后函数再次开始运行。虽然代码可能看起来像普通的顺序阻塞请求,协程也能确保网络请求不会阻塞主线程 -在主线程进行的**暂停协程**和**恢复协程**的两个操作,既实现了将耗时任务交由后台线程完成,保障了主线程安全,又以同步代码的方式完成了实际上的多线程异步调用。可以说,在 Android 平台上协程主要就用来解决两个问题: +在主线程进行的 **暂停协程** 和 **恢复协程** 的两个操作,既实现了将耗时任务交由后台线程完成,保障了主线程安全,又以同步代码的方式完成了实际上的多线程异步调用。可以说,在 Android 平台上协程主要就用来解决两个问题: -1. **处理耗时任务 (Long running tasks)**,这种任务常常会阻塞住主线程 -2. **保证主线程安全 (Main-safety)** ,即确保安全地从主线程调用任何 suspend 函数 +1. 处理耗时任务 (Long running tasks),这种任务常常会阻塞主线程 +2. 保证主线程安全 (Main-safety),即确保安全地从主线程调用任何 suspend 函数 # 五、CoroutineScope -CoroutineScope 即**协程作用域**,用于对协程进行追踪。如果我们启动了多个协程但是没有一个可以对其进行统一管理的途径的话,那么就会导致我们的代码臃肿杂乱,甚至发生**内存泄露**或者**任务泄露**。为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用 CoroutineScope 的情况下启动新的协程。CoroutineScope 可被看作是一个具有超能力的 ExecutorService 的轻量级版本。它能启动新的协程,同时这个协程还具备上文所说的 suspend 和 resume 的优势 +CoroutineScope 即 **协程作用域**,用于对协程进行追踪。如果我们启动了多个协程但是没有一个可以对其进行统一管理的途径的话,就会导致我们的代码臃肿杂乱,甚至发生**内存泄露**或者**任务泄露**。为了确保所有的协程都会被追踪,Kotlin 不允许在没有 CoroutineScope 的情况下启动协程。CoroutineScope 可被看作是一个具有超能力的 ExecutorService 的轻量级版本。它能启动协程,同时这个协程还具备上文所说的 suspend 和 resume 的优势 -所有的协程都需要通过 CoroutineScope 来启动,它会跟踪它使用 `launch` 或 `async` 创建的所有协程,你可以随时调用 `scope.cancel()` 取消正在运行的协程。CoroutineScope 本身并不运行协程,它只是确保你不会失去对协程的追踪,即使协程被挂起也是如此。在 Android 中,某些 KTX 库为某些生命周期类提供了自己的 `CoroutineScope`。例如,`ViewModel` 有 [`viewModelScope`](https://developer.android.google.cn/reference/Kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.ViewModel).viewModelScope:Kotlinx.coroutines.CoroutineScope),`Lifecycle` 有 [`lifecycleScope`](https://developer.android.google.cn/reference/Kotlin/androidx/lifecycle/package-summary#lifecyclescope) +所有的协程都需要通过 CoroutineScope 来启动,它会跟踪通过 `launch` 或 `async` 创建的所有协程,你可以随时调用 `scope.cancel()` 取消正在运行的协程。CoroutineScope 本身并不运行协程,它只是确保你不会失去对协程的追踪,即使协程被挂起也是如此。在 Android 中,某些 ktx 库为某些生命周期类提供了自己的 CoroutineScope,例如,ViewModel 有 `viewModelScope`,Lifecycle 有 `lifecycleScope` CoroutineScope 大体上可以分为三种: - GlobalScope。即全局协程作用域,在这个范围内启动的协程可以一直运行直到应用停止运行。GlobalScope 本身不会阻塞当前线程,且启动的协程相当于守护线程,不会阻止 JVM 结束运行 - runBlocking。一个顶层函数,和 GlobalScope 不一样,它会阻塞当前线程直到其内部所有相同作用域的协程执行结束 -- 自定义 CoroutineScope。可用于实现主动控制协程的生命周期范围,对于 Android 开发来说最大意义之一就是可以避免内存泄露 +- 自定义 CoroutineScope。可用于实现主动控制协程的生命周期范围,对于 Android 开发来说最大意义之一就是可以在 Activity、Fragment、ViewModel 等具有生命周期的对象中按需取消所有协程任务,从而确保生命周期安全,避免内存泄露 ## 1、GlobalScope -GlobalScope 属于**全局作用域**,这意味着通过 GlobalScope 启动的协程的生命周期只受整个应用程序的生命周期的限制,只要整个应用程序还在运行且协程的任务还未结束,协程就可以一直运行 +GlobalScope 属于 **全局作用域**,这意味着通过 GlobalScope 启动的协程的生命周期只受整个应用程序的生命周期的限制,只要整个应用程序还在运行且协程的任务还未结束,协程就可以一直运行 GlobalScope 不会阻塞其所在线程,所以以下代码中主线程的日志会早于 GlobalScope 内部输出日志。此外,GlobalScope 启动的协程相当于守护线程,不会阻止 JVM 结束运行,所以如果将主线程的休眠时间改为三百毫秒的话,就不会看到 launch A 输出日志 @@ -154,9 +156,9 @@ fun main() { ```kotlin [main] start [main] end -[DefaultDispatcher-worker-1 @coroutine#1] GlobalScope -[DefaultDispatcher-worker-3 @coroutine#3] launch B -[DefaultDispatcher-worker-3 @coroutine#2] launch A +[DefaultDispatcher-worker-1] GlobalScope +[DefaultDispatcher-worker-3] launch B +[DefaultDispatcher-worker-3] launch A ``` `GlobalScope.launch` 会创建一个顶级协程,尽管它很轻量级,但在运行时还是会消耗一些内存资源,且可以一直运行直到整个应用程序停止(只要任务还未结束),这可能会导致内存泄露,所以在日常开发中应该谨慎使用 GlobalScope @@ -166,15 +168,14 @@ fun main() { 也可以使用 runBlocking 这个顶层函数来启动协程,runBlocking 函数的第二个参数即协程的执行体,该参数被声明为 CoroutineScope 的扩展函数,因此执行体就包含了一个隐式的 CoroutineScope,所以在 runBlocking 内部可以来直接启动协程 ```kotlin -public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T +public fun runBlocking(context: CoroutineContext = + EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T ``` runBlocking 的一个方便之处就是:只有当内部**相同作用域**的所有协程都运行结束后,声明在 runBlocking 之后的代码才能执行,即 runBlocking 会阻塞其所在线程 看以下代码。runBlocking 内部启动的两个协程会各自做耗时操作,从输出结果可以看出来两个协程还是在交叉并发执行,且 runBlocking 会等到两个协程都执行结束后才会退出,外部的日志输出结果有明确的先后顺序。即 runBlocking 内部启动的协程是非阻塞式的,但 runBlocking 阻塞了其所在线程。此外,runBlocking 只会等待相同作用域的协程完成才会退出,而不会等待 GlobalScope 等其它作用域启动的协程 -**所以说,runBlocking 本身带有阻塞线程的意味,但其内部运行的协程又是非阻塞的,读者需要意会这两者的区别** - ```kotlin fun main() { log("start") @@ -215,6 +216,8 @@ fun main() { [main] end ``` +**所以说,runBlocking 本身带有阻塞线程的意味,但其内部运行的协程又是非阻塞的,读者需要明白这两者的区别** + 基于是否会阻塞线程的区别,以下代码中 runBlocking 会早于 GlobalScope 输出日志 ```kotlin @@ -241,7 +244,7 @@ fun main() { ## 3、coroutineScope -`coroutineScope` 函数用于创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。`runBlocking` 和 `coroutineScope` 看起来很像,因为它们都需要等待其内部所有相同作用域的协程结束后才会结束自己。两者的主要区别在于 `runBlocking` 方法会阻塞当前线程,而 `coroutineScope`不会阻塞线程,而是会挂起并释放底层线程以供其它协程使用。由于这个差别,`runBlocking` 是一个普通函数,而 `coroutineScope` 是一个挂起函数 +`coroutineScope` 函数用于创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。`runBlocking` 和 `coroutineScope` 看起来很像,因为它们都需要等待其内部所有相同作用域的协程结束后才会结束自己。两者的主要区别在于 `runBlocking` 方法会阻塞当前线程,而 `coroutineScope`不会,而是会挂起并释放底层线程以供其它协程使用。基于这个差别,`runBlocking` 是一个普通函数,而 `coroutineScope` 是一个挂起函数 ```kotlin fun main() = runBlocking { @@ -254,7 +257,7 @@ fun main() = runBlocking { delay(500) log("Task from nested launch") } - delay(100) + delay(50) log("Task from coroutine scope") } log("Coroutine scope is over") @@ -294,17 +297,17 @@ fun main() = runBlocking { ``` ```kotlin -[main @coroutine#2] Task from runBlocking -[main @coroutine#3] Task throw Exception -[main @coroutine#4] Task from nested launch -[main @coroutine#1] Coroutine scope is over +[main] Task from runBlocking +[main] Task throw Exception +[main] Task from nested launch +[main] Coroutine scope is over ``` ## 5、自定义 CoroutineScope 假设我们在 Activity 中先后启动了多个协程用于执行异步耗时操作,那么当 Activity 退出时,必须取消所有协程以避免内存泄漏。我们可以通过保留每一个 Job 引用然后在 `onDestroy`方法里来手动取消,但这种方式相当来说会比较繁琐和低效。kotlinx.coroutines 提供了 CoroutineScope 来管理多个协程的生命周期 -我们可以通过创建与 Activity 生命周期相关联的协程作用域的实例来管理协程的生命周期。CoroutineScope 的实例可以通过 `CoroutineScope()` 或 `MainScope()` 的工厂函数来构建。前者创建通用作用域,后者创建 UI 应用程序的作用域并使用 Dispatchers.Main 作为默认的调度器 +我们可以通过创建与 Activity 生命周期相关联的协程作用域来管理协程的生命周期。CoroutineScope 的实例可以通过 `CoroutineScope()` 或 `MainScope()` 的工厂函数来构建。前者创建通用作用域,后者创建 UI 应用程序的作用域并使用 Dispatchers.Main 作为默认的调度器 ```kotlin class Activity { @@ -347,11 +350,7 @@ class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) { } } -``` -从输出结果可以看出,当回调了`onDestroy()`方法后协程就不会再输出日志了 - -```kotlin fun main() = runBlocking { val activity = Activity() activity.onCreate() @@ -361,15 +360,17 @@ fun main() = runBlocking { } ``` +从输出结果可以看出,当回调了`onDestroy()`方法后协程就不会再输出日志了 + ```kotlin -[main @coroutine#1] Activity Created -[DefaultDispatcher-worker-1 @coroutine#2] 0 -[DefaultDispatcher-worker-1 @coroutine#2] 1 -[DefaultDispatcher-worker-1 @coroutine#2] 2 -[main @coroutine#1] Activity Destroyed +[main] Activity Created +[DefaultDispatcher-worker-1] 0 +[DefaultDispatcher-worker-1] 1 +[DefaultDispatcher-worker-1] 2 +[main] Activity Destroyed ``` -已取消的作用域无法再创建协程。因此,仅当控制其生命周期的类被销毁时,才应调用 `scope.cancel()`。例如,使用 `viewModelScope` 时,[`ViewModel`](https://developer.android.google.cn/topic/libraries/architecture/viewmodel) 类会在 ViewModel 的 `onCleared()` 方法中自动取消作用域 +已取消的作用域无法再创建协程。因此,仅当控制其生命周期的类被销毁时,才应调用 `scope.cancel()`。例如,使用 `viewModelScope` 时, ViewModel 会在自身的 `onCleared()` 方法中自动取消作用域 # 六、CoroutineBuilder @@ -388,7 +389,7 @@ public fun CoroutineScope.launch( `launch` 函数共包含三个参数: 1. context。用于指定协程的上下文 -2. start。用于指定协程的启动方式。默认值为 `CoroutineStart.DEFAULT`,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态。可以通过将其设置为`CoroutineStart.LAZY`来实现延迟启动,即懒加载 +2. start。用于指定协程的启动方式,默认值为 `CoroutineStart.DEFAULT`,即协程会在声明的同时就立即进入等待调度的状态,即可以立即执行的状态。可以通过将其设置为`CoroutineStart.LAZY`来实现延迟启动,即懒加载 3. block。用于传递协程的执行体,即希望交由协程执行的任务 可以看到 launchA 和 launchB 是并行交叉执行的 @@ -421,32 +422,32 @@ fun main() = runBlocking { ## 2、Job -Job 是协程的句柄。使用 `launch` 或 `async` 创建的每个协程都会返回一个 `Job` 实例,该实例唯一标识协程并管理其生命周期。Job 是一个接口类型,这里列举 Job 几个比较有用的属性和函数 +Job 是协程的句柄。使用 `launch` 或 `async` 创建的每个协程都会返回一个 Job 实例,该实例唯一标识协程并管理其生命周期。Job 是一个接口类型,这里列举 Job 几个比较有用的属性和函数 ```kotlin - //当 Job 处于活动状态时为 true - //如果 Job 未被取消或没有失败,则均处于 active 状态 - public val isActive: Boolean +//当 Job 处于活动状态时为 true +//如果 Job 未被取消或没有失败,则均处于 active 状态 +public val isActive: Boolean - //当 Job 正常结束或者由于异常结束,均返回 true - public val isCompleted: Boolean +//当 Job 正常结束或者由于异常结束,均返回 true +public val isCompleted: Boolean - //当 Job 被主动取消或者由于异常结束,均返回 true - public val isCancelled: Boolean +//当 Job 被主动取消或者由于异常结束,均返回 true +public val isCancelled: Boolean - //启动 Job - //如果此调用的确启动了 Job,则返回 true - //如果 Job 调用前就已处于 started 或者是 completed 状态,则返回 false - public fun start(): Boolean +//启动 Job +//如果此调用的确启动了 Job,则返回 true +//如果 Job 调用前就已处于 started 或者是 completed 状态,则返回 false +public fun start(): Boolean - //用于取消 Job,可同时通过传入 Exception 来标明取消原因 - public fun cancel(cause: CancellationException? = null) +//用于取消 Job,可同时通过传入 Exception 来标明取消原因 +public fun cancel(cause: CancellationException? = null) - //阻塞等待直到此 Job 结束运行 - public suspend fun join() +//阻塞等待直到此 Job 结束运行 +public suspend fun join() - //当 Job 结束运行时(不管由于什么原因)回调此方法,可用于接收可能存在的运行异常 - public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle +//当 Job 结束运行时(不管由于什么原因)回调此方法,可用于接收可能存在的运行异常 +public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle ``` Job 具有以下几种状态值,每种状态对应的属性值各不相同 @@ -548,9 +549,9 @@ fun main() { > 由于 launch 和 async 仅能够在 CouroutineScope 中使用,所以任何创建的协程都会被该 scope 追踪。Kotlin 禁止创建不能够被追踪的协程,从而避免协程泄漏 -## 4、async 的错误用法 +## 4、async 错误用法 -修改下上述代码,可以发现两个协程的总耗时就会变为七秒左右 +修改上述代码,可以发现两个协程的总耗时就会变为七秒左右 ```kotlin fun main() { @@ -576,7 +577,7 @@ fun main() { [main] 7077 ``` -会造成这不同区别是因为 `CoroutineStart.LAZY`不会主动启动协程,而是直到调用`async.await()`或者`async.satrt()`后才会启动(即懒加载模式),所以`asyncA.await() + asyncB.await()`会导致两个协程其实是在顺序执行。而默认值 `CoroutineStart.DEFAULT` 参数会使得协程在声明的同时就被启动了(实际上还需要等待被调度执行,但可以看做是立即就执行了),所以即使 `async.await()`会阻塞当前线程直到协程返回结果值,但两个协程其实都是处于运行状态,所以总耗时就是四秒左右 +会造成这不同区别是因为 `CoroutineStart.LAZY` 不会主动启动协程,而是直到调用`async.await()`或者`async.satrt()`后才会启动(即懒加载模式),所以`asyncA.await() + asyncB.await()`会导致两个协程其实是在顺序执行。而默认值 `CoroutineStart.DEFAULT` 参数会使得协程在声明的同时就被启动了(实际上还需要等待被调度执行,但可以看做是立即就执行了),所以此时调用第一个 `async.await()`时两个协程其实都是处于运行状态,所以总耗时就是四秒左右 此时可以通过先调用`start()`再调用`await()`来实现第一个例子的效果 @@ -590,67 +591,75 @@ log(asyncA.await() + asyncB.await()) 由 `suspend` 函数启动的所有协程都必须在该函数返回结果时停止,因此你可能需要保证这些协程在返回结果之前完成。借助 Kotlin 中的结构化并发机制,你可以定义用于启动一个或多个协程的 `coroutineScope`。然后,你可以使用 `await()`(针对单个协程)或 `awaitAll()`(针对多个协程)保证这些协程在从函数返回结果之前完成 -例如,假设我们定义一个用于异步获取两个文档的 `coroutineScope`。通过对每个延迟引用调用 `await()`,我们可以保证这两项 `async` 操作在返回值之前完成: +假设我们定义一个用于异步获取两个文档的 `coroutineScope`,通过对每个延迟引用调用 `await()`,我们可以保证这两项 `async` 操作在返回值之前完成: ```kotlin - suspend fun fetchTwoDocs() = - coroutineScope { - val deferredOne = async { fetchDoc(1) } - val deferredTwo = async { fetchDoc(2) } - deferredOne.await() - deferredTwo.await() - } +suspend fun fetchTwoDocs() = coroutineScope { + val deferredOne = async { fetchDoc(1) } + val deferredTwo = async { fetchDoc(2) } + deferredOne.await() + deferredTwo.await() +} ``` -你还可以对集合使用 `awaitAll()`,如以下示例所示: +还可以对集合使用 `awaitAll()`来达到相同效果。虽然 `fetchTwoDocs()` 使用 `async` 启动新协程,但该函数使用 `awaitAll()` 等待启动的协程完成后才会返回结果。不过,即使我们没有调用 `awaitAll()`,`coroutineScope` 构建器也会等到所有内部协程都完成后才会恢复名为 `fetchTwoDocs` 的协程。此外,`coroutineScope` 会捕获协程抛出的所有异常,并将其传送给调用方 ```kotlin -suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main) - coroutineScope { - val deferreds = listOf( // fetch two docs at the same time - async { fetchDoc(1) }, // async returns a result for the first doc - async { fetchDoc(2) } // async returns a result for the second doc - ) - deferreds.awaitAll() // use awaitAll to wait for both network requests +suspend fun fetchTwoDocs() = coroutineScope { + val deferreds = listOf( + async { fetchDoc(1) }, + async { fetchDoc(2) } + ) + deferreds.awaitAll() } ``` -虽然 `fetchTwoDocs()` 使用 `async` 启动新协程,但该函数使用 `awaitAll()` 等待启动的协程完成后才会返回结果。不过请注意,即使我们没有调用 `awaitAll()`,`coroutineScope` 构建器也会等到所有新协程都完成后才恢复名为 `fetchTwoDocs` 的协程。此外,`coroutineScope` 会捕获协程抛出的所有异常,并将其传送回调用方 - ## 6、Deferred -`async` 函数的返回值是一个 Deferred 对象。Deferred 是一个接口类型,继承于 Job 接口,所以 Job 包含的属性和方法 Deferred 都有,其主要就是在 Job 的基础上扩展了 `await()`方法 +`async` 函数的返回值是一个 Deferred 对象。Deferred 是一个接口类型,继承于 Job 接口,所以 Job 包含的属性和方法 Deferred 都有,其主要是在 Job 的基础上扩展了 `await()`方法 # 七、CoroutineContext CoroutineContext 使用以下元素集定义协程的行为: - Job:控制协程的生命周期 -- CoroutineDispatcher:将工作分派到适当的线程 +- CoroutineDispatcher:将任务指派给适当的线程 - CoroutineName:协程的名称,可用于调试 - CoroutineExceptionHandler:处理未捕获的异常 ## 1、Job -协程中的 Job 是其上下文 CoroutineContext 中的一部分,可以通过 `coroutineContext[Job]` 表达式从上下文中获取到 - -以下两个 log 语句虽然是运行在不同的协程上,但是其指向的 Job 其实是同个对象 +协程中的 Job 是其上下文 CoroutineContext 中的一部分,可以通过 `coroutineContext[Job]` 表达式从上下文中获取到,我们可以通过控制 Job 来控制 CoroutineScope 的生命周期 ```kotlin -fun main() = runBlocking { - val job = launch { - log("My job is ${coroutineContext[Job]}") +val job = Job() + +val scope = CoroutineScope(job + Dispatchers.IO) + +fun main(): Unit = runBlocking { + log("job is $job") + val job = scope.launch { + try { + delay(3000) + } catch (e: CancellationException) { + log("job is cancelled") + throw e + } + log("end") } - log("My job is $job") + delay(1000) + log("scope job is ${scope.coroutineContext[Job]}") + scope.coroutineContext[Job]?.cancel() } ``` ```kotlin -[main @coroutine#1] My job is "coroutine#2":StandaloneCoroutine{Active}@75a1cd57 -[main @coroutine#2] My job is "coroutine#2":StandaloneCoroutine{Active}@75a1cd57 +[main] job is JobImpl{Active}@759ebb3d +[main] scope job is JobImpl{Active}@759ebb3d +[DefaultDispatcher-worker-1] job is cancelled ``` -实际上 CoroutineScope 的 `isActive` 这个扩展属性只是 `coroutineContext[Job]?.isActive == true` 的一种简便写法 +实际上 CoroutineScope 的 `isActive` 这个扩展属性只是 `coroutineContext[Job]?.isActive ?: true` 的一种简便写法 ```kotlin public val CoroutineScope.isActive: Boolean @@ -659,15 +668,16 @@ public val CoroutineScope.isActive: Boolean ## 2、CoroutineDispatcher -CoroutineContext 包含一个 CoroutineDispatcher(协程调度器)用于指定执行协程的目标载体,即**运行于哪个线程**。CoroutineDispatcher 可以将协程的执行操作限制在特定线程上,也可以将其分派到线程池中,或者让它无限制地运行。所有的协程构造器(如 launch 和 async)都接受一个可选参数,即 CoroutineContext ,该参数可用于显式指定要创建的协程和其它上下文元素所要使用的 CoroutineDispatcher +CoroutineContext 包含一个 CoroutineDispatcher(协程调度器)用于指定执行协程的目标载体,即 **运行于哪个线程**。CoroutineDispatcher 可以将协程的执行操作限制在特定线程上,也可以将其分派到线程池中,或者让它无限制地运行。所有的协程构造器(如 launch 和 async)都接受一个可选参数,即 CoroutineContext ,该参数可用于显式指定要创建的协程和其它上下文元素所要使用的 CoroutineDispatcher -要在主线程之外运行代码,可以让 Kotlin 协程在 Default 或 IO 调度程序上执行工作。在 Kotlin 中,所有协程都必须在 CoroutineDispatcher 中运行,即使它们在主线程上运行也是如此。协程可以自行暂停,而 CoroutineDispatcher 负责将其恢复 +要在主线程之外运行代码,可以指定 Kotlin 协程在 Default 或 IO 调度程序上执行工作。在 Kotlin 中,所有协程都必须在 CoroutineDispatcher 中运行,即使它们在主线程上运行也是如此。协程可以自行暂停,而 CoroutineDispatcher 负责将其恢复 -Kotlin 协程库提供了四个 Dispatcher 用于指定在何处运行协程,大部分情况下我们只会接触以下三个: +Kotlin 协程库提供了四个 Dispatcher 用于指定在哪一类线程中执行协程: -- **Dispatchers.Main** - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 `suspend` 函数、运行 Android 界面框架操作,以及更新 [`LiveData`](https://developer.android.google.cn/topic/libraries/architecture/livedata) 对象 -- **[Dispatchers.IO](http://Dispatchers.IO)** - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 [Room 组件](https://developer.android.google.cn/topic/libraries/architecture/room)、从文件中读取数据或向文件中写入数据,以及运行任何网络操作 -- **Dispatchers.Default** - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON +- Dispatchers.Default。默认调度器,适合用于执行占用大量 CPU 资源的任务。例如:对列表排序和解析 JSON +- Dispatchers.IO。适合用于执行磁盘或网络 I/O 的任务。例如:使用 Room 组件、读写磁盘文件,执行网络请求 +- Dispatchers.Unconfined。对执行协程的线程不做限制,可以直接在当前调度器所在线程上执行 +- Dispatchers.Main。使用此调度程序可用于在 Android 主线程上运行协程,只能用于与界面交互和执行快速工作,例如:更新 UI、调用 `LiveData.setValue` ```kotlin fun main() = runBlocking { @@ -676,24 +686,48 @@ fun main() = runBlocking { } launch(Dispatchers.Default) { log("Default") + launch(Dispatchers.Unconfined) { + log("Unconfined 1") + } } launch(Dispatchers.IO) { log("IO") + launch(Dispatchers.Unconfined) { + log("Unconfined 2") + } } launch(newSingleThreadContext("MyOwnThread")) { log("newSingleThreadContext") + launch(Dispatchers.Unconfined) { + log("Unconfined 4") + } + } + launch(Dispatchers.Unconfined) { + log("Unconfined 3") + } + GlobalScope.launch { + log("GlobalScope") } } ``` ```kotlin -[DefaultDispatcher-worker-1 @coroutine#3] Default -[DefaultDispatcher-worker-2 @coroutine#4] IO -[MyOwnThread @coroutine#5] newSingleThreadContext -[main @coroutine#2] main runBlocking +[DefaultDispatcher-worker-2] Default +[DefaultDispatcher-worker-1] IO +[DefaultDispatcher-worker-2] Unconfined 1 +[DefaultDispatcher-worker-1] Unconfined 2 +[MyOwnThread] newSingleThreadContext +[main] Unconfined 3 +[MyOwnThread] Unconfined 4 +[DefaultDispatcher-worker-1] GlobalScope +[main] main runBlocking ``` -当 `launch {...}` 在不带参数的情况下使用时,它从外部的协程作用域继承上下文和调度器,即和 runBlocking 保持一致。而在 GlobalScope 中启动协程时默认使用的调度器是 Dispatchers.default,并使用共享的后台线程池,因此 `launch(Dispatchers.default){...}` 与 `GlobalScope.launch{...}` 是使用相同的调度器。`newSingleThreadContext` 用于为协程专门创建一个新的线程来运行,专用线程是一种成本非常昂贵的资源,在实际的应用程序中必须在不再需要时释放掉,或者存储在顶级变量中以便在整个应用程序中进行重用 +- `launch` 在不执行 Dispatchers 的情况下使用时,它从外部的协程作用域继承上下文和调度器,即和 runBlocking 保持一致,均在 main 线程执行 +- IO 和 Default 均依靠后台线程池来执行 +- Unconfined 则不限定具体的线程类型,当前调度器在哪个线程,就在该线程上进行执行,因此上述例子中每个 Unconfined 协程所在线程均不一样 +- GlobalScope 启动协程时默认使用的调度器是 Dispatchers.Default,因此也是在后台线程池中执行 +- `newSingleThreadContext` 用于为协程专门创建一个新的线程,专用线程是一种成本非常昂贵的资源,在实际开发时必须当不再需要时释放掉线程资源,或者存储在顶级变量中以便在整个应用程序中进行复用 ## 3、withContext @@ -712,13 +746,13 @@ suspend fun get(url: String) = // Dispatchers.Main } ``` -借助协程,你可以细粒度地来调度线程。由于`withContext()`支持让你在不引入回调的情况下控制任何代码的执行线程池,因此你可以将其应用于非常小的函数,例如从数据库中读取数据或执行网络请求。一种不错的做法是使用 `withContext()` 来确保每个函数都是主线程安全的,这意味着,你可以从主线程调用每个函数。这样,调用方就从不需要考虑应该使用哪个线程来执行函数了 +借助协程,你可以细粒度地来调度线程。由于`withContext()`支持在不引入回调的情况下控制任何代码的执行线程池,因此你可以将其应用于非常小的函数,例如从数据库中读取数据或执行网络请求。一种不错的做法是使用 `withContext()` 来确保每个函数都是主线程安全的,这意味着,你可以从主线程调用每个函数。这样,调用方就从不需要考虑应该使用哪个线程来执行函数了 -在前面的示例中,`fetchDocs()` 方法在主线程上执行;不过,它可以安全地调用 `get`方法,这样会在后台执行网络请求。由于协程支持 `suspend` 和 `resume`,因此 `withContext` 块完成后,主线程上的协程会立即根据 `get` 结果恢复 +在前面的示例中,`fetchDocs()` 方法在主线程上执行,不过它可以安全地调用 `get`方法,因为`get`方法已确保网络请求会在子线程中执行。由于协程支持 `suspend` 和 `resume`操作,因此 `withContext` 块完成后,主线程上的协程会立即根据 `get` 结果恢复 -与基于回调的等效实现相比,[`withContext()`](https://Kotlin.github.io/Kotlinx.coroutines/Kotlinx-coroutines-core/Kotlinx.coroutines/with-context.html) 不会增加额外的开销。此外在某些情况下,还可以优化 `withContext()` 调用,使其超越基于回调的等效实现。例如,如果某个函数对一个网络进行十次调用,你可以使用外部 `withContext()` 让 Kotlin 只切换一次线程。这样,即使网络库多次使用 `withContext()`,它也会留在同一调度程序上,并避免切换线程。此外,Kotlin 还优化了 `Dispatchers.Default` 与 `Dispatchers.IO` 之间的切换,以尽可能避免线程切换 +与基于回调的等效实现相比,`withContext()` 不会增加额外的开销。此外,在某些情况下,还可以优化 `withContext()` 调用,使其超越基于回调的等效实现。例如,如果某个函数需要先后调用十次网络请求,你可以在最外层调用 `withContext()` 让协程只切换一次线程,这样即使每个网络请求内部均会使用 `withContext()`,它也会留在同一调度程序上,从而避免频率切换线程。此外,协程还优化了 `Dispatchers.Default` 与 `Dispatchers.IO` 之间的切换,以尽可能避免线程切换 -> 利用一个使用线程池的调度程序(例如 `Dispatchers.IO` 或 `Dispatchers.Default`)不能保证代码块一直在同一线程上从上到下执行。在某些情况下,Kotlin 协程在 `suspend` 和 `resume` 后可能会将执行工作移交给另一个线程。这意味着,对于整个 `withContext()` 块,线程局部变量可能并不指向同一个值 +> 使用线程池的调度器(例如 `Dispatchers.IO` 或 `Dispatchers.Default`)不能保证代码块一直在同一线程上从上到下执行,在某些情况下,协程在 `suspend` 和 `resume` 后可能会将任务移交给另一个线程来执行。这意味着,对于整个 `withContext()` 块,由于多线程并发之间的**原子性**和**可见性**等原因,先后读取到的线程局部变量可能并非是同个值 ## 4、CoroutineName @@ -740,19 +774,13 @@ fun main() = runBlocking(CoroutineName("RunBlocking")) { } ``` -```kotlin -[main @RunBlocking#1] start -[main @Coroutine#B#4] launch B -[main @Coroutine#A#3] launch A -``` - ## 5、CoroutineExceptionHandler 在下文的异常处理会讲到 ## 6、组合上下文元素 -有时我们需要为协程上下文定义多个元素,那就可以用 `+` 运算符。例如,我们可以同时为协程指定 Dispatcher 和 CoroutineName +有时我们需要为协程上下文定义多个元素,此时就可以用 `+` 运算符。例如,我们可以同时为协程指定 Dispatcher 和 CoroutineName ```kotlin fun main() = runBlocking { @@ -762,15 +790,11 @@ fun main() = runBlocking { } ``` -```kotlin -[DefaultDispatcher-worker-1 @test#2] Hello World -``` - -此外,由于 CoroutineContext 是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext。比如,`(Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")` +而由于 CoroutineContext 是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,从而组成新的 CoroutineContext。比如,`(Dispatchers.Main, "name") + (Dispatchers.IO)` 的运行结果是:`(Dispatchers.IO, "name")` # 八、取消协程 -如果用户退出某个启动了协程的 Activity/Fragment 的话,那么大部分情况下就应该取消所有协程 +如果用户退出启动了协程的 Activity / Fragment,那正常情况下就应该取消所有协程 `job.cancel()`就用于取消协程,`job.join()`用于阻塞等待协程运行结束。因为 `cancel()` 函数调用后会马上返回而不是等待协程结束后再返回,所以此时协程不一定就是已经停止运行了。如果需要确保协程结束运行后再执行后续代码,就需要调用 `join()` 方法来阻塞等待。也可以通过调用 Job 的扩展函数 `cancelAndJoin()` 来完成相同操作,它结合了 `cancel` 和 `join`两个操作 @@ -800,7 +824,7 @@ fun main() = runBlocking { ## 1、协程可能无法取消 -并不是所有协程都可以响应取消操作,协程的取消操作是需要协作(cooperative)完成的,协程必须协作才能取消。协程库中的所有挂起函数都是可取消的,它们在运行时会检查协程是否被取消了,并在取消时抛出 CancellationException 从而结束整个任务。但如果协程正在执行计算任务并且未检查是否已处于取消状态的话,就无法取消协程 +并不是所有协程都可以响应取消操作,协程的取消操作是需要协作 (cooperative) 完成的,**协程必须协作才能被取消**。协程库中的所有挂起函数都是可取消的,它们在运行前检查协程是否被取消了,并在取消时抛出 CancellationException 从而结束整个任务。而如果协程在执行计算任务前没有判断自身是否已被取消的话,此时就无法取消协程 所以即使以下代码主动取消了协程,协程也只会在完成既定循环后才结束运行,因为协程没有在每次循环前先进行检查,导致任务不受取消操作的影响 @@ -860,7 +884,9 @@ fun main() = runBlocking { } ``` -取消协程这个操作类似于在 Java 中调用`Thread.interrupt()`方法来向线程发起中断请求,这两个操作都不会强制停止协程和线程,外部只是相当于发起一个停止运行的请求,需要依靠协程和线程响应请求后主动停止运行。Kotlin 和 Java 之所以均没有提供一个可以直接强制停止协程或线程的方法,是因为这个操作可能会带来各种意想不到的情况。在停止协程和线程的时候,它们可能还持有着某些排他性资源(例如:锁,数据库链接),如果强制性地停止,它们持有的锁就会一直无法得到释放,导致其他协程和线程一直无法得到目标资源,最终可能导致线程死锁。所以`Thread.stop()`方法目前也是处于废弃状态,Java 官方并没有提供可靠的停止线程的方法 +取消协程这个操作类似于在 Java 中调用`Thread.interrupt()`方法来向线程发起中断请求,这两个操作都不会强制停止协程和线程,外部只是相当于发起一个停止运行的请求,需要依靠协程和线程响应请求后主动停止运行 + +Java 和 Kotlin 之所以均没有提供一个可以直接强制停止线程或协程的方法,是因为这个操作可能会带来各种意想不到的情况。例如,在停止线程或协程的时候,它们可能还持有着某些排他性资源(例如:锁,数据库链接),如果强制性地停止,它们持有的锁就会一直无法得到释放,导致其它线程或协程一直无法得到目标资源,最终就可能导致线程死锁。所以`Thread.stop()`方法目前也是处于废弃状态,Java 官方并没有提供一个可靠的停止线程的方法 ## 2、用 finally 释放资源 @@ -969,15 +995,15 @@ fun main() = runBlocking { ``` ```kotlin -[main @coroutine#2] request: I'm done and I don't explicitly join my children that are still active -[main @coroutine#3] Coroutine 0 is done -[main @coroutine#4] Coroutine 1 is done -[main @coroutine#5] Coroutine 2 is done +[main] request: I'm done and I don't explicitly join my children that are still active +[main] Coroutine 0 is done +[main] Coroutine 1 is done +[main] Coroutine 2 is done ``` ## 5、传播取消操作 -一般情况下,协程的取消操作会通过协程的层次结构来进行传播。如果取消父协程或者父协程抛出异常,那么子协程都会被取消。而如果子协程被取消,则不会影响同级协程和父协程,但如果子协程抛出异常则也会导致同级协程和父协程被取消 +一般情况下,协程的取消操作会通过协程的层次结构来进行传播:如果取消父协程或者父协程抛出异常,那么子协程都会被取消;而如果子协程被取消,则不会影响同级协程和父协程,但如果子协程抛出异常则也会导致同级协程和父协程被取消 对于以下代码,子协程 jon1 被取消并不影响子协程 jon2 和父协程继续运行,但父协程被取消后子协程都会被递归取消 @@ -1009,16 +1035,16 @@ fun main() = runBlocking { ``` ```kotlin -[main @coroutine#3] job1: 0 -[main @coroutine#4] job2: 0 -[main @coroutine#3] job1: 1 -[main @coroutine#4] job2: 1 -[main @coroutine#3] job1: 2 -[main @coroutine#3] job1 canceled -[main @coroutine#4] job2: 2 -[main @coroutine#4] job2: 3 -[main @coroutine#4] job2: 4 -[main @coroutine#1] parent job canceled +[main] job1: 0 +[main] job2: 0 +[main] job1: 1 +[main] job2: 1 +[main] job1: 2 +[main] job1 canceled +[main] job2: 2 +[main] job2: 3 +[main] job2: 4 +[main] parent job canceled ``` ## 6、withTimeout @@ -1062,44 +1088,27 @@ Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Time - 取消它自己 - 将异常传播并传递给它的父级 -异常会到达层级的根部,而且当前 CoroutineScope 所启动的所有协程都会被取消,但协程并非都是一发现异常就执行以上流程,launch 和 async 在处理异常方面有着很大的差异 +异常会到达层级的根部,且当前 CoroutineScope 所启动的所有协程都会被取消,但协程并非都是一发现异常就执行以上流程,launch 和 async 在处理异常方面有着一些差异 -launch 将异常视为未捕获异常,类似于 Java 的 Thread.uncaughtExceptionHandler,当发现异常时就会马上抛出。async 期望最终是通过调用 await 来获取结果 (或者异常),所以默认情况下它不会抛出异常。这意味着如果使用 async 启动新的协程,它会静默地将异常丢弃,直到调用 `async.await()` 才会得到目标值或者抛出存在的异常 +launch 将异常视为未捕获异常,类似于 Java 的 `Thread.uncaughtExceptionHandler`,当发现异常时就会马上抛出。async 期望最终通过调用 await 来获取结果 (或者异常),所以默认情况下它不会抛出异常,这意味着如果使用 async 启动新的协程,它会静默地将异常丢弃,直到调用 `async.await()` 才会得到目标值或者抛出存在的异常 -例如,以下代码中 launchA 抛出的异常会先连锁导致 launchB 也被取消(抛出 JobCancellationException),然后再导致父协程 BlockingCoroutine 也被取消 +例如,以下的 `fetchDocs()` 方法由于并没有调用 `Deferred.await()`,因此异常并不会被抛给调用方,而如果使用的是 launch 而非 async 的话,异常就会马上被抛出 ```kotlin -fun main() = runBlocking { - val launchA = launch { - delay(1000) - 1 / 0 - } - val launchB = launch { - try { - delay(1300) - log("launchB") - } catch (e: CancellationException) { - e.printStackTrace() - } +private val ioScope = CoroutineScope(Dispatchers.IO) + +private fun fetchDocs() { + ioScope.async { + delay(500) + log("taskA throw AssertionError") + throw AssertionError() } - launchA.join() - launchB.join() } ``` -```kotlin -kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=BlockingCoroutine{Cancelling}@5eb5c224 -Caused by: java.lang.ArithmeticException: / by zero - at coroutines.CoroutinesMainKt$main$1$launchA$1.invokeSuspend(CoroutinesMain.kt:11) - ··· -Exception in thread "main" java.lang.ArithmeticException: / by zero - at coroutines.CoroutinesMainKt$main$1$launchA$1.invokeSuspend(CoroutinesMain.kt:11) - ··· -``` - ## 1、CoroutineExceptionHandler -如果不想将所有的异常信息都打印到控制台上,那么可以使用 CoroutineExceptionHandler 作为协程的上下文元素之一,在这里进行自定义日志记录或异常处理,它类似于对线程使用 Thread.uncaughtExceptionHandler。但是,CoroutineExceptionHandler 只会在预计不会由用户处理的异常上调用,因此在 async 中使用它没有任何效果,当 async 内部发生了异常且没有捕获时,那么调用 `async.await()` 依然会导致应用崩溃 +如果想主动捕获异常信息,可以使用 CoroutineExceptionHandler 作为协程的上下文元素之一,在这里进行自定义日志记录或异常处理,它类似于对线程使用 `Thread.uncaughtExceptionHandler`。但是,CoroutineExceptionHandler 只会在预计不会由用户处理的异常上调用,因此在 async 中使用它没有任何效果,当 async 内部发生了异常且没有捕获时,那么调用 `async.await()` 依然会导致应用崩溃 以下代码只会捕获到 launch 抛出的异常 @@ -1124,11 +1133,11 @@ fun main() = runBlocking { ## 2、SupervisorJob -由于异常导致的取消在协程中是一种双向关系,会在整个协程层次结构中传播,但如果我们需要的是单向取消该怎么实现呢? +由于异常导致的取消在协程中是一种双向关系,会在整个协程层次结构中传播,那如果我们需要的是**单向取消**该怎么实现呢? 例如,假设在 Activity 中启动了多个协程,如果单个协程所代表的子任务失败了,此时并不一定需要连锁终止整个 Activity 内部的所有其它协程任务,即此时希望子协程的异常不会传播给同级协程和父协程。而当 Activity 退出后,父协程的异常(即 CancellationException)又应该连锁传播给所有子协程,终止所有子协程 -可以使用 SupervisorJob 来实现上述效果,它类似于常规的 Job,唯一的区别就是取消操作只会向下传播,一个子协程的运行失败不会影响到其他子协程 +可以使用 SupervisorJob 来实现上述效果,取消操作只会向下传播,一个子协程的运行失败不会影响到同级协程和父协程 例如,以下示例中 firstChild 抛出的异常不会导致 secondChild 被取消,但当 supervisor 被取消时 secondChild 也被同时取消了 @@ -1167,22 +1176,75 @@ fun main() = runBlocking { 但是,如果异常没有被处理且 CoroutineContext 没有包含一个 CoroutineExceptionHandler 的话,异常会到达默认线程的 ExceptionHandler。在 JVM 中,异常会被打印在控制台;而在 Android 中,无论异常在那个 Dispatcher 中发生,都会直接导致应用崩溃。所以如果上述例子中移除了 firstChild 包含的 CoroutineExceptionHandler 的话,就会导致 Android 应用崩溃 -💥 **未被捕获的异常一定会被抛出,无论使用的是哪种 Job** - -# 十、Android KTX +# 十、Android ktx -Android KTX 是包含在 Android [Jetpack](https://developer.android.google.cn/jetpack) 及其他 Android 库中的一组 Kotlin 扩展程序。KTX 扩展程序可以为 Jetpack、Android 平台及其他 API 提供简洁的惯用 Kotlin 代码。为此,这些扩展程序利用了多种 Kotlin 语言功能,其中就包括了对 Kotlin 协程的支持 +Android ktx 是包含在 Android Jetpack 及其他 Android 库中的一组 Kotlin 扩展程序。ktx 扩展程序可以为 Jetpack、Android 平台及其他 API 提供简洁的惯用 Kotlin 代码,这些扩展程序利用了多种 Kotlin 语言功能,其中就包括了对 Kotlin 协程的支持 -## 1、ViewModel KTX +## 1、Lifecycle ktx -ViewModel KTX 库提供了一个 `viewModelScope`,用于在 ViewModel 启动协程,该作用域的生命周期和 ViewModel 相等,当 ViewModel 回调了 `onCleared()`方法后会自动取消所有当前 ViewModel 中的所有协程 +Lifecycle ktx 为每个 Lifecycle 对象(Activity、Fragment、Process 等)定义了一个 LifecycleScope,该作用域具有生命周期安全的保障,在此范围内启动的协程会在 Lifecycle 被销毁时同时取消,可以使用 `lifecycle.coroutineScope` 或 `lifecycleOwner.lifecycleScope` 属性来拿到该 CoroutineScope -引入依赖: +引入依赖 ```groovy - dependencies { - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" +dependencies { + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0" +} +``` + +使用示例 + +```kotlin +class MyActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycleScope.launch { + //Do Something + } + lifecycle.coroutineScope.launch { + //Do Something + } } + +} +``` + +`lifecycleScope` 和 `lifecycle.coroutineScope` 两者是等价的,`lifecycleScope`只是 ktx 库提供的一种简便写法。从源码也可以看到,lifecycleScope 是存储在抽象类 Lifecycle 的 `mInternalScopeRef`字段中,且使用的是 SupervisorJob 和 `Dispatchers.Main.immediate`,因此我们不必担心任意子协程的异常情况会影响到全局的协程任务,且其默认就是在主线程运行协程 + +```kotlin +public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope + get() = lifecycle.coroutineScope + +public val Lifecycle.coroutineScope: LifecycleCoroutineScope + get() { + while (true) { + val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl? + if (existing != null) { + return existing + } + val newScope = LifecycleCoroutineScopeImpl( + this, + SupervisorJob() + Dispatchers.Main.immediate + ) + if (mInternalScopeRef.compareAndSet(null, newScope)) { + newScope.register() + return newScope + } + } + } +``` + +## 2、ViewModel ktx + +ViewModel ktx 库提供了一个 viewModelScope,用于在 ViewModel 中启动协程,该作用域的生命周期和 ViewModel 相等,当 ViewModel 回调了 `onCleared()`方法时会自动取消该作用域 + +引入依赖 + +```groovy +dependencies { + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0" +} ``` 例如,以下 `fetchDocs()` 方法内就依靠 `viewModelScope` 启动了一个协程,用于在后台线程发起网络请求 @@ -1202,60 +1264,139 @@ class MyViewModel : ViewModel() { } ``` -## 2、Lifecycle KTX - -Lifecycle KTX 为每个 [`Lifecycle`](https://developer.android.google.cn/topic/libraries/architecture/lifecycle) 对象定义了一个 `LifecycleScope`,该作用域具有生命周期安全的保障,在此范围内启动的协程会在 `Lifecycle` 被销毁时同时取消,可以使用 `lifecycle.coroutineScope` 或 `lifecycleOwner.lifecycleScope` 属性来拿到该 CoroutineScope - -引入依赖: +从源码可以看到其大体实现思路和`lifecycleScope` 类似,存储在 ViewModel 类的 `mBagOfTags` 这个 Map 中,且使用的也是 SupervisorJob 和 `Dispatchers.Main.immediate` -```groovy - dependencies { - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" +```kotlin +public 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) + ) } -``` -以下示例演示了如何使用 `lifecycleOwner.lifecycleScope` 异步创建预计算文本: +internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope { + override val coroutineContext: CoroutineContext = context -```kotlin -class MyFragment: Fragment() { - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - viewLifecycleOwner.lifecycleScope.launch { - val params = TextViewCompat.getTextMetricsParams(textView) - val precomputedText = withContext(Dispatchers.Default) { - PrecomputedTextCompat.create(longTextContent, params) - } - TextViewCompat.setPrecomputedText(textView, precomputedText) - } + override fun close() { + coroutineContext.cancel() } - } ``` -## 3、LiveData KTX +## 3、LiveData ktx -使用 LiveData 时,你可能需要异步计算值。例如,你可能需要检索用户的偏好设置并将其传送给界面。在这些情况下,LiveData KTX 提供了一个 `liveData` 构建器函数,该函数会调用 suspend 函数并将结果赋值给 LiveData +在某些情况下,我们需要先完成特定的异步计算任务,根据计算结果来向 LiveData 回调值,此时就可以使用 LiveData ktx 提供的 `liveData` 构建器函数来执行 suspend 函数所代表的异步计算任务(耗时任务),并将结果赋值给 LiveData -引入依赖: +引入依赖 ```groovy - dependencies { - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" - } +dependencies { + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0" +} ``` -在以下示例中,`loadUser()` 是在其他地方声明的 suspend 函数。 你可以使用 `liveData` 构建器函数异步调用 `loadUser()`,然后使用 `emit()` 来发出结果: +在以下示例中,`loadUser()` 是在其它地方声明的 suspend 函数,你可以使用 `liveData` 构建器函数异步调用 `loadUser()`,然后使用 `emit()` 来发出结果: ```kotlin val user: LiveData = liveData { - val data = database.loadUser() // loadUser is a suspend function. + val data = database.loadUser() emit(data) } ``` +从源码可以看到,我们所传入的 suspend 任务体 block 最终是会被 CoroutineLiveData 包装为一个 BlockRunner 对象,而 CoroutineLiveData 会在自身开始有 Observer 监听时执行 blockRunner,并在所有 Observer 均被移除时自动 Cancel 掉 blockRunner + +```kotlin +public fun liveData( + context: CoroutineContext = EmptyCoroutineContext, + timeoutInMs: Long = DEFAULT_TIMEOUT, + @BuilderInference block: suspend LiveDataScope.() -> Unit +): LiveData = CoroutineLiveData(context, timeoutInMs, block) + +internal class CoroutineLiveData( + context: CoroutineContext = EmptyCoroutineContext, + timeoutInMs: Long = DEFAULT_TIMEOUT, + block: Block +) : MediatorLiveData() { + private var blockRunner: BlockRunner? + private var emittedSource: EmittedSource? = null + + init { + val supervisorJob = SupervisorJob(context[Job]) + val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob) + blockRunner = BlockRunner( + liveData = this, + block = block, + timeoutInMs = timeoutInMs, + scope = scope + ) { + blockRunner = null + } + } + + override fun onActive() { + super.onActive() + blockRunner?.maybeRun() + } + + override fun onInactive() { + super.onInactive() + blockRunner?.cancel() + } +} + +internal class BlockRunner( + private val liveData: CoroutineLiveData, + private val block: Block, + private val timeoutInMs: Long, + private val scope: CoroutineScope, + private val onDone: () -> Unit +) { + // currently running block job. + private var runningJob: Job? = null + + // cancelation job created in cancel. + private var cancellationJob: Job? = null + + @MainThread + fun maybeRun() { + cancellationJob?.cancel() + cancellationJob = null + if (runningJob != null) { + return + } + runningJob = scope.launch { + val liveDataScope = LiveDataScopeImpl(liveData, coroutineContext) + block(liveDataScope) + onDone() + } + } + + @MainThread + fun cancel() { + if (cancellationJob != null) { + error("Cancel call cannot happen without a maybeRun") + } + cancellationJob = scope.launch(Dispatchers.Main.immediate) { + delay(timeoutInMs) + if (!liveData.hasActiveObservers()) { + runningJob?.cancel() + runningJob = null + } + } + } +} +``` + # 十一、参考资料 +本文参考了以下文章中的很多资料,在此表示感谢 + - https://github.com/Kotlin/Kotlinx.coroutines/blob/master/coroutines-guide.md - https://developer.android.google.cn/kotlin/coroutines - https://juejin.cn/post/6844904118180380680 diff --git "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250RxJava2.md" "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250RxJava2.md" index d0590c0..65f76c7 100644 --- "a/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250RxJava2.md" +++ "b/android_core/\344\270\200\346\226\207\345\277\253\351\200\237\345\205\245\351\227\250RxJava2.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 # 一、概述 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Android\344\270\273\346\265\201\345\261\217\345\271\225\351\200\202\351\205\215\346\226\271\346\241\210.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Android\344\270\273\346\265\201\345\261\217\345\271\225\351\200\202\351\205\215\346\226\271\346\241\210.md" index 6b1aa11..850e172 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Android\344\270\273\346\265\201\345\261\217\345\271\225\351\200\202\351\205\215\346\226\271\346\241\210.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Android\344\270\273\346\265\201\345\261\217\345\271\225\351\200\202\351\205\215\346\226\271\346\241\210.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 关于 Android 的屏幕适配现在已经有很多成熟的方案了,已经不是一个热门话题了。印象中 2018 年是讨论适配方案最火热的一段时间,那时候字节跳动技术团队发文介绍了其适配方案,之后就带动起了很多位大佬陆续发表文章进行讨论。当时我刚工作不久,在面试时也有被问到过关于屏幕适配的问题,因为对于一些概念认识不清导致回答得并不好 🤣🤣 所以本篇文章就想要从头到尾讲清楚关于屏幕适配的主要知识点,希望对你有所帮助,有错误也希望读者能够指出来 🤣🤣 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202AsyncTask.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202AsyncTask.md" index 81cc9e0..1d6510b 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202AsyncTask.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202AsyncTask.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 AsyncTask 是一个较为轻量级的异步任务类,在底层通过封装 ThreadPool 和 Handler 实现了线程的复用、后台任务执行顺序的控制、子线程和主线程的切换,使得开发者可以通过很简单的方法来执行一些耗时任务 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Fragment\347\232\204\346\226\271\346\226\271\351\235\242\351\235\242.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Fragment\347\232\204\346\226\271\346\226\271\351\235\242\351\235\242.md" index 590c779..31ced63 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Fragment\347\232\204\346\226\271\346\226\271\351\235\242\351\235\242.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Fragment\347\232\204\346\226\271\346\226\271\351\235\242\351\235\242.md" @@ -1,3 +1,7 @@ +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 + Fragment 是 Android 中历史十分悠久的一个组件,在 Android 3.0 (API 级别 11)的时候推出,时至今日已成为 Android 开发中最常用的组件之一 在一开始的时候,引入 Fragment 的目的是为了在大屏幕(如平板电脑)上能够更加动态和灵活地设计界面,被定义为一个 **轻量级 Activity** 而进行设计。通过 Fragment 可以将整个界面划分为多个分散的区域块,且同个 Fragment 可以被应用于多个 Activity 中,从而实现界面的**模块化**并提高**可重用性**。随着 Android 系统的逐渐升级,系统功能越来越丰富,Fragment 因此也自然而然的就被添加了很多和 Activity 完全一样的 API。例如,想要跳转到某个 Activity 并获取返回值,Activity 和 Fragment 就都加上了`startActivityForResult`方法;在 6.0 的时候有了运行时权限,就都加上了`requestPermissions`方法;在 8.0 的时候有了画中画模式,就又都加上了`onPictureInPictureModeChanged`方法 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Handler\346\234\272\345\210\266.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Handler\346\234\272\345\210\266.md" index 64d0b2d..761b53e 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Handler\346\234\272\345\210\266.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Handler\346\234\272\345\210\266.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 Handler 在整个 Android 开发体系中占据着很重要的地位,是一种标准的事件驱动模型,对开发者来说起到的作用很明确,就是为了实现线程切换或者是执行延时任务,稍微更高级一点的用法可能是为了保证多个任务在执行时的有序性。由于 Android 系统中的主线程有特殊地位,所以像 EventBus 和 Retrofit 这类并非 Android 独有的三方库,都是通过 Handler 来实现对 Android 系统的特殊平台支持。大部分开发者都已经对如何使用 Handler 很熟悉了,这里就再来了解下其内部具体是如何实现的,希望对你有所帮助 🤣🤣 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Java\345\222\214Kotlin\347\232\204\346\263\233\345\236\213\351\232\276\347\202\271.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Java\345\222\214Kotlin\347\232\204\346\263\233\345\236\213\351\232\276\347\202\271.md" index 5a36e97..9a6c519 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Java\345\222\214Kotlin\347\232\204\346\263\233\345\236\213\351\232\276\347\202\271.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202Java\345\222\214Kotlin\347\232\204\346\263\233\345\236\213\351\232\276\347\202\271.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 Java 和 Kotlin 的泛型算作是一块挺大的知识难点了,涉及到很多很难理解的概念:**泛型型参、泛型实参、类型参数、不变、型变、协变、逆变、内联**等等。本篇文章就将 Java 和 Kotlin 结合着一起讲,按照我的个人理解来阐述泛型的各个知识难点,希望对你有所帮助 🤣🤣 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202SharedPreferences\347\232\204\347\274\272\351\231\267\345\217\212\344\270\200\347\202\271\347\202\271\346\200\235\350\200\203.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202SharedPreferences\347\232\204\347\274\272\351\231\267\345\217\212\344\270\200\347\202\271\347\202\271\346\200\235\350\200\203.md" index 3ac3175..9ad6de7 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202SharedPreferences\347\232\204\347\274\272\351\231\267\345\217\212\344\270\200\347\202\271\347\202\271\346\200\235\350\200\203.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202SharedPreferences\347\232\204\347\274\272\351\231\267\345\217\212\344\270\200\347\202\271\347\202\271\346\200\235\350\200\203.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 SharedPreferences 是系统提供的一个适合用于存储少量键值对数据的持久化存储方案,结构简单,使用方便,很多应用都会使用到。另一方面,SharedPreferences 存在的问题也挺多的,当中 ANR 问题就屡见不鲜,字节跳动技术团队就曾经发布过一篇文章专门来阐述该问题:[剖析 SharedPreference apply 引起的 ANR 问题](https://mp.weixin.qq.com/s?__biz=MzI1MzYzMjE0MQ==&mid=2247484387&idx=1&sn=e3c8d6ef52520c51b5e07306d9750e70&scene=21#wechat_redirect)。到了现在,Google Jetpack 也推出了一套新的持久化存储方案:DataStore,大有取代 SharedPreferences 的趋势 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View&Window\346\234\272\345\210\266.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View&Window\346\234\272\345\210\266.md" index 22c1cad..4d7f35e 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View&Window\346\234\272\345\210\266.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View&Window\346\234\272\345\210\266.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 Android 系统中,Window 在代码层次上是一个抽象类,在概念上表示的是一个窗口。Android 中所有的视图都是通过 Window 来呈现的,例如 Activity、Dialog 和 Toast 等,它们实际上都是挂载在 Window 上的。大部分情况下应用层开发者很少需要来和 Window 打交道,Activity 已经隐藏了 Window 的具体实现逻辑了,但我觉得来了解 Window 机制的一个比较大的好处是**可以加深我们对 View 绘制流程以及事件分发机制的了解**,这两个操作就涉及到我们的日常开发了,实现自定义 View 和解决 View 的滑动冲突时都需要我们掌握这方面的知识点,而这两个操作和 Window 机制有很大的关联。视图树只有被挂载到 Window 后才会触发视图树的绘制流程,之后视图树才有机会接收到用户的触摸事件。也就是说,视图树被挂载到了 Window 上是 Activity 和 Dialog 能够展示到屏幕上且和用户做交互的前置条件 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202ViewPost\347\232\204\345\216\237\347\220\206\345\217\212\347\274\272\351\231\267.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202ViewPost\347\232\204\345\216\237\347\220\206\345\217\212\347\274\272\351\231\267.md" index f311a33..071f923 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202ViewPost\347\232\204\345\216\237\347\220\206\345\217\212\347\274\272\351\231\267.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202ViewPost\347\232\204\345\216\237\347\220\206\345\217\212\347\274\272\351\231\267.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 很多开发者都了解这么一个知识点:在 Activity 的 `onCreate` 方法里我们无法直接获取到 View 的宽高信息,但通过 `View.post(Runnable)`这种方式就可以,那背后的具体原因你是否有了解过呢? diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View\344\272\213\344\273\266\345\210\206\345\217\221\346\234\272\345\210\266.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View\344\272\213\344\273\266\345\210\206\345\217\221\346\234\272\345\210\266.md" index 0d15948..e1f6135 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View\344\272\213\344\273\266\345\210\206\345\217\221\346\234\272\345\210\266.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View\344\272\213\344\273\266\345\210\206\345\217\221\346\234\272\345\210\266.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 View 的事件分发机制一直是 Android 开发中比较难啃的一块知识点,想要理顺 MotionEvent 在 ViewGroup 和 View 这两者之间流转的规则十分不容易,整个过程涉及分发、拦截、消费三个过程,每个过程根据返回值的不同在流程就会有很大差别,且 Activity 也会参与进这个过程,不参照源码进行分析的话就很难明白触摸事件的分发规则。在很久前我就想过要来动笔写这一块知识点,熬夜肝了一篇,希望对你有所帮助 🤣🤣 diff --git "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View\347\232\204MeasureLayoutDraw\346\265\201\347\250\213.md" "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View\347\232\204MeasureLayoutDraw\346\265\201\347\250\213.md" index bf3ff5d..96dc7b5 100644 --- "a/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View\347\232\204MeasureLayoutDraw\346\265\201\347\250\213.md" +++ "b/android_core/\344\270\200\346\226\207\350\257\273\346\207\202View\347\232\204MeasureLayoutDraw\346\265\201\347\250\213.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 Activity 通过 Window 与 View 系统进行交互,Window 再通过 ViewRootImpl 与 DecorView(视图树的根布局) 进行交互。平时我们都是通过 `setContentView` 方法来指定 Activity 要承载的具体布局文件,布局文件中可能包含多个 ViewGroup 和 View,Activity 势必需要将所有 View 绘制到屏幕上,整个绘制流程就分为 measure、layout、draw 三大步骤。作为应用层的开发者,我们很多时候都需要通过实现自定义 View 来满足一些产品需求,这就要求我们对 View 的绘制流程有一个比较清晰的认知,本文就来介绍下这整个流程是如何流转的,希望对你有所帮助 🤣🤣 diff --git "a/android_core/\344\270\200\346\255\245\346\255\245\345\260\201\350\243\205\345\256\236\347\216\260\350\207\252\345\267\261\347\232\204\347\275\221\347\273\234\350\257\267\346\261\202\346\241\206\346\236\2663.0.md" "b/android_core/\344\270\200\346\255\245\346\255\245\345\260\201\350\243\205\345\256\236\347\216\260\350\207\252\345\267\261\347\232\204\347\275\221\347\273\234\350\257\267\346\261\202\346\241\206\346\236\2663.0.md" index 4fdd30c..26a83e9 100644 --- "a/android_core/\344\270\200\346\255\245\346\255\245\345\260\201\350\243\205\345\256\236\347\216\260\350\207\252\345\267\261\347\232\204\347\275\221\347\273\234\350\257\267\346\261\202\346\241\206\346\236\2663.0.md" +++ "b/android_core/\344\270\200\346\255\245\346\255\245\345\260\201\350\243\205\345\256\236\347\216\260\350\207\252\345\267\261\347\232\204\347\275\221\347\273\234\350\257\267\346\261\202\346\241\206\346\236\2663.0.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 在 2019 年的时候,我先后写过两篇文章来介绍我是如何一步步封装实现一个网络请求框架的,可以分别看做是 1.0 和 2.0 版本 😇😇 diff --git "a/android_core/\346\216\242\347\251\266Kotlin\347\232\204\351\232\220\350\227\217\346\200\247\350\203\275\345\274\200\351\224\200\344\270\216\351\201\277\345\235\221\346\214\207\345\215\227.md" "b/android_core/\346\216\242\347\251\266Kotlin\347\232\204\351\232\220\350\227\217\346\200\247\350\203\275\345\274\200\351\224\200\344\270\216\351\201\277\345\235\221\346\214\207\345\215\227.md" index 4ce9cfb..fa0c442 100644 --- "a/android_core/\346\216\242\347\251\266Kotlin\347\232\204\351\232\220\350\227\217\346\200\247\350\203\275\345\274\200\351\224\200\344\270\216\351\201\277\345\235\221\346\214\207\345\215\227.md" +++ "b/android_core/\346\216\242\347\251\266Kotlin\347\232\204\351\232\220\350\227\217\346\200\247\350\203\275\345\274\200\351\224\200\344\270\216\351\201\277\345\235\221\346\214\207\345\215\227.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://link.juejin.cn/?target=https%3A%2F%2Fp6-juejin.byteimg.com%2Ftos-cn-i-k3u1fbpfcp%2F0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image) 希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 在 2019 年 Google I/O 大会上,Google 宣布了今后 Android 开发将优先使用 Kotlin ,即 Kotlin-first,随之在 Android 开发界兴起了一阵全民学习 Kotlin 的热潮。之后 Google 也推出了一系列用 Kotlin 实现的 ktx 扩展库,例如 `activity-ktx`、`fragment-ktx`、`core-ktx`等,提供了各种方便的扩展方法用于简化开发者的工作,Kotlin 协程目前也是官方在 Android 上进行异步编程的推荐解决方案 diff --git "a/android_core/\350\201\212\350\201\212Activity\347\232\204\345\220\257\345\212\250\346\250\241\345\274\217.md" "b/android_core/\350\201\212\350\201\212Activity\347\232\204\345\220\257\345\212\250\346\250\241\345\274\217.md" index 2df9761..d407329 100644 --- "a/android_core/\350\201\212\350\201\212Activity\347\232\204\345\220\257\345\212\250\346\250\241\345\274\217.md" +++ "b/android_core/\350\201\212\350\201\212Activity\347\232\204\345\220\257\345\212\250\346\250\241\345\274\217.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 Activity 的启动模式本身是一个挺难理解的知识点,大多数开发者对这个概念的了解可能只限于四种 launchMode 属性值,但启动模式其实还需要受 Intent flag 的影响。而且 Activity 启动模式并不只是单纯地用来启动一个 Activity,实际上还会直接影响到用户的直观感受和使用体验,因为启动模式直接就决定了应用的任务栈和返回栈,这都是用户能直接接触到的 diff --git "a/android_core/\350\201\212\350\201\212Bitmap\347\232\204\344\270\200\344\272\233\347\237\245\350\257\206\347\202\271.md" "b/android_core/\350\201\212\350\201\212Bitmap\347\232\204\344\270\200\344\272\233\347\237\245\350\257\206\347\202\271.md" index 07010f7..f763567 100644 --- "a/android_core/\350\201\212\350\201\212Bitmap\347\232\204\344\270\200\344\272\233\347\237\245\350\257\206\347\202\271.md" +++ "b/android_core/\350\201\212\350\201\212Bitmap\347\232\204\344\270\200\344\272\233\347\237\245\350\257\206\347\202\271.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 Bitmap 应该是很多应用中最占据内存空间的一类资源了,Bitmap 也是导致应用 OOM 的常见原因之一。例如,Pixel 手机的相机拍摄的照片最大可达 4048 * 3036 像素(1200 万像素),如果使用的位图配置为 ARGB_8888(Android 2.3 及更高版本的默认设置),将单张照片加载到内存大约需要 48MB 内存(4048 * 3036 * 4 字节),如此庞大的内存需求可能会立即耗尽应用的所有可用内存 diff --git "a/android_core/\350\201\212\350\201\212Context\347\232\204\344\270\200\344\272\233\347\237\245\350\257\206\347\202\271.md" "b/android_core/\350\201\212\350\201\212Context\347\232\204\344\270\200\344\272\233\347\237\245\350\257\206\347\202\271.md" index 2e9a38f..36f7514 100644 --- "a/android_core/\350\201\212\350\201\212Context\347\232\204\344\270\200\344\272\233\347\237\245\350\257\206\347\202\271.md" +++ "b/android_core/\350\201\212\350\201\212Context\347\232\204\344\270\200\344\272\233\347\237\245\350\257\206\347\202\271.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 Context 在我们的日常开发中经常会使用到,在代码层次上指的是**一个抽象类**,在概念上指的是**上下文环境**,在功能上则起到了**访问系统服务及系统资源的作用**。Activity、Service 和 Application 都间接地继承于 Context diff --git "a/android_jetpack/1-Lifecycle\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_jetpack/1-Lifecycle\346\272\220\347\240\201\350\257\246\350\247\243.md" index 69a44a6..c6eed96 100644 --- "a/android_jetpack/1-Lifecycle\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_jetpack/1-Lifecycle\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 对于现在的 Android Developer 来说,Google Jetpack 可以说是最为基础的架构组件之一了,自从推出以后极大地改变了我们的开发模式并降低了开发难度,这也要求我们对当中一些子组件的实现原理具有一定程度的了解,所以我就打算来写一系列关于 Jetpack 源码解析的文章,希望对你有所帮助 🤣🤣 diff --git "a/android_jetpack/2-Lifecycle\350\241\215\347\224\237\347\211\251\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_jetpack/2-Lifecycle\350\241\215\347\224\237\347\211\251\346\272\220\347\240\201\350\257\246\350\247\243.md" index 9b1f512..d8974a3 100644 --- "a/android_jetpack/2-Lifecycle\350\241\215\347\224\237\347\211\251\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_jetpack/2-Lifecycle\350\241\215\347\224\237\347\211\251\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 对于现在的 Android Developer 来说,Google Jetpack 可以说是最为基础的架构组件之一了,自从推出以后极大地改变了我们的开发模式并降低了开发难度,这也要求我们对当中一些子组件的实现原理具有一定程度的了解,所以我就打算来写一系列关于 Jetpack 源码解析的文章,希望对你有所帮助 🤣🤣 diff --git "a/android_jetpack/3-LiveData\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_jetpack/3-LiveData\346\272\220\347\240\201\350\257\246\350\247\243.md" index 831a3b2..77291d3 100644 --- "a/android_jetpack/3-LiveData\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_jetpack/3-LiveData\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 对于现在的 Android Developer 来说,Google Jetpack 可以说是最为基础的架构组件之一了,自从推出以后极大地改变了我们的开发模式并降低了开发难度,这也要求我们对当中一些子组件的实现原理具有一定程度的了解,所以我就打算来写一系列关于 Jetpack 源码解析的文章,希望对你有所帮助 🤣🤣 diff --git "a/android_jetpack/4-LiveData\350\241\215\347\224\237\347\211\251\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_jetpack/4-LiveData\350\241\215\347\224\237\347\211\251\346\272\220\347\240\201\350\257\246\350\247\243.md" index e7254b4..d8362fd 100644 --- "a/android_jetpack/4-LiveData\350\241\215\347\224\237\347\211\251\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_jetpack/4-LiveData\350\241\215\347\224\237\347\211\251\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 对于现在的 Android Developer 来说,Google Jetpack 可以说是最为基础的架构组件之一了,自从推出以后极大地改变了我们的开发模式并降低了开发难度,这也要求我们对当中一些子组件的实现原理具有一定程度的了解,所以我就打算来写一系列关于 Jetpack 源码解析的文章,希望对你有所帮助 🤣🤣 diff --git "a/android_jetpack/5-Startup\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_jetpack/5-Startup\346\272\220\347\240\201\350\257\246\350\247\243.md" index 9023fcc..ee66713 100644 --- "a/android_jetpack/5-Startup\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_jetpack/5-Startup\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 对于现在的 Android Developer 来说,Google Jetpack 可以说是最为基础的架构组件之一了,自从推出以后极大地改变了我们的开发模式并降低了开发难度,这也要求我们对当中一些子组件的实现原理具有一定程度的了解,所以我就打算来写一系列关于 Jetpack 源码解析的文章,希望对你有所帮助 🤣🤣 diff --git "a/android_jetpack/6-ViewModel\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_jetpack/6-ViewModel\346\272\220\347\240\201\350\257\246\350\247\243.md" index 80991cf..ce40e34 100644 --- "a/android_jetpack/6-ViewModel\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_jetpack/6-ViewModel\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 对于现在的 Android Developer 来说,Google Jetpack 可以说是最为基础的架构组件之一了,自从推出以后极大地改变了我们的开发模式并降低了开发难度,这也要求我们对当中一些子组件的实现原理具有一定程度的了解,所以我就打算来写一系列关于 Jetpack 源码解析的文章,希望对你有所帮助 🤣🤣 diff --git "a/android_jetpack/7-SavedStateHandle\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_jetpack/7-SavedStateHandle\346\272\220\347\240\201\350\257\246\350\247\243.md" index a9431a8..6552d88 100644 --- "a/android_jetpack/7-SavedStateHandle\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_jetpack/7-SavedStateHandle\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 对于现在的 Android Developer 来说,Google Jetpack 可以说是最为基础的架构组件之一了,自从推出以后极大地改变了我们的开发模式并降低了开发难度,这也要求我们对当中一些子组件的实现原理具有一定程度的了解,所以我就打算来写一系列关于 Jetpack 源码解析的文章,希望对你有所帮助 🤣🤣 diff --git "a/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246Jetpack Compose\345\206\231\344\270\200\344\270\252IMAPP\357\274\210\344\270\200\357\274\211.md" "b/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246Jetpack Compose\345\206\231\344\270\200\344\270\252IMAPP\357\274\210\344\270\200\357\274\211.md" index 72637f4..19bfc78 100644 --- "a/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246Jetpack Compose\345\206\231\344\270\200\344\270\252IMAPP\357\274\210\344\270\200\357\274\211.md" +++ "b/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246Jetpack Compose\345\206\231\344\270\200\344\270\252IMAPP\357\274\210\344\270\200\357\274\211.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1abcd1e5ce9a4f36b6f150500ef65fa8~tplv-k3u1fbpfcp-zoom-1.image) diff --git "a/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246Jetpack Compose\345\206\231\344\270\200\344\270\252IMAPP\357\274\210\344\272\214\357\274\211.md" "b/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246Jetpack Compose\345\206\231\344\270\200\344\270\252IMAPP\357\274\210\344\272\214\357\274\211.md" index 1f9f017..11b4968 100644 --- "a/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246Jetpack Compose\345\206\231\344\270\200\344\270\252IMAPP\357\274\210\344\272\214\357\274\211.md" +++ "b/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246Jetpack Compose\345\206\231\344\270\200\344\270\252IMAPP\357\274\210\344\272\214\357\274\211.md" @@ -1,4 +1,4 @@ -> 公众号:字节数组 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > > 希望对你有所帮助 🤣🤣 diff --git "a/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246JetpackCompose\347\216\251\344\270\200\346\212\212\344\277\204\347\275\227\346\226\257\346\226\271\345\235\227.md" "b/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246JetpackCompose\347\216\251\344\270\200\346\212\212\344\277\204\347\275\227\346\226\257\346\226\271\345\235\227.md" index 20a03f7..b50ab98 100644 --- "a/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246JetpackCompose\347\216\251\344\270\200\346\212\212\344\277\204\347\275\227\346\226\257\346\226\271\345\235\227.md" +++ "b/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246JetpackCompose\347\216\251\344\270\200\346\212\212\344\277\204\347\275\227\346\226\257\346\226\271\345\235\227.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 你的童年是否有**俄罗斯方块**呢 🤣🤣 本文就来介绍如何通过 Jetpack Compose 实现一个俄罗斯方块 ~~ diff --git "a/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246JetpackCompose\350\207\252\345\256\232\344\271\211View\344\275\240\345\255\246\344\270\215\345\255\246.md" "b/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246JetpackCompose\350\207\252\345\256\232\344\271\211View\344\275\240\345\255\246\344\270\215\345\255\246.md" index 73f9ab0..71f0f7c 100644 --- "a/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246JetpackCompose\350\207\252\345\256\232\344\271\211View\344\275\240\345\255\246\344\270\215\345\255\246.md" +++ "b/android_jetpack_compose/\345\255\246\344\270\215\345\212\250\344\271\237\350\246\201\345\255\246JetpackCompose\350\207\252\345\256\232\344\271\211View\344\275\240\345\255\246\344\270\215\345\255\246.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 我曾经写过两篇文章,分别在不同平台上实现了同一种自定义 View 效果: diff --git "a/android_opensource/1-EventBus\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_opensource/1-EventBus\346\272\220\347\240\201\350\257\246\350\247\243.md" index a186b1d..228775a 100644 --- "a/android_opensource/1-EventBus\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_opensource/1-EventBus\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/10-Glide\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\347\237\245\350\257\206\347\202\271.md" "b/android_opensource/10-Glide\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\347\237\245\350\257\206\347\202\271.md" index f81a7c3..b9127e3 100644 --- "a/android_opensource/10-Glide\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\347\237\245\350\257\206\347\202\271.md" +++ "b/android_opensource/10-Glide\344\275\240\345\217\257\350\203\275\344\270\215\347\237\245\351\201\223\347\232\204\347\237\245\350\257\206\347\202\271.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/11-OkHttp\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_opensource/11-OkHttp\346\272\220\347\240\201\350\257\246\350\247\243.md" index e013bf6..87256fb 100644 --- "a/android_opensource/11-OkHttp\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_opensource/11-OkHttp\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -39,8 +39,10 @@ @@: ``` -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/12-OkHttp_Retrofit\345\274\200\345\217\221\350\260\203\350\257\225\345\210\251\345\231\250.md" "b/android_opensource/12-OkHttp_Retrofit\345\274\200\345\217\221\350\260\203\350\257\225\345\210\251\345\231\250.md" index 27e8a8b..abca0a8 100644 --- "a/android_opensource/12-OkHttp_Retrofit\345\274\200\345\217\221\350\260\203\350\257\225\345\210\251\345\231\250.md" +++ "b/android_opensource/12-OkHttp_Retrofit\345\274\200\345\217\221\350\260\203\350\257\225\345\210\251\345\231\250.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/13-\345\217\257\350\203\275\346\230\257\345\205\250\347\275\221\347\254\254\344\270\200\347\257\207Coil\347\232\204\346\272\220\347\240\201\345\210\206\346\236\220\346\226\207\347\253\240.md" "b/android_opensource/13-\345\217\257\350\203\275\346\230\257\345\205\250\347\275\221\347\254\254\344\270\200\347\257\207Coil\347\232\204\346\272\220\347\240\201\345\210\206\346\236\220\346\226\207\347\253\240.md" index 9112b1b..0022df8 100644 --- "a/android_opensource/13-\345\217\257\350\203\275\346\230\257\345\205\250\347\275\221\347\254\254\344\270\200\347\257\207Coil\347\232\204\346\272\220\347\240\201\345\210\206\346\236\220\346\226\207\347\253\240.md" +++ "b/android_opensource/13-\345\217\257\350\203\275\346\230\257\345\205\250\347\275\221\347\254\254\344\270\200\347\257\207Coil\347\232\204\346\272\220\347\240\201\345\210\206\346\236\220\346\226\207\347\253\240.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/2-EventBus\350\207\252\345\267\261\345\256\236\347\216\260\344\270\200\344\270\252.md" "b/android_opensource/2-EventBus\350\207\252\345\267\261\345\256\236\347\216\260\344\270\200\344\270\252.md" index 0bbeef4..652978f 100644 --- "a/android_opensource/2-EventBus\350\207\252\345\267\261\345\256\236\347\216\260\344\270\200\344\270\252.md" +++ "b/android_opensource/2-EventBus\350\207\252\345\267\261\345\256\236\347\216\260\344\270\200\344\270\252.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/3-ARouter\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_opensource/3-ARouter\346\272\220\347\240\201\350\257\246\350\247\243.md" index 8c06d81..d2acbe1 100644 --- "a/android_opensource/3-ARouter\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_opensource/3-ARouter\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/4-ARouter\350\207\252\345\267\261\345\256\236\347\216\260\344\270\200\344\270\252.md" "b/android_opensource/4-ARouter\350\207\252\345\267\261\345\256\236\347\216\260\344\270\200\344\270\252.md" index 0fc1f0d..9be2520 100644 --- "a/android_opensource/4-ARouter\350\207\252\345\267\261\345\256\236\347\216\260\344\270\200\344\270\252.md" +++ "b/android_opensource/4-ARouter\350\207\252\345\267\261\345\256\236\347\216\260\344\270\200\344\270\252.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/5-LeakCanary\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_opensource/5-LeakCanary\346\272\220\347\240\201\350\257\246\350\247\243.md" index 63d04a1..5e374f5 100644 --- "a/android_opensource/5-LeakCanary\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_opensource/5-LeakCanary\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/6-LeakCanary\346\211\251\345\261\225\351\230\205\350\257\273.md" "b/android_opensource/6-LeakCanary\346\211\251\345\261\225\351\230\205\350\257\273.md" index bcee364..e339705 100644 --- "a/android_opensource/6-LeakCanary\346\211\251\345\261\225\351\230\205\350\257\273.md" +++ "b/android_opensource/6-LeakCanary\346\211\251\345\261\225\351\230\205\350\257\273.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/7-Retrofit\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_opensource/7-Retrofit\346\272\220\347\240\201\350\257\246\350\247\243.md" index 38a85aa..f83e593 100644 --- "a/android_opensource/7-Retrofit\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_opensource/7-Retrofit\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/8-Retrofit\344\270\216LiveData\347\232\204\347\273\223\345\220\210\344\275\277\347\224\250.md" "b/android_opensource/8-Retrofit\344\270\216LiveData\347\232\204\347\273\223\345\220\210\344\275\277\347\224\250.md" index 19f7a59..d8c3551 100644 --- "a/android_opensource/8-Retrofit\344\270\216LiveData\347\232\204\347\273\223\345\220\210\344\275\277\347\224\250.md" +++ "b/android_opensource/8-Retrofit\344\270\216LiveData\347\232\204\347\273\223\345\220\210\344\275\277\347\224\250.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/android_opensource/9-Glide\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/android_opensource/9-Glide\346\272\220\347\240\201\350\257\246\350\247\243.md" index 3693edc..57c782f 100644 --- "a/android_opensource/9-Glide\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/android_opensource/9-Glide\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -1,5 +1,7 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) > +> 希望对你有所帮助 🤣🤣 + > 对于 Android Developer 来说,很多开源库都是属于**开发必备**的知识点,从使用方式到实现原理再到源码解析,这些都需要我们有一定程度的了解和运用能力。所以我打算来写一系列关于开源库**源码解析**和**实战演练**的文章,初定的目标是 **EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil** 等七个知名开源库,希望对你有所帮助 🤣🤣 系列文章导航: diff --git "a/flutter_core/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" "b/flutter_core/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" index c674c86..432b97a 100644 --- "a/flutter_core/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" +++ "b/flutter_core/Dart\345\205\245\351\227\250\346\225\231\347\250\213.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 # 一、开篇 diff --git "a/flutter_core/Flutter\345\256\236\346\210\230-\347\224\250\350\264\235\345\241\236\345\260\224\346\233\262\347\272\277\347\224\273\344\270\200\344\270\252\345\270\246\346\226\207\346\234\254\347\232\204\346\263\242\346\265\252\347\220\203Widget.md" "b/flutter_core/Flutter\345\256\236\346\210\230-\347\224\250\350\264\235\345\241\236\345\260\224\346\233\262\347\272\277\347\224\273\344\270\200\344\270\252\345\270\246\346\226\207\346\234\254\347\232\204\346\263\242\346\265\252\347\220\203Widget.md" index 4df819f..6c6b630 100644 --- "a/flutter_core/Flutter\345\256\236\346\210\230-\347\224\250\350\264\235\345\241\236\345\260\224\346\233\262\347\272\277\347\224\273\344\270\200\344\270\252\345\270\246\346\226\207\346\234\254\347\232\204\346\263\242\346\265\252\347\220\203Widget.md" +++ "b/flutter_core/Flutter\345\256\236\346\210\230-\347\224\250\350\264\235\345\241\236\345\260\224\346\233\262\347\272\277\347\224\273\344\270\200\344\270\252\345\270\246\346\226\207\346\234\254\347\232\204\346\263\242\346\265\252\347\220\203Widget.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://link.juejin.cn/?target=https%3A%2F%2Fp6-juejin.byteimg.com%2Ftos-cn-i-k3u1fbpfcp%2F0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image) 希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 flutter 中的自定义 Widget 算作是 flutter 体系中比较高阶的知识点之一了,相当于原生开发中的自定义 View,以我个人的感受来说,自定义 widget 的难度要低于自定义 View,不过由于当前 flutter 的开源库还不算多丰富,所以有些效果还是需要开发者自己动手来实现,而本篇文章就来介绍如何用 flutter 来实现一个带文本的波浪球 Widget,实现的的效果如下所示: diff --git "a/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" index a51e00d..f4c4c62 100644 --- "a/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" +++ "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2101\357\274\211.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 本系列文章会陆续对 Java 和 Android 的集合框架(JDK 1.8,Android SDK 30)中的几个常见容器结合源码进行介绍,了解不同容器在**数据结构、适用场景、优势点**上的不同,希望对你有所帮助 🤣🤣 diff --git "a/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" index 932a27a..e538031 100644 --- "a/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" +++ "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2102\357\274\211.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 本系列文章会陆续对 Java 和 Android 的集合框架(JDK 1.8,Android SDK 30)中的几个常见容器结合源码进行介绍,了解不同容器在**数据结构、适用场景、优势点**上的不同,希望对你有所帮助 🤣🤣 diff --git "a/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" index f3a84d4..8687ab4 100644 --- "a/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" +++ "b/java_android_collections/Java&Android\351\233\206\345\220\210\346\241\206\346\236\266\351\241\273\347\237\245\351\241\273\344\274\232\357\274\2103\357\274\211.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 本系列文章会陆续对 Java 和 Android 的集合框架(JDK 1.8,Android SDK 30)中的几个常见容器结合源码进行介绍,了解不同容器在**数据结构、适用场景、优势点**上的不同,希望对你有所帮助 🤣🤣 diff --git "a/java_multithreading/1-\344\273\200\344\271\210\346\230\257\345\244\232\347\272\277\347\250\213.md" "b/java_multithreading/1-\344\273\200\344\271\210\346\230\257\345\244\232\347\272\277\347\250\213.md" index 41610f7..5ffe1d3 100644 --- "a/java_multithreading/1-\344\273\200\344\271\210\346\230\257\345\244\232\347\272\277\347\250\213.md" +++ "b/java_multithreading/1-\344\273\200\344\271\210\346\230\257\345\244\232\347\272\277\347\250\213.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > > 目前,多线程编程可以说是在大部分平台和应用上都需要实现的一个基本需求。本系列文章就来对 **Java 平台下的多线程编程知识**进行讲解,从**概念入门**、**底层实现**到**上层应用**都会涉及到,预计一共会有五篇文章,希望对你有所帮助 😎😎 > diff --git "a/java_multithreading/2-\346\200\216\344\271\210\345\256\236\347\216\260\345\244\232\347\272\277\347\250\213\345\220\214\346\255\245.md" "b/java_multithreading/2-\346\200\216\344\271\210\345\256\236\347\216\260\345\244\232\347\272\277\347\250\213\345\220\214\346\255\245.md" index 1185294..2850953 100644 --- "a/java_multithreading/2-\346\200\216\344\271\210\345\256\236\347\216\260\345\244\232\347\272\277\347\250\213\345\220\214\346\255\245.md" +++ "b/java_multithreading/2-\346\200\216\344\271\210\345\256\236\347\216\260\345\244\232\347\272\277\347\250\213\345\220\214\346\255\245.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > > 目前,多线程编程可以说是在大部分平台和应用上都需要实现的一个基本需求。本系列文章就来对 **Java 平台下的多线程编程知识**进行讲解,从**概念入门**、**底层实现**到**上层应用**都会涉及到,预计一共会有五篇文章,希望对你有所帮助 😎😎 > diff --git "a/java_multithreading/3-\347\272\277\347\250\213\346\264\273\346\200\247\346\225\205\351\232\234\346\234\211\345\223\252\344\272\233.md" "b/java_multithreading/3-\347\272\277\347\250\213\346\264\273\346\200\247\346\225\205\351\232\234\346\234\211\345\223\252\344\272\233.md" index 685aa08..683330c 100644 --- "a/java_multithreading/3-\347\272\277\347\250\213\346\264\273\346\200\247\346\225\205\351\232\234\346\234\211\345\223\252\344\272\233.md" +++ "b/java_multithreading/3-\347\272\277\347\250\213\346\264\273\346\200\247\346\225\205\351\232\234\346\234\211\345\223\252\344\272\233.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > > 目前,多线程编程可以说是在大部分平台和应用上都需要实现的一个基本需求。本系列文章就来对 **Java 平台下的多线程编程知识**进行讲解,从**概念入门**、**底层实现**到**上层应用**都会涉及到,预计一共会有五篇文章,希望对你有所帮助 😎😎 > diff --git "a/java_multithreading/4-\351\224\201\347\232\204\345\210\206\347\261\273\346\234\211\350\277\231\344\271\210\345\244\232.md" "b/java_multithreading/4-\351\224\201\347\232\204\345\210\206\347\261\273\346\234\211\350\277\231\344\271\210\345\244\232.md" index 8ef4436..044c446 100644 --- "a/java_multithreading/4-\351\224\201\347\232\204\345\210\206\347\261\273\346\234\211\350\277\231\344\271\210\345\244\232.md" +++ "b/java_multithreading/4-\351\224\201\347\232\204\345\210\206\347\261\273\346\234\211\350\277\231\344\271\210\345\244\232.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > > 目前,多线程编程可以说是在大部分平台和应用上都需要实现的一个基本需求。本系列文章就来对 **Java 平台下的多线程编程知识**进行讲解,从**概念入门**、**底层实现**到**上层应用**都会涉及到,预计一共会有五篇文章,希望对你有所帮助 😎😎 > diff --git "a/java_multithreading/5-\350\266\205\350\257\246\347\273\206\347\232\204ThreadPoolExecutor\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/java_multithreading/5-\350\266\205\350\257\246\347\273\206\347\232\204ThreadPoolExecutor\346\272\220\347\240\201\350\247\243\346\236\220.md" index 5330884..c8adeef 100644 --- "a/java_multithreading/5-\350\266\205\350\257\246\347\273\206\347\232\204ThreadPoolExecutor\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/java_multithreading/5-\350\266\205\350\257\246\347\273\206\347\232\204ThreadPoolExecutor\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > > 目前,多线程编程可以说是在大部分平台和应用上都需要实现的一个基本需求。本系列文章就来对 **Java 平台下的多线程编程知识**进行讲解,从**概念入门**、**底层实现**到**上层应用**都会涉及到,预计一共会有五篇文章,希望对你有所帮助 😎😎 > diff --git "a/kotlin_core/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" "b/kotlin_core/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" index a9df591..7e54325 100644 --- "a/kotlin_core/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" +++ "b/kotlin_core/1-\345\215\217\347\250\213\345\237\272\347\241\200.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" "b/kotlin_core/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" index a7e0cc1..5616dd7 100644 --- "a/kotlin_core/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" +++ "b/kotlin_core/2-\345\217\226\346\266\210\345\222\214\350\266\205\346\227\266.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" "b/kotlin_core/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" index b2e8c03..a2dec0e 100644 --- "a/kotlin_core/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" +++ "b/kotlin_core/3-\347\273\204\345\220\210\346\214\202\350\265\267\345\207\275\346\225\260.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" "b/kotlin_core/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" index 2ec7ae9..4756338 100644 --- "a/kotlin_core/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" +++ "b/kotlin_core/4-\345\215\217\347\250\213\344\270\212\344\270\213\346\226\207\345\222\214\350\260\203\345\272\246\345\231\250.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/5-\345\274\202\346\255\245\346\265\201.md" "b/kotlin_core/5-\345\274\202\346\255\245\346\265\201.md" index c4873f4..f18e9bb 100644 --- "a/kotlin_core/5-\345\274\202\346\255\245\346\265\201.md" +++ "b/kotlin_core/5-\345\274\202\346\255\245\346\265\201.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/6-\351\200\232\351\201\223.md" "b/kotlin_core/6-\351\200\232\351\201\223.md" index e2e11aa..250eda4 100644 --- "a/kotlin_core/6-\351\200\232\351\201\223.md" +++ "b/kotlin_core/6-\351\200\232\351\201\223.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" "b/kotlin_core/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" index f42626b..d1f8d41 100644 --- "a/kotlin_core/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" +++ "b/kotlin_core/7-\345\274\202\345\270\270\345\244\204\347\220\206.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" "b/kotlin_core/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" index 752ad32..0a8aa13 100644 --- "a/kotlin_core/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" +++ "b/kotlin_core/8-\345\205\261\344\272\253\345\217\257\345\217\230\347\212\266\346\200\201\345\222\214\345\271\266\345\217\221\346\200\247.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" "b/kotlin_core/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" index 95e7432..f948fdb 100644 --- "a/kotlin_core/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" +++ "b/kotlin_core/9-\351\200\211\346\213\251\350\241\250\350\276\276\345\274\217.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 > 最近一直在了解关于**kotlin协程**的知识,那最好的学习资料自然是官方提供的学习文档了,看了看后我就萌生了翻译官方文档的想法。前后花了要接近一个月时间,一共九篇文章,在这里也分享出来,希望对读者有所帮助。个人知识所限,有些翻译得不是太顺畅,也希望读者能提出意见 > diff --git "a/kotlin_core/\344\270\244\344\270\207\345\205\255\345\215\203\345\255\227\345\270\246\344\275\240Kotlin\345\205\245\351\227\250.md" "b/kotlin_core/\344\270\244\344\270\207\345\205\255\345\215\203\345\255\227\345\270\246\344\275\240Kotlin\345\205\245\351\227\250.md" index f9a2123..4f1b2f2 100644 --- "a/kotlin_core/\344\270\244\344\270\207\345\205\255\345\215\203\345\255\227\345\270\246\344\275\240Kotlin\345\205\245\351\227\250.md" +++ "b/kotlin_core/\344\270\244\344\270\207\345\205\255\345\215\203\345\255\227\345\270\246\344\275\240Kotlin\345\205\245\351\227\250.md" @@ -1,4 +1,6 @@ -> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image),希望对你有所帮助 🤣🤣 +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 开头先说一下,我不是标题党,本教程真的有两万六千字(严格点说应该是两万六千五百八十四),以前我也曾写过 Kotlin 的入门教程,但由于隔得比较久了,所以就对原有教程进行了修正,并且补充了 Kotlin 新版本的一些新特性,希望对你有所帮助 🤣🤣 From f9f2a5c00e7771839926f7c6dd181cdb68bd2935 Mon Sep 17 00:00:00 2001 From: leavesc <1990724437@qq.com> Date: Thu, 16 Dec 2021 10:32:01 +0800 Subject: [PATCH 03/62] =?UTF-8?q?ASM=E5=AD=97=E8=8A=82=E7=A0=81=E6=8F=92?= =?UTF-8?q?=E6=A1=A9=E5=AE=9E=E7=8E=B0=E5=8F=8C=E5=87=BB=E9=98=B2=E6=8A=96?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0=E5=A7=8B=E6=9C=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...36\347\216\260\345\247\213\346\234\253.md" | 595 ++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" diff --git "a/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" new file mode 100644 index 0000000..f57a5ed --- /dev/null +++ "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" @@ -0,0 +1,595 @@ +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 + +最近打算通过字节码插桩的方式来实现一个应用**双击防抖**的功能。简单来说,我希望通过字节码插桩的方式来为整个项目中所有使用了 OnClickListener 的回调方法中都插入一段逻辑代码,该段逻辑代码会对前后两次点击的时间进行判断,如果判断到时间小于某个阈值的话就直接 return + +```kotlin +//插桩前 +view.setOnClickListener(object : View.OnClickListener { + override fun onClick(view: View) { + //TODO + } +}) + +//插桩后 +view.setOnClickListener(object : View.OnClickListener { + override fun onClick(view: View) { + if (!ViewDoubleClickCheck.canClick(view)){ + return + } + //TODO + } +}) +``` + +一个大型项目经过长久迭代后,设置 OnClickListener 的方式往往会有很多种,可能是使用了多种扩展框架,也可能是同类代码有多种实现形式。此时就至少需要考虑以下几种场景: + +- 通过代码直接为 View 设置了 OnClickListener +- 在 XML 中为 View 声明了 onClick 属性 +- 对第三方框架的支持程度。例如,如果项目中使用了 ButterKnife 的话,要照顾声明了 @OnClick 注解的方法。如果使用了 BaseRecyclerViewAdapterHelper 的话,要照顾为每个 Adapter 设置的 onItemClickListener 或者是 onItemChildClickListener + +实际上,以上几种场景最终都要通过第一种方式来实现,其它几种方式的区别只在于设置 OnClickListener 的代码并不直接显式声明在我们的项目中,只要能解决第一种场景,其它场景只需要改变 hook 的范围和判断条件即可 + +而通过代码直接为 View 设置 OnClickListener,在代码的实现方式上又可以根据是否使用了 lambda 表达式分为两种: + +```kotlin +view.setOnClickListener(object : View.OnClickListener { + override fun onClick(v: View) { + onClickView() + } +}) + +view.setOnClickListener { + onClickView() +} +``` + +我在通过 ASM 实现双击防抖功能的过程中,最大的难点就在于处理 lambda 表达式了,通过常规的 **定位包名全路径和方法签名信息** 没法直接对其进行处理,当中涉及到了原生 Java 平台和 Android 平台两者对 Java 8 的实现区别,以及一点点关于 Android 编译流程的知识点 + +本文要介绍的就是我在实现字节码插桩过程中总结出来的一些知识点,希望对你有所帮助 🤣🤣 + +# Java 8 的 Lambda + +先看一个关于非 lambda 形式的例子 + +开发者应该都知道一个知识点:如果想要获取某个接口或者是抽象类的实例的话,我们可以不显式声明其实现类,而是可以采取**匿名内部类**的方式来声明实例对象 + +```java +public class Demo { + + void test() { + Runnable runnable = new Runnable() { + @Override + public void run() { + System.out.println("leavesC"); + } + }; + runnable.run(); + } + +} +``` + +将 Java 文件编译为字节码 + +```java +javac Demo.java +``` + +最终会生成两个 class 文件:`Demo.class`、`Demo$1.class` + +可以看到 `Demo$1`实现了 Runnable 接口,也即所谓的匿名内部类了。上述代码中声明的 Runnable 变量指向的就是该具体的实现类,从`Demo.class` 第十一行也可以看到最终 new 出来的对象指向的也是 `Demo$1.class` + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/91939d14f74747a1ac5c8bf8cbf96885~tplv-k3u1fbpfcp-zoom-1.image) + +将上述代码转换为 lambda 形式,再来查看其字节码有何变化 + +```java +public class Demo { + + void test() { + Runnable runnable = () -> System.out.println("leavesC"); + runnable.run(); + } + +} +``` + +此时生成的就只会有一个`Demo.class`文件 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98a14defb8564f0784f5b5b5603fcdcf~tplv-k3u1fbpfcp-zoom-1.image) + +前后两份字节码文件主要的差异点在于: + +- 非 lambda 方式下,声明的 Runnable 变量最终会指向一个具体的接口实现类,也即我们常说的匿名内部类,在字节码中也可以看到有明确的生成该匿名内部类对象的过程,对应 new、dup、aload_0、invokespecial 等指令。Runnable 所要执行的代码块是在 `Demo$1.class` 的 run 方法中 +- lambda 方式下,声明 Runnable 变量这个操作对应的是 invokedynamic、astore_1 等指令。Runnable 所要执行的代码块是在自动生成的私有静态方法`lambda$test$0()` 中 + +从这可以推导出一个结论:**在编译阶段,lambda 表达式不会生成相应的实现类,lambda 语法的实现机制有别于以前的匿名内部类** + +当中的重点就在于 invokedynamic 指令了,Java 目前一共包含五种字节码调用指令 + +| 指令 | 作用 | +| :-------------- | :----------------------------------------------------------- | +| invokevirtual | 调用实例方法 | +| invokestatic | 调用静态方法 | +| invokeinterface | 调用接口方法 | +| invokespecial | 调用特殊实例方法,包括实例初始化方法、父类方法 | +| invokedynamic | 由用户引导方法决定,运行时动态解析出调用点限定符所引用的方法 | + +在编译期间生成的 class 文件中,前四种指令通过常量池(Constant Pool)已经固定了目标方法的符号信息,包括 **类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符** 等,运行阶段就可以依靠该符号信息直接定位到具体的方法从而直接调用 + +而 invokedynamic 是在 Java 7 中新增的字节码调用指令,作为 Java 支持动态类型语言的改进之一,在 Java 8 开始应用,lambda 表达式底层就依靠该指令来实现。invokedynamic 指令在常量池中并没有包含其目标方法的具体符号信息,存储的是 BootstapMethod 信息,在运行时再来通过引导方法机制动态确定方法的所属者和类型 + +进一步查看 `Demo.class` 的详细字节码信息 + +```java +javap -verbose Demo.class +``` + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/44a6fc859fe64e88a677edbb8a755c8e~tplv-k3u1fbpfcp-zoom-1.image) + +在执行了第十七行的 `invokedynamic` 指令后,就会存储栈顶值,也即 this,然后将其作为参数来调用 Runnable 接口的 `run()`方法。这说明了通过 `invokedynamic` 指令能够获取到一个 Runnable 对象 + +```java +0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; +5: astore_1 +6: aload_1 +7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V +12: return +``` + +`InvokeDynamic` 指向了第四十二行的 BootstapMethods 区域, 当中会通过 invokestatic 指令去调用 LambdaMetafactory 的静态方法 `metafactory()`,此时就会在内存中来生成关联的接口实现类了 + +此外,标明的入参参数有三个: + +- ()V。原始方法泛型擦除后的方法签名信息,也即 run 方法。由于 run 方法不包含泛型,所以和第三个参数的签名信息一样 +- invokestatic lambdademo/Demo.lambda$test$0:()V。invokestatic 表达的是调用静态方法的操作,后面跟着的是自动生成的私有静态方法`lambda$test$0()`的签名信息,当中就包含了 lambda 表达式原本所要执行的代码块 +- ()V。原始方法泛型擦除前的方法签名信息,也即 run 方法 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a01349c360704724b90d03fcefcdca82~tplv-k3u1fbpfcp-zoom-1.image) + +从 InnerClassLambdaMetafactory 也可以看到,这里使用到了 ASM,生成的实现类按照 `ClassName + $$Lambda$ + 整数索引` 的方式进行命名,该实现类就会去负责调用 `lambda$test$0()` 方法 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d9e91c65e37a42c593c6684685f29a16~tplv-k3u1fbpfcp-zoom-1.image) + +通过打印 runnable 对象的 class 信息可以验证出其隶属于其它类而非 DemoClass + +```kotlin +public class Demo { + + void test() { + Runnable runnable = () -> System.out.println("leavesC"); + System.out.println(runnable.getClass().getSimpleName()); + + Runnable runnable2 = () -> System.out.println("leavesC"); + System.out.println(runnable2.getClass().getSimpleName()); + } + + public static void main(String[] args) { + Demo demo = new Demo(); + demo.test(); + //Demo$$Lambda$1 + //Demo$$Lambda$2 + } + +} +``` + +所以说,使用 lambda 表达式并不是不会生成对应的实现类,而是将生成时机改成了运行时,当第一次执行到 lambda 表达式时,JVM 就会在内存中动态生成对应的实现类,当后续再次执行到该 lambda 表达式时也可以直接复用该类 + +# Android 的 Lambda + +知道原生 Java 平台是如何实现 lambda 表达式后,再来讲下 Android 平台是如何支持 lambda 表达式的,因为 Android 的 lambda 和 Java 的 lambda 并不等同 + +大多数开发者应该知道,Java-Bytecode(JVM 字节码)是不能直接运行在 Android 系统上的,需要转换成 Android-Bytecode(Dalvik / ART 字节码),而 Dalvik / ART 并不支持 invokedynamic 指令,导致目前低版本的 Android 系统对 Java 8 支持得并不彻底, 此外某些 API 我们也无法使用,例如 `LocalDateTime.now()` + +Android Gradle 插件 3.0.0 及更高版本支持所有 Java 7 语言功能,以及部分 Java 8 语言功能(具体因平台版本而异)。使用 Android Gradle 插件 4.0.0 及更高版本构建应用时,可以使用多种 Java 8 语言 API,而无需为应用设置最低 API 级别 + +为了能够支持 Java 8,目前 Android Gradle 插件是通过在 D8/R8 将 class 文件编译成 dex 文件的过程中插入字节码转换的操作来实现的,这个转换过程称为 desugar,也即 **脱糖** + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f903bf22428c47968c1e59de98a7fa2b~tplv-k3u1fbpfcp-zoom-1.image) + +desugar 操作就用于将某些目前 Android 系统还不支持的语法糖还原为简单的**基础语法结构**。例如,lambda 表达式经过 desugar 之后就会被转换为具体的实现类,并将生成的实现类直接写入到 dex 文件中,从而保证了 lambda 表达式也能够在低版本系统上正常运行,从而也就不存在兼容性问题了 + +可以写一个简单的 Android 应用来进行验证,随便写一段 lambda 表达式,然后反编译其 dex 文件,就可以看到 lambda 表达式在编译后的 dex 文件中就已经是直接以实现类的形式存在的了 + +```kotlin +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + val runnable = Runnable { println("leavesC") } + runnable.run() + } + +} +``` + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/138032e9f74d436fa25823e0a6c73dea~tplv-k3u1fbpfcp-zoom-1.image) + +# 解析 Lambda 指令 + +有了以上基础后,此时就知道了 desugar 会给字节码插桩带来什么影响了:由于 Transform 是在 desugar 之前执行的,所以此时还未生成各个 lambda 表达式的具体实现类,导致我们没法直接根据 `View.OnClickListener` 的签名信息来定位到所有实现类 + +因此,在以下代码中,第一种方式的匿名内部类可以正常 hook,第二种方式就不行了,此时就只能通过解析 invokedynamic 指令来进行辅助判断了 + +```kotlin +view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + } +}); + +view.setOnClickListener(v -> { + +}); +``` + +对于第二种方式,通过 ASM 框架,在字节码层面上我们能够获取到的信息有: + +- 该方式包含 lambda 表达式,也即包含了一条 invokedynamic 指令,对应 ASM 中的 InvokeDynamicInsnNode +- invokedynamic 指令中包含了要生成的接口实例的签名信息,即 invokedynamic 指令中标明了要生成的是 OnClickListener 对象,且包含一个 onClick 方法,所以此时就可以通过遍历项目全局的 InvokeDynamicInsnNode 的 name 和 desc 两个属性,来查找到和 OnClickListener lambda 表达式关联的 InvokeDynamicInsnNode +- invokedynamic 指令会指向 BootstapMethod 区域,而 BootstapMethod 中已经标明了三个入参参数,第二个参数是编译期间就自动生成的私有静态方法,当中就包含了 onClick 方法应该执行的代码块。这三个参数就对应 InvokeDynamicInsnNode 的 bsmArgs 属性,所以通过 bsmArgs 我们就能够知道 onClick 方法最终要调用的方法的签名信息,通过向该方法插入需要的逻辑就可以实现 hook 了 + +看下实际的编码实现 + +声明两个扩展方法,用于筛选 InvokeDynamicInsnNode 指令,此处对比的就是 InvokeDynamicInsnNode 的 name 和 desc 两个属性 + +```kotlin +fun MethodNode.findHookPointLambda(config: DoubleClickConfig): List { + val onClickListenerLambda = findLambda { + val nodeName = it.name + val nodeDesc = it.desc + val find = config.hookPointList.find { point -> + nodeName == point.methodName && nodeDesc.endsWith(point.interfaceSignSuffix) + } + return@findLambda find != null + } + return onClickListenerLambda +} + +private fun MethodNode.findLambda( + filter: (InvokeDynamicInsnNode) -> Boolean +): List { + val handleList = mutableListOf() + val instructions = instructions?.iterator() ?: return handleList + while (instructions.hasNext()) { + val nextInstruction = instructions.next() + if (nextInstruction is InvokeDynamicInsnNode) { + if (filter(nextInstruction)) { + handleList.add(nextInstruction) + } + } + } + return handleList +} +``` + +DoubleClickConfig 代表的是当前进行 hook 的所有配置信息,HookPoint 用于筛选 invokedynamic 指令 + +```kotlin +class DoubleClickConfig( + private val doubleCheckClass: String = "github.leavesc.asm.double_click.ViewDoubleClickCheck", + val doubleCheckMethodName: String = "canClick", + val doubleCheckMethodDescriptor: String = "(Landroid/view/View;)Z", + val hookPointList: List = extraHookPoints +) { + + val formatDoubleCheckClass: String + get() = doubleCheckClass.replace(".", "/") + +} + +data class HookPoint( + val interfaceName: String, + val methodName: String, + val methodSign: String, +) { + + val interfaceSignSuffix = "L$interfaceName;" + +} + +private val extraHookPoints = listOf( + HookPoint( + interfaceName = "android/view/View\$OnClickListener", + methodName = "onClick", + methodSign = "onClick(Landroid/view/View;)V" + ) +) +``` + +每当拿到到一份字节码,就遍历其所有方法,判断方法内部是否包含和 OnClickListener 相关的 lambda 表达式,有的话则其指向的静态方法的签名信息保存起来。当拿到所有需要 hook 的方法后,再来向其插入 ViewDoubleClickCheck 防抖指令 + +```kotlin +class DoubleClickTransform(private val config: DoubleClickConfig) : BaseTransform() { + + override fun modifyClass(byteArray: ByteArray): ByteArray { + val classReader = ClassReader(byteArray) + val classNode = ClassNode() + classReader.accept(classNode, ClassReader.EXPAND_FRAMES) + val methods = classNode.methods + if (!methods.isNullOrEmpty()) { + val shouldHookMethodList = mutableSetOf() + for (methodNode in methods) { + //判断方法内部是否有需要处理的 lambda 表达式 + val invokeDynamicInsnNodes = methodNode.findHookPointLambda(config) + invokeDynamicInsnNodes.forEach { + val handle = it.bsmArgs[1] as? Handle + if (handle != null) { + shouldHookMethodList.add(handle.name + handle.desc) + } + } + } + if (shouldHookMethodList.isNotEmpty()) { + for (methodNode in methods) { + val methodNameWithDesc = methodNode.nameWithDesc + if (shouldHookMethodList.contains(methodNameWithDesc)) { + val argumentTypes = Type.getArgumentTypes(methodNode.desc) + val viewArgumentIndex = argumentTypes?.indexOfFirst { + it.descriptor == ViewDescriptor + } ?: -1 + if (viewArgumentIndex >= 0) { + val instructions = methodNode.instructions + if (instructions != null && instructions.size() > 0) { + //插入 ViewDoubleClickCheck 防抖指令 + val list = InsnList() + list.add( + VarInsnNode( + Opcodes.ALOAD, getVisitPosition( + argumentTypes, + viewArgumentIndex, + methodNode.isStatic + ) + ) + ) + list.add( + MethodInsnNode( + Opcodes.INVOKESTATIC, + config.formatDoubleCheckClass, + config.doubleCheckMethodName, + config.doubleCheckMethodDescriptor + ) + ) + val labelNode = LabelNode() + list.add(JumpInsnNode(Opcodes.IFNE, labelNode)) + list.add(InsnNode(Opcodes.RETURN)) + list.add(labelNode) + instructions.insert(list) + } + } + } + } + val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS) + classNode.accept(classWriter) + return classWriter.toByteArray() + } + } + return byteArray + } + +} +``` + +# 匿名内部类 + +如果不使用 lambda 表达式的话,OnClickListener 在编译阶段就已经生成具体的接口实现类了,所以当判断到当前遍历的 Class 对象实现了 OnClickListener 接口的话,就拿到其 onClick 方法即可 + +`isHookPoint` 方法就用于判断 ClassNode 实现的所有接口中是否包含 OnClickListener,以及当前 MethodNode 的签名信息是否等于 `"onClick(Landroid/view/View;)V"`,都满足的话该 MethodNode 就是目标方法 + +```kotlin +class DoubleClickTransform(private val config: DoubleClickConfig) : BaseTransform() { + + override fun modifyClass(byteArray: ByteArray): ByteArray { + val classReader = ClassReader(byteArray) + val classNode = ClassNode() + classReader.accept(classNode, ClassReader.EXPAND_FRAMES) + val methods = classNode.methods + if (!methods.isNullOrEmpty()) { + val shouldHookMethodList = mutableSetOf() + for (methodNode in methods) { + val methodNameWithDesc = methodNode.nameWithDesc + //判断当前 methodNode 是否符合 hook 要求 + if (classNode.isHookPoint(config, methodNode)) { + shouldHookMethodList.add(methodNameWithDesc) + continue + } + //判断方法内部是否有需要处理的 lambda 表达式 + ··· + } + if (shouldHookMethodList.isNotEmpty()) { + //插入 ViewDoubleClickCheck 防抖指令 + ··· + val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS) + classNode.accept(classWriter) + return classWriter.toByteArray() + } + } + return byteArray + } + +} + +fun ClassNode.isHookPoint(config: DoubleClickConfig, methodNode: MethodNode): Boolean { + val myInterfaces = interfaces + if (myInterfaces.isNullOrEmpty()) { + return false + } + val extraHookMethodList = config.hookPointList + extraHookMethodList.forEach { + if (myInterfaces.contains(it.interfaceName) && methodNode.nameWithDesc == it.methodSign) { + return true + } + } + return false +} +``` + +# XML onClick + +在 XML 中声明的 onClick 属性会在 View 类解析 AttributeSet 时进行读取,View 类会为其设置一个自定义的 OnClickListener,通过反射 handlerName 方法的形式来实现回调 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a68cc44dc8b4f1c8fe124c9bcd0e9de~tplv-k3u1fbpfcp-zoom-1.image) + +由于 Transform 无法作用于系统源码,所以我们没法对 DeclaredOnClickListener 进行 hook,有其它两种解决思路 + +第一种方案是为 XML 指向的 onClick 方法添加一个自定义注解,通过该注解来标明此方法需要进行 hook,也是本文采用的方法,采用此方案后使用场景就不只是局限于 XML onClick 了,也可以作用于任何符合签名信息的方法 + +首先是声明一个自定义注解,为需要进行 hook 的方法添加该注解 + +```kotlin +package github.leavesc.asm.double_click + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class CheckViewOnClick +``` + +之后需要先校验 MethodNode 的方法签名是否符合要求,即要求 **有一个入参参数,参数类型为 View ,方法返回值类型为 void。** 符合要求且包含 CheckViewOnClick 注解的话即目标方法 + +```kotlin +private const val OnClickViewMethodDescriptor = "(Landroid/view/View;)V" + +val MethodNode.onlyOneViewParameter: Boolean + get() = desc == OnClickViewMethodDescriptor + +private fun MethodNode.hasAnnotation(annotationDesc: String): Boolean { + return visibleAnnotations?.find { it.desc == annotationDesc } != null +} + +fun MethodNode.hasCheckViewAnnotation(config: DoubleClickConfig): Boolean { + return hasAnnotation(config.formatCheckViewOnClickAnnotation) +} + + +val methodNameWithDesc = methodNode.nameWithDesc +if (methodNode.onlyOneViewParameter) { + if (methodNode.hasCheckViewAnnotation(config)) { + //添加了 CheckViewOnClick 注解的情况 + shouldHookMethodList.add(methodNameWithDesc) + continue + } +} +``` + +> 相对应的,在某些情况下我们也不想对特定 OnClickListener 进行 hook,例如在业务上需要多次快速点击的时候,此时一样可以定义一个特定注解来作为白名单,对应本文源码中的 UncheckViewOnClick,详情可以参考文末给出的源码链接 + +第二种方案是对 AppCompat 包中的 AppCompatViewInflater 类进行 hook。目前大多数情况下我们使用的 Activity 都会继承于 AppCompatActivity, 而 AppCompatActivity 会通过 AppCompatViewInflater 来解析生成 View 对象,内部会尝试代理 onClick 属性并为 View 设置一个自定义的 OnClickListener。由于 AppCompatViewInflater 属于依赖库,Transform 可以通过扩大作用范围从而扫描到 AppCompatViewInflater 类,所以我们对 `checkOnClickListener` 方法或者是 DeclaredOnClickListener 类进行 hook 都可以 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd0e1987d6d4414d8cbd986bf129121f~tplv-k3u1fbpfcp-zoom-1.image) + +# ButterKnife + +ButterKnife 会为每个使用了 @OnClick 注解的类自动生成一个辅助文件,就像以下的 MainActivity_ViewBinding,而 ButterKnife 为每个 View 设置的 OnClickListener 都是框架内部自定义的实现类 DebouncingOnClickListener + +```java +public final class MainActivity_ViewBinding implements Unbinder { + + ··· + + @UiThread + public MainActivity_ViewBinding(final MainActivity target, View source) { + this.target = target; + ··· + view.setOnClickListener(new DebouncingOnClickListener() { + @Override + public void doClick(View p0) { + target.onClickViewByButterKnife(p0); + } + }); + } + +} + +public abstract class DebouncingOnClickListener implements View.OnClickListener { + private static final Runnable ENABLE_AGAIN = () -> enabled = true; + private static final Handler MAIN = new Handler(Looper.getMainLooper()); + + static boolean enabled = true; + + @Override public final void onClick(View v) { + if (enabled) { + enabled = false; + MAIN.post(ENABLE_AGAIN); + doClick(v); + } + } + + public abstract void doClick(View v); + +} +``` + +对 ButterKnife 进行 hook 的方案也有多种: + +- 直接根据 @OnClick 注解来进行定位,只要解析到包含该注解的方法就对其进行 hook +- 根据 DebouncingOnClickListener 的匿名内部类来进行定位。ButterKnife 在为 View 设置 DebouncingOnClickListener 时没有使用 lambda 表达式,所以只要解析到其实现类然后对其 doClick 方法进行 hook 即可 +- 将 Transform 的作用域扩大到所有依赖库。前两种方式仅需要扫描项目自有代码和通过 APT 生成的代码即可,而如果将 Transform 范围扩大到所有依赖库的话,Transform 阶段就可以看到 DebouncingOnClickListener 类了,此时就相当于在处理匿名内部类的情况 + +我采取的是第一种方案,最简单而且影响范围也最小 + +```kotlin +val methodNameWithDesc = methodNode.nameWithDesc +if (methodNode.onlyOneViewParameter) { + if (methodNode.hasCheckViewAnnotation(config)) { + //添加了 CheckViewOnClick 注解的情况 + shouldHookMethodList.add(methodNameWithDesc) + continue + } else if (methodNode.hasButterKnifeOnClickAnnotation()) { + //使用了 ButterKnife,且当前 method 添加了 OnClick 注解 + shouldHookMethodList.add(methodNameWithDesc) + continue + } +} +``` + +# BaseRecyclerViewAdapterHelper + +BaseRecyclerViewAdapterHelper 是一个封装了 RecyclerViewAdapter 常用操作的的三方库,可以很方便地来设置 onItemClickListener 和 onItemChildClickListener,按道理来说一个应用的双击防抖功能除了要作用于单个 View 外,也要对 RecyclerView 的每个 Item 进行处理才对 + +onItemClickListener 和 onItemChildClickListener 方法内部也一样是通过为 View 设置 OnClickListener 来实现的,此时一样可以通过扩大 Transform 的作用范围来处理,但为了减小影响范围,我 hook 的是这两个 onItem 方法 + +重点在于此时也需要考虑使用了 lambda 表达式的情况,也即需要同时处理以下两类代码: + +```kotlin +adapter.onItemClickListener = BaseQuickAdapter.OnItemClickListener { adapter, view, position -> + onClickView() +} + +adapter.onItemClickListener = object :BaseQuickAdapter.OnItemClickListener{ + override fun onItemClick(adapter: BaseQuickAdapter<*, *>?, view: View?, position: Int) { + + } +} +``` + +两个 onItem 方法的总体处理流程和 OnClickListener 一样,根据是否使用了 lambda 表达式走不同的判断逻辑,所以此时只要为 extraHookPoints 多添加两个需要 hook 的节点即可,具体的签名信息就需要查看该开源库的源码来得到了 + +```kotlin +private val extraHookPoints = listOf( + HookPoint( + interfaceName = "android/view/View\$OnClickListener", + methodName = "onClick", + methodSign = "onClick(Landroid/view/View;)V" + ), + HookPoint( + interfaceName = "com/chad/library/adapter/base/BaseQuickAdapter\$OnItemClickListener", + methodName = "onItemClick", + methodSign = "onItemClick(Lcom/chad/library/adapter/base/BaseQuickAdapter;Landroid/view/View;I)V" + ), + HookPoint( + interfaceName = "com/chad/library/adapter/base/BaseQuickAdapter\$OnItemChildClickListener", + methodName = "onItemChildClick", + methodSign = "onItemChildClick(Lcom/chad/library/adapter/base/BaseQuickAdapter;Landroid/view/View;I)V", + ) +) +``` + +# 结尾 + +最后也给出完整的源码:[ViewDoubleClickCheck](https://github.com/leavesC/ViewDoubleClickCheck) + +ViewDoubleClickCheck 应该会是一个比较好的让读者入门字节码插桩的案例,读者根据项目实际情况进行简单修改后也可以把该双击防抖功能引入到自己项目中 \ No newline at end of file From 33ed46413e47ebd5e63b7954a8321edd2fb28f70 Mon Sep 17 00:00:00 2001 From: leavesC <1990724437@qq.com> Date: Thu, 16 Dec 2021 23:16:55 +0800 Subject: [PATCH 04/62] =?UTF-8?q?ASM=E5=AD=97=E8=8A=82=E7=A0=81=E6=8F=92?= =?UTF-8?q?=E6=A1=A9=E5=AE=9E=E7=8E=B0=E5=8F=8C=E5=87=BB=E9=98=B2=E6=8A=96?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0=E5=A7=8B=E6=9C=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...32\204\345\256\236\347\216\260\345\247\213\346\234\253.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" index f57a5ed..8f6b519 100644 --- "a/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" +++ "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" @@ -590,6 +590,6 @@ private val extraHookPoints = listOf( # 结尾 -最后也给出完整的源码:[ViewDoubleClickCheck](https://github.com/leavesC/ViewDoubleClickCheck) +最后也给出完整的源码:[ASM_Transform](https://github.com/leavesC/ASM_Transform) -ViewDoubleClickCheck 应该会是一个比较好的让读者入门字节码插桩的案例,读者根据项目实际情况进行简单修改后也可以把该双击防抖功能引入到自己项目中 \ No newline at end of file +这应该会是一个比较好的让读者入门字节码插桩的案例,读者根据项目实际情况进行简单修改后也可以把该双击防抖功能引入到自己项目中 \ No newline at end of file From 30b5911e4f247eae2caecc764a769b1dbbeab1b4 Mon Sep 17 00:00:00 2001 From: leavesC <1990724437@qq.com> Date: Fri, 17 Dec 2021 23:42:41 +0800 Subject: [PATCH 05/62] =?UTF-8?q?ASM=E5=AD=97=E8=8A=82=E7=A0=81=E6=8F=92?= =?UTF-8?q?=E6=A1=A9=E5=AE=9E=E7=8E=B0=E5=8F=8C=E5=87=BB=E9=98=B2=E6=8A=96?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0=E5=A7=8B=E6=9C=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b47d2d..136f96d 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,12 @@ ![公众号:字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) -文章链接均导航至掘金:[业志陈](https://juejin.cn/user/923245496518439/posts) 如果发现掘金文章内容不全,请查看此处文件夹中的文章,这里会一直保留最新最全的文章内容 +文章链接均导航至掘金:[业志陈](https://juejin.cn/user/923245496518439/posts) + +如果发现掘金文章内容不全,请查看此处文件夹中的文章,这里会一直保留最新最全的文章内容 * [Android 精讲]() + * [ASM 字节码插桩实现双击防抖的实现始末](https://juejin.cn/post/7042328862872567838) * [Android WebView H5 秒开方案总结](https://juejin.cn/post/7016883220025180191) * [Jetpack LiveData 的设计理念及改进](https://juejin.cn/post/6903096576734920717) * [探究 Kotlin 的隐藏性能开销与避坑指南](https://juejin.cn/post/7010367024916660237) From 0e93834c36e7497c45ab13f4fb56f605f5ae1dba Mon Sep 17 00:00:00 2001 From: leavesC <1990724437@qq.com> Date: Tue, 21 Dec 2021 14:38:53 +0800 Subject: [PATCH 06/62] =?UTF-8?q?ASM=20=E5=AD=97=E8=8A=82=E7=A0=81?= =?UTF-8?q?=E6=8F=92=E6=A1=A9=E8=BF=9B=E8=A1=8C=E7=BA=BF=E7=A8=8B=E6=95=B4?= =?UTF-8?q?=E6=B2=BB=E7=9A=84=E5=AE=9E=E7=8E=B0=E5=A7=8B=E6=9C=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + ...36\347\216\260\345\247\213\346\234\253.md" | 404 ++++++++++++++++++ 2 files changed, 405 insertions(+) create mode 100644 "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" diff --git a/README.md b/README.md index 136f96d..cb1081b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ 如果发现掘金文章内容不全,请查看此处文件夹中的文章,这里会一直保留最新最全的文章内容 * [Android 精讲]() + * [ASM 字节码插桩进行线程整治的实现始末](https://juejin.cn/post/7043999874076901406) * [ASM 字节码插桩实现双击防抖的实现始末](https://juejin.cn/post/7042328862872567838) * [Android WebView H5 秒开方案总结](https://juejin.cn/post/7016883220025180191) * [Jetpack LiveData 的设计理念及改进](https://juejin.cn/post/6903096576734920717) diff --git "a/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" new file mode 100644 index 0000000..d46604a --- /dev/null +++ "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" @@ -0,0 +1,404 @@ +> 公众号:[字节数组](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0357ed9ee08d4a5d92af66a72b002169~tplv-k3u1fbpfcp-watermark.image) +> +> 希望对你有所帮助 🤣🤣 + +最近看了 京东零售技术 发表的一篇文章:[AOP 技术在 APP 开发中的多场景实践](https://mp.weixin.qq.com/s?__biz=MzUyMDAxMjQ3Ng==&mid=2247497288&idx=1&sn=a3aa54ab49f428106037886462c72335&chksm=f9f2619bce85e88dbc95d76f3a3528f2948687e38c75eedf9e2dee4c39d38e2910737fa0d847&scene=178&cur_album_id=1844068124342484996#rd) ,文章中介绍了 AOP 技术的一种使用场景:线程使用数优化。看完之后感觉挺有实践意义的,但文章中并没有给出具体的实现代码,所以我就做了一次实操,基本实现了文章中介绍的效果,本文就来详细介绍下我的实现始末,并给出具体的实现代码。本文部分内容也直接引用自该文章,在此表示感谢 + +在实现 Android 端 APP 稳定性治理的过程中,和线程相关的 OOM 问题都是避免不了的。线程是一种昂贵的系统资源,其“昂贵”不仅在于创建线程所需要的资源开销,还在于使用过程中带来的资源消耗。一个系统能够支持同时运行的线程总数受限于该系统所拥有的处理器数目和内存大小等硬件条件,线程的运行需要占用处理器时间片,系统中处于运行状态的线程越多,每个线程单位时间内能分配到的时间片就会越少,线程调度带来的上下文切换的次数就会越多,最终导致处理器真正用于运算的时间就会越少。此外,在现实场景中一个程序在其整个生命周期内需要交由线程执行的任务数量往往是远多于系统所能支持同时运行的最大线程数,如果线程数不断累加的话就会不可避免地抛出 OOM。当线程数量超载时就会在安卓虚拟机层抛出异常:`java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed` + +由于线程数超载导致 OOM 的原因可以归类为三种: + +- 线程池数量过多。每个线程池都需要守护线程以及过多的空间开销,过多的线程池使用对于内存资源消耗过大 +- 常驻线程过多。常驻线程指的是处于 waiting(等待)、blocked(阻塞)以及 runnable(运行)的线程,这在使用了线程池时比较容易出现。每个线程池会包含有一定数量的核心线程和非核心线程,默认情况下核心线程即使处于闲置状态也是不会被回收的,即默认不受 KeepAliveTime 属性的限制,这使得核心线程有可能一直处于闲置状态得不到释放,极大浪费系统资源 +- 大量的匿名线程。匿名线程指的是在代码中随处启动的线程,这种方式虽然可以实现快速、优先级最高的异步化,但过多的匿名线程对于问题排查难度、稳定性都是一种挑战 + +对于一个经过长久迭代的项目来说,以上问题不仅仅会出现在自研业务中,同时还会涉及到多个三方 SDK 或者是开源库,而想要同时推动三方 SDK 和开源库进行线程优化是不太现实的,此时采用字节码插桩这种不局限于特定业务形态和非侵入式的方式就成了比较靠谱的一种选择 + +进行线程整治想要实现的效果有两点: + +- 将各个匿名线程进行统一命名,命名规则:创建线程时所在类的类名 + 不断递增的线程号 + 原始代码中设置的线程名(如果有的话),方便出现异常时能够快速定位问题所在 +- 统一各类线程池的超时机制。对 Executors 中的各个 newXXXXThreadPool 进行 hook,将线程池中的每个线程均按照匿名线程的规则进行命名,为线程设置统一的超时时间,且允许回收核心线程 + +# 一、匿名线程 + +先随意声明一个 Thread 对象,对应在项目中存在的各类匿名线程 + +![](https://upload-images.jianshu.io/upload_images/2552605-e9e2e9ae07f85b8c.png) + +反编译查看对应的字节码指令 + +![](https://upload-images.jianshu.io/upload_images/2552605-9ca971b771086e50.png) + +从当中就可以看出几点信息: + +- 第 23 行到第 27 行就对应调用 `new Thread(Runnable , String)` 这个过程 +- 第 23 行的 new 指令明确指向了 java/lang/Thread +- 第 26 行的 ldc 指令代表的是从常量池中获取常量值的操作,也即获取线程名字符串 `thread name` +- 第 27 行的 invokespecial 指令对应的是调用 Thread 构造函数的过程,包含两个入参参数 + +该匿名线程在声明时主动设置了线程名,而项目中其它匿名线程可能并没有进行设置,而是只传入了 Runnable。因此,在字节码层面上就要考虑两种不同的指令结构了:**包含一个入参参数** 和 **包含两个入参参数** + +而如果要在字节码层面上来手动生成或者是拼接 ThreadName 的话会比较麻烦,所以我采取的是一个比较巧妙的方法:将要声明的 Thread 对象替换为我们自定义的 Thread 子类,为该子类多声明一个用于传入类名的构造参数,通过这种方式就无需关心在声明 Thread 对象时到底传入了几个构造参数了,只需要在调用 invokespecial 指令时固定在末尾多传入一个代表当前类名的 String 参数即可 + +来看下实际的编码实现 + +先定义一个 Thread 子类,为其多声明一个入参参数 className,然后手动将 **原始代码中设置的线程名** 和 **className** 按照既定规则拼接在一起作为最终的线程名 + +```kotlin +/** + * @Author: leavesC + * @Date: 2021/12/18 17:57 + * @Desc: + * @公众号:字节数组 + */ +class OptimizedThread(runnable: Runnable?, name: String?, className: String) : + Thread(runnable, generateThreadName(name, className)) { + + companion object { + + private val threadId = AtomicInteger(0) + + private fun generateThreadName(name: String?, className: String): String { + return className + "-" + threadId.getAndIncrement() + if (name.isNullOrBlank()) { + "" + } else { + "-$name" + } + } + + } + + constructor(runnable: Runnable, className: String) : this(runnable, null, className) + + constructor(name: String, className: String) : this(null, name, className) + +} +``` + +之后,在 Transform 阶段只要遍历到指向 `java/lang/Thread ` 的 TypeInsnNode,且不是包含在 ThreadFactory 内部的话,那就是要处理的匿名线程了 + +```kotlin +/** + * @Author: leavesC + * @Date: 2021/12/16 15:11 + * @Desc: + * @Github:https://github.com/leavesC + */ +class OptimizedThreadTransform(private val config: OptimizedThreadConfig) : BaseTransform() { + + companion object { + + private const val threadClass = "java/lang/Thread" + + private const val threadFactoryClass = "java/util/concurrent/ThreadFactory" + + private const val threadFactoryNewThreadMethodDesc = + "newThread(Ljava/lang/Runnable;)Ljava/lang/Thread;" + + } + + override fun modifyClass(byteArray: ByteArray): ByteArray { + val classNode = ClassNode() + val classReader = ClassReader(byteArray) + classReader.accept(classNode, ClassReader.EXPAND_FRAMES) + val methods = classNode.methods + val taskList = mutableListOf<() -> Unit>() + if (!methods.isNullOrEmpty()) { + for (methodNode in methods) { + val instructionIterator = methodNode.instructions?.iterator() + if (instructionIterator != null) { + while (instructionIterator.hasNext()) { + val instruction = instructionIterator.next() + when (instruction.opcode) { + Opcodes.NEW -> { + val typeInsnNode = instruction as? TypeInsnNode + if (typeInsnNode?.desc == threadClass) { + //如果是在 ThreadFactory 内初始化线程,则不处理 + if (!classNode.isThreadFactoryMethod(methodNode)) { + taskList.add { + transformNew( + classNode, + methodNode, + instruction + ) + } + } + } + } + } + } + } + } + } + taskList.forEach { + it.invoke() + } + val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS) + classNode.accept(classWriter) + return classWriter.toByteArray() + } + + private fun ClassNode.isThreadFactoryMethod(methodNode: MethodNode): Boolean { + return this.interfaces?.contains(threadFactoryClass) == true + && methodNode.nameWithDesc == threadFactoryNewThreadMethodDesc + } + +} +``` + +找到到目标指令后,就将 `java/lang/Thread ` 替换为 OptimizedThread,然后继续向后遍历找到调用 Thread 构造参数的指令,为该指令多插入一个 String 类型的方法入参参数声明,然后将 className 作为构造参数传给 OptimizedThread,至此就完成替换了 + +```kotlin +private fun transformNew( + classNode: ClassNode, + methodNode: MethodNode, + typeInsnNode: TypeInsnNode +) { + val instructions = methodNode.instructions + val typeInsnNodeIndex = instructions.indexOf(typeInsnNode) + //从 typeInsnNode 指令开始遍历,找到调用 Thread 构造函数的指令,然后对其进行替换 + for (index in typeInsnNodeIndex + 1 until instructions.size()) { + val node = instructions[index] + if (node is MethodInsnNode && node.isThreadInitMethodInsn()) { + //将 Thread 替换为 OptimizedThread + typeInsnNode.desc = config.formatOptimizedThreadClass + node.owner = config.formatOptimizedThreadClass + //为调用 Thread 构造函数的指令多插入一个 String 类型的方法入参参数声明 + node.insertArgument(String::class.java) + //将 ClassName 作为构造参数传给 OptimizedThread + instructions.insertBefore(node, LdcInsnNode(classNode.simpleClassName)) + break + } + } +} +``` + +# 二、线程池 + +`java.util.concurrent.Executors` 类中用于获取线程池的方法有十二个之多 + +```java +public class Executors { + + public static ExecutorService newFixedThreadPool(int nThreads) + + public static ExecutorService newWorkStealingPool(int parallelism) + + public static ExecutorService newWorkStealingPool() + + public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) + + public static ExecutorService newSingleThreadExecutor() + + public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) + + public static ExecutorService newCachedThreadPool() + + public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) + + public static ScheduledExecutorService newSingleThreadScheduledExecutor() + + public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) + + public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) + +} +``` + +这十二个静态方法按照入参参数来进行区分的话有三种: + +- 不包含入参参数 +- 包含一个入参参数:代表线程数的 int 类型或者是 ThreadFactory +- 包含两个入参参数:代表线程数的 int 类型加 ThreadFactory + +按照返回值来进行区分的话有两种: + +- ExecutorService +- ScheduledExecutorService + +在进行 hook 的时候就需要考虑对应代码传入了多少个入参参数以及方法的返回值类型了。为了统一线程池中的线程命名规则,就需要替换传入的 ThreadFactory,并且传入代表当前类名的 String 参数。为了能够为线程设置统一的超时时间,以及允许回收核心线程,就需要能够获取到 ThreadPoolExecutor 对象了 + +JDK 中的源码我们是没法进行 hook 的,而如果要在字节码层面上来进行以上修改的话会比较麻烦,所以我采取的是和处理匿名线程时类似的思路:声明一个 OptimizedExecutors 类,先在当中声明和 Executors 中各类 `newXXXThreadPool` 签名信息一致的方法,然后再为每个方法多添加一个 String 类型的入参参数,将指向 Executors 的字节码均改为 OptimizedExecutors,这样我们就可以 OptimizedExecutors 中自由配置线程池参数了 + +来看下实际的编码实现 + +首先是定义拿来进行替换的 OptimizedExecutors,为每个生成的 ThreadPoolExecutor 统一设置五秒的超时时间并允许回收核心线程,在 NamedThreadFactory 中定义统一的线程命名规则 + +```kotlin +/** + * @Author: leavesC + * @Date: 2021/12/16 15:58 + * @Desc: + */ +object OptimizedExecutors { + + private const val defaultThreadKeepAliveTime = 5000L + + @JvmStatic + fun newFixedThreadPool(nThreads: Int, className: String): ExecutorService { + return newFixedThreadPool(nThreads, null, className) + } + + @JvmStatic + fun newFixedThreadPool( + nThreads: Int, + threadFactory: ThreadFactory?, + className: String + ): ExecutorService { + return getOptimizedExecutorService( + nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + LinkedBlockingQueue(), + threadFactory, className + ) + } + + ··· + + private fun getOptimizedExecutorService( + corePoolSize: Int, + maximumPoolSize: Int, + keepAliveTime: Long, + unit: TimeUnit, + workQueue: BlockingQueue, + threadFactory: ThreadFactory? = null, + className: String, + ): ExecutorService { + val executor = ThreadPoolExecutor( + corePoolSize, maximumPoolSize, + keepAliveTime, unit, + workQueue, + NamedThreadFactory(threadFactory, className) + ) + executor.setKeepAliveTime(defaultThreadKeepAliveTime, TimeUnit.MILLISECONDS) + executor.allowCoreThreadTimeOut(true) + return executor + } + + private class NamedThreadFactory( + private val threadFactory: ThreadFactory?, + private val className: String + ) : ThreadFactory { + + private val threadId = AtomicInteger(0) + + override fun newThread(runnable: Runnable): Thread { + val originThread = threadFactory?.newThread(runnable) + val threadName = + className + "-" + threadId.getAndIncrement() + if (originThread != null) { + "-" + originThread.name + } else { + "" + } + val thread = originThread ?: Thread(runnable) + thread.name = threadName + if (thread.isDaemon) { + thread.isDaemon = false + } + if (thread.priority != Thread.NORM_PRIORITY) { + thread.priority = Thread.NORM_PRIORITY + } + return thread + } + + } + +} +``` + +之后,在 Transform 阶段只要遍历到调用了 Executors 中静态方法的指令的话,就将该指令中的 Executors 替换为 OptimizedExecutors,然后添加多插入的参数 + +```kotlin +/** + * @Author: leavesC + * @Date: 2021/12/16 15:11 + * @Desc: + * @Github:https://github.com/leavesC + */ +class OptimizedThreadTransform(private val config: OptimizedThreadConfig) : BaseTransform() { + + companion object { + + private const val executorsClass = "java/util/concurrent/Executors" + + } + + override fun modifyClass(byteArray: ByteArray): ByteArray { + val classNode = ClassNode() + val classReader = ClassReader(byteArray) + classReader.accept(classNode, ClassReader.EXPAND_FRAMES) + val methods = classNode.methods + val taskList = mutableListOf<() -> Unit>() + if (!methods.isNullOrEmpty()) { + for (methodNode in methods) { + val instructionIterator = methodNode.instructions?.iterator() + if (instructionIterator != null) { + while (instructionIterator.hasNext()) { + val instruction = instructionIterator.next() + when (instruction.opcode) { + Opcodes.INVOKESTATIC -> { + val methodInsnNode = instruction as? MethodInsnNode + if (methodInsnNode?.owner == executorsClass) { + taskList.add { + transformInvokeStatic( + classNode, + methodNode, + instruction + ) + } + } + } + } + } + } + } + } + taskList.forEach { + it.invoke() + } + val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS) + classNode.accept(classWriter) + return classWriter.toByteArray() + } + + private fun transformInvokeStatic( + classNode: ClassNode, + methodNode: MethodNode, + methodInsnNode: MethodInsnNode + ) { + val pointMethod = config.threadHookPointList.find { it.methodName == methodInsnNode.name } + if (pointMethod != null) { + //将 Executors 替换为 OptimizedExecutors + methodInsnNode.owner = config.formatOptimizedThreadPoolClass + //为调用 newFixedThreadPool 等方法的指令多插入一个 String 类型的方法入参参数声明 + methodInsnNode.insertArgument(String::class.java) + //将 ClassName 作为入参参数传给 newFixedThreadPool 等方法 + methodNode.instructions.insertBefore( + methodInsnNode, + LdcInsnNode(classNode.simpleClassName) + ) + } + } + +} +``` + +# 三、需慎重 + +需要注意,上述的手段并不全是可以在项目中进行全盘套用的 + +为线程设置一个统一和容易标识的线程名有利于出现异常时能够快速定位问题,这是比较有意义的。但为线程池设置统一的超长时间和允许回收核心线程,这是需要考虑使用场景的,设置不当反而可能会加大 OOM 的概率 + +如果大部分情况下线程池需要处理的任务比较少且又不允许回收核心线程的话,就会导致核心线程大部分时间处于阻塞状态得不到释放,浪费系统资源,此时通过 hook 来允许回收核心线程是比较有意义的。而如果线程池需要处理的任务是属于定时执行的话,例如固定每六秒就会有一个新任务到来,此时通过 hook 设置了线程超时时间为五秒,那就有可能导致线程刚被回收不久又需要重新创建了,这种反复的创建和回收反而加大了系统开销,反而不如直接允许核心线程常驻。此外,线程从被创建到被系统选中调度执行是有一定耗时的,这不适合于某些对响应速度要求比较高的场景,此时允许回收核心线程也是不太合适的 + +所以说,在针对线程池进行整治的时候,需要同时考虑该线程池是应用于什么业务场景,根据用途来进行特殊配置 + +关于线程池的实现细节,可以看我写的一篇 ThreadPoolExecutor 解读文章,对设计初衷和实现始末进行了详细介绍:[聊聊 Java 多线程(5)-超详细的 ThreadPoolExecutor 源码解析](https://juejin.cn/post/6901317365561032712) + +# 四、源码 + +最后也给出完整的源码:[ASM_Transform](https://github.com/leavesC/ASM_Transform) + +当中包含了本文的全部实现源码,也包含上一篇文章:[ASM 字节码插桩实现双击防抖的实现始末](https://juejin.cn/post/7042328862872567838) 中所实现的双击防抖功能,希望对你有所帮助 🤣🤣 \ No newline at end of file From 4130f684d58a234dc4aa803ca9c8df7272881f96 Mon Sep 17 00:00:00 2001 From: leavesc <1990724437@qq.com> Date: Thu, 23 Dec 2021 20:28:49 +0800 Subject: [PATCH 07/62] =?UTF-8?q?ASM=20=E5=AD=97=E8=8A=82=E7=A0=81?= =?UTF-8?q?=E6=8F=92=E6=A1=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- ...16\260\345\217\214\345\207\273\351\230\262\346\212\226.md" | 0 ...41\214\347\272\277\347\250\213\346\225\264\346\262\273.md" | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" => "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226.md" (100%) rename "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" => "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273.md" (100%) diff --git a/README.md b/README.md index cb1081b..d5432c6 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ 如果发现掘金文章内容不全,请查看此处文件夹中的文章,这里会一直保留最新最全的文章内容 * [Android 精讲]() - * [ASM 字节码插桩进行线程整治的实现始末](https://juejin.cn/post/7043999874076901406) - * [ASM 字节码插桩实现双击防抖的实现始末](https://juejin.cn/post/7042328862872567838) + * [ASM 字节码插桩进行线程整治](https://juejin.cn/post/7043999874076901406) + * [ASM 字节码插桩实现双击防抖](https://juejin.cn/post/7042328862872567838) * [Android WebView H5 秒开方案总结](https://juejin.cn/post/7016883220025180191) * [Jetpack LiveData 的设计理念及改进](https://juejin.cn/post/6903096576734920717) * [探究 Kotlin 的隐藏性能开销与避坑指南](https://juejin.cn/post/7010367024916660237) diff --git "a/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226.md" similarity index 100% rename from "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" rename to "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\256\236\347\216\260\345\217\214\345\207\273\351\230\262\346\212\226.md" diff --git "a/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273.md" similarity index 100% rename from "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273\347\232\204\345\256\236\347\216\260\345\247\213\346\234\253.md" rename to "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\350\277\233\350\241\214\347\272\277\347\250\213\346\225\264\346\262\273.md" From 91d931787c4e9aaf379038cb9d3fd68039f7203f Mon Sep 17 00:00:00 2001 From: leavesc <1990724437@qq.com> Date: Mon, 27 Dec 2021 10:54:03 +0800 Subject: [PATCH 08/62] =?UTF-8?q?ASM=20=E5=AD=97=E8=8A=82=E7=A0=81?= =?UTF-8?q?=E6=8F=92=E6=A1=A9=E5=8A=A9=E5=8A=9B=E9=9A=90=E7=A7=81=E5=90=88?= =?UTF-8?q?=E8=A7=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- ...20\347\247\201\345\220\210\350\247\204.md" | 408 ++++++++++++++++++ 2 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 "android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\212\251\345\212\233\351\232\220\347\247\201\345\220\210\350\247\204.md" diff --git a/README.md b/README.md index d5432c6..82a1bdd 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ 如果发现掘金文章内容不全,请查看此处文件夹中的文章,这里会一直保留最新最全的文章内容 * [Android 精讲]() - * [ASM 字节码插桩进行线程整治](https://juejin.cn/post/7043999874076901406) * [ASM 字节码插桩实现双击防抖](https://juejin.cn/post/7042328862872567838) + * [ASM 字节码插桩进行线程整治](https://juejin.cn/post/7043999874076901406) + * [ASM 字节码插桩助力隐私合规](https://juejin.cn/post/7046207125785149448) * [Android WebView H5 秒开方案总结](https://juejin.cn/post/7016883220025180191) * [Jetpack LiveData 的设计理念及改进](https://juejin.cn/post/6903096576734920717) * [探究 Kotlin 的隐藏性能开销与避坑指南](https://juejin.cn/post/7010367024916660237) diff --git "a/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\212\251\345\212\233\351\232\220\347\247\201\345\220\210\350\247\204.md" "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\212\251\345\212\233\351\232\220\347\247\201\345\220\210\350\247\204.md" new file mode 100644 index 0000000..f117b6c --- /dev/null +++ "b/android_core/ASM\345\255\227\350\212\202\347\240\201\346\217\222\346\241\251\345\212\251\345\212\233\351\232\220\347\247\201\345\220\210\350\247\204.md" @@ -0,0 +1,408 @@ +> 公众号:[字节数组](https://upload-images.jianshu.io/upload_images/2552605-57915be42c4f6a82.jpg) +> +> 希望对你有所帮助 🤣🤣 + +近两年来工信部对于应用的隐私合规安全问题愈加重视,对 Android 平台的管控程度也要比 IOS 平台严格很多,很多不合规的应用也先后被下架要求整改 + +![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f01addaeea7c4865a77119108fd2c86c~tplv-k3u1fbpfcp-zoom-1.image) + +要避免出现隐私合规安全问题,最主要的就是要做到以下两点: + +- 在用户同意隐私协议之前,不能有收集用户隐私数据的行为。例如,在用户同意之前不能去获取 Android ID、Device ID、MAC 等隐私数据 +- 在用户同意隐私协议之后,搜集用户隐私数据的行为不能超出实现服务场景所必需的最低频率。例如,某些应用会在每次网络请求时将当前设备的 Android ID 作为 header 一起上报,如果没有对 Android ID 进行缓存处理的话,搜集该数据的行为频率就会非常高,此时一样存在隐私合规问题 + +针对以上两点问题有不同的应对措施 + +- 对于第一点。需要统计出整个项目中所有涉及到隐私行为的相关代码,根据业务流程来判断该隐私行为是否合理、以及是否会在用户同意隐私协议之前被触发。这就需要对整个项目进行 **静态扫描** 了 +- 对于第二点。需要在应用运行时动态记录每次触发隐私行为的时间点和调用链信息,根据触发时间来判断该隐私行为是否过量执行,根据调用链信息来辅助判断具体是哪一块业务在获取隐私数据。这就需要对应用进行 **动态记录** 了 + +以上应对措施如果单纯靠开发人员来肉眼识别代码和编码统计的话,工作量非常大而且也很不现实,因为一个大型项目往往都会引入多个依赖库和第三方 SDK,我们可以规范自有代码,但没法修改和有效约束外部依赖,也很难理清楚依赖库的内部逻辑和调用链关系。此外,当检测到隐私行为后,也要输出相对应的记录报告,以便开发人员能够在开发阶段排查问题 + +此时就可以依靠 ASM + Transform 来应对这种非业务侵入式的开发场景了,本文要介绍的就是如何通过 ASM + Transform 来助力通过隐私合规安全审核 + +# 静态扫描 + +要进行静态扫描,首先就需要先明确哪些操作是属于隐私行为,这里以获取 Device ID 和 Build.BRAND 为例。在 Activity 中通过点击按钮来模拟获取隐私数据的行为,后续实现 **动态记录** 时需要记录的调用链指的也就是从点击按钮到执行 `DeviceUtils.getDeviceId` 方法的整个过程 + +```kotlin +package github.leavesc.asm.privacy_sentry + +class PrivacySentryActivity : AppCompatActivity() { + + private val btnGetDeviceId by lazy { + findViewById