想统计自己总共提交了多少行代码?

作为一名程序员,我们很想知道自己到底提交了多少行代码到远程仓库,有没有什么工具能够帮我们统计自己写过的代码行数呢?答案是有的。这是本次博文的最终效果。
在这里插入图片描述

对于代码提交行数统计,通过git 的系统命令就能做到,如下代码所示

1
2
git log --author='username' --pretty=tformat: --numstat | awk '
{add += $1; subs += $2; loc += $1 - $2 } END { printf "添加了%s,删除了%s,合计%s\n", add, subs, loc }' -

只需要在如下命令输入自己的username就行了,效果如图所示·
在这里插入图片描述

但是有的人由于环境原因,为了区分一些环境,比如办公司叫:username.office 在家的电脑上叫做: user.home 诸如此类,难道得手动一个一个统计么?当然不行了。

众所周知,由于工程项目变得更越来越大,拆库也说见不鲜,于是自己的代码分布不同的项目工程,我们想要利用git的统计命令的话就有点吃力了,需要一个一个地进入相应目录进行命令输入?当然不行了。

今天自己写了一份脚本主要用于统计分布在某个文件夹下所有的代码提交行数,git开源地址:https://github.com/VomPom/ForFun源码如下

如何使用?

0、将自己需要统计的项目文件目录整理到一个文件夹

1、讲users_name换成自己的的用户名

2、由于文件夹下可能有一些例外的不需要统计,添加该文件夹名

3、讲该shell脚本移动到某个名录下

4、最后利用 sh codeLine.sh 执行命令

在这里插入图片描述

在这里插入图片描述

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
########################################################
#
# Created by https://julis.wang on 2020/02/28
#
# Description : 统计代码提交行数
#
########################################################

#!/bin/bash

#这里添加你的git常用用户名。考虑到每个人的账号可能有很多个,所以定义成数组
users_name=("julis" "julis.wang" "julis.wang.hp")

#过滤一些不需要去遍历的文件夹
filter_path=("Backend" "test" "sdk" "fork" "ArProject")




########################################################
# 以下代码不需动
########################################################

export index=0 #记录当前的位置
export add_line_count=0 #添加的line总行数
export remove_line_count=0 #删除的总行数

export array_git_repositories=() #用于记录仓库名
export add_code=() #记录所有用户对某个库的添加的行数
export remove_code=() #记录所有用户对某个库的删除的行数

#判断是否需要过滤该目录
function is_fileter_dir() {
for i in "${!filter_path[@]}"; do
if [ $1 == "${filter_path[$i]}" ]; then
return 1
fi
done
return 0
}
#对命令执行的返回值进行数据切割
function get_add_remove_count() {
string=$1
array=(${string//,/ })
if [ ! ${array[0]} ]; then
add_line=0
else
add_line=${array[0]}
fi

if [ ! ${array[1]} ]; then
remove_line=0
else
remove_line=${array[1]}
fi

if [ ! ${add_code[$index]} ]; then
add_code[$index]=0

fi
if [ ! ${remove_code[$index]} ]; then
remove_code[$index]=0

fi
remove_code[$index]=`expr ${remove_code[$index]} + $remove_line`
add_code[$index]=`expr ${add_code[$index]} + $add_line`

echo "用户"$2"添加了="$add_line"行 删除了"$add_line"行"

}
#获取该用户在该文件夹下的提交代码数
function get_user_line() {
# output分别去接收 该文件夹下的提交以及删除行数
output=$(git log --author=${1} --pretty=tformat: --numstat | awk '
{add += $1; subs += $2; loc += $1 - $2 } END { printf "添加了%s,删除了%s,合计%s\n", add, subs, loc }' -)
get_add_remove_count $output ${1}
}

#遍历每个用户名
function trans_every_user() {
for i in "${!users_name[@]}"; do
get_user_line "${users_name[$i]}"
done
cd ..
}

# 整体流程,从文件夹出发
for path in `ls -l $(dirname $0)|awk -F " " '{print $9}'`
do
if [ -d $path ]
then
is_fileter_dir $path
if [ $? == 1 ]
then
echo "<=========过滤了【"$path"】======>"
else
echo "<=========获取【"$path"】的Git代码提交数据======>"
index=${#array_git_repositories[@]} #用于记录当前在第几个文件夹下处理
array_git_repositories=(${array_git_repositories[@]} $path)

cd $path
trans_every_user
fi
fi
done
all_add_line=0
all_remove_line=0
echo '==============================================================================='
echo " 本次共统计了【"${#array_git_repositories[@]}"】个仓库 by julis.wang "
echo '==============================================================================='
printf "%-30s %10s %10s %10s\n" "Folder" "Add" "Remove" "All"
echo '-------------------------------------------------------------------------------'
for ((i=0;i<${#array_git_repositories[@]};i++))
do
all_add_line=`expr $all_add_line + ${add_code[$i]}`
all_remove_line=`expr $all_remove_line + ${remove_code[$i]}`
printf "%-30s %10s %10s %10s\n" ${array_git_repositories[$i]} ${add_code[$i]} ${remove_code[$i]} `expr ${add_code[$i]} - ${remove_code[$i]}`
done
echo '-------------------------------------------------------------------------------'
printf "%-30s %10s %10s %10s\n" "Total" $all_add_line $all_remove_line `expr $all_add_line - $all_remove_line`
echo '==============================================================================='

写在最后:
由于本人不太擅长编写shell脚本,所有其中的代码实现方式可能比较粗糙,望理解。

从奶酪夹心饼干生产中来学习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中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表达式、函数式接口、方法引用

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监听截屏事件之媒体读取的探索

最近做了一个需求:监听用户截屏,然后生成相关海报。
参考了Android 截屏事件监听的文章,大致思路是:a

1、利用ContentObserver用来监听指定Uri的所有资源变化,当媒体库中有相关图片新增的时候,则发送相关的通知。

2、得到回调的Uri后,借助ContentResolver在媒体数据库中查询最后一条数据

3、对数据做一些过滤。比如短时间重复截屏的情况以及其他App也插入了媒体文件等情况做处理。

不过有一些适配性的问题:

1、截屏后读取文件数据库后获取到件的绝对路径后,利用“screenshot”等关键字判断是否是截屏图片,并不能适配所有手机截屏的命名规则,以及其他应用同时间产生带有“screenshot”等关键词的文件也会有问题。

2、在某些型号手机中(现遇到Vivo)从数据库中读取的文件并不是获取到的最新的截屏文件,而且其他目录的文件,这里就有些难以理解了,所以今天取探究一下媒体数据库的读取。

其中ContentObserver如下代码所示:

` /**
 * 媒体内容观察者(观察媒体数据库的改变)
 */
private class MediaContentObserver extends ContentObserver {
    private Uri mContentUri;
    public MediaContentObserver(Uri contentUri, Handler handler) {
        super(handler);
        mContentUri = contentUri;
    }
    [@Override](https://my.oschina.net/u/1162528)
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
        handleMediaContentChange(mContentUri);
    }
}`

其中获取最后一次更新的媒体文件时的代码(为便于查看 删除了判空处理代码):

 /**
 * 处理媒体数据库的内容改变
 */
private void handleMediaContentChange(Uri contentUri) {
    Cursor cursor = null;
    /** 读取媒体数据库时需要读取的列 */
    private static final String[] MEDIA_PROJECTIONS =  {
        MediaStore.Images.ImageColumns.DATA,
        MediaStore.Images.ImageColumns.DATE_TAKEN };
    try {
        // 数据改变时查询数据库中最后加入的一条数据
        cursor = mContext.getContentResolver().query(
                contentUri,
                 MEDIA_PROJECTIONS,
                null,
                null,
                MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
        );
        // 获取各列的索引
        int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
        int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);

        // 获取行数据
        String data = cursor.getString(dataIndex);
        long dateTaken = cursor.getLong(dateTakenIndex);

        // 处理获取到的第一行数据
        handleMediaRowData(data, dateTaken);
}

这次的目的主要探究的是从数据库获取相关信息的过程

1、Android 的多媒体如何存储?

Android的多媒体文件主要存储在 /data/data/com.android.providers.media/databases 目录下,该目录下有连个db文件:

内部存储数据库文件:internal.db

存储卡数据库:external-XXXX.db

媒体文件的操作主要是围绕着这两个数据库来进行,这两个数据库的结构是一样的。

这两个数据库包含这些表:
album_art 、audio 、search 、album_info 、audio_genres、 searchhelpertitle、albums、 audio_genres_map、 thumbnails、
android_metadata、 audio_meta、 video、artist_info 、audio_playlists 、videothumbnails、artists 、audio_playlists_map、
artists_albums_map 、images

2、表的结构
对于Images表:主要存储images信息。表结构如下:

CREATE TABLE images (
_id INTEGER PRIMARY KEY, 
_data TEXT,
_size INTEGER,
_display_name TEXT,
mime_type TEXT,
title TEXT, 
date_added INTEGER, 
date_modified INTEGER,
description TEXT,
picasa_id TEXT,
isprivate INTEGER,
latitude DOUBLE, 
longitude DOUBLE, 
datetaken INTEGER, 
orientation INTEGER, 
mini_thumb_magic INTEGER, 
bucket_id TEXT, 
bucket_display_name TEXT );

`

各字段所表示意思,如图所示:

图片来自:Android MediaProvider数据库模式说明

所以在截屏监听数据的时候所读取的数据库返回值,分别为:

_data :图片据对路径

datetaken:取子EXIF照片拍摄事件,空的话为文件修改时间

private static final String[] MEDIA_PROJECTIONS =  {
      MediaStore.Images.ImageColumns.DATA,
      MediaStore.Images.ImageColumns.DATE_TAKEN };

在查询过程中构造的数据库代码为:

public final Cursor query (Uri uri, 
    String[] projection,
    String selection, 
    String[] selectionArgs, 
    String sortOrder)

`
其中对应的构造参数官方解释为:

uri The URI, using the content:// scheme, for the content to retrieve.

projection A list of which columns to return. Passing null will return all columns, which is inefficient.

selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI.

selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings.

sortOrder How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.


所以参数依次为:
所要查找的目标、所要的返回值、条件限制(类似sql中where)、匹配项、排序规则

所以这里的查询就显而易见了:获取最新图片数据库下data和datatoken列的数据

cursor = mContext.getContentResolver().query(
              contentUri,
              MEDIA_PROJECTIONS,
              null,
              null,
              MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
      );

然而…并不能解释vivo手机为什么查找出来不是最新截图的图片的问题

Android在子线程中创建Handler为什么会抛出异常?

复习一下消息机制,如下代码:

new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            handler = new Handler();
        }
    }.start();

如果执行会抛出异常:

Can't create handler inside thread Thread.currentThread() that has not called Looper.prepare()

这是为什么呢?

我们进入Handler的构造方法

    public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

从上面的代码我们很清新的可以知道mLooper 为null,那么就会抛出这样的异常,那么mLooper 为什么会为空呢?这是因为在线程中的Looper还未被创建,所以在Looper.myLooper()中sThreadLocal.get()就会返回null。

我们知道 Handler的作用是处理消息,将消息传递给MessageQueue,而MessageQueue存在于Looper中,如果没有Looper那么就没有MessageQueue,所以创建Handler时,Looper不能够为空。

所以以上代码可以进行一个修改:

private void test() {
    new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            Looper.prepare();
            handler = new Handler();
            Looper.loop();
        }
    }.start();
}

其中 Looper.prepare();为当前线程创建Looper并绑定在ThreadLocal中
Looper.loop();执行消息循环,这样子 Handler就能够正常工作了。

Java虚拟机类的加载机制

什么是虚拟机类的加载机制?

虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

类从加载到虚拟机的内存中开始,直到卸载出内存为止,整个生命周期为:

  • 加载(loading)

  • 验证(verification)

  • 准备(preparation)

  • 解析(resolution)

  • 初始化(initialization)

  • 使用(using)

  • 卸载(unloading)

其中 验证、准备、解析部分统称为连接

接下来依次讲解,各个步骤所做的事

第一部分 加载


“加载”是“类加载”的一个阶段,注意区分概念。类的加载由类加载器(后面介绍)加载主要完成三件事情:

1、通过一个类的全限定名来获取其定义的二进制字节流。

2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

加载阶段完成后,虚拟机外部的二进制字节流将按照虚拟机所需的格式存储在方法区中,同时在内存中实例化一个java.lang.Class的实例对象。相对于HotSpot,这个实例对象比较特殊,虽然是一个对象,但并没有放置在堆中,而是放置在方法区中。这个对象将作为程序访问方法区中这些类数据的外部接口。

第二部分 验证


这一步主要是确保Class文件的字节流符合虚拟机的规范

主要验证以下几个部分:

1、文件格式验证
验证是否以魔数开头、主次版本号是否在当前虚拟机处理范围内…

这一验证阶段主要是保证输入的字节流能正确地解析并存储与方法区内,格式上符合Java类型信息的要求。只有通过这个阶段,字节流才会进入内存的方法区中存储,后面的三个验证方式也都是给予方法区中的数据验证,不再会操作字节流。

2、元数据验证 验证这个类是否有父类、这个类是否继承了不允许继承的类…

该阶段主要对类的元数据进行语义校验,保证符合java语言规范的元数据信息。

3、字节码验证

最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是否合法、符合逻辑。

4、符号引用验证

这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作发生在连接的解析阶段。目的是确保解析动作正常执行,如果无法通过验证,将抛出 IllegalAccessError、NoSuchFieldError、NoSuchMethodError等异常。

第三部分 准备


准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所实用的内存将在方法区中进行分配。

这时候的分配仅仅是类变量(staic修饰的变量),而实例变量将会在对象实例化时随对象一起分配在Java对中。

假设一个类变量为:public static int count = 10;这时候会分配0,而不是10,分配10是在程序编译后。

第四部分 解析


解析阶段是虚拟机将常量池的符号引用替换为直接引用的阶段

1、类或者接口的的解析

2、字段解析

3、类方法解析

4、接口方法解析

符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

第五部分 初始化


在准备阶段,变量已经被分配赋值过初始值,在初始化阶段根据代码的逻辑初始化真实的变量和其他资源。

关于类加载器


什么是类加载器?

在“加载”阶段中,通过一个类的全限定名来获取其定义的二进制字节流。这一动作是放到了Java虚拟机外部去实现的,是为了方便让应用自己去决定如何获取所需要的类,实现这个动作的功能是常说的“类加载器(ClassLoader)”

类加载器主要有三种:

1.启动类加载器(Bootstrap ClassLoader)

负责加载<JAVA_HOME>\lib

2.扩展类加载器(Exension ClassLoader)

负责加载<JAVA_HOME>\lib\ext

3.应用程序类加载器(Applicaion ClassLoader)

负责加载ClassPath上指定的类库

类加载器工作原理

介绍类加载器原理之前,必须得了解双亲委派模型(Parents Delegation Model)

双亲委派模式的工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。

如图所示,这种层次结构关系被称为双亲委派模型
以下为其实现代码,集中在java.lang.ClassLoader中的loadClass()方法中

    protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
    //首先检查类是否被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
        //如果有父加载器,则先委托父加载,否则由启动类加载器加载,如果启动类加载器没有找到,则返回null
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
        //这里的ClassNotFoundException来自父加载器
            }
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
        //在父类Classloader还没办法加载的时候
        //再调用本身的findclass方法来加载类
                long t1 = System.nanoTime();
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

代码的逻辑很清楚:

先加载类是否已经被加载过,若没有则调用父的loadClass()方法,如果父 类加载器为空,则使用启动类加载器作为父加载器,如果父 类加载器加载失败,再调用自己的findClass()方法进行加载

Your browser is out-of-date!

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

×