开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

深入理解23种设计模式(4) -- 建造者模式 介绍 案例

发表于 2020-11-02

@[toc]

介绍

  1. 建造者模式(Builder Pattern)又叫生成器模式,是一种对象构建模式,它可以将复制的建造过程抽象出来,使这个抽象过程的不同实现方法可以构造出不同的属性
  2. 建造者模式是一步步创建一个复制的对象,它允许用户只通过指定复制对象的类型和内容就可以构建它们,用户不需要指定细节

在这里插入图片描述

  • Product(产品角色): 一个具体的产品对象
  • Builder (抽象建造者):创建一个Product对象的各个不见指定的接口/抽象类
  • ConcreteBuilder (具体建造这):实现接口,构造和装配各个部件
  • Diretor(指挥者):构建一个使用builder接口的对象,它主要是用于创建一个复杂的对象,它主要又2个作用,一是:隔离了客户与对象的生辰过程,二是:负责控制产品对象的生产过程。

案例

盖房子需求:

  1. 需要建房子,这一过程为打桩、砌墙、封顶
  2. 房子各种各样的,比如普通房,高楼,别墅,各种房子过程虽然一样,但是要求不要相同

新建抽象类 AbstractHouse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public abstract class AbstractHouse {

//打地基
public abstract void buildBasic();

//砌墙
public abstract void buildWalls();

//封顶
public abstract void roofed();

public void build() {
buildBasic();
buildWalls();
roofed();
}
}

构建普通房子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class CommonHouse extends AbstractHouse{
@Override
public void buildBasic() {
System.out.println("打地基。。。。");
}

@Override
public void buildWalls() {
System.out.println("砌墙。。。。");
}

@Override
public void roofed() {
System.out.println("封顶。。。。");
}
}

测试Clinet

1
2
3
4
5
6
java复制代码public class Client {
public static void main(String[] args) {
CommonHouse commonHouse = new CommonHouse();
commonHouse.build();
}
}

在这里插入图片描述

传统方式解决

  1. 优点好理解,简单
  2. 设计的程序结构,过于简单,没有设计缓存层对象,程序的扩展和维护不好,也就是说,这种设计方案,耦合度增强了

使用建造者模式

新建House 产品

1
2
3
4
5
6
7
java复制代码@Data
public class House {
private String baise;
private String wall;
private String roofed;

}

抽象类 HouseBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public abstract class HouseBuilder {

protected House house = new House();

//打地基
public abstract void buildBasic();

//砌墙
public abstract void buildWalls();

//封顶
public abstract void roofed();

//建造房子
public House buildHouse() {
return house;
}
}

普通房子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class CommonHouse extends HouseBuilder {
@Override
public void buildBasic() {
System.out.println("打地基5m。。。。");
}

@Override
public void buildWalls() {
System.out.println("砌墙5m。。。。");
}

@Override
public void roofed() {
System.out.println("封顶5m。。。。");
}
}

高楼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class HignBuilding extends HouseBuilder{
@Override
public void buildBasic() {
System.out.println("高楼地基100m");
}

@Override
public void buildWalls() {
System.out.println("高楼砌墙100m");
}

@Override
public void roofed() {
System.out.println("高楼封顶100m");
}
}

指挥者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class HouseDirector {
HouseBuilder houseBuilder = null;

//构造器传入
public HouseDirector(HouseBuilder houseBuilder){
this.houseBuilder = houseBuilder;
}

//setter传入
public void setHouseBuilder(HouseBuilder houseBuilder){
this.houseBuilder = houseBuilder;
}


//如何处理建造房子流程,交给指挥者
public House constructHouse(){
houseBuilder.buildBasic();
houseBuilder.buildWalls();
houseBuilder.roofed();
return houseBuilder.buildHouse();
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Client {
public static void main(String[] args) {
//普通房子
CommonHouse commonHouse = new CommonHouse();
HouseDirector houseDirector = new HouseDirector(commonHouse);
House house = houseDirector.constructHouse();

HignBuilding hignBuilding = new HignBuilding();
HouseDirector houseDirector2 = new HouseDirector(hignBuilding);
houseDirector2.constructHouse();
}
}

在这里插入图片描述

优点和缺点

优点:

1
2
复制代码1)封装性。是客户端不必知道产品内部组成的细节。
2)便于控制细节风险。可以对建造过程逐步细化,而不对其他模块产生任何影响。

缺点:

1
2
复制代码 1)如果内部变化复杂,会有很多建造类。
2)产品必须有共同点,范围有限制。

适用场景:

1
2
复制代码 1) 多个部件或零件,都可以装配到一个对象中,但产生的结果又不相同时。
2)需要生成的对象具有复杂的内部结构时。

在我们JDK 中 java.lang.StringBuilder中就使用了 建造者模式


github Demo地址 : ~~~传送门~~~

个人博客地址:blog.yanxiaolong.cn/

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

这行代码告诉你!为什么你地下城与勇士(DNF)的装备强化老是

发表于 2020-11-02

模拟地下城与勇士(DNF)的装备强化

tip1:

  • DNF装备强化在+1~+3 不会失败;
  • +4~+7,失败后物品原有强化等级降低1级;
  • +8~+10,失败后掉3级;
  • 10上11或以上就爆了。

tip2:

  • DNF装备强化1~3级,成功率100%
  • DNF装备强化3~4级,成功率95%
  • DNF装备强化4~5级,成功率90%
  • DNF装备强化5~6级,成功率80%
  • DNF装备强化6~7级,成功率75%
  • DNF装备强化7~8级,成功率62.1%
  • DNF装备强化8~9级,成功率53.7%
  • DNF装备强化9~10级,成功率41.4%
  • DNF装备强化10~11级,成功率33.9%
  • DNF装备强化11~12级,成功率28%
  • DNF装备强化12~13级,成功率20.7%
  • DNF装备强化13~14级,成功率17.3%
  • DNF装备强化14~15级,成功率13.6%
  • DNF装备强化15~16级,成功率10.1%

要求输入装备的原始等级,输入1执行强化,根据原始等级以及强化的成功率,显示装备的强化结果

完整代码如下:

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
ini复制代码package lesson;
/**
* 模拟地下城与勇士(DNF)的装备强化过程
*
*/
import java.util.Scanner;

public class Test {

public static void main(String[] args) {
//创建输入对象
Scanner shuru = new Scanner(System.in);
//用户输入强化等级
System.out.println("请输入强化等级:");
int a = shuru.nextInt();
System.out.println("请输入1开始强化:");
int b = shuru.nextInt();
//创建随机概率
double m = Math.random();

switch(a) {
case 0:
case 1:
case 2:
a++;
System.out.println("您的装备强化成功,等级为:"+a);
break;
case 3:
if(m<0.95) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a--;
System.out.println("您的装备强化失败,等级为:"+a);
}
break;
case 4:
if(m<0.90) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a--;
System.out.println("您的装备强化失败,等级为:"+a);
}
break;
case 5:
if(m<0.80) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a--;
System.out.println("您的装备强化失败,等级为:"+a);
}
break;
case 6:
if(m<0.621) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a--;
System.out.println("您的装备强化失败,等级为:"+a);
}
break;
case 7:
if(m<0.537) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a-=3;
System.out.println("您的装备强化失败,等级为:"+a);
}
break;
case 8:
if(m<0.414) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a-=3;
System.out.println("您的装备强化失败,等级为:"+a);
}
break;
case 9:
if(m<0.339) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a-=3;
System.out.println("您的装备强化失败,等级为:"+a);
}
break;
case 10:
if(m<0.28) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a=0;
System.out.println("您的装备爆了!");
}
break;
case 11:
if(m<0.207) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a=0;
System.out.println("您的装备爆了!");
}
break;
case 12:
if(m<0.173) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a=0;
System.out.println("您的装备爆了!");
}
break;
case 13:
if(m<0.136) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a=0;
System.out.println("您的装备爆了!");
}
break;
case 14:
if(m<0.75) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a=0;
System.out.println("您的装备爆了!");
}
break;
case 15:
if(m<0.101) {
a++;
System.out.println("您的装备强化成功,等级为:"+a);
}else {
a=0;
System.out.println("您的装备爆了!");
}
break;
default:
System.out.println("输入错误!");
break;

}
}

}

最后

代码仅供娱乐,为什么装备上不去那是你脸黑!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

优秀 !华为是这样使用Git rebase的 引言 git

发表于 2020-11-02

世界上最快的捷径,就是脚踏实地,本文已收录【架构技术专栏】关注这个喜欢分享的地方。

引言

使用git参与多人之间的合作开发大概有三年的时间,大多数场景下使用的git命令一只手多一点就能数的过来

  • git add
  • git commit
  • git push
  • git merge
  • git pull
  • git log

理论上来说,只要能合理管理项目分支,这几个命令已经足以应付所有的日常开发工作。但是如果我们偶尔看一下自己的git graph,我的天呐,为什么会这么乱。

鉴于分支管理的混乱(或者根本就没有进行过分支管理),我们经常遇到一些意想不到的问题,因此需要使用很多面生的git命令来解决我们的问题,比如说本文讲到的git rebase。

git rebase 和 git merge 区别

Git rebase 的中文名是变基,就是改变一次提交记录的base。在这一环节,我们不妨带着这样一个假设:git rebase ≈ git merge,并用两种命令实现同一工作流来对比他们之间的异同。

回想我们日常的工作流,假设a和b两人合作开发,三个分支:develop, develop_a, develop_b。两个人分别在develop_a和develop_b分支上进行日常开发,阶段性地合入到develop。

那么从a的角度来看,可能的工作流是这样的:

(1)个人在develop_a分支上开发自己的功能

(2)在这期间其他人可能不断向develop合入新特性

(3)个人功能开发完毕后通过merge 的方式合入别人开发的功能

git merge

img
上图为日常merge 工作流,对应的git操作命令如下:

1
2
3
4
5
ini复制代码git checkout develop_a

// 本地功能开发...

git pull origin develop = git fetch origin develop + git merge develop复制代码

git rebase

同样走完这样一个工作流如果我们使用git rebase来实现,结果如下:

git rebase 之前,如图:

img
git rebase 之中,如图:

img
git rebase 之后,如图:

img
git rebase 操作对应命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码git checkout develop_a

// 本地功能开发...

git fetch origin develop

git rebase develop

git checkout develop

git merge develop_a

git br -d develop_a 复制代码

由此可见,git rebase 和git merge的异同之处如下:

(1)两者都可以用于本地代码合并

(2)git merge 保留真实的用户提交记录,且在merge时会生成一个新的提交

(3)git rebase 会改写历史提交记录,这里的改写不仅限于树的结构,树上的节点的commit id也会别改写,因此图3和图4用e’代表图2的e’,收益是可以保证提交记录非常清爽

如何使用git rebase -i 修改历史提交记录

git rebase -i,中文名叫交互式变基。意思就是在变基的过程中是可以掺入用户交互的,通过交互过程我们可以主动改写历史提交记录,包括修改、合并和删除等。我们以上面使用rebase后得到的提交记录为例,来进行历史提交记录的修改,在修改之前,提交记录是这个样子的。

img

使用git rebase -i 修改历史提交的过程主要包含三步:

(1)列出一个提交记录的范围,并指出你在这个范围内需要对哪些记录进行什么样的修改

(2)以次执行上述的修改,如果遇到冲突需要解决

(3)完成rebase 操作

以上面截图中的提交记录为例,来对历史提交的commit msg进行修改,操作步骤如下:

1
2
3
arduino复制代码// 查看最近6次提交记录,选择对哪一条记录进行修改

git rebase -i HEAD~6复制代码

img

执行完上述命令后,会以vim的方式打开一个文件,文件中显示了最近6次的提交信息,从上到下,由远到近。

从下面的注释可以看到,我们分别把每一行前面的pick修改成r, s, d的方式就可以实现对历史记录的修改,合并和删除。

首先我们尝试修改提交信息,把第二行前面的pick改成r,保存退出。当前页面关闭的同时会打开一个新的页面,让你对选中的提交信息进行编辑。

img

编辑完信息之后保存退出,就完成了对历史提交记录的修改。通过观察下图可以发现,develop_a的提交记录中的commit msg 仍然是feat_c,但是develop 分支中对应的提交记录,commit msg 已经变成了feat: c-update.。

这里需要留意到的一个现象是develop 和develop_a 分支上相同提交的commit id 已经发生了变化,这个在后面会再次提到。

img

除了修改提交的commit msg 之外,我们也可以通过把pick 改为e,结合git reset –soft HEAD^ 的方式对档次提交的改动内容进行修改。

合并与删除历史提交的操作步骤与编辑类似,只需要把pick分别改为s 和d 即可,各位看官可以自行尝试。如果在rebase的过程中遇到了冲突,需要手工解决,然后使用git rebase –continue 完成rebase 操作。

git rebase 的提示还是非常友好的,它会告诉你需要进行哪些操作解决当前的问题。

img

使用git rebase -i 必须遵循的规则是什么?

从修改历史提交记录这个功能来看,交互式变基是一个非常强大的功能。但是使用这个功能必须要遵循一个铁则:不要对线上分支的提交记录进行变基!

引用git 官方指导文档的话来说大概是这样:

如果你遵循这条金科玉律,就不会出差错。 否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。

在说为什么不能对线上提交执行交互式变基之前,先说一下如果要对线上功能执行这个操作要怎么做。

首先,你需要在自己本地变基成功,然后使用git push -f 强行push 并覆盖远程对应分支,之所以需要执行覆盖式push 是因为如果你不覆盖,当前变基过后产生的新提交会与远程合并,导致你在本地的变基行为失去意义。

因为我们上面提到过,从变基那个节点开始往后的所有节点的commit id 都会发生变化。

同样的原因,即使你使用git push -f 使远程分支发生了变基,如果你的同事的开发分支中还存在你执行变基操作(不论是修改、合并还是删除)时针对的那些分支,那么当你的同事merge 你的提交之后,你所有想使用变基改变的东西都回来了!

如果打破了git rebase -i 的使用规则应该如何补救

此处我们尝试通过要点描述的方式,说明线上提交执行变基会导致什么结果以及如何避免这个结果:

(1)你在本地对部分线上提交进行了变基,这部分提交我们称之为a,a在变基之后commit id 发生了变化

(2)你在本地改变的这些提交有可能存在于你的同事的开发分支中,我们称之为b,他们与a的内容相同,commit id 不同

(3)如果你把变基结果强行push 到远程仓库后,你的同事在本地执行git pull 的时候会导致a 和b 发生融合,且都出现在了历史提交中,导致你的变基行为无效

(4)我们想要的是你的同事拉取线上代码时跳过对a 和b 的合并,只是把他本地分支上新增的修改合并进来

讲了这么多,最终的结论就是,使用变基解决变基带来的问题。即你的同事使用git rebase 的方式把他本地的修改rebase 到远程你执行过rebase 的分支上。

简言之,就是你的同事使用git pull –rebase 而不是git pull 来拉取远程分支。在这个操作的过程中,git 会对我们上面提到几个要点的信息进行检查并把真正属于同事本地的修改合入远程分支的最后。

文字描述可能有些乏力,更多详细信息可以参考这里:git-scm.com/book/zh/v2/…

所以我们应该如何使用git rebase

鉴于上面描述的git rebase 可能带来的问题,最后要回答的一个问题是我们应该如何在日常工作中使用git rebase,同样借用git 官方文档中的一句话:

总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式(rebase 和merge)带来的便利。

原文地址:www.mdeditor.tw/pl/pMHD

作者:DevUI 华为团队

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

leetcode之二进制求和

发表于 2020-11-01

序

本文主要记录一下leetcode之二进制求和

题目

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
sql复制代码给你两个二进制字符串,返回它们的和(用二进制表示)。

输入为 非空 字符串且只包含数字 1 和 0。

 

示例 1:

输入: a = "11", b = "1"
输出: "100"
示例 2:

输入: a = "1010", b = "1011"
输出: "10101"
 

提示:

每个字符串仅由字符 '0' 或 '1' 组成。
1 <= a.length, b.length <= 10^4
字符串如果不是 "0" ,就都不含前导零。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/add-binary
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码class Solution {
public String addBinary(String a, String b) {
StringBuilder builder = new StringBuilder();
int i = a.length() - 1;
int j = b.length() - 1;

int sum = 0;
while(i >= 0 || j >= 0) {
if(i >= 0) {
sum += a.charAt(i) - '0';
i--;
}
if(j >= 0) {
sum += b.charAt(j) - '0';
j--;
}
builder.append(sum % 2);
sum = sum/2;
}

String result = builder.reverse().toString();
return sum > 0 ? '1' + result : result;
}
}

小结

这里对两个字符串从后开始遍历,然后进行累加,对2取余数添加到结果集,然后对2取模,继续循环,最后将结果反转一下,最后再判断一下sum是否大于0,大于0的话,再补下前缀1。

doc

  • 二进制求和

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Java 路线」 反射机制(含 Kotlin) 目录 1

发表于 2020-11-01

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 反射(Reflection)是一种在运行时 动态访问类型信息 的机制。
  • 在这篇文章里,我将带你梳理Java & Kotlin反射的使用攻略,追求简单易懂又不失深度,如果能帮上忙,请务必点赞加关注!

相关文章

  • 《Java | 深入理解方法调用的本质(含重载与重写区别)》
  • 《Java | 请概述一下 Class 文件的结构》
  • 《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》

目录

  1. 类型系统的基本概念

首先,梳理一一下类型系统的基础概念:

  • 问:什么是强 / 弱类型语言?:

答:强 / 弱类型语言的区分,关键在于变量是否 (倾向于) 类型兼容。例如,Java 是强类型语言,变量有固定的类型,以下代码在 Java 中是非法的:

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public class MyRunnable {
public abstract void run();
}

// 编译错误:Incompatible types
java.lang.Runnable runnable = new MyRunnable() {
@Override
public void run() {

}
}
runnable.run(); // X

相对地,JavaScript 是弱类型语言,一个变量没有固定的类型,允许接收不同类型的值:

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码function MyRunnable(){
this.run = function(){
}
}
function Runnable(){
this.run = function(){
}
}
var ss = new MyRunnable();
ss.run(); // 只要对象有相同方法签名的方法即可
ss = new Runnable();
ss.run();

更具体地描述,Java的强类型特性体现为:变量仅允许接收相同类型或子类型的值。 嗯(黑人问号脸)?和你的理解一致吗?请看下面代码,哪一行是有问题的:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码注意,请读者假设 1 ~ 4 号代码是单独运行的

long numL = 1L;
int numI = 0;
numL = numI; // 1
numI = (int)numL; // 2

Integer integer = new Integer(0);
Object obj = new Object();
integer = (Integer) obj; // 3 ClassCastException
obj = integer; // 4

在这里,第 3 句代码会发生运行时异常,结论:

  • 1:调用字节码指令 i2l,将 int 值转换为 long 值。(此时,numL 变量接收的是相同类型的值,命题正确)
  • 2:调用字节码指令 l2i,将 long 值转换为 int 值。(此时,numI 变量接收的是相同类型的值,命题正确)
  • 3:调用字节码指令 checkcast,发现 obj 变量的值不是 Integer 类型,抛出 ClassCastException。(此时,Integer 变量不允许接收 Object 对象,命题正确)
  • 4:integer 变量的值是 obj 变量的子类型,可以接收。(此时,Object 变量允许接收 Integer 对象,命题正确)

用一张图概括一下:

  • 问:什么是静态 / 动态类型语言?

答:静态 / 动态类型语言的区分,关键在于类型检查是否 (倾向于) 编译时执行。例如, Java & C/C++ 是静态类型语言,而 JavaScript 是动态类型语言。需要注意的是,这个定义并不是绝对的,例如 Java 也存在运行时类型检查的方式,例如上面提到的 checkcast 指令本质上是在运行时检查变量的类型与对象的类型是否相同。 那么 Java 是如何在运行时获得类型信息的呢?这就是我们下一节要讨论的问题。


  1. 反射的基本概念

  • 问:什么是反射?为什么要使用反射?

答:反射(Reflection)是一种在运行时 动态访问类型信息 的机制。Java 是静态强类型语言,它倾向于在编译时进行类型检查,因此当我们访问一个类时,它必须是编译期已知的,而使用反射机制可以解除这种限制,赋予 Java 语言动态类型的特性。例如:

1
2
3
4
5
6
7
8
9
scss复制代码void func(Object obj) {
try {
Method method = obj.getClass().getMethod("run",null);
method.invoke(obj,null);
}
... 省略 catch 块
}
func(runnable); 调用 Runnale#run()
func(myRunnable); 调用 MyRunnale#run()
  • 问:Java 运行时类型信息是如何表示的?

所有的类在第一次使用时动态加载到内存中,并构造一个 Class 对象,其中包含了与类有关的所有信息,Class 对象是运行时访问类型信息的入口。需要注意的是,每个类 / 内部类 / 接口都拥有各自的 Class 对象。

  • 问:获取 Class 对象有几种方式,有什么区别?
    答:获取 Class 对象是反射的起始步骤,具体来说,分为以下三种方式:
  • 问:为什么反射性能差,怎么优化?

答:主要有以下原因:

性能差原因 优化方法
产生大量中间变量 缓存元数据对象
增加了检查可见性操作 调用Method#setAccessible(true),减少不必要的检查
Inflation 机制会生成字节码,而这段字节码没有经过优化 /
缺少编译器优化,普通调用有一系列优化手段,例如方法内联,而反射调用无法应用此优化 /
增加了装箱拆箱操作,反射调用需要构建包装类 /

  1. 反射调用的 Inflation 机制

反射调用是反射的一个较为常用的场景,这里我们来分析下反射调用的源码。反射调用需要使用Method#invoke(...),源码如下:

Method.java

1
2
3
4
5
6
7
ini复制代码public Object invoke(Object obj, Object... args) {
MethodAccessor ma = methodAccessor;
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

NativeMethodAccessorImpl.java

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
kotlin复制代码class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;

NativeMethodAccessorImpl(Method var1) {
this.method = var1;
}

public Object invoke(Object var1, Object[] var2) {
1. 检查调用次数是否超过阈值
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
2. ASM 生成新类
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
3. 设置为代理
this.parent.setDelegate(var3);
}
4. 调用 native 方法
return invoke0(this.method, var1, var2);
}

void setParent(DelegatingMethodAccessorImpl var1) {
this.parent = var1;
}

private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

ReflectionFactory.java

1
2
3
4
5
6
7
8
csharp复制代码public class ReflectionFactory {

private static int inflationThreshold = 15;

static int inflationThreshold() {
return inflationThreshold;
}
}

可以看到,反射调用最终会委派给 NativeMethodAccessorImpl ,要点如下:

  • 当反射调用执行次数较少时,直接通过 native 方法调用;
  • 当反射调用执行次数较多时,则通过 ASM 字节码生成技术生成新的类,以后的反射调用委派给新生成的类来处理。

提示: 为什么不一开始就生成新类呢?因为生成字节码的时间成本高于执行一次 native 方法的时间成本,所以在反射调用执行次数较少时,就直接调用 native 方法了。


  1. 反射的应用场景

4.1 类型判断

4.2 创建对象

  • 1、使用 Class.newInstance(),适用于类拥有无参构造方法
1
2
ini复制代码Class<?> classType = Class.forName("java.lang.String");
String str= (String) classType.newInstance();
  • 2、Constructor.newInstance(),适用于使用带参数的构造方法
1
2
3
4
vbnet复制代码Class<?> classType = Class.forName("java.lang.String");
Constructor<?> constructor = classType.getConstructor(new Class[]{String.class});
constructor.setAccessible(true);
String employee3 = (String) constructor.newInstance(new Object[]{"123"});

4.3 创建数组

创建数组需要元素的 Class 对象作为 ComponentType:

  • 1、创建一维数组
1
2
3
4
ini复制代码Class<?> classType = Class.forName("java.lang.String");
String[] array = (String[]) Array.newInstance(classType, 5); 长度为5
Array.set(array, 3, "abc"); 设置元素
String string = (String) Array.get(array,3); 读取元素
  • 2、创建多维数组
1
2
ini复制代码Class[] dimens = {3, 3};
Class[][] array = (Class[][]) Array.newInstance(int.class, dimens);

4.3 访问字段、方法

Editting…

4.4 获取泛型信息

我们知道,编译期会进行类型擦除,Code 属性中的类型信息会被擦除,但是在类常量池属性(Signature属性、LocalVariableTypeTable属性)中还保留着泛型信息,因此我们可以通过反射来获取这部分信息。在这篇文章里,我们详细讨论:《Java | 关于泛型能问的都在这里了(含Kotlin)》,请关注!

4.5 获取运行时注解信息

注解是一种添加到声明上的元数据,而RUNTIME注解在类加载后会保存在 Class 对象,可以反射获取。在这篇文章里,我们详细讨论:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》,请关注!


参考资料

  • 《Kotlin实战》 (第10章)—— [俄] Dmitry Jemerov,Svetlana Isakova 著
  • 《Kotlin 核心编程》(第8章)—— 水滴技术团队 著
  • 《深入理解JVM字节码》(第3.5节)—— 张亚 著
  • 《Java编程思想》 (第19、23章)—— [美] Bruce Eckel 著
  • 《深入理解Java虚拟机(第3版)》(第8、10章)—— 周志明 著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android 使用 AspectJ 限制按钮快速点击

发表于 2020-10-30

请点赞关注,你的支持对我意义重大。

🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。

前言

  • 在Android开发中,限制按钮快速点击(按钮防抖)是一个常见的需求;
  • 在这篇文章里,我将介绍一种使用AspectJ的方法,基于注解处理器 & 运行时注解反射的原理。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

系列文章

  • 《Android | 一文带你全面了解 AspectJ 框架》
  • 《Android | 使用 AspectJ 限制按钮快速点击》

延伸文章

  • 关于 反射,请阅读:《Java | 反射:在运行时访问类型信息(含 Kotlin)》
  • 关于 注解,请阅读:《Java | 这是一篇全面的注解使用攻略(含 Kotlin)》
  • 关于 注解处理器(APT),请阅读:《Java | 注解处理器(APT)原理解析 & 实践》

目录


  1. 定义需求

在开始讲解之前,我们先 定义需求,具体描述如下:

  • 限制快速点击需求 示意图:


  1. 常规处理方法

目前比较常见的限制快速点击的处理方法有以下两种,具体如下:

2.1 封装代理类

封装一个代理类处理点击事件,代理类通过判断点击间隔决定是否拦截点击事件,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
csharp复制代码// 代理类
public abstract class FastClickListener implements View.OnClickListener {
private long mLastClickTime;
private long interval = 1000L;

public FastClickListener() {
}

public FastClickListener(long interval) {
this.interval = interval;
}

@Override
public void onClick(View v) {
long currentTime = System.currentTimeMillis();
if (currentTime - mLastClickTime > interval) {
// 经过了足够长的时间,允许点击
onClick();
mLastClickTime = nowTime;
}
}

protected abstract void onClick();
}

在需要限制快速点击的地方使用该代理类,具体如下:

1
2
3
4
5
6
less复制代码tv.setOnClickListener(new FastClickListener() {
@Override
protected void onClick() {
// 处理点击逻辑
}
});

2.2 RxAndroid 过滤表达式

使用RxJava的过滤表达式throttleFirst也可以限制快速点击,具体如下:

1
2
3
4
5
6
7
8
java复制代码RxView.clicks(view)
.throttleFirst(1, TimeUnit.SECONDS)
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
// 处理点击逻辑
}
});

2.3 小结

代理类和RxAndroid过滤表达式这两种处理方法都存在两个缺点:

  • 1. 侵入核心业务逻辑,需要将代码替换到需要限制点击的地方;
  • 2. 修改工作量大,每一个增加限制点击的地方都要修改代码。

我们需要一种方案能够规避这两个缺点 —— AspectJ。 AspectJ是一个流行的Java AOP(aspect-oriented programming)编程扩展框架,若还不了解,请务必查看文章:《Android | 一文带你全面了解 AspectJ 框架》


  1. 详细步骤

在下面的内容里,我们将使用AspectJ框架,把限制快速点击的逻辑作为核心关注点从业务逻辑中抽离出来,单独维护。具体步骤如下:

步骤1:添加AspectJ依赖

    1. 依赖沪江的AspectJXGradle插件 —— 在项目build.gradle中添加插件依赖:
1
2
3
4
5
arduino复制代码// 项目级build.gradle
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
}

如果插件下载速度过慢,可以直接依赖插件 jar文件,将插件下载到项目根目录(如/plugins),然后在项目build.gradle中添加插件依赖:

1
2
3
4
5
php复制代码// 项目级build.gradle
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath fileTree(dir:'plugins', include:['*.jar'])
}
    1. 应用插件 —— 在App Module的build.gradle中应用插件:
1
2
3
arduino复制代码// App Module的build.gradle
apply plugin: 'android-aspectjx'
...
    1. 依赖AspectJ框架 —— 在包含AspectJ代码的Module的build.gradle文件中添加依赖:
1
2
3
4
5
6
arduino复制代码// Module级build.gradle
dependencies {
...
api 'org.aspectj:aspectjrt:1.8.9'
...
}

步骤2:实现判断快速点击的工具类

  • 我们先实现一个判断View是否快速点击的工具类;
  • 实现原理是使用View的tag属性存储最近一次的点击时间,每次点击时判断当前时间距离存储的时间是否已经经过了足够长的时间;
  • 为了避免调用View#setTag(int key,Object tag)时传入的key与其他地方传入的key冲突而造成覆盖,务必使用在资源文件中定义的 id,资源文件中的 id 能够有效保证全局唯一性,具体如下:
1
2
3
4
xml复制代码// ids.xml
<resources>
<item type="id" name="view_click_time" />
</resources>
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
java复制代码public class FastClickCheckUtil {

/**
* 判断是否属于快速点击
*
* @param view 点击的View
* @param interval 快速点击的阈值
* @return true:快速点击
*/
public static boolean isFastClick(@NonNull View view, long interval) {
int key = R.id.view_click_time;

// 最近的点击时间
long currentClickTime = System.currentTimeMillis();

if(null == view.getTag(key)){
// 1. 第一次点击

// 保存最近点击时间
view.setTag(key, currentClickTime);
return false;
}
// 2. 非第一次点击

// 上次点击时间
long lastClickTime = (long) view.getTag(key);
if(currentClickTime - lastClickTime < interval){
// 未超过时间间隔,视为快速点击
return true;
}else{
// 保存最近点击时间
view.setTag(key, currentClickTime);
return false;
}
}
}

步骤3:定义Aspect切面

使用@Aspect注解定义一个切面,使用该注解修饰的类会被AspectJ编译器识别为切面类:

1
2
3
4
kotlin复制代码@Aspect
public class FastClickCheckerAspect {
// 随后填充
}

步骤4:定义PointCut切入点

使用@Pointcut注解定义一个切入点,编译期AspectJ编译器将搜索所有匹配的JoinPoint,执行织入:

1
2
3
4
5
6
7
8
9
10
java复制代码@Aspect
public class FastClickAspect {

// 定义一个切入点:View.OnClickListener#onClick()方法
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void methodViewOnClick() {
}

// 随后填充 Advice
}

步骤5:定义Advice增强

增强的方式有很多种,在这里我们使用@Around注解定义环绕增强,它将包装PointCut,在PointCut前后增加横切逻辑,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
less复制代码@Aspect
public class FastClickAspect {

// 定义切入点:View.OnClickListener#onClick()方法
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void methodViewOnClick() {}

// 定义环绕增强,包装methodViewOnClick()切入点
@Around("methodViewOnClick()")
public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {
// 取出目标对象
View target = (View) joinPoint.getArgs()[0];
// 根据点击间隔是否超过2000,判断是否为快速点击
if (!FastClickCheckUtil.isFastClick(target, 2000)) {
joinPoint.proceed();
}
}
}

步骤6:实现View.OnClickListener

在这一步我们为View设置OnClickListener,可以看到我们并没有添加限制快速点击的相关代码,增强的逻辑对原有逻辑没有侵入,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scala复制代码// 源码:
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("AspectJ","click");
}
});
}
}

编译代码,随后反编译AspectJ编译器执行织入后的.class文件。还不了解如何查找编译后的.class文件,请务必查看文章:《Android | 一文带你全面了解 AspectJ 框架》

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
java复制代码public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(2131361820);
findViewById(2131165349).setOnClickListener(new View.OnClickListener() {
private static final JoinPoint.StaticPart ajc$tjp_0;

// View.OnClickListener#onClick()
public void onClick(View v) {
View view = v;
// 重构JoinPoint,执行环绕增强,也执行@Around修饰的方法
JoinPoint joinPoint = Factory.makeJP(ajc$tjp_0, this, this, view);
onClick_aroundBody1$advice(this, view, joinPoint, FastClickAspect.aspectOf(), (ProceedingJoinPoint)joinPoint);
}

static {
ajc$preClinit();
}

private static void ajc$preClinit() {
Factory factory = new Factory("MainActivity.java", null.class);
ajc$tjp_0 = factory.makeSJP("method-execution", (Signature)factory.makeMethodSig("1", "onClick", "com.have.a.good.time.aspectj.MainActivity$1", "android.view.View", "v", "", "void"), 25);
}

// 原来在View.OnClickListener#onClick()中的代码,相当于核心业务逻辑
private static final void onClick_aroundBody0(null ajc$this, View v, JoinPoint param1JoinPoint) {
Log.i("AspectJ", "click");
}

// @Around方法中的代码,即源码中的aroundViewOnClick(),相当于Advice
private static final void onClick_aroundBody1$advice(null ajc$this, View v, JoinPoint thisJoinPoint, FastClickAspect ajc$aspectInstance, ProceedingJoinPoint joinPoint) {
View target = (View)joinPoint.getArgs()[0];
if (!FastClickCheckUtil.isFastClick(target, 2000)) {
// 非快速点击,执行点击逻辑
ProceedingJoinPoint proceedingJoinPoint = joinPoint;
onClick_aroundBody0(ajc$this, v, (JoinPoint)proceedingJoinPoint);
null;
}
}
});
}
}

小结

到这里,我们就讲解完使用AspectJ框架限制按钮快速点击的详细,总结如下:

  • 使用@Aspect注解描述一个切面,使用该注解修饰的类会被AspectJ编译器识别为切面类;
  • 使用@Pointcut注解定义一个切入点,编译期AspectJ编译器将搜索所有匹配的JoinPoint,执行织入;
  • 使用@Around注解定义一个增强,增强会被织入匹配的JoinPoint

  1. 演进

现在,我们回归文章开头定义的需求,总共有4点。其中前两点使用目前的方案中已经能够实现,现在我们关注后面两点,即允许定制时间间隔与覆盖尽可能多的点击场景。

  • 需求回归 示意图:

4.1 定制时间间隔

在实际项目不同场景中的按钮,往往需要限制不同的点击时间间隔,因此我们需要有一种简便的方式用于定制不同场景的时间间隔,或者对于一些不需要限制快速点击的地方,有办法跳过快速点击判断,具体方法如下:

  • 定义注解
1
2
3
4
5
6
7
8
less复制代码/**
* 在需要定制时间间隔地方添加@FastClick注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FastClick {
long interval() default FastClickAspect.FAST_CLICK_INTERVAL_GLOBAL;
}
  • 修改切面类的Advice
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
java复制代码@Aspect
public class SingleClickAspect {

public static final long FAST_CLICK_INTERVAL_GLOBAL = 1000L;

@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void methodViewOnClick() {}

@Around("methodViewOnClick()")
public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {
// 取出JoinPoint的签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 取出JoinPoint的方法
Method method = methodSignature.getMethod();

// 1. 全局统一的时间间隔
long interval = FAST_CLICK_INTERVAL_GLOBAL;

if (method.isAnnotationPresent(FastClick.class)) {
// 2. 如果方法使用了@FastClick修饰,取出定制的时间间隔

FastClick singleClick = method.getAnnotation(FastClick.class);
interval = singleClick.interval();
}
// 取出目标对象
View target = (View) joinPoint.getArgs()[0];
// 3. 根据点击间隔是否超过interval,判断是否为快速点击
if (!FastClickCheckUtil.isFastClick(target, interval)) {
joinPoint.proceed();
}
}
}
  • 使用注解
1
2
3
4
5
6
7
less复制代码findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {
@FastClick(interval = 5000L)
@Override
public void onClick(View v) {
Log.i("AspectJ","click");
}
});

4.2 完整场景覆盖

ButterKnife @OnClick
android:onClick OK
RecyclerView / ListView
Java Lambda NO
Kotlin Lambda OK
DataBinding OK

Editting…


我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐]私信我提问。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中的微信支付(2):API V3 微信平台证书的获取

发表于 2020-10-30
  1. 前言

在Java中的微信支付(1):API V3版本签名详解一文中胖哥讲解了微信支付V3版本API的签名,当我方(你自己的服务器)请求微信支付服务器时需要根据我方的API证书对参数进行加签,微信服务器会根据我方签名验签以确定请求来自我方服务器。那么同样的道理我方的服务器也要对微信支付服务器的响应进行鉴别来确定响应真的来自微信支付服务器,这就是验签。验签使用的是**【微信支付平台证书公钥**】,不是商户API证书。使用商户API证书是验证不过的。今天就来分享一下如何获得微信平台公钥和动态刷新微信平台公钥。

  1. 获取微信平台证书公钥

微信平台证书是微信支付平台自己的证书,我们是管不了的,而且是有效期的。

微信服务器会定期更换,所以也要求我方定期获取公钥。而且我们只能通过调用接口/v3/certificates来获得,此接口也需要进行签名(可参考上一篇文章)。你可以获取证书后静态放到服务器上,手动更新静态证书;也可以动态获取一劳永逸。本文采取一劳永逸的办法。

平台证书接口文档:wechatpay-api.gitbook.io/wechatpay-a…

  1. 证书和回调报文解密

为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。也就是说我们拿到响应的信息是被加密的,需要解密后才能获得真正的微信平台证书公钥。响应体大致是这样的,具体根据你调用平台证书接口,应该大差不差是下面这个结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
json复制代码{
"data": [
{
"effective_time": "2020-10-21T14:48:49+08:00",
"encrypt_certificate": {
// 加密算法
"algorithm": "AEAD_AES_256_GCM",
// 附加数据包(可能为空)
"associated_data": "certificate",
// Base64编码后的密文
"ciphertext": "",
// 加密使用的随机串初始化向量)
"nonce": "88b4e15a0db9"
},
"expire_time": "2025-10-20T14:48:49+08:00",
// 证书序列号
"serial_no": "217016F42805DD4D5442059D373F98BFC5252599"
}
]
}

你可以使用各种JSON类库取得下面方法的参数进行解密以获取证书,同时这里需要用到APIv3密钥,通用的解密方式为:

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
java复制代码/**
* 解密响应体.
*
* @param apiV3Key API V3 KEY API v3密钥 商户平台设置的32位字符串
* @param associatedData response.body.data[i].encrypt_certificate.associated_data
* @param nonce response.body.data[i].encrypt_certificate.nonce
* @param ciphertext response.body.data[i].encrypt_certificate.ciphertext
* @return the string
* @throws GeneralSecurityException the general security exception
*/
public String decryptResponseBody(String apiV3Key,String associatedData, String nonce, String ciphertext) {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8));

cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8));

byte[] bytes;
try {
bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext));
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException(e);
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}

回调的请求体也是此方法进行解密。

  1. 动态刷新

然后就能拿到微信平台证书公钥。然后你可以定义个Map,以证书的序列号为KEY,以证书为Value来动态刷新,关键伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码// 定义全局容器 保存微信平台证书公钥  注意线程安全 
private static final Map<String, Certificate> CERTIFICATE_MAP = new ConcurrentHashMap<>();

// 下面是刷新方法 refreshCertificate 的核心代码
String publicKey = decryptResponseBody(associatedData, nonce, ciphertext);

final CertificateFactory cf = CertificateFactory.getInstance("X509");

ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8));
Certificate certificate = null;
try {
certificate = cf.generateCertificate(inputStream);
} catch (CertificateException e) {
e.printStackTrace();
}
String responseSerialNo = objectNode.get("serial_no").asText();
// 清理HashMap
CERTIFICATE_MAP.clear();
// 放入证书
CERTIFICATE_MAP.put(responseSerialNo, certificate);

动态刷新的策略就很好写了:

1
2
3
4
5
6
java复制代码// 当证书容器为空 或者 响应提供的证书序列号不在容器中时  就应该刷新了
if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) {
refreshCertificate();
}
// 然后调用
Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);
  1. 总结

虽然验签你不做可以拿到其它接口的响应结果,但是从资金安全的角度来说这是十分必要的。同时因为微信平台证书不收我方控制,采取动态刷新也会更加方便,不必再担心过期的问题。本文我们通过调用接口拿到密文并解密获得证书。下一篇我们将通过获得的证书进行签名验证来确保我们的响应是微信服务器发过来的,请关注:码农小胖哥 及时获得相关的更新。

关注公众号:Felordcn获取更多资讯

个人博客:https://felord.cn

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

并发编程从操作系统底层工作的整体认识开始

发表于 2020-10-30

点赞再看,养成习惯,公众号搜一搜【一角钱技术】关注更多原创技术文章。本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章。

前言

在多线程、多处理器、分布式环境的编程时代,并发是一个不可回避的问题。既然并发问题摆在面前一个到无法回避的坎,倒不如拥抱它,把它搞清楚,花一定的时间从操作系统底层原理到Java的基础编程再到分布式环境等几个方面深入探索并发问题。先就从原理开始吧。

计算机系统层次结构

早期计算机系统的层次

最早的计算机用机器语言编程,机器语言称为第一代程序设计语言

汇编语言编程

汇编语言编程

现代(传统)计算机系统的层次

现代计算机用高级语言编程

  • 第三代程序设计语言(3GL)为过程式 语言,编码时需要描述实现过程,即“ 如何做”。
  • 第四代程序设计语言(4GL) 为非过程 化语言,编码时只需说明“做什么”, 不需要描述具体的算法实现细节。

语言处理系统包括:各种语言处理程序(如编译、汇编、 链接)、运行时系统(如库函数,调试、优化等功能)

操作系统包括人机交互界面、 提供服务功能的内核例程

可以看出:语言的发展是一 个不断“抽象”的过程,因而,相应的计算机系统也不断有新的层次出现。

计算机系统抽象层的转换

功能转换:上层是下层的抽象,下层是上层的实现 底层为上层提供支撑环境!

计算机系统的不同用户

  • 最终用户工作在由应用程序提供的最上面的抽象层
  • 系统管理员工作在由操作系统提供的抽象层
  • 应用程序员工作在由语言处理系统(主要有编译器和汇编器)的抽象层
  • 语言处理系统建立在操作系统之上
  • 系统程序员(实现系统软件)工作在ISA层次,必须对ISA非常了解

编译器和汇编器的目标程序由机器级代码组成

操作系统通过指令直接对硬件进行编程控制ISA处于软件和硬件的交界面(接口)

ISA是对硬件的抽象所有软件功能都建立在ISA之上

指令集体系结构(ISA)

ISA指 Instruction Set Architecture,即指令集体系结构,有时简称为指令系统

  • ISA是一种规约(Specification),它规定了如何使用硬件
    • 可执行的指令的集合,包括指令格式、操作种类以及每种操作对应的 操作数的相应规定;
    • 指令可以接受的操作数的类型;
    • 操作数所能存放的寄存器组的结构,包括每个寄存器的名称、编号、 长度和用途;
    • 操作数所能存放的存储空间的大小和编址方式;
    • 操作数在存储空间存放时按照大端还是小端方式存放;
    • 指令获取操作数的方式,即寻址方式;
    • 指令执行过程的控制方式,包括程序计数器(PC)、条件码定义等。
  • ISA在通用计算机系统中是必不可少的一个抽象层,
    • 没有它,软件无法使用计算机硬件!
    • 没有它,一台计算机不能称为“通用计算机”

ISA和计算机组成(微结构)之间的关系

ISA是计算机组成的抽象,不同ISA规定的指令集不同

  • 如,IA-32、MIPS、ARM等 计算机组成必须能够实现ISA规定的功能
  • 如提供GPR、标志、运算电路等 同一种ISA可以有不同的计算机组成
  • 如乘法指令可用ALU或乘法器实现

现代计算机的原型

现代计算机模型是基于-冯诺依曼计算机模型

1946年,普林斯顿高等研究院(the Institute for Advance Study at Princeton,IAS )开始设计“存储程序”计算机,被称为IAS计算机.

  • 冯·诺依曼结构最重要的思想是“存储程序(Stored-program)”
  • 工作方式:
    • 任何要计算机完成的工作都要先被编写成程序,然后将程序和原始数据送入主存并启动执行。一旦程序被启动,计算机应能在不需操作人员干预下,自动完成逐条取出指令和执行指令的任务。
    • 冯·诺依曼结构计算机也称为冯·诺依曼机器(Von Neumann Machine)。
    • 几乎现代所有的通用计算机大都采用冯·诺依曼结构,因此,IAS计算机是现代计算机的原型机。

计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存储器中取出数据进行指定的运算和逻辑操作等加工,然后再按地址把结果送到内存中去。接下来,再取出第二条指令,在控制器的指挥下完成规定操作。依此进行下去。直至遇到停止指令。

程序与数据一样存贮,按程序编排的顺序,一步一步地取出指令,自动地完成指令规定的操作是计算机最基本的工作模型。这一原理最初是由美籍匈牙利数学家冯.诺依曼于1945年提出来的,故称为冯.诺依曼计算机模型。

冯·诺依曼结构是怎样的?

  • 有主存,用来存放程序和数据
  • 一个自动逐条取 出指令的部件
  • 具体执行指令 (即运算)的部件
  • 程序由指令构成
  • 指令描述如何对数据进 行处理
  • 将程序和原始数据输入计算机的部件
  • 将运算结果输出计算机的部件

冯·诺依曼结构的主要思想

  • 计算机应由计算器(运算器)、控制器、存储器、输入设备和输出设备 五个基本部件组成。
  • 各基本部件的功能是:
    • 控制器(Control):是整个计算机的中枢神经,其功能是对程序规定的控制信息进行解释,根据其要求进行控制,调度程序、数据、地址,协调计算机各部分工作及内存与外设的访问等。
    • 运算器(Datapath):运算器的功能是对数据进行各种算术运算和逻辑运算,即对数据进行加工处理。
    • 存储器(Memory):存储器的功能是存储程序、数据和各种信号、命令等信息,并在需要时提供这些信息。
    • 输入(Input system):输入设备是计算机的重要组成部分,输入设备与输出设备合并为外部设备,简称外设,输入设备的作用是将程序、原始数据、文字、字符、控制命令或现场采集的数据等信息输入到计算机。常见的输入设备有键盘、鼠标器、光电输入机、磁带机、磁盘机、光盘机等。
    • 输出(Output system):输出设备与输入设备同样是计算机的重要组成部分,它把外算机的中间结果或最后结果、机内的各种数据符号及文字或各种控制信号等信息输出出来。微机常用的输出设备有显示终端CRT、打印机、激光印字机、绘图仪及磁带、光盘机等。
  • 内部以二进制表示指令和数据。每条指令由操作码和地址码 两部分组成。操作码指出操作类型,地址码指出操作数的地址。由一串指令组成程序。
  • 采用“存储程序”工作方式。

现代计算机结构模型

基于冯·诺依曼计算机理论的抽象简化模型,它的具体应用就是现代计算机当中的硬件结构设计:

在上图硬件结构当中,配件很多,但最核心的只有两部分:CPU、内存。所以我们重点学习的也是这两部分。

CPU:中央处理器;PC:程序计数器;
MAR:存储器地址寄存器 ALU:算术逻辑部件;
IR:指令寄存器;MDR:存储器数据寄存器 GPRs:通用寄存器组(由若干通用寄存器组成,早期就是累加器)

CPU指令结构

CPU内部结构

  • 控制单元
  • 运算单元
  • 数据单元

控制单元

控制单元是整个CPU的指挥控制中心,由指令寄存器IR(Instruction Register)、指令译码器ID(Instruction Decoder)和 操作控制器OC(Operation Controller) 等组成,对协调整个电脑有序工作极为重要。它根据用户预先编好的程序,依次从存储器中取出各条指令,放在指令寄存器IR中,通过指令译码(分析)确定应该进行什么操作,然后通过操作控制器OC,按确定的时序,向相应的部件发出微操作控制信号。操作控制器OC中主要包括:节拍脉冲发生器、控制矩阵、时钟脉冲发生器、复位电路和启停电路等控制逻辑。

运算单元

运算单元是运算器的核心。可以执行算术运算(包括加减乘数等基本运算及其附加运算)和逻辑运算(包括移位、逻辑测试或两个值比较)。相对控制单元而言,运算器接受控制单元的命令而进行动作,即运算单元所进行的全部操作都是由控制单元发出的控制信号来指挥的,所以它是执行部件。

存储单元

存储单元包括 CPU 片内缓存Cache和寄存器组,是 CPU 中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU 访问寄存器所用的时间要比访问内存的时间短。 寄存器是CPU内部的元件,寄存器拥有非常高的读写速度,所以在寄存器之间的数据传送非常快。采用寄存器,可以减少 CPU 访问内存的次数,从而提高了 CPU 的工作速度。寄存器组可分为专用寄存器和通用寄存器。专用寄存器的作用是固定的,分别寄存相应的数据;而通用寄存器用途广泛并可由程序员规定其用途。

下表列出了CPU关键技术的发展历程以及代表系列,每一个关键技术的诞生都是环环相扣的,处理器这些技术发展历程都围绕着如何不让“CPU闲下来”这一个核心目标展开。

关键技术 时间 描述
指令缓存(L1) 1982 预读多条指令
数据缓存(L1) 1985 预读一定长度的数据
流水线 1989 一条指令被拆分由多个单元协同处理, i486
多流水线 1993 多运算单元多流水线并行处理, 奔腾1
乱序+分支预测 1995 充分利用不同组件协同处理, 奔腾Pro
超线程 2002 引入多组前端部件共享执行引擎, 奔腾4
多核处理器 2006 取消超线程,降低时钟频率,改用多核心, Core酷睿
多核超线程 2008 重新引入超线程技术,iX系列

CPU缓存结构

现代CPU为了提升执行效率,减少CPU与内存的交互(交互影响CPU效率),一般在CPU上集成了多级缓存架构,常见的为三级缓存结构

  • L1 Cache,分为数据缓存和指令缓存,逻辑核独占
  • L2 Cache,物理核独占,逻辑核共享
  • L3 Cache,所有物理核共享

  • 存储器存储空间大小:内存>L3>L2>L1>寄存器;
  • 存储器速度快慢排序:寄存器>L1>L2>L3>内存;

注意:缓存是由最小的存储区块-缓存行(cacheline)组成,缓存行大小通常为64byte。

缓存行是什么意思呢?
比如你的L1缓存大小是512kb,而cacheline = 64byte,那么就是L1里有512 * 1024/64个cacheline

CPU读取存储器数据过程

  1. CPU要取寄存器X的值,只需要一步:直接读取。
  2. CPU要取L1 cache的某个值,需要1-3步(或者更多):把cache行锁住,把某个数据拿来,解锁,如果没锁住就慢了。
  3. CPU要取L2 cache的某个值,先要到L1 cache里取,L1当中不存在,在L2里,L2开始加锁,加锁以后,把L2里的数据复制到L1,再执行读L1的过程,上面的3步,再解锁。
  4. CPU取L3 cache的也是一样,只不过先由L3复制到L2,从L2复制到L1,从L1到CPU。
  5. CPU取内存则最复杂:通知内存控制器占用总线带宽,通知内存加锁,发起内存读请求,等待回应,回应数据保存到L3(如果没有就到L2),再从L3/2到L1,再从L1到CPU,之后解除总线锁定。

CPU为何要有高速缓存

CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。

在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。

  • 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

比如循环、递归、方法的反复调用等。

  • 空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

比如顺序执行的代码、连续创建的两个对象、数组等。

空间局部性案例:

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
java复制代码public class TwoDimensionalArraySum {
private static final int RUNS = 100;
private static final int DIMENSION_1 = 1024 * 1024;
private static final int DIMENSION_2 = 6;
private static long[][] longs;

public static void main(String[] args) throws Exception {
/*
* 初始化数组
*/
longs = new long[DIMENSION_1][];
for (int i = 0; i < DIMENSION_1; i++) {
longs[i] = new long[DIMENSION_2];
for (int j = 0; j < DIMENSION_2; j++) {
longs[i][j] = 1L;
}
}
System.out.println("Array初始化完毕....");

long sum = 0L;
long start = System.currentTimeMillis();
for (int r = 0; r < RUNS; r++) {
for (int i = 0; i < DIMENSION_1; i++) {//DIMENSION_1=1024*1024
for (int j=0;j<DIMENSION_2;j++){//6
sum+=longs[i][j];
}
}
}
System.out.println("spend time1:"+(System.currentTimeMillis()-start));
System.out.println("sum1:"+sum);

sum = 0L;
start = System.currentTimeMillis();
for (int r = 0; r < RUNS; r++) {
for (int j=0;j<DIMENSION_2;j++) {//6
for (int i = 0; i < DIMENSION_1; i++){//1024*1024
sum+=longs[i][j];
}
}
}
System.out.println("spend time2:"+(System.currentTimeMillis()-start));
System.out.println("sum2:"+sum);
}
}

带有高速缓存的CPU执行计算的流程

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU的高速缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

CPU运行安全等级

CPU有4个运行级别,分别为:

  • ring0
  • ring1
  • ring2
  • ring3

Linux与Windows只用到了2个级别:ring0、ring3,操作系统内部内部程序指令通常运行在ring0级别,操作系统以外的第三方程序运行在ring3级别,第三方程序如果要调用操作系统内部函数功能,由于运行安全级别不够,必须切换CPU运行状态,从ring3切换到ring0,然后执行系统函数,说到这里相信大家明白为什么JVM创建线程,线程阻塞唤醒是重型操作了,因为CPU要切换运行状态。
下面我大概梳理一下JVM创建线程CPU的工作过程

  • step1:CPU从ring3切换ring0创建线程
  • step2:创建完毕,CPU从ring0切换回ring3
  • step3:线程执行JVM程序
  • step4:线程执行完毕,销毁还得切会ring0

操作系统内存管理

执行空间保护

操作系统有用户空间与内核空间两个概念,目的也是为了做到程序运行安全隔离与稳定,以32位操作系统4G大小的内存空间为例


Linux为内核代码和数据结构预留了几个页框,这些页永远不会被转出到磁盘上。从 0x00000000 到 0xC0000000(PAGE_OFFSET) 的线性地址可由用户代码 和 内核代码进行引用(即用户空间)。从0xC0000000(PAGE_OFFSET)到 0xFFFFFFFFF的线性地址只能由内核代码进行访问(即内核空间)。内核代码及其数据结构都必须位于这 1 GB的地址空间中,但是对于此地址空间而言,更大的消费者是物理地址的虚拟映射。

这意味着在 4 GB 的内存空间中,只有 3 GB 可以用于用户应用程序。进程与线程只能运行在用户方式(usermode)或内核方式(kernelmode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈(用户空间的堆栈),而内核方式下用的是固定大小的堆栈(内核空间的对战,一般为一个内存页的大小),即每个进程与线程其实有两个堆栈,分别运行与用户态与内核态。

由空间划分我们再引申一下,CPU调度的基本单位线程,也划分为:

  1. 内核线程模型(KLT)
  2. 用户线程模型(ULT)

内核线程模型

内核线程(KLT):系统内核管理线程(KLT),内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器上并行运行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快。

用户线程模型

用户线程(ULT):用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/内核态切换,速度快。内核对ULT无感知,线程阻塞则进程(包括它的所有线程)阻塞。

到这里,大家不妨思考一下,jvm是采用的哪一种线程模型?

进程与线程

什么是进程?

现代操作系统在运行一个程序时,会为其创建一个进程;例如,启动一个Java程序,操作系统就会创建一个Java进程。进程是OS(操作系统)资源分配的最小单位。

什么是线程?

线程是OS(操作系统)调度CPU的最小单元,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。CPU在这些线程上高速切换,让使用者感觉到这些线程在同时执行,即并发的概念,相似的概念还有并行!

线程上下文切换过程:

虚拟机指令集架构

虚拟机指令集架构主要分两种:

  1. 栈指令集架构
  2. 寄存器指令集架构

关于指令集架构的wiki详细说明:zh.wikipedia.org/wiki/指令集架構

栈指令集架构

  1. 设计和实现更简单,适用于资源受限的系统;
  2. 避开了寄存器的分配难题:使用零地址指令方式分配;
  3. 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈,指令集更小,编译器容易实现;
  4. 不需要硬件支持,可移植性更好,更好实现跨平台。

寄存器指令集架构

  1. 典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机。
  2. 指令集架构则完全依赖硬件,可移植性差。
  3. 性能优秀和执行更高效。
  4. 花费更少的指令去完成一项操作。
  5. 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。

Java符合典型的栈指令集架构特征,像Python、Go都属于这种架构。

文章持续更新,可以公众号搜一搜「 一角钱技术 」第一时间阅读, 本文 GitHub org_hejianhui/JavaStudy 已经收录,欢迎 Star。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android 说说从 android text 到 T

发表于 2020-10-29

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 在 Android UI 开发中,经常需要用到 属性,例如使用android:text设置文本框的文案,使用android:src设置图片。那么,android:text是如何设置到 TextView 上的呢?
  • 其实这个问题主要还是考察应试者对于源码(包括:LayoutInflater 布局解析、Style/Theme 系统 等)的熟悉度,在这篇文章里,我将跟你一起探讨。另外,文末的应试建议也不要错过哦,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

相关文章

  • 《Android | 一个进程有多少个 Context 对象(答对的不多)》
  • 《Android | 带你探究 LayoutInflater 布局解析原理》
  • 《Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?》
  • 《Android | 说说从 android:text 到 TextView 的过程》

目录


  1. 属性概述

1.1 属性的本质

属性 (View Attributes) 本质上是一个键值对关系,即:属性名 => 属性值。

1.2 如何定义属性?

定义属性需要用到<declare-styleable>标签,需要定义 属性名 与 属性值类型,格式上可以分为以下 2 种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码格式 1 :

1.1 先定义属性名和属性值类型
<attr name="textColor" format="reference|color"/>

<declare-styleable name="TextView">
1.2 引用上面定义的属性
<attr name="textColor" />
</declare-styleable>

格式 2:

<declare-styleable name="TextView">
一步到位
<attr name="text" format="string" localization="suggested" />
</declare-styleable>
  • 格式 1:分为两步,先定义属性名和属性值类型,然后在引用;
  • 格式 2:一步到位,直接指定属性名和属性值类型。

1.3 属性的命名空间

使用属性时,需要指定属性的命名空间,命名空间用于区分属性定义的位置。目前一共有 4 种 命名空间:

  • 1、工具 —— tools:xmlns:tools="http://schemas.android.com/tools"

只在 Android Studio 中生效,运行时不生效。比如以下代码,背景色在编辑器的预览窗口显示白色,但是在运行时显示黑色:

1
2
ini复制代码tools:background="@android:color/white"
android:background="@android:color/black"
  • 2、原生 —— android:xmlns:android="http://schemas.android.com/apk/res/android"

原生框架中attrs定义的属性,例如,我们找到 Android P 定义的属性 attrs.xml,其中可以看到一些我们熟知的属性:

1
2
3
4
5
6
xml复制代码<!-- 文本颜色 -->
<attr name="textColor" format="reference|color"/>
<!-- 高亮文本颜色 -->
<attr name="textColorHighlight" format="reference|color" />
<!-- 高亮文本颜色 -->
<attr name="textColorHint" format="reference|color" />

你也可以在 SDK 中找到这个文件,有两种方法:

  • 文件夹:sdk/platform/android-28/data/res/values/attrs.xml
  • Android Studio(切换到 project 视图):External Libraries/<Android API 28 Platform>/res/values/attrs.xml

(你在这里看到的版本号是在app/build.gradle中的compileSdkVersion设置的)

  • 3、AppCompat 兼容库 —— 无需命名空间

Support 库 或 AndroidX 库中定义的属性,比如:

1
ini复制代码<attr format="color" name="colorAccent"/>

你也可以在 Android Studio 中找到这个文件:

  • Android Studio(切换到 project 视图):External Libraries/Gradle:com.android.support:appcompat-v7:[版本号]@aar/res/values/values.xml
  • 4、自定义 —— app:xmlns:app="http://schemas.android.com/apk/res-auto"

用排除法,剩下的属性就是自定义属性了。包括 项目中自定义 的属性与 依赖库中自定义 的属性,比如ConstraintLayout中自定义的属性:

1
2
3
ini复制代码<attr format="reference|enum" name="layout_constraintBottom_toBottomOf">
<enum name="parent" value="0"/>
</attr>

你也可以在 Android Studio 中找到这个文件:

  • Android Studio(切换到 project 视图):External Libraries/Gradle:com.android.support:constraint:constraint-layout:[版本号]@aar/res/values/values.xml

  1. 样式概述

需要注意的是:虽然样式和主题长得很像,虽然两者截然不同!

2.1 样式的本质

样式(Style)是一组键值对的集合,本质上是一组可复用的 View 属性集合,代表一种类型的 Widget。类似这样:

1
2
3
4
5
ini复制代码<style name="BaseTextViewStyle">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:includeFontPadding">false</item>
</style>

2.2 样式的作用

使用样式可以 复用属性值,避免定义重复的属性值,便于项目维护。

随着业务功能的叠加,项目中肯定会存在一些通用的,可以复用的样式。例如在很多位置会出现的标签样式:

观察可以发现,这些标签虽然颜色不一样,但是也是有共同之处:圆角、边线宽度、字体大小、内边距。如果不使用样式,那么这些相同的属性都需要在每处标签重复声明。

此时,假设 UI 需要修改全部标签的内边距,那么就需要修改每一处便签的属性值,那就很繁琐了。而使用样式的话,就可以将重复的属性 收拢 到一份样式上,当需要修改样式时,只需要修改一个文件,类似这样:

1
2
3
4
5
6
7
8
9
xml复制代码<style name="smallTagStyle" parent="BaseTextViewStyle">
<item name="android:paddingTop">3dp</item>
<item name="android:paddingBottom">3dp</item>
<item name="android:paddingLeft">4dp</item>
<item name="android:paddingRight">4dp</item>
<item name="android:textSize">10sp</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
</style>

2.3 在 xml 中使用样式

使用样式时,需要用到style="",类似这样:

1
2
3
ini复制代码<TextView
android:text="标签"
style="@style/smallTagStyle"/>

关于这两句属性是如何生效的,我后文再说。

2.4 样式的注意事项

  • 样式不在多层级传递

样式只有在使用它的 View 上才起作用,而在它的子 View 上样式是无效的。举个例子,假设 ViewGroup 有三个按钮,若设置 MyStyle 样式到此 ViewGroup 上,此时,仅这个 ViewGroup 有效,而对三个按钮来说是无效的。


  1. 主题概述

3.1 主题的本质

与样式相同的是,**主题(Theme)**也是一组键值对的集合,但是它们的本质截然不同。样式的本质是一组可复用的 View 属性集合,而主题是 一组可引用的命名资源集合。类似这样:

1
2
3
4
5
6
ini复制代码<style name="AppBaseTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="dialogTheme">@style/customDialog</item>
</style>

3.2 主题的作用

主题背景定义了一组可以在多处引用的资源集合,这些资源可以在样式、布局文件、代码等位置使用。使用主题,可以方便全局替换属性的值。

举个例子,首先你可以定义一套深色主题和一套浅色主题:

1
2
3
4
5
6
7
xml复制代码<style name="BlackTheme" parent="AppBaseTheme">
<item name="colorPrimary">@color/black</item>
</style>

<style name="WhiteTheme" parent="AppBaseTheme">
<item name="colorPrimary">@color/white</item>
</style>

然后,你在需要主题化的地方引用它,类似这样:

1
2
ini复制代码<ViewGroup …
android:background="?attr/colorPrimary">

此时,如果应用了 BlackTheme ,那么 ViewGroup 的背景就是黑色;反之,如果引用了 WhiteTheme,那么 ViewGroup 的背景就是白色。

在 xml 中使用主题属性,需要用到?,表示获得此主题中的语义属性代表的值。我把所有格式都总结在这里:

格式 描述
android:background=”?attr/colorAccent“ /
android:background=”?colorAccent“ (”?attr/colorAccent” 的缩写)
android:background=”?android:attr/colorAccent“ (属性的命名空间为 android)
android:background=”?android:colorAccent“ (”?android:attr/colorAccent”)

3.3 在 xml 中使用主题

在 xml 中使用主题,需要用到android:theme,类似这样:

1
2
3
4
5
6
7
8
9
10
11
ini复制代码1. 应用层
<application …
android:theme="@style/BlackTheme ">

2. Activity 层
<activity …
android:theme="@style/BlackTheme "/>

3. View 层
<ConstraintLayout …
android:theme="@style/BlackTheme ">

需要注意的是,android:theme本质上也是用到 ContextThemeWrapper 来使用主题的,这在我之前写过的两篇文章里说过:《Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?》、《Android | 带你探究 LayoutInflater 布局解析原理》。这里我简单复述一下:

LayoutInflater.java

1
2
3
4
5
6
7
8
9
10
ini复制代码private static final int[] ATTRS_THEME = new int[] {
com.android.internal.R.attr.theme
};

final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
构造 ContextThemeWrapper
context = new ContextThemeWrapper(context, themeResId);
}
  • 1、LayoutInflater 在进行布局解析时,需要根据 xml 实例化 View;
  • 2、在解析流程中,会判断 View 是否使用了android:theme;
  • 3、如果使用,则使用 ContextThemeWrapper 包装 Context,并将包装类用于子 View 的实例化过程。

3.4 在代码中使用主题

在代码中使用主题,需要用到ContextThemeWrapper & Theme,它们都提供了设置主题资源的方法:

ContextThemeWrapper.java

1
2
3
4
5
6
7
8
scss复制代码@Override
public void setTheme(int resid) {
if (mThemeResource != resid) {
mThemeResource = resid;
最终调用的是 Theme#applyStyle(...)
initializeTheme();
}
}

Theme.java

1
2
3
arduino复制代码public void applyStyle(int resId, boolean force) {
mThemeImpl.applyStyle(resId, force);
}

当构造新的 ContextThemeWrapper 之后,它会分配新的主题 (Theme) 和资源 (Resources) 实例。那么,最终主题是在哪里生效的呢,我在 第 4 节 说。

3.5 主题的注意事项

  • 主题会在多层级传递

与样式不同的是,主题对于更低层级也是有效的。举个例子,假设 Activity 设置 BlackTheme,那么对于 Activity 上的所有 View 是有效的。此时,如果其中 View 单独指定了 android:theme,那么此 View 将单独使用新的主题。

  • 勿使用 Application Context 加载资源

Application 是 ContextWrapper 的子类,因此Application Context 不保留任何主题相关信息,在 manifest 中设置的主题仅用作未明确设置主题背景的 Activity 的默认选择。切勿使用 Application Context 加载可使用的资源。


  1. 问题回归

现在,我们回过头来讨论 从 android:text 到 TextView 的过程。其实,这说的是如何将android:text属性值解析到 TextView 上。这个过程就是 LayoutInflater 布局解析的过程,我之前专门写过一篇文章探讨布局解析的核心过程:《Android | 带你探究 LayoutInflater 布局解析原理》,核心过程如下图:

4.1 AttributeSet

在前面的文章里,我们已经知道 LayoutInflater 通过反射的方式实例化 View。其中的参数args分别是 Context & AttributeSet:

  • Context:上下文,有可能是包装类 ContextThemeWrapper
  • AttributeSet:属性列表,xml 中 View声明的属性都会解析到这个对象上。

LayoutInflater.java

1
ini复制代码final View view = constructor.newInstance(args);

举个例子,假设有布局文件,我们尝试输出 LayoutInflater 实例化 View 时传入的 AttributeSet:

1
2
3
4
5
ini复制代码<...MyTextView
android:text="标签"
android:theme="@style/BlackTheme"
android:textColor="?colorPrimary"
style="@style/smallTagStyle"/>

MyTextView.java

1
2
3
4
5
6
7
perl复制代码public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
总共有 4 个属性
for (int index = 0; index < attrs.getAttributeCount(); index++) {
System.out.println(attrs.getAttributeName(index) + " = " + attrs.getAttributeValue(index));
}
}

AttributeSet.java

1
2
3
4
5
arduino复制代码返回属性名称字符串(不包括命名空间)
public String getAttributeValue(int index);

返回属性值字符串
public String getAttributeValue(int index);

输出如下:

1
2
3
4
ini复制代码theme = @2131558563
textColor = ?2130837590
text = 标签
style = @2131558752

可以看到,AttributeSet 里只包含了在 xml 中直接声明的属性,对于引用类型的属性,AttributeSet 只是记录了资源 ID,并不会把它拆解开来。

4.2 TypedArray

想要取到真实的属性值,需要用到 TypeArray,另外还需要一个 int 数组(其中,int 值是属性 ID)。类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码private static final int[] mAttr = {android.R.attr.textColor, android.R.attr.layout_width};

private static final int ATTR_ANDROID_TEXTCOLOR = 0;
private static final int ATTR_ANDROID_LAYOUT_WIDTH = 1;

1. 从 AttributeSet 中加载属性
TypedArray a = context.obtainStyledAttributes(attrs, mAttr);
for (int index = 0; index < a.getIndexCount(); index++) {
2. 解析每个属性
switch (index) {
case ATTR_ANDROID_TEXTCOLOR:
System.out.println("attributes : " + a.getColor(index, Color.RED));
break;
case ATTR_ANDROID_LAYOUT_WIDTH:
System.out.println("attributes : " + a.getInt(index, 0));
break;
}
}

在这里,mAttr 数组是两个 int 值,分别是android.R.attr.textColor和android.R.attr.layout_width,表示我们感兴趣的属性。当我们将 mAttr 用于Context#obtainStyledAttributes(),则只会解析出我们感兴趣的属性来。

输出:

1
2
ini复制代码-16777216 ,即:Color.BLACK => 这个值来自于 ?attr/colorPrimary 引用的主题属性
-2 ,即:WRAP_CONTENT => 这个值来自于 @style/smallTagStyle 中引用的样式属性

需要注意的是,大多数情况下并不需要在代码中硬编码,而是使用<declare-styleable>标签。编译器会自动在R.java中为我们声明相同的数组,类似这样:

1
2
3
4
ini复制代码<declare-styleable name="MyTextView">
<attr name="android:textColor" />
<attr name="android:layout_width" />
</declare-styleable>

R.java

1
2
3
4
5
arduino复制代码public static final int[] MyTextView={ 相当于 mAttr
0x01010098, 0x010100f4
};
public static final int MyTextView_android_textColor=0; 相当于 ATTR_ANDROID_TEXTCOLOR
public static final int MyTextView_android_layout_width=1; 相当于 ATTR_ANDROID_LAYOUT_WIDTH

提示: 使用R.styleable.设计的优点是:避免解析不需要的属性。

4.3 Context#obtainStyledAttributes() 取值顺序

现在,我们来讨论obtainStyledAttributes()解析属性值的优先级顺序,总共分为以下几个顺序。当在越优先的级别找到属性时,优先返回该处的属性值:View > Style > Default Style > Theme。

  • View

指 xml 直接指定的属性,类似这样:

1
2
3
ini复制代码<TextView
...
android:textColor="@color/black"/>
  • Style

指 xml 在样式中指定的属性,类似这样:

1
2
3
4
5
6
ini复制代码<TextView
...
android:textColor="@style/colorTag"/>

<style name="colorTag">
<item name="android:textColor">@color/black</item>
  • Default Style

指在 View 构造函数中指定的样式,它是构造方法的第 3 个参数,类似于 TextView 这样:

1
2
3
4
5
6
7
8
less复制代码public AppCompatTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}

public AppCompatTextView(Context context, AttributeSet attrs, @AttrRes int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
...
}

其中,android.R.attr.textViewStyle表示引用主题中的textViewStyle属性,这个值在主题资源中指定的是一个样式资源:

1
ini复制代码<item name="android:textViewStyle">@style/Widget.AppCompat.TextView</item>

提示: 从@AttrRes可以看出,defStyleAttr 一定要引用主题属性。

  • Default Style Resource

指在 View 构造函数中指定的样式资源,它是构造方法的第 3 个参数:

1
2
less复制代码public View(Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
}

提示: 从@StyleRes 可以看出,defStyleRes 一定要引用样式资源。

  • Theme

如果以上层级全部无法匹配到属性,那么就会使用主题中的主题属性,类似这样:

1
2
3
4
xml复制代码<style name="AppTheme" parent="...">
...
<item name="android:textColor">@color/black</item>
</style>

  1. 属性值类型

前文提到,定义属性需要指定:属性名 与 属性值类型,属性值类型可以分为资源类与特殊类

5.1 资源类

属性值类型 描述 TypedArray
fraction 百分数 getFraction(…)
float 浮点数 getFloat(…)
boolean 布尔值 getBoolean(…)
color 颜色值 getColor(…)
string 字符串 getString(…)
dimension 尺寸值 getDimensionPixelOffset(…) getDimensionPixelSize(…)getDimension(…)
integer 整数值 getInt(…)getInteger(…)

5.2 特殊类

属性值类型 描述 TypedArray
flag 标志位 getInt(…)
enum 枚举值 getInt(…)等
reference 资源引用 getDrawable(…)等

fraction 比较难理解,这里举例解释下:

  • 1、属性定义
1
2
3
4
5
6
ini复制代码<declare-styleable name="RotateDrawable">
// ...
<attr name="pivotX" format="float|fraction" />
<attr name="pivotY" format="float|fraction" />
<attr name="drawable" />
</declare-styleable>
  • 设置属性值
1
2
3
4
5
6
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:pivotX="50%"
android:pivotY="50%"
android:drawable="@drawable/fifth">
</animated-rotate>
  • 应用(RotateDrawable)
1
2
3
4
5
6
7
8
ini复制代码if (a.hasValue(R.styleable.RotateDrawable_pivotX)) {
// 取出对应的TypedValue
final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX);
// 判断属性值是float还是fraction
state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
// 取出最终的值
state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
}

可以看到,pivotX 支持 float 和 fraction 两种类型,因此需要通过TypedValue#type判断属性值的类型,分别调用TypedValue#getFraction()与TypedValue#getFloat()。

getFraction(float base,float pbase)的两个参数为基数,最终的返回值是 基数*百分数。举个例子,当设置的属性值为 50% 时,返回值为 base*50% ;当设置的属性值为 50%p 时,返回值为 pbase*50%。


  1. 总结

  • 应试建议
    • 应理解样式和主题的区别,两者截然不同:样式是一组可复用的 View 属性集合,而主题是一组命名的资源集合。
    • 应掌握属性来源优先级顺序:View > Style > Default Style > Theme

参考资料

  • 《Android 样式系统 | 主题背景和样式》 —— Android Developers
  • 《What’s your text’s appearance?》 —— Nick Butcher(Google) 著
  • 《Style resource》 — Android Developers
  • 《Styles and Themes》 — Android Developers
  • 《Creating a Custom View Class》 — Android Developers
  • 《Best Practices for Themes and Styles》 — Android Dev Summit ‘18
  • 《Android themes & styles demystified》 — Google I/O 2016
  • 《Android 编程权威指南》[美]Bill Phillips, Chris Stewart, Kristin Marsicano 著

推荐阅读

  • 密码学 | Base64是加密算法吗?
  • 算法面试题 | 回溯算法解题框架
  • 算法面试题 | 链表问题总结
  • Java | 带你理解 ServiceLoader 的原理与设计思想
  • Android | 面试必问的 Handler,你确定不看看?
  • Android | 带你理解 NativeAllocationRegistry 的原理与设计思想
  • 计算机组成原理 | Unicode 和 UTF-8是什么关系?
  • 计算机组成原理 | 为什么浮点数运算不精确?(阿里笔试)
  • 计算机网络 | 图解 DNS & HTTPDNS 原理

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java 八 (一) 使用工具 jstat jmap jha

发表于 2020-10-29

使用 jstat 摸清线上系统的JVM运行状况

功能强大的jstat

它可以轻易的让你看到当前运行中的系统,他的JVM内的Eden、Survivor、老年代的内存使用情况,还有Young GC和Full gC的执行次数以及耗时。

通过这些指标,我们可以轻松的分析出当前系统的运行情况,判断当前系统的内存使用压力以及GC压力,还有就是内存分配是否合理。下面我们就一点点来看看这个jstat工具的使用。

jstat -gc PID

运行这个命令之后会看到如下列,给大家解释一下:

参数 说明
S0C 这是From Survivor区的大小
S1C 这是To Survivor区的大小
S0U 这是From Survivor区当前使用的内存大小
S1U 这是To Survivor区当前使用的内存大小
EC 这是Eden区的大小
EU 这是Eden区当前使用的内存大小
OC 这是老年代的大小
OU 这是老年代当前使用的内存大小
MC 这是方法区(永久代、元数据区)的大小
MU 这是方法区(永久代、元数据区)的当前使用的内存大小
YGC 这是系统运行迄今为止的Young GC次数
YGCT 这是Young GC的耗时
FGC 这是系统运行迄今为止的Full GC次数
FGCT 这是Full GC的耗时
GCT 这是所有GC的总耗时

jstat命令其他参数

除了上面的jstat -gc命令是最常用的以外,他还有一些命令可以看到更多详细的信息,如下所示:

命令 解释
jstat -gccapacity PID 堆内存分析
jstat -gcnew PID 年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄
jstat -gcnewcapacity PID 年轻代内存分析
jstat -gcold PID 老年代GC分析
jstat -gcoldcapacity PID 老年代内存分析
jstat -gcmetacapacity PID 元数据区内存分析

到底该如何使用jstat工具

接着教教大家一些jstat工具使用的小技巧,先明确一下,我们分析线上的JVM进程,最想要知道的信息有哪些?

包括如下:新生代对象增长的速率,Young GC的触发频率,Young GC的耗时,每次Young GC后有多少对象是存活下来的,每次Young GC过后有多少对象进入了老年代,老年代对象增长的速率,Full GC的触发频率,Full GC的耗时。

只要知道了这些信息,其实我们就可以结合之前几周的文章分析过的JVMGC优化的方法,合理分配内存空间,尽可能让对象留在年轻代不进入老年代,避免发生频繁的Full GC。这就是对JVM最好的性能优化了!

新生代对象增长的速率

在线上linux机器上运行如下命令:jstat -gc PID 1000 10

这行命令,他的意思就是每隔1秒钟更新出来最新的一行jstat统计信息,一共执行10次jstat统计

通过这个命令,你可以非常灵活的对线上机器通过固定频率输出统计信息,观察每隔一段时间的jvm中的Eden区对象占用变化。

比如给大家举个例子,执行这个命令之后,第一秒先显示出来Eden区使用了200MB内存,第二秒显示出来的那行统计信息里,发信Eden区使用了205MB内存,第三秒显示出来的那行统计信息里,发现Eden区使用了209MB内存,以此类推。

此时你可以轻易的推断出来,这个系统大概每秒钟会新增5MB左右的对象。

而且这里大家可以根据自己系统的情况灵活多变的使用,比如你们系统负载很低,不一定每秒都有请求,那么可以把上面的1秒钟调整为1分钟,甚至10分钟,去看你们系统每隔1分钟或者10分钟大概增长多少对象。

还有就是一般系统都有高峰和日常两种状态,比如系统高峰期用的人很多,此时你就应该在系统高峰期去用上述命令看看高峰期的对象增长速率。然后你再得在非高峰的日常时间段内看看对象的增长速率。

按照上述思路,基本上你可以对线上系统的高峰和日常两个时间段内的对象增长速率有很清晰的了解。

Young GC的触发频率和每次耗时

其实多久触发一次Young GC就很容易推测出来了,因为系统高峰和日常时候的对象增长速率你都知道了,那么非常简单就可以推测出来高峰期多久发生一次Young GC,日常期多久发生一次Young GC。

比如你Eden区有800MB内存,那么发现高峰期每秒新增5MB对象,大概高峰期就是3分钟会触发一次Young GC。日常期每秒新增0.5MB对象,那么日常期大概需要半个小时才会触发一次Young GC。

那么每次Young GC的平均耗时呢?

简单,之前给大家说过,jstat会告诉你迄今为止系统已经发生了多少次Young GC以及这些Young GC的总耗时。

比如系统运行24小时后共发生了260次Young GC,总耗时为20s。那么平均下来每次Young GC大概就耗时几十毫秒的时间。

你大概就知道每次Young GC的时候会导致系统停顿几十毫秒。

每次Young GC后有多少对象是存活和进入老年代

其实每次Young GC过后有多少对象会存活下来,这个没法直接看出来,但是有办法可以大致推测出来。

之前我们已经推算出来高峰期的时候多久发生一次Young GC,比如3分钟会有一次Young GC

那么此时我们可以执行下述jstat命令:jstat -gc PID 180000 10。这就相当于是让他每隔三分钟执行一次统计,连续执行10次。

此时大家可以观察一下,每隔三分钟之后发生了一次Young GC,此时Eden、Survivor、老年代的对象变化。

正常来说,Eden区肯定会在几乎放满之后重新变得里面对象很少,比如800MB的空间就使用了几十MB。Survivor区肯定会放入一些存活对象,老年代可能会增长一些对象占用。所以这里的关键,就是观察老年代的对象增长速率。

从一个正常的角度来看,老年代的对象是不太可能不停的快速增长的,因为普通的系统其实没那么多长期存活的对象。如果你发现比如每次Young GC过后,老年代对象都要增长几十MB,那很有可能就是你一次Young GC过后存活对象太多了。

存活对象太多,可能导致放入Survivor区域之后触发了动态年龄判定规则进入老年代,也可能是Survivor区域放不下了,所以大部分存活对象进入老年代。

最常见的就是这种情况。如果你的老年代每次在Young GC过后就新增几百KB,或者几MB的对象,这个还算情有可缘,但是如果老年代对象快速增长,那一定是不正常的。

所以通过上述观察策略,你就可以知道每次Young GC过后多少对象是存活的,实际上Survivor区域里的和进入老年代的对象,都是存活的。

你也可以知道老年代对象的增长速率,比如每隔3分钟一次Young GC,每次会有50MB对象进入老年代,这就是年代对象的增长速率,每隔3分钟增长50MB。

Full GC的触发时机和耗时

只要知道了老年代对象的增长速率,那么Full GC的触发时机就很清晰了,比如老年代总共有800MB的内存,每隔3分钟新增50MB对象,那么大概每小时就会触发一次Full GC。

然后可以看到jstat打印出来的系统运行起劲为止的Full GC次数以及总耗时,比如一共执行了10次Full GC,共耗时30s,每次Full GC大概就是需要耗费3s左右。

使用jmap和jhat摸清线上系统的对象分布

使用jmap了解系统运行时的内存区域

有的时候可能我们会发现JVM新增对象的速度很快,然后就想要去看看,到底什么对象占据了那么多的内存。

如果发现有的对象在代码中可以优化一下创建的时机,避免那种对象对内存占用过大,那么也许甚至可以去反过来优化一下代码。

当然,其实如果不是出现OOM那种极端情况,也并没有那么大的必要去着急优化代码。

但是这篇文章我们来学习一下如何了解线上系统jvm中的对象分布,也是有好处的,比如之前我们在上周的案例中就发现年轻代里总是有500kb左右的未知对象,大家是不是会很好奇?如果可以看到jvm中这500kb的对象到底是什么就好了,所以学习一下这个技巧是有用的。

先看一个命令:

1
复制代码jmap -heap PID

这个命令可以打印出来一系列的信息,我们就不长篇大论的粘贴出来具体的信息了,因为内容篇幅太大了,其实也没太大意义,因为里面的东西大家自己看字面意思都能看懂的。我们就简单给大家说一下这里会打印出来什么东西。

大致来说,这个信息会打印出来堆内存相关的一些参数设置,然后就是当前堆内存里的一些基本各个区域的情况

比如Eden区总容量、已经使用的容量、剩余的空间容量,两个Survivor区的总容量、已经使用的容量和剩余的空间容量,老年代的总容量、已经使用的容量和剩余的容量。

但是这些信息大家会想了,其实jstat已经有了啊!对的,所以一般不会用jmap去看这些信息,毕竟他信息还没jstat全呢,因为没有gc相关的统计。

使用jmap了解系统运行时的对象分布

其实jmap命令比较有用的一个使用方式,是如下的:

1
复制代码jmap -histo PID

这个命令会打印出来类似下面的信息:

他会按照各种对象占用内存空间的大小降序排列,把占用内存最多的对象放在最上面。

所以如果你只是想要简单的了解一下当前jvm中的对象对内存占用的情况,只要直接用jmap -histo命令即可,非常好用

你可以快速了解到当前内存里到底是哪个对象占用了大量的内存空间。

使用jmap生成堆内存转储快照

但是如果你仅仅只是看一个大概,感觉就只是看看上述那些对象占用内存的情况,感觉还不够,想要来点深入而且仔细点的

那就可以用jmap命令生成一个堆内存快照放到一个文件里去,用如下的命令即可:

1
lua复制代码jmap -dump:live,format=b,file=dump.hprof PID

这个命令会在当前目录下生成一个dump.hrpof文件,这里是二进制的格式,你不能直接打开看的,他把这一时刻JVM堆内存里所有对象的快照放到文件里去了,供你后续去分析。

使用jhat在浏览器中分析堆转出快照

接着就可以使用jhat去分析堆快照了,jhat内置了web服务器,他会支持你通过浏览器来以图形化的方式分析堆转储快照

使用如下命令即可启动jhat服务器

1
lua复制代码jhat dump.hprof

接着你就在浏览器上访问当前这台机器的7000端口号,就可以通过图形化的方式去分析堆内存里的对象分布情况了。

从测试到上线:如何分析JVM运行状况及合理优化

开发好系统之后的预估性优化

就是自行估算系统每秒大概多少请求,每个请求会创建多少对象,占用多少内存,机器应该选用什么样的配置,年轻代应该给多少内存,Young GC触发的频率,对象进入老年代的速率,老年代应该给多少内存,Full GC触发的频率。

这些东西其实是可以根据你自己写的代码,大致合理的预估一下的。在预估完成之后,就可以采用之前多个案例介绍的优化思路,先给自己的系统设置一些初始性的JVM参数

比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值,等

优化思路其实简单来说就一句话:尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

系统压测时的JVM优化

从本地的单元测试,到系统集成测试,再到测试环境的功能测试,预发布环境的压力测试,要保证系统的功能全部正常

而且在一定压力下性能、稳定性和并发能力都正常,最后才会部署到生产环境运行。

这里非常关键的一个环节就是预发布环境的压力测试,通常在这个环节,会使用一些压力测试工具模拟比如1000个用户同时访问系统,造成每秒500个请求的压力,然后看系统能否支撑住每秒500请求的压力。同时看系统各个接口的响应延时是否在比如200ms之内,也就是接口性能不能太慢,或者是在数据库中模拟出来百万级单表数据,然后看系统是否还能稳定运行。

然后根据压测环境中的JVM运行状况,如果发现对象过快进入老年代,可能是因为年轻代太小导致频繁Young GC,然后Young GC的时候很多对象还是存活的,结果Survivor也太小,导致很多对象频繁进入老年代。当然也可能是别的什么原因。

大家记住一点:真正的优化,必须是你根据自己的系统,实际观察之后,然后合理调整内存分布,根本没什么固定的JVM优化模板。

对线上系统进行JVM监控

当你的系统上线之后,你就需要对线上系统的JVM进行监控,这个监控通常来说有两种办法。

第一种方法会“low”一些,其实就是每天在高峰期和低峰期都用jstat、jmap、jhat等工具去看看线上系统的JVM运行是否正常,有没有频繁Full GC的问题。

如果有就优化,没有的话,平时每天都定时去看看,或者每周都去看看即可。

第二种方法在中大型公司里会多一些,大家都知道,很多中大型公司都会部署专门的监控系统,比较常见的有Zabbix、OpenFalcon、Ganglia,等等。

然后你部署的系统都可以把JVM统计项发送到这些监控系统里去。

此时你就可以在这些监控系统可视化的界面里,看到你需要的所有指标,包括你的各个内存区域的对象占用变化曲线,直接可以看到Eden区的对象增速,还会告诉你Young GC发生的频率以及耗时,包括老年代的对象增速以及Full GC的频率和耗时。

而且这些工具还允许你设置监控。也就是说,你可以指定一个监控规则,比如线上系统的JVM,如果10分钟之内发生5次以上Full GC,就需要发送报警给你。比如发送到你的邮箱、短信里,这样你就不用自己每天去看着了。

但是这些监控工具的使用不在我们专栏范畴里,因为这些内容并不一定每个公司都一样,也不一定每个公司都有

大家如果有兴趣,完全可以自行百度学习,比如“OpenFalcon监控JVM”,会看到很多资料。

简单一句话总结:对线上运行的系统,要不然用命令行工具手动监控,发现问题就优化,要不然就是依托公司的监控系统进行自动监控,可视化查看日常系统的运行状态。

转自:www.cnblogs.com/klvchen/art…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…770771772…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%