致二零一九

告别了二零一九,迎来了二零二零,”二零二零”真的是一个很陌生的词,邓小平在四十年前提出的口号:“全面建设成小康社会”,而如今已经到了这个节点,我与大多数人一样,其实都对“小康”没有一个很明确的理解,亦不知道现在是否可以定义为“小康社会”。然而这并不重要,这并不是我该去研究探索的实物,毕竟人人都有自己的“小康”目标。
已经挥手告别了二零一九,在此做一个年度的总结吧。

二零一九或许给人最大的改变,那就是身份的改变吧,结束了长达十六年的学校学生的身份,成为了一名社会人,也拥有了自己的职业称呼–Android开发工程师。很荣幸,我选择了我自己所感兴趣的行业进行了就业,在初高中就萌发出来的对于编程相关的兴趣,是我从事此行业的动力,我并不感到厌倦;也很荣幸,我加入了一家很不错的的公司,并没有像其他互联网公司进行压榨的“996”,也没有无休止地加班,跟随了一个很不错的师傅,教会了我很多很多……我很感激;也很荣幸,我不再是一名学生,但是我也并没有忘记学习,今年学习了很多,也读了很多书:

二零一九我完成了一次毕业旅行,至今依然还能记得青岛“静听海浪拍岸,坐看海天一色”,烟台那种天际辽阔无垠感。也与朋友去了趟武汉,感受到了武汉的生机活力。望明年能够去更多的城市去体会更多的不同。

二零一九我依然完成了很多自己的年初设置的目标:

1、坚持写日记

2、去现场看五场足球比赛

3、学习一门外语(日语)

4、技术博客数不少于10篇

5、体重控制在70kg以内

6、看至少十部电影

……

时间的流逝带来了很多的东西,也让人丢掉了很多东西。几个月前,我买了一个ps4游戏机,希望找回童年那时的那种快乐,可是很难……我并不能静下心来去做这样一件事,也没有那种儿时花大精力去探索去钻研游戏的各个细节,一个游戏能重复玩个很多遍也不腻味。儿时面对着黑白电视机,像素点十分明显的游戏能够没日没夜的玩着,甚至到了吃饭也不愿意去,即便现在拥有了更华丽更丰富的画面,更好的游戏体验,可是它也并没有一局短暂的Dota游戏带来快感更划算。难道是说游戏不好玩么?不是的,是那颗心已经没有了吧……

二零一九,一直是一个人,发现自己变得更自卑些了,亦或是害怕了。害怕去打破那样的宁静,害怕自己不够优秀。总是低着头,甚至不敢抬头看一眼,可我也很想抬起头来,也期待着”只愿君心似我心,定不负相思意”。

然而生活总是要继续的吧,还有很多很多的美好等着自己。

二零二零,又是一个新的年代,望向远方吧!

从奶酪夹心饼干生产中来学习Android 中的gradle构建

最近终于有机会做一些关于Android plugin相关的东西,之前虽然有学习过《Android 权威指南》一书,但是并没有进行一个实战操作,都是一些理论相关的学习。最近做了一个plugin主要是为了提取class文件里面的注解信息然后讲起搜集并上传。在实践中回过头发现很多知识都已经遗忘,所以本文对相关一些比较核心的知识进行一个回顾与梳理。
Gradle的思维导图
内容如下:

一、Gradle概述
二、Groovy
三、Gradle的依赖
四、Gradle的Task构建与执行
五、Gradle插件

一、Gradle概述

在Android开发中,Gradle是每个开发者都会接触的,Gradle 是一个非常优秀的项目构建工具。这是大家都知道的,但是又有啥用呢?

最开始的时候很难理解gradle到底是干什么的,相关知识都比较离散,所以很多东西没有串起来,从而导致理解起来比较困难。

我自己总结就是:Gradle是一个构建工具,它存在的目的是产生一套“流水线”,对于安卓开发而言这个流水线就是从本地的编写代码以及资源整合到最终生成的产品过程。

用一个很形象的例子举例,我们现在要生产一包奶酪夹心饼干,于是我们得定义一个生产顺序:先让有的地方去生成饼干,有的地方生成出来奶酪,之后再让两块饼干夹着一块奶酪,最后再将它们装进一个小包装袋里面。

另一种情况:如果我想在奶酪中加一点果酱,那么我们不需要重新建立一套生产线,只需要在两块饼干与奶酪结合的过程中修改一下加入果酱的流程。

再另一种情况:如果我生产出来的奶酪夹心饼干不需要包装,那只需要在最后一个步骤让它另外走一条线路,毕竟没有包装的又不是不能吃,对吧?
奶酪饼干不同的生产流程
如上图所示,我们定义了三种流程,每种流程最后的产出物是不一样的,因为流程的“初始化”的东西是不一样的以及过程中的“配置”,所以“执行”的时候就不一样。

对比我们安卓开发:本地的Java文件以及资源文件就是对应的饼干以及奶酪,最终生成的面向用户的apk文件就是包装好的奶酪夹心饼干。

如果我们想打Debug包,那么就像是一个散装的饼干,我们能自己用用,但是还不能面向用户,如果想打Release包那么就是最终的产品形态能直接面向用户。

上面的例子讲得比较长,其实主要想让更多人能够更好地去理解gradle的用处。

当我们每次点击Android Studio的 run运行按钮之后,会看到控制台输出一大堆相关日志,例如下图所示:
Android Studio系统封装好的gradle Task
其实这些都是系统为我们封装好的一些task
点击 run 按钮,就相当于执行了一次 Gradle Task,一般来说,是Task assembleDebug或者Task assembleRelease

Gradle是目前Android主流的构建工具,无论通过命令行还是通过AndroidStudio来build,最终都是通过Gradle来实现的。以及Android领域的探索已经越来越深,不少技术领域如插件化、热修复、构建系统等都对Gradle有相关的需要。

二、Groovy

知道了Gradle的用处之后,我们很形象的知道Gradle是为了去产生一个流水线。那这个流水线是利用什么做到的呢?对于奶酪饼干生产的工厂他们是不同的车间机械工具直接的逻辑组装。而对于Gradle则是利用groovy语言编写出来的相关脚本从而来进行一个编译相关的配置。这里不再具体描述groovy语言的具体用法,这里我列举出来几个自己认为比较重要的几个技术点。

1、Closure(闭包)
闭包是的groovy语言具有,而Java语言不具有的特性,有人说Lambda表达式就是闭包,但是两则还是有一定的差异的,有兴趣的同学可以去看看这篇Java中Lambda表达式解析

定义闭的语意 :

{ [closureParameters -> ] statements }

其中[closureParameters->]代表参数,多参数用逗号分割,用->隔开参数与内容,没有参数可以不写->例如我们精彩在.gradle文件里面看到这样的内容:
闭包
其中projcet就是[closureParameters->]->之后的respositories就是statements,对于这段代码而言,statements里面又是一个闭包,如果改写成Java的样子就更形象了:

1
2
3
void subprojercts(Project projct) {
doSomething....
}

2、方法的输入参数优化
groovy中定义的函数,如果至少有一个参数,在调用的时候可以省略括号。比如这样

1
2
3
def func(String a){
println(a)
}
1
func 'hello'

在gradle有大量省略括号调用函数的例子,比如

1
2
3
4
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.targetSdkVersion
}

比如这里minSdkVersion 和 targetSdkVersion 其实就是调用了两个函数,传入了不同的参数,在AndroidStudio里面可以点进去查看函数实现

当然如果某个函数没有参数,那就不能省略括号,否则会当成一个变量使用

3、类的Property

如果类的成员变量没有加任何权限访问,则称为Property, 否则是Field,filed和Java中的成员变量相同,但是Property的话,它是一个private field和getter setter的集合,也就是说groovy会自动生成getter setter方法,因此在类外面的代码,都是会透明的调用getter和setter方法。

4、Trait

特性使用关键字 trait 声明,可以拥有普通成员和抽象成员。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

trait MessageHandler {
// 属性
int minLenght
// 方法
// 普通方法
void echo(String msg) {
println(msg)
}
// 抽象方法
abstract void show(String msg)
}
trait AnotherMessageHandler {
// 抽象方法
abstract void show(String msg)
}

class Message implements AnotherMessageHandler, MessageHandler {
.......
}

Groovy 中特质本质上是运行时对接口的实现,所以其方法的访问控制符只支持 public 和 private。从代码的书写可以看出来trait又像java中的abstract类又像interface
说他像interface是因为从编写上看就是使用了implements关键字,但是接口又不能使用普通方法。说他像抽象类,因为其内部使用了abstract定义抽象方法。但是它又能implements多个,而达到“多继承”的特性。因此它不是接口,也不是抽象类,它是 trait

三、Gradle的依赖

我们继续回到上面奶酪夹心饼干的生产上面,在产出奶酪夹心饼干之前,我们需要分别生产好单独的饼干与奶酪。假如我们的饼干原料有很多种,姑且我们叫他饼干v1,饼干v2……饼干vn ,奶酪也有很多种,我们叫它奶酪v1,奶酪v2……奶酪vn。那这么多种具体生产起来就应该有相关的选择,在Android开发中各种库都被单独抽了出来,只需要单独声明出来需要用哪个库即可。

我们平时看的的dependencies如下所示

1
2
3
4
5
6
7
8
9
10
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.victor:lib:1.0.4'
api 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation('com.wanjian:sak:0.1.0') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
}

1、依赖配置
Gradle依赖的配置中主要使用以下关键字,摘自谷歌官方Gradle文档->添加编译依赖项

implementation
Gradle 会将依赖项添加到编译类路径,并将依赖项打包到编译输出。不过,当您的模块配置 implementation 依赖项时,会让 Gradle 了解您不希望该模块在编译时将该依赖项泄露给其他模块。也就是说,其他模块只有在运行时才能使用该依赖项。

api
Gradle 会将依赖项添加到编译类路径和编译输出。当一个模块包含 api 依赖项时,会让 Gradle 了解该模块要以传递方式将该依赖项导出到其他模块,以便这些模块在运行时和编译时都可以使用该依赖项

annotationProcessor
要添加对作为注解处理器的库的依赖关系,您必须使用 annotationProcessor 配置将其添加到注解处理器类路径。这是因为,使用此配置可以将编译类路径与注解处理器类路径分开,从而提高编译性能。如果 Gradle 在编译类路径上找到注解处理器,则会禁用避免编译功能,这样会对编译时间产生负面影响(Gradle 5.0 及更高版本会忽略在编译类路径上找到的注解处理器)。

2、依赖的传递与冲突
在Maven仓库中,构件通过POM(一种XML文件)来描述相关信息以及传递性依赖。Gradle 可以通过分析该文件获取获取所以依赖以及依赖的依赖和依赖的依赖的依赖,为了更加直观的表述,可以通过下面的输出结果了解。

1
2
3
4
5
6
7
8
9
10
11
+--- com.github.hotchemi:permissionsdispatcher:2.2.0
| \--- com.android.support:support-v4:23.1.1 -> 28.0.0
| +--- com.android.support:support-compat:28.0.0
| | +--- com.android.support:support-annotations:28.0.0
| | +--- com.android.support:collections:28.0.0
| | | \--- com.android.support:support-annotations:28.0.0
| | +--- android.arch.lifecycle:runtime:1.1.1
| | | +--- android.arch.lifecycle:common:1.1.1
| | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
| | | +--- android.arch.core:common:1.1.1
| | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0

我依赖hotchemi:permissionsdispatcher这个库,而它内部又陆陆续续地依赖了后面的一大堆。借助Gradle的传递性依赖特性,你无需再你的脚本中把这些依赖都声明一遍,你只需要简单的一行,Gradle便会帮你将传递性依赖一起下载下来。

然而问题来了这里面依赖了:android.arch.core:common:1.1.1 而我本地其他地方又使用了android.arch.core:common:1.0.0老版本。那我该如何去做这件事呢?

于是便有了如下关键词:
exclude
force
transitive

具体作用如下代码所示

1
2
3
4
5
6
7
8
9
implementation ('com.google.code.gson:gson:2.8.6') {
force = true //强制使用这个版本的库
}
implementation ('de.hdodenhof:circleimageview:3.0.1') {
transitive = true //防止向外暴露
}
implementation('com.wanjian:sak:0.1.0') {
exclude group: 'com.android.support', module: 'appcompat-v7' //排除里面不需要的库
}

四、Gradle的Task构建与执行

再回到生产饼干的例子上面来,最开始我们定义了一些流程,然后再让机器以该流程去执行。

比如先准备两块饼干再与奶酪进行加工生成夹心饼干,最后再加入包装。这是一条正确的流水,我们不可能让加入包装在加工生成夹心饼干之前。于是我们得定义一些约束,让其有正确的执行顺序。或者我们像加入果酱,那么就应该对原始的流程进行一些添加。

对于Gradle中,我们以Task为单位,类比生产奶酪饼干,生成饼干是一个专门的Task,生成奶酪也是一个专门的Task,加工成夹心也是一个Task……对于Android开发,将java文件编译为class,再到最后的dex生成都是Task

在Task的构建与执行中主要分为三个流程:

初始化(Initialization)
settings.gradle确定参与构建的module
为每个module创建Project对象实例

配置(Configuration )
build.gradle脚本执行,配置对应project实例
创建有向无环图
通过finalizedBy指定后续
通过must/shouldRunAfter约束执行顺序

执行(Execution )
根据关系图执行task
监听器

主要流程如图所示(图片摘自https://www.jianshu.com/p/0acdb31eef2d):
图片来自https://www.jianshu.com/p/0acdb31eef2d

五、Gradle插件

继续奶酪夹心饼干的故事,如果夹心饼干模样规规矩矩没有花纹,岂不是很low?于是工厂专门研发了一款能让饼干产生纹路的机器,并在加工成夹心饼干之前将纹路印到饼干上面去,假如这台机器我们把它叫做“印花纹机”,是一个能从整个生产流程中独立的出来的机器,这台“印花纹机”也能用在生产其他的饼干上。

对应在我们的Android开发中,在构建流程中我们抽离出来一些功能,将其独立开来,这就是plugin,这里不再讲解plugin的编写相关操作,可以参考Gradle 自定义 plugin

1、插件分类

脚本插件
顾名思义,如下图所示我们将对应的插件脚本中加入相关插件的逻辑,如下图所示,“other.gradle”便是一个插件

1
apply from: 'other.gradle'

二进制插件
二进制插件就是实现了 org.gradle.api.Plugin 接口的插件,每个 Java Gradle 插件都有一个 plugin id,可以通过如下方式使用一个 Java 插件:

1
apply plugin : 'maven'

通过上述代码就将 Java 插件应用到我们的项目中了,其中 maven 是 Java 插件的 plugin id,对于 Gradle 自带的核心插件都有唯一的 plugin id

2、打包方式

build script
在插件分类中我们提到有apply from: 'other.gradle' 其中other.gradle就是一个打包好的build script

buildSrc
将插件写在工程根目录下的buildSrc目录下,这样可以在多个模块之间复用该插件。
buildSrc是Gradle在项目中配置自定义插件的默认目录,但它并不是标准的Android工程目录,所以使用这种方式需要我们事先手动创建一个buildSrc目录
buildSrc插件
独立项目
创建独立的插件项目具有更强的灵活性,能让更多的工程使用这个插件,但流程也会相对复杂一点.这里不再具体讲解,可以参考Gradle 自定义 plugin

参考资料:

https://www.jianshu.com/p/6dc2074480b8
https://www.jianshu.com/p/bcaf9a269d96
https://juejin.im/entry/59918304518825489151732d
https://www.jianshu.com/p/0acdb31eef2d
https://juejin.im/post/5cc5929bf265da036706b350
https://doc.yonyoucloud.com/doc/wiki/project/GradleUserGuide-Wiki/gradle_plugins/binary_plugins.html

一款快速生成安卓shape的插件--NoCodeShape

一、NoCodeShape介绍

NoCodeShape是一个能可视化界面操作生成Android中Shape.xml的Android Studio插件。对于新手能更好的去接受Android中相对应的属性,对于资深程序员则能简化操作,快速生成shape.xml。

二、NoCodeShape使用方法

1. 下载和安装

同一般的Android Studio插件下载一样,可以直接Preferences->Plugins 搜索 NoCodeShape搜索出来结果直接安装再重启即可。
在这里插入图片描述
也可以去jetbrains插件管理的官网下载各个版本的插件
https://plugins.jetbrains.com/plugin/13325-nocodeshape/versions
然后Preferences->Plugins 再Install plugins from disk从本地安装

2. 如何使用

在新建一个shape.xml文件后,右键选择NoCodeShape或者直接按快捷键Common+U
在这里插入图片描述
然后选择自己想要的属性,随着点击事件的进行会生成相关xml代码,并在Android Studio右边有对应的shape形状的展示。如果对应shape.xml有相关属性,NoCodeShape也会生成对应shape.xml属性的操作界面,非常方便。
示例:
在这里插入图片描述

三、实现原理

实现原理总体来说并不复杂,主要是界面相关操作逻辑比较繁琐。

对于新生成的一个shape.xml来说只需要弹出一个新的操作界面,用户只需要点击对应模块的属性即可。项目使用了单例模式+Bulider建造者模式去管理各个Shape属性,分别生成 Shape、Solid、Corners、Stroke、Gradient的单例,其内部拥有一个Builder用来去承各类型的具体属性。

通过界面的操作,对其内部的Builder进行数据的填充,最后在完成各类操作后,将各类型中的Builder中的所有属性提取出并生成一份完整的xml字符串并将其粘贴到Android Studio的操作界面上。

实现主要分为两大类:

1. 拼接生成xml字符串

字符串拼接算是其中最复杂的部分,一是各shape的类型拥有较多数据,其中一些属性有逻辑存在性,二是生成最终Android Studio的xml字符串的时候格式存在比较多的处理。对于各类属性都继承于`BaseXml`,其内部拥有一个静态内部`Builder`类,以相对简单的Solid类来举例,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Solid extends BaseXml {

private static Builder builder;
private static Solid instance = null;

public static Solid getInstance() {
if (instance == null) {
builder = new Builder();
instance = new Solid();
}
return instance;
}

public static class Builder extends BaseBuilder {
String color;
String colorValue;

public void setColor(String color) {
this.colorValue = color;
this.color = getAttrWithOutUnitStr("color", color);
}
@Override
public String getBuilderString() {
return StringUtils.getString(color);
}

@Override
public void clearData() {
StringUtils.clearObjectData(this);
}

@Override
public void analysisAttribute(Attributes attributes) {
Solid.getInstance().setChecked(true);
setColor(attributes.getValue("android:color"));
}
}

其类继承于抽象类BaseXml,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class BaseXml {
private boolean isChecked = false;
public String getCloser() {
return " />";
}
public String getStartTag() {
return "";
}
public String generateXmlString() {
return "";
}
protected String getLineFeedString() {
return "\n";
}
public boolean isChecked() {
return isChecked;
}
public BaseXml setChecked(boolean checked) {
isChecked = checked;
return this;
}
}

抽象类提取出来在字符串拼接阶段,各类型常用的基本操作,例如:返回“<solid”这类的开始标签,” />”结束标签等

其内部BaseXml拥有对应类拥有的所有属性的常用操作,其继承与抽象类BaseBuilder,代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class BaseBuilder {
public abstract String getBuilderString();
public abstract void clearData();
public abstract void analysisAttribute(Attributes attributes);
protected final String getAttrWithUnitStr(String attributeType, String value) {
String unit;
if (TextUtils.isEmpty(value)) {
return "";
}
if (value.contains("px") || value.contains("dp")) {
unit = "";
} else {
unit = DefaultData.UNIT;
}
return "android:" + attributeType + "=\"" + value + unit + "\"";
}
protected final String getAttrWithOutUnitStr(String attributeType, String value) {
if (TextUtils.isEmpty(value)) {
return "";
}
return "android:" + attributeType + "=\"" + value + "\"";
}
protected final String getValueOutUnit(String value) {
if (TextUtils.isEmpty(value)) {
return value;
}
return value.replace("dp", "").replace("px", "");
}
}

BaseBuilder内部封装了一些属性的常用操作,例如生成:android:color="#FFFFFF"这样的字符串,获取是否带有单位的字串等。
并提供三个抽象方法:

1
2
3
public abstract String getBuilderString(); //获取Builder中所有属性拼接好的字符串
public abstract void clearData(); //清空Builder内部属性值
public abstract void analysisAttribute(Attributes attributes); //分析xml数据中的值,这个在第二大点“将原有shape.xml字符串转化为对应操作界面”中将会讲到

以上是基本数据的构造,在最后在控件交互的地方会调用CommonAction类的refreshAndWriteData

1
2
3
4
5
6
7
8
abstract class CommonAction {
JComponent component;
NoShapeDialog noShapeDialog;

void refreshAndWriteData() {
NoCodeShapeAction.callWriteData();
}
}

最后会调用基本的Action中的writeData()方法,其具体逻辑为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 数据写入xml文件
*/
private static void writeData() {
final Document document = FileDocumentManager.getInstance().getDocument(file);
if (document == null) {
try {
throw new Exception("Document对象为空");
} catch (Exception e) {
e.printStackTrace();
}
return;
}
new WriteCommandAction.Simple(project) {
@Override
protected void run() {
document.setText(XMLString.getInstance().generateXmlString());
//formatCode();
}
}.execute();
}

利用XMLString.getInstance().generateXmlString()获取各操作类型的所有属性将其拼接为一份完整的shape.xml文件的字段,最后调用插件系统的相关命令将字符串粘贴在系统对应的输入框中。

2. 将原有shape.xml字符串转化为对应操作界面

上面已经讲述类如何拼接生成xml字符串并将其粘贴到Android Studio界面上,此外NoCodeShape不仅支持新生成的shape.xml,同样也支持对旧shape.xml进行修改的能力。与第一步相比较主要多了一步读取Android Studio xml文档并将其转化为对应操作界面的过程。其主要是在操作界面初始化之前执行了如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void initSax() {
String text = FileDocumentManager.getInstance().getDocument(file).getText();
ShapeSaxHandler handler = new ShapeSaxHandler();
try {
handler.createViewList(text);
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

利用ShapeSaxHandler去解析xml里面的元素,参考了FindViewByMe的解析原理。

具体操作逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void createViewList(String string) throws ParserConfigurationException, SAXException, IOException {
InputStream xmlStream = new ByteArrayInputStream(string.getBytes("UTF-8"));
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(xmlStream, this);
}
@Override
public void startDocument() throws SAXException {
if (shapePartList == null) {
shapePartList = new ArrayList<ShapePart>();
}
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
switch (qName) {
case "shape":
Shape.getInstance().getBuilder().analysisAttribute(attributes);
break;
case "stroke":
Stroke.getInstance().getBuilder().analysisAttribute(attributes);
break;
case "solid":
Solid.getInstance().getBuilder().analysisAttribute(attributes);
break;
case "gradient":
Gradient.getInstance().getBuilder().analysisAttribute(attributes);
break;
case "corners":
Corners.getInstance().getBuilder().analysisAttribute(attributes);
break;
default:
break;
}

逻辑其实很清楚,主要对startTag的判断,然后通过调用 public abstract void analysisAttribute(Attributes attributes);的方法对相应的 类型的Buidler进行一个赋值操作。

以Stroke为例:

1
2
3
4
5
6
7
8
@Override
public void analysisAttribute(Attributes attributes) {
Stroke.getInstance().setChecked(true);
setColor(attributes.getValue("android:color"));
setDashGap(attributes.getValue("android:dashGap"));
setWidth(attributes.getValue("android:width"));
setDashWidth(attributes.getValue("android:dashWidth"));
}

主要是获取到其中的属性,并对初始化的界面进行一些操作(例如选中或者赋值相关操作)。

四、总结

这个插件算是自己第一次做一个相对较实用的插件,都是利用工作的空闲事件进行编写,前前后后进行了将近一个月,其中收获颇多,但也踩过了各种各样的坑。在开发过程中由于相关文档较少,通过阅读官方文档还是有点小吃力,不过一步步还是走了下来,自己也得到了成长。其中坑也都添平了,但由于一些基础技术的欠缺,比如对Java GUI界面编程不太熟悉,导致开发过程中有很大一段时间都在跟界面作对,因此后面有机会将去深入了解Java的界面编程,争取能够将页面交互能够做到更好。

另外由于自身开发精力的原因,不能将插件做到完美,目前插件中还有如下几个问题:

1、Gradient中对应相关逻辑还需要再优化
2、还未支持Size跟Pading(从自身所处环境考虑用得较少,所以暂未支持)
3、对于颜色选择器默认打开后不支持对本地颜色字符串处理
4、存在大量的界面操作逻辑代码,需要优化

最后,希望大家能在使用过程中提出相关的意见或建议,也欢迎能一起加入到开发中,从而能将该插件做得更加完美。

项目地址:

https://github.com/VomPom/NoCodeShape

Java中的Reference解析

前言

Java执行GC判断对象是否存活有两种方式其中一种是引用计数

引用计数:Java堆中每一个对象都有一个引用计数属性,引用每新增1次计数加1,引用每释放1次计数减1。

在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于(reachable)可达状态,程序才能使用它。

从JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用,本文主要讲解各个引用的用途以及引用队列的作用。

1、强引用(StrongReference)

强引用指的是程序代码中普遍存在的,类似如下代码

1
Object object = new Object();

当内存空间不足时,只要强引用还在,Java虚拟机会抛出OutOfMemoryError错误,使程序异常终止,也不会靠回收强引用的对象来解决内存不足的问题。

如果强引用对象不使用时,需要弱化从而使GC能够回收,如下所示:

1
object = null;

2、软引用(SoftReference)

软引用是用来描述一些有用但并不是必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回首范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等,软引用可用来实现内存敏感的高速缓存。

1
2
3
4
5
// 强引用
String strongReference = new String("abc");
// 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);

软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

1
2
3
4
5
6
7
8
9
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<>(str, referenceQueue);
str = null;
// Notify GC
System.gc();
System.out.println(softReference.get()); // abc
Reference<? extends String> reference = referenceQueue.poll();
System.out.println(reference); //null

注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。

当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:

1
2
3
4
5
6
if(JVM内存不足) {
// 将软引用中的对象引用置为null
str = null;
// 通知垃圾回收器进行回收
System.gc();
}

也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的较新的软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因,后面会具体讲解。

3、弱引用(WeakReference)

弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的
对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,
都会回收掉只被弱引用关联的对象。

1
2
3
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
str = null;

复制代码JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:

1
2
str = null;
System.gc();

注意:如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用Weak Reference来记住此对象。

下面的代码会让一个弱引用再次变为一个强引用:

1
2
3
4
String str = new String("abc");
WeakReference<String> weakReference = new WeakReference<>(str);
// 弱引用转强引用
String strongReference = weakReference.get();

同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

4、虚引用(PhantomReference)

虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用获得一个对象实例。

虚引用主要用来跟踪对象被垃圾回收器回收的活动,虚引用与软引用和弱引用的一个区别在于:

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

1
2
3
4
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

5、引用队列(ReferenceQueue)

在前面软引用、弱引用和虚引用都有讲到引用队列,那引用队列具体是做什么的呢?
官方对于引用队列类的注释是:

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.
译为:引用队列是将垃圾收集器在监测到适当的可达性更改后将已注册的引用对象添加到该队列。

对于软引用和弱引用和虚引用,我们希望当一个对象被gc掉的时候通知用户线程,进行额外的处理时,就需要使用引用队列了。ReferenceQueue即这样的一个对象,当一个obj被gc掉之后,其相应的包装类,即ref对象会被放入queue中。我们可以从queue中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理等。

查看源码发现ReferenceQueue的代码结构并不复杂,从源码上看,实际上ReferenceQueue只是名义上的引用队列,它只保存了Reference链表的头(head)节点,并且提供了队列出队入队删除操作,而Reference实际上本身提供单向链表的功能,也就是说Reference通过成员属性next构建单向链表,而链表的操作是委托给ReferenceQueue完成。详细参考:深入理解JDK中的Reference原理和源码实现
在这里插入图片描述

6、总结

1、Java中四种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用

2、ReferenceQueue引用队列用来记录被回收的引用为用户线程做额外操作作铺垫

3、对各种引用回收时间、用途、生成时间作总结:

参考:

1、《深入理解Java虚拟机》

2、深入理解JDK中的Reference原理和源码实现

3、理解Java的强引用、软引用、弱引用和虚引用

(转)算法之优先队列 PriorityQueue解决Top K 问题

转自:https://www.jianshu.com/p/a4a1984fc4ff

解决方法:
维护一个大小为 K 的小顶堆,依次将数据放入堆中,当堆的大小满了的时候,只需要将堆顶元素与下一个数比较:如果大于堆顶元素,则将当前的堆顶元素抛弃,并将该元素插入堆中。遍历完全部数据,Top K 的元素也自然都在堆里面了。

当然,如果是求前 K 个最小的数,只需要改为大顶堆即可

将数据插入堆 95 大于 20,进行替换 95 下沉,维持小顶堆
对于海量数据,我们不需要一次性将全部数据取出来,可以一次只取一部分,因为我们只需要将数据一个个拿来与堆顶比较。

另外还有一个优势就是对于动态数组,我们可以一直都维护一个 K 大小的小顶堆,当有数据被添加到集合中时,我们就直接拿它与堆顶的元素对比。这样,无论任何时候需要查询当前的前 K 大数据,我们都可以里立刻返回给他。

整个操作中,遍历数组需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK),加起来就是 O(nlogK) 的复杂度,换个角度来看,如果 K 远小于 n 的话, O(nlogK) 其实就接近于 O(n) 了,甚至会更快,因此也是十分高效的。

最后,对于 Java,我们可以直接使用优先队列 PriorityQueue 来实现一个小顶堆,这里给个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static List<Integer> solutionByHeap(int[] input, int k) {
List<Integer> list = new ArrayList<>();
if (k > input.length || k == 0) {
return list;
}
Queue<Integer> queue = new PriorityQueue<>();
for (int num : input) {
if (queue.size() < k) {
queue.add(num);
} else if (queue.peek() < num) {
queue.poll();
queue.add(num);
}
}
while (k-- > 0) {
list.add(queue.poll());
}
return list;
}

Java中Lambda表达式解析

在大部分开发者看来,Lambda表达式只是一种语法糖,简化了书写匿名内部类的写法。实际上Lambda表达式并不仅仅是匿名内部类的语法糖,JVM内部是通过invokedynamic指令来实现Lambda表达式的,与内部类的实现有很大的差异。

本文主要讲解以下知识点:

一、函数式接口

二、Lambda表达式与匿名内部类

三、Lambda实现原理

一、函数式接口

众所周知Javascript具有一个强大的特性:闭包。Java中最接近闭包概念的东西就是lambda表达式了,而Lambda为Java添加了缺失函数式编程的特点。所以什么是函数是接口呢?

函数式接口需满足以下两个条件:

  1. 它是接口
  2. 这个接口有且仅有一个抽象方法

例如我们常用的:Runnable、View.OnClickListener、Comparable等都是函数式接口,因为它们都只有一个方法,而且都是抽象的。虽然只有一个抽象方法,是不是就意味着只能有一个方法呢?实际并不是,虽然有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

嗯?!Java接口中难道还可以定义非抽象方法么?平时我们的接口大概长这样:

public interface IdiomSubmitListener {
    void verifyResult(String result);
    void onSuceess();
}

那接口的非抽象方法是啥?原来在JDK 1.8 对于接口而言具有以下新特性:
接口可以定义非抽象方法,但必须使用default或者staic关键字来修饰
具体细节点可以参考 JAVA 8新特性 允许接口定义非抽象方法 快速入门案例

如果一个接口符合函数式接口的定义,那么我们就可以在该接口上面声明FunctionalInterface注解,用来表示该接口是一个函数式接口,并按照函数式接口的规范在编译的时候对该接口进行检查。

当然如果某个接口只有一个抽象方法,但我们并没有给该接口声明FunctionalInterface注解,那么编译器依旧会将该接口看做是函数式接口。

那Lambda表达式跟函数式接口又有什么关联呢?
在JDK 1.8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型–函数式接口。

因此可以说 在JDK 1.8中,Lambda表达式就是一个函数式接口的实例。
所以如果一个实例是函数式接口的实例,那么该对象就可以用Lambda表达式来表示

二、Lambda表达式与匿名内部类

我们知道代码IDE如果是在JDK1.8的环境下,使用匿名内部类作为一个参数传入到方法中,编译器会提示我们:Anonymous new Runnable() can be replaced with lambda,匿名内部类XXX可以替换为lambda表达式。

如下所示,匿名内部类 Runnable是一个函数式接口的实例,所以我们可以用lambda表达式来将之替换,从而将代码变得更加简洁。

在这里插入图片描述
那么我们是否就认为:Lambda表达式只是为匿名内部类中提供的一种语法糖,他们有什么区别呢?底层原理是完全一样的呢?

他们主要区别如下:

1、关键字this。匿名内部类的this指向匿名类,而Lambda表达式的this指向被Lambda包围的外部类

2、编译方式。Java编译器将Lambda表达式编译成类的私有方法,使用Java7的invokedynamic字节码动态绑定这个方法。而匿名内部类将编译成外部类$数字编号的新类。这也造成第1点关键字this指向不同地方的原因。

三、Lambda实现原理

我们知道如果使用匿名内部类,编译期间会生成一个外部类$数字编号的类,如图所示:

在这里插入图片描述

而如果使用Lambda表达式进行编译后并没有生成新类。
在这里插入图片描述

我们对Lambda表达式生成的class文件使用:javap -p -v Test.class 进行反编译生成如下内容,为便于观察,删除了一些无用内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class wang.julis.jwbase.basecompact.Test

Constant pool:
#1 = Methodref #9.#18 // java/lang/Object."<init>":()V
{
public wang.julis.jwbase.basecompact.Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 12: 0

private void testLambda();
descriptor: ()V
flags: (0x0002) ACC_PRIVATE
Code:
stack=3, locals=1, args_size=1
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: pop
13: return
LineNumberTable:
line 14: 0
line 18: 13

private static void lambda$testLambda$0();
descriptor: ()V
flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String lambda
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 15: 0
line 16: 8
}
SourceFile: "Test.java"
InnerClasses:
public static final #50= #49 of #53; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #21 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#22 ()V
#23 REF_invokeStatic wang/julis/jwbase/basecompact/Test.lambda$testLambda$0:()V
#22 ()V

从反编译的结果我们可以看到:

1、编译期间自动生成私有静态类lambda$testLambda$0而这里面就就是lambda的具体实现逻辑

2、使用invokedynamic去执行lambda表达式 关于invokedynamic命令具体细节可以参考: 08 | JVM是怎么实现invokedynamic的?(上)

3、lambda表达式编译后并没有生成外部类$数字编号的类

总结:

1、函数式接口:有且仅有一个抽象方法,可以用非抽象方法1.8后支持

2、匿名内部类的this指向匿名类,而Lambda表达式的this指向被Lambda包围的外部类

3、lambda表达式编译后不会生成外部类$数字编号的类

4、Java编译器将Lambda表达式编译成类的私有方法,使用Java7的invokedynamic字节码动态绑定这个方法。

参考:
1、《深入探索Android热修复技术原理》2.3.8章节
2、Java8 lambda表达式、函数式接口、方法引用

观《百鸟朝凤》有感

此刻子时,在朋友的推荐下看完了整部《百鸟朝凤》,看完之后我立马起身来打开电脑,心中思绪万千,准备写下来记录下来。自我观看了这么多电影以来,头一次是如此的触动,头一次留下来这样的眼泪,记得上一次很有感触还是因为《忠犬八公的故事》,那次泪水到了眼角,我忍住了,没有掉下来。这部电影实在是让我太感动了,我的泪水憋不下去了,源源不断地从眼眶里面流出来。我捂着自己的嘴巴,我怕自己嚎啕大哭起来,不停地抽泣。对一代匠人的崇敬之情,当然也让我想起来那个人……

他已经离我六年之余,从离开他之后,我的梦大部分都是关于他,梦中的情节很多都是让我感觉他躲起来了,然后又出现再了我的面前。每次梦醒,思前想后,万端交集,竟不能复寐,他便是我的爷爷-兴武老大人。

关于他的记忆开始慢慢变得模糊,有很多事情都已经忘记,但又有很多我根本不能忘记。影片中的焦师傅,爷爷的形象简直和他一模一样,都一样抽着大烟,瘦弱的身躯,头顶的中间光秃秃的,走路总是半佝偻着腰,但是眼神又是那么的坚毅。他们都对传统文化有着独特的情感,我的爷爷也算是一个匠人,他是一名竹匠(“四川话叫 mie匠”),他只告诉过我,那个年代他们靠卖竹制的东西(大抵是一些席子、竹帘、晒席之类的东西)过日子,每天都帮着各种编织,才养活了包括我爸在内的五个子女。家中很多东西,也都是竹子做的。在我小的时候,我记得基本上我们整个村,每家每户都有一片属于自家的竹林,它不仅能提供我们编制各种东西的原材料,也能提供做饭生火的材料。在我现在看来,它更能提供那种接近大自然的方式。每当夜里,如果有阵阵微风,竹林中便会的竹叶洒洒作响的声音,我也很久没有听到这种声音了。

如果我看到我的爷爷拿着一把弯刀去后院,那我一定知道他应该是要去砍竹子,只听哐哐哐一阵声,竹子便倒了下来,他会剔除掉竹子的枝丫,以及顶部较为脆弱细小的部分。然后用取当中的一节,再用刀将其剖开,根据所做的东西的不同,会将竹子的圆分成不同的等分。然后会做一个比较难得的操作,具体用文字表达我也不知道该用什么术语去说,就是将竹子外层坚韧的部分与内部相分离,分离后的竹条厚度均匀。这便是编制前的准备阶段,对于编制一些特殊的物品,竹条可能还会经过一些其他一些操作才能继续编制。

为了编一个撮箕或者簸箕,爷爷可能会花上一整天的时间,弯腰在地一点一点的编织着。我经常在旁边看着,我可能也想去尝试,但一不下心手就会因为竹子的锋利而流血。而爷爷的手并不一样,我记得他的手上有很多老茧,手指头也特别的硬,我感觉他的手就像带了一个硬皮手套一样,这都是岁月留下来的痕迹吧。他总是给我讲他们过去的故事,也会给我讲解人生大道理。他给我讲梁山伯与祝英台的故事,他也给我讲毛泽东,他也讲以前文化大革命的故事……讲以前生活是多么的不容易,他们遭受了多少苦,我都还记得。爷爷编织好的竹制品真的很精致耐用,家里现在也还留着一些当时他编好的东西。我觉得那都是艺术品,都是源于一个人对生活的热爱以及对匠人的诠释吧。

后来搬家了,原先家里的那片竹林离我们有一些距离,我爸想把它们都挖了卖掉,因为现在我们不需要那么多竹子了。我爷爷当然不允许啊!卖掉?直接就跟我爸翻脸。在我看来,那片竹林可能承载了太多爷爷的记忆吧,太多的舍不得。但爷爷去世之后,那片竹林还是被移除掉了,实在痛惜。我后悔没有跟我的爷爷学这些编制技术,我爸会一些,但是我认为他只是一个半吊子,和爷爷做的东西比起来实在是差太多了。我爸曾说:“现在谁还做那些啊?有钱我们可以买。”,对于他的话我有反驳过。买来的那些都是些没有被注入感情的废铜烂铁吧,与爷爷的东西差太远了。或许真的因为时代的发展吧,这些东西渐渐地都被淘汰掉了,但我认为这些都是才是真正应该被传承下来的,不仅仅是因为这一门技术,更多的那一份精神吧。是用钱买不来的,机器造不来的。

《百鸟朝凤》这部电影令人肃然起敬,质朴的人物形象,体现出来了当代中国传统文化的潦倒与窘境,传承不是说说而已,是每一个中国人应该有的责任。在现代多元文化的冲击下,更多人的失去了对精神的追求和向往,取而代之的是短暂的愉悦刺激以及碎片化的东西。
我们的生活其实可以更有内涵……

困了,就写这么多吧,讲了一些乱七八糟的东西,懂得人自然懂吧。

晚安~

Android骨架屏效果的实现与原理解析

0、前言

大家在使用淘宝的时候,如下图所示有遇到这样的效果,其会只展示一部分骨架大致图,等数据加载完毕之后再展示真正的页面数据。与菊花图相比起来,这样的实现能更好的提升用户的体验,这种效果称做:Skeleton Screen Loading,中文叫做骨架屏

1、骨架屏的实现方式

在现在主流的骨架屏实现效果中有两种方式:

这些开源库中,自己比较喜欢今天Skeleton这个开源库,总结了有如下一些优缺点:

优点:

  1. 代码方案实现及使用方式简单,通过替换View和Adapter实现效果,使用Builder设计模式来构造。
  2. 代码耦合程度不高。没有复杂的设计模式,使得代码结构清晰明了。
  3. 骨架屏的效果使用相对于较灵活,可以对整个布局实现骨架屏效果,也可以对单一View实现骨架屏效果。

缺点:

  1. 需要对每个骨架屏效果单独写一套xml布局。
  2. 使用的removeView和addView对 原有布局的view进行替换,存在一定的风险性
  3. 必须清晰的知道所bind的View类型,存在一定的类型转化问题。
  4. 依赖了shimmerlayout第三方库

2、Skeleton解读

一、Skeleton的使用方式

展示骨架屏效果:

View rootView = findViewById(R.id.rootView);
skeletonScreen = Skeleton.bind(rootView)
           .load(R.layout.activity_view_skeleton)//骨架屏UI
           .duration(1000)//动画时间,以毫秒为单位
           .shimmer(true)//是否开启动画
           .color(R.color.shimmer_color)//shimmer的颜色
           .angle(30)//shimmer的倾斜角度
           .show();

关闭骨架屏效果并展示原有View:

skeletonScreen.hide()

流程:

**1. 选择需要替换的目标view

  1. 将骨架效果xml与目标view进行绑定
  2. 添加一些效果属性,比如:动画时间、是否开启展示动画、动画颜色等
  3. 在合适的实际关闭骨架屏效果**

二、Skeleton源码实现

Skeleton提供两个绑定方法,分别绑定普通View与RecyclerView,分别返回对应的Builder

1
2
3
4
5
6
7
8
public class Skeleton {
public static RecyclerViewSkeletonScreen.Builder bind(RecyclerView recyclerView) {
return new RecyclerViewSkeletonScreen.Builder(recyclerView);
}
public static ViewSkeletonScreen.Builder bind(View view) {
return new ViewSkeletonScreen.Builder(view);
}
}

我们首先来看看如何实现与普通View绑定,构造方法中传入目标View,并对shimmer动画效果设置默认的颜色,在Builder里面我们可以看到各种相关参数的设定。

1
2
3
4
public Builder(View view) {
this.mView = view;
this.mShimmerColor = ContextCompat.getColor(mView.getContext(), R.color.shimmer_color);
}

接下来再到show的步骤,主要实现还是由ViewSkeletonScreen来实现

1
2
3
4
5
public ViewSkeletonScreen show() {
ViewSkeletonScreen skeletonScreen = new ViewSkeletonScreen(this);
skeletonScreen.show();
return skeletonScreen;
}

其中ViewSkeletonScreen与绑定的RecyclerViewSkeletonScreen都实现了SkeletonScreen接口,SkeletonScreen有两个接口方法分别是

void show();
void hide();

对于ViewSkeletonScreen.show()进入源码,这里出现一个比较重要的类ViewReplacer,等下再进行解析,通过show的源码清楚的知道逻辑:
1、生成骨架效果View
2、利用生成的View替换目标View。

其中生成骨架效果View阶段主要还是通过LayoutInflater去加载传入mSkeletonResID

1
2
3
4
5
6
7
@Override
public void show() {
View skeletonLoadingView = generateSkeletonLoadingView();
if (skeletonLoadingView != null) {
mViewReplacer.replace(skeletonLoadingView);
}
}

接下来主要讲解ViewReplacer类,其构造方法传入目标View

1
2
3
4
5
6
public ViewReplacer(View sourceView) {
mSourceView = sourceView;
mSourceViewLayoutParams = mSourceView.getLayoutParams();
mCurrentView = mSourceView;
mSourceViewId = mSourceView.getId();
}

其比较重要的方法有两个:replace()restore() 这两个方法分别为SkeletonScreen 的show()和hide()的最终实现,首先看replace()方法,有两个方法重载,分别传入targetViewResID或者targetView,最终还是会走到replace(View targetView)中。
其主要逻辑为:

**1. 判断所替换的View和骨架屏效果View是否为同一个View

  1. remove掉在父布局中的目标View
  2. 将骨架屏效果View添加到目标View的父布局中**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void replace(int targetViewResID) {
if (mTargetViewResID == targetViewResID) {
return;
}
if (init()) {
mTargetViewResID = targetViewResID;
replace(LayoutInflater.from(mSourceView.getContext()).inflate(mTargetViewResID, mSourceParentView, false));
}
}

public void replace(View targetView) {
if (mCurrentView == targetView) {
return;
}
if (targetView.getParent() != null) {
((ViewGroup) targetView.getParent()).removeView(targetView);
}
if (init()) {
mTargetView = targetView;
mSourceParentView.removeView(mCurrentView);
mTargetView.setId(mSourceViewId);
mSourceParentView.addView(mTargetView, mSourceViewIndexInParent, mSourceViewLayoutParams);
mCurrentView = mTargetView;
}
}

在执行添加到目标View的父布局中,有执行一个init方法,主要做两件事:

**1. 获取目标View的父View

  1. 找到目标View在父View 中的位置索引,为之后添加骨架屏View到父View中做铺垫**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private boolean init() {
if (mSourceParentView == null) {
mSourceParentView = (ViewGroup) mSourceView.getParent();
if (mSourceParentView == null) {
Log.e(TAG, "the source view have not attach to any view");
return false;
}
int count = mSourceParentView.getChildCount();
for (int index = 0; index < count; index++) {
if (mSourceView == mSourceParentView.getChildAt(index)) {
mSourceViewIndexInParent = index;
break;
}
}
}
return true;
}

至此对普通View的骨架屏效果实现流程已经完全梳理完成,那对于RecyclerView呢?其实两者实现逻辑差不多,主要有两个差异:

  1. RecyclerViewSkeletonScreen的Builder中,相比ViewSkeletonScreen多了一个adapter()方法,传入目标RecyclerViewAdapter
  2. 在show的时候对目标RecyclerView的adapter进行替换,使用骨架屏效果的adapter。hide的时候恢复为原先的Adapter

3、总结

  1. Skeleton的原理主要是通过替换目标View和RecyclerView的Adapter
  2. 在Skeleton的使用过程中最需要关心的两个问题是:show()和hide()的时机
  3. 对于整个页面的骨架屏效果实现,个人推荐在布局中添加一个全屏的空View盖在原先内容上
  4. 注意一些异常情况下的hide(),要不然整个页面就“假死”状态了。

参考:
https://juejin.im/post/5c789a4ce51d457c042d3b31

Android 插件化之ClassLoader

0、前言:

插件化要解决的三个核心问题:类加载、资源加载、组件生命周期管理。

在Android插件化中其原理实际是 Java ClassLoader的原理,此博文主要对Android插件化中类加载中的DexClassLoader做总结,便于之后对Android插件化的理解学习。

Android的Dalvik虚拟机和Java虚拟机的运行原理相同都是将对应的java类加载在内存中运行。而Java虚拟机是加载class文件,也可以将一段二进制流通过defineClass方法生产Class进行加载。Dalvik虚拟机加载的dex文件。dex文件是Android对与Class文件做的优化,以便于提高手机的性能。可以想象dex为class文件的一个压缩文件。dex在Android中的加载和class在jvm中的相同都是基于双亲委派模型,都是调用ClassLoader的loadClass方法加载类。

1、DexClassLoader和PathClassLoader区别

Android 也有自己的 ClassLoader,分为 DexClassLoaderPathClassLoader,这两者有什么区别和关联呢?

阅读源码可以看到两者的构造方法分别为:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以发现DexClassLoaderPathClassLoader 多一个参数String optimizedDirectory,那这个参数具体做什么的呢?继续查看源码我们可以知道optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile 对象,其具体体现在如下代码区域:

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

因此两者区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。所以在插件化中我们使用DexClassLoader来加载class的,接下来讲解DexClassLoader的用法。

2、DexClassLoader用法

其构造方法为:

DexClassLoader(
    String dexPath, 
       String optimizedDirectory, 
    String librarySearchPath,
     ClassLoader parent)

dexPath:被解压的apk路径,不能为空。
optimizedDirectory:解压后的.dex文件的存储路径,不能为空。这个路径强烈建议使用应用程序的私有路径,不要放到sdcard上,否则代码容易被注入攻击。
libraryPath:os库的存放路径,可以为空,若有os库,必须填写。
parent:父亲加载器,一般为context.getClassLoader(),使用当前上下文的类加载器。

接下来讲解具体使用流程:

1、新建一个名为plugin的project,其中新建一个Bean类,只有一个方法getName()返回一个字符串“My App”,然后对plugin这个工程打包为apk,将apk放在主工程的asser目录中。
在这里插入图片描述
2、构造Classloader

File extractFile = getFileStreamPath("app-debug.apk");
String dexPath = extractFile.getPath();
File fileRelease = getDir("dex", 0);
ClassLoader classLoader = new DexClassLoader(dexPath, fileRelease.getAbsolutePath(), null, getClassLoader());

3、利用构造好的Classloader反射调用插件类中的方法

Class mLoadClassBean;
try {
      mLoadClassBean = classLoader.loadClass("com.example.plugin.Bean");
      Object beanObject = mLoadClassBean.newInstance();
      Method getNameMethod = mLoadClassBean.getMethod("getName");
      getNameMethod.setAccessible(true);
      String name = (String) getNameMethod.invoke(beanObject);
      Log.e("julis", name);
  } catch(Exception e) {
      e.printStackTrace();
  }

成功打印出结果:
在这里插入图片描述

参考:

https://www.jianshu.com/p/4b4f1fa6633c

https://www.jianshu.com/p/53aa2de20cf8

https://cloud.tencent.com/developer/article/1071815

忙碌中求生活-记录23岁生日

此时周六晚七点半,刚吃了一碗自己做的番茄鸡蛋面,将浸泡了很久的银耳原料装入了电饭煲内,静待熟时。回到自己的房间,窗外不断传来来往的车轱辘声,好像在告诉我他们很忙吧。看到书桌上的日历,还沉浸在六月份,我可能也很忙吧,都忘记将它带入七月份。

来杭州一年有余,从一名大三学生暑假实习,到大四学生实习,再到应届毕业生签订转正合同,从而成为一名正式的“社会人儿”。时间过得真的很快吧,一周周一下子就没有了。进入了七月份,明显感觉比之前更忙了,或者说是因为自己身份的转变,导致肩上的责任变得不太一样了吧。因为工作节奏的改变,有时候也变得有些麻木吧,日记有时候总会忘记写,写字这件事感觉也变得有些奢侈起来,日语学习的节奏好像变得慢了起来。但是时间的脚步并不因为你的忙碌而停下来。

我最近在读《时间简史》,讲述了从认知革命到农业革命再到人类文化的融合统一再到科学革命,作者用通俗的话语讲完了整个人类历史,真的很值得阅读。其中有一章节引发了我的思考,作者认为:农业革命是史上最大的骗局。因为在长达250万里里人类都靠狩猎或者采集果实为生,而到大约一万年前全然改变,从日升到日落,人类忙着对植物的培育,一心认为这样就能得到更多的水果、谷物和肉类,使得人类生活能够变得更加容易。然而事实上是人变得越来越辛苦。人类每次决定多做一点事情(比如用锄头来耕地,而不是直接将种子撒在地里面),我们认为这样没错,这样会使我们的收成更好一点,有了更好的收成,就不用更多地去担心荒年的问题了,不用挨饿了。工作努力一点,生活也能过得好一点。不过这都是理想的状态。

人们确实工作得更努力也更辛苦,但没想到大家的孩子也更多了,人口慢慢地增加了,所得到的食物也就变得少了,生存压力也就更大了,资源也开始变得稀缺,而引发各种低问题。可是为什么他们不赶快放弃农耕回到原始的采集社会?原因在于,所有的改变都是必须点滴积累,经过许多代,才能改变社会,等到那个时候,已经没有人记得过去的生活方式和现在有什么不一样了,也没得选了。采用了农耕生活,村落的人口从100人到了110人,难道会有10个人自愿挨饿,好让其他人回到过去的美好时光?但这已经无法回头。于是人类付出了更多,但得到的却变得没有以前那样多。

我们都各自为生活变得轻松而努力,但是事实上我们过得并不轻松。人的欲望永远也满足不了,今天拥有了这个,明天还想拥有更好的。现在,我们随手可以发送一条信息,传到地球另一方,而他立马能够回你。我们确实省下了很多时间和麻烦,但生活真的更轻松了么?我们以为省了时间,然而我们其实是把生活的步调调成了过去的10倍,于是我们整天忙忙碌碌、焦躁不安。

这是原书作者对农业革命是史上最大的骗局的论证吧,我再赞同不过了。每每与我的爸妈打电话,我都会有很大的感触,他们总是在忙碌,他们每天都在拼命的挣钱,可是日子真的好起来了么?生活真的轻松了么?不久前,在家族微信群里面,我妈拍了一张我爸的照片放在群里,我爸双手背后,露出一脸笑意,背景好像是他们住那里的一个普通的建筑吧。想表现出: 你看我和你妈在玩,我们多开心啊。 当我看到这张照片的时候,我心里五谷杂粮。

一方面是 我看到我爸的头发,愈发的白了,白了大多半了。记得上一次有这感触的时候,还是我大二的时候,那次我从学校回家,他到车站来接我,我坐他后面,我发现他的白头已经有很多很多了,那时我差点哭了出来。爸妈真的老了,可我还没有给他们带来好的生活,我曾告诉他们说:等我实习你们就回家吧,不要工作了,我能养活我自己,以后也能养活你们。可是他们并不,他们还是在工作,烈日下,每每打电话跟我说太阳是有多么多大温度是有多么高,我心里都很难受。可是他们总会说一句话:“这都是为了你以后更轻松一点”。此时我却不知道该说些什么。

另一方面是 我看着我妈拍的那张照片,真的很糊,不是我妈不会拍照,也不是她眼神不好,真的是她的手机像素不好。我的爸妈,辛苦了大半辈子,其实也有很多积蓄了,虽然没有大城市里的那样多,但在老家里也算是稍微有一些钱的,可是他们却舍不得给自己多花一分钱,能将就用则用。每次电话,我都给他们讲让他们对自己好一点,我说 你们到底挣钱是为了做什么?给他们讲了很多道理,可是他们也总是会一句:“这都是为了你以后更轻松一点”。而我也只能强忍着,因为我现在还没有足够的能力,心里暗暗发誓:我会让他们过上轻松地日子。

我其实一直都在思考一个问题:人到底活在这个世界上是为了什么?科学家无法解释,这是一个哲学问题,没有人知道正确的答案。我们只是沧海中的一粟,如果掀不起波浪,那么就好好感受海的浩瀚吧。忙忙碌碌,短视频以及直播的崛起,可能真的是因为都市生活节奏变得太快,于是都在夹缝中去寻找那一丝丝快感,其实可以做的很多吧,摘自网上“试着每天自己为自己做美味的饭菜,试着经常联络一下家人好友,试着拾起丢下很久的小说,试着用心养一颗植物,试着在空气清新的清晨去跑步,试着约一下自己暗恋已久的女孩…”。看看足球比赛,看看电影,多出去走走,再忙也不要忘记生活吧。

快九点了,删删写写,也不道该放一些什么在日志上,那就这样吧。
祝自己二十三岁生日快乐

记录两张此时自己二十三岁的照片,没出门没刮胡子没收拾,一张沧桑一张微笑,生活亦如此吧。

xxx.jpg
xxx.jpg 隐私保护

—-二零一九年六月十一

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×