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

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


  • 首页

  • 归档

  • 搜索

Android - 彻底消灭OOM的实战经验分享(千分之1

发表于 2019-08-25

前言

这是我在掘金的第一篇博客分享,最近在掘金上看了许多大佬的文章,学到了非常多的东西,实在是忍不住想要把我们平时工作中用到的一些优化方案分享出来,其实也是一个大家一起讨论学习的过程,希望大家可以多多交流 ~

自我介绍

第一篇博客,总得介绍下自己~,有校友或者其他间接挨得着边的联系的可以私聊交流,前1/4 -> 1/3人生实在没啥交集的也可以眼熟一下。祖籍赣,天府磨子桥文理学院七年计算机,18年夏天毕业,目前在北京海淀768工作,「脉脉」平台客户端开发一枚。喜欢打游戏唱歌撸猫次好次的,其他的没了

背景

先简单讲讲跟oom纠结的历史吧。

在18年年底,我们app进行了一次非常大的版本更迭,因为时间紧急、业务繁忙、人数也没达到可以凑人数可以让某些人准点下班的那种数量(各个公司的常规原因),业务线在对一些模块进行重构和大量新需求的开发过程中,许许多多的细节没有注意到,直接导致了后面一个月的崩溃率、OOM率猛增, 且居高不下。大概快到了千分之2的这个数量级,这是非常非常恐怖的。因此我们花了一段时间,集中的fix了一把OOM的相关问题,一顿操作,直接让主版本的崩溃率来到了「万分之一」,OOM率来到了十万分之一这个数量级。

干掉OOM,我们干了什么?

不讲废话了,也不讲那些网上都可以查到的一些常规优化方法来填字数了,我会针对如何去fix OOM这个目标,将思考的历程以及解决问题的办法分享出来,希望其中会有某一条经验正好击中你们,能起到一些帮助~~

开干!!下面的内容,我会用一级标题的字体~ 显眼一些哈哈,毕竟前面都是啰嗦的废话

一、排查内存泄漏

首先fix OOM第一件事肯定是来排查内存泄漏。想要排查内存泄漏,那就第一步要对内存泄漏进行监控、上报。

我们采用了LeakCanary,实现了一个自定义的Service继承自DisplayLeakService,重写afterDefaultHandling方法,将内存泄漏上报到Sentry。

样例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
less复制代码public static class LeakReportService extends DisplayLeakService {   
@SuppressWarnings("ThrowableNotThrown")
@Override
protected void afterDefaultHandling(@NonNull HeapDump heapDump, @NonNull AnalysisResult result, @NonNull String leakInfo) {
if (!result.leakFound || result.excludedLeak) {
return;
}
try {
Exception exception = new Exception("Memory Leak from LeakCanary");
exception.setStackTrace(result.leakTraceAsFakeException().getStackTrace());
Sentry.capture(exception);
} catch (Exception e) {
e.printStackTrace();
}
}
}

当内存泄漏上报到sentry上面之后,我们直接观察是哪里泄漏的就好了。通过sentry进行监控之后,项目里面的大部分内存泄漏无处可逃~ ,内存泄漏比较简单,我就不花大量篇幅去赘述了~,我自己看文章的过程中,最讨厌篇幅太长。。。

除了LeakCanary,我们还使用了Android Studio自带的Profiler工具对内存有进行分析,包括内存泄漏的问题和内存峰值过高的问题。

profiler工具的使用方法我就不赘述了吧,讲一下小技巧吧。

在排查bitmap对象,我们可以用Profiler直接看java 堆中的bitmap对象图片的预览~ 这样可以直接定位到是哪里泄漏了以及哪里bitmap加载过大

1
复制代码方法:找到对应的Bitmap对象,然后~ ,点击它,然后就可以preview,如下图:

二、兜底策略

我们可以知道的是,当一个Activity的生命周期要走完了,那就说明我们绝大概率不会再使用这个Activity对象了,因此完全可以对他的可能导致整个Activity泄露的引用进行清空,将其中的一些资源释放干净,比如有EditText的TextWatcher,这是非常容易泄露且在我们项目中大量出现的一个case,然后,于是乎我们加上了更加丧心病狂的兜底策略,

话不多说,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码private void traverse(ViewGroup root) {    
final int childCount = root.getChildCount();
for (int i = 0; i < childCount; ++i) {
final View child = root.getChildAt(i);
if (child instanceof ViewGroup) {
child.setBackground(null);
traverse((ViewGroup) child);
} else {
if (child != null) {
child.setBackground(null);
}
if (child instanceof ImageView) {
((ImageView) child).setImageDrawable(null);
} else if (child instanceof EditText) {
((EditText) child).cleanWatchers();
}
}
}
}

我们在基类BaseActivity的onDestory()方法中进行了一些资源和引用的清除

三、内存峰值太高

在我们把能fix的内存泄漏都盘了一便之后,上线一周并没有发现数据好转,OOM率还是高居不下,于是乎,我们开始怀疑内存峰值太高的问题,在我们的项目中不仅仅只有native的部分模块,还有混合的H5、RN模块,当起一个ReactActivity的实例时,内存峰值总是涨的特别特别厉害,同时项目中有消息流的展现,其中会包含着大量的图片展示,这也是导致内存峰值太高的原因(Bitmap对象太大以及太多)

我们又拿出了老伙伴 - Profiler,这可是分析bitmap对象的利器,可以直接看到大小、图片的预览,以及可以通过 go to instance一层一层的找到到底是谁在引用它。比如下面这个例子,直接看引用就知道是被Fresco所引用了~ 直接就在CountingMemoryCache中。

其实我们主要还是需要去关注Bitmap对象的分配和不合法持有导致的内存峰值问题,如果一个bitmap对象有3M,然后持有一个几十上百个在内存中,这谁吃得消,低端机器老早直接OOM了。

查Bitmap分配查出来的问题

目前我们项目中用的图片加载框架有两个,UIL、Fresco,UIL我吐槽很久了,这么多年没更新,老早就该换了~

  1. UIL加载图片在我们项目中的问题:
  • 没有传入合适的Config,绝大多数地方传的都是ARGB_8888,其实根本没必要,改成565直接少一半内存占用
  • 用UIL进行loadImage时,没有传入targetSize,这就直接导致了UIL内部是以屏幕的尺寸去Decode的Bitmap对象,想象一下,一个特别小的头像View,持有着一个屏幕大小尺寸的Bitmap对象,这谁顶得住。
  • 许多地方不需要存内存缓存,比如闪屏广告图,app启动之后就不会再使用了,可以加载的时候 memoryCache(false)
  • 许多地方不需要磁盘缓存,比如发布动态,从图库中选图,不需要再存一份磁盘缓存了,本身那些图片都是本地图片。直接 diskCache(false)

2.Fresco在RN页面中使用的问题,

通过看代码可以知道,RN页面销毁的时候,连带着Fresco的内存缓存都会被清空,

直接上代码图:

代码看到这里,似乎Fresco不用担心了,既然会清空Fresco的内存缓存,何愁会引起内存峰值过高,如果读者看到这里,也有这个想法,那就大错特错了。话不多说,直接上图。

Fresco相关源码的逻辑这篇文章就不分析了,主要讲思路,具体的源码分析后面我会用单独的篇幅去讲~

为什么我会对Fresco的动图缓存这么敏感,那还是Profiler的功劳,我在用Profiler查看内存中bitmap的分配的时候,发现有上百张的Loading图没有销毁(我们Loading图是动图,大概每帧的Bitmap对象在360K左右), 且打开的页面越多,Loading的bitmap就会越多。(这是因为我们每一个RN页面都会带一个Loading动画)

0.3M * 100 = 30M,不少了。。。,说实话有点恐怖

于是乎,干掉他们,这里用了反射,正常情况下不需要反射。直接拿ImagePipelineFactory中的对象来clear就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码public static void clearAnimationCache() {    
if (frescoAnimationCache == null) {
//采用反射的方法,如果native、rn同时初始化Fresco,会造成Fresco内部存储动图的CountingMemoryCache不是Fresco.getImagePipelineFactory().getBitmapCountingMemoryCache()了
//暂时用反射的方法,拿到存储动图缓存的cache,并清空
try {
Class imagePipelineFactoryClz = Class.forName("com.facebook.imagepipeline.core.ImagePipelineFactory");
Field mAnimatedFactoryField = imagePipelineFactoryClz.getDeclaredField("mAnimatedFactory");
mAnimatedFactoryField.setAccessible(true);
AnimatedFactoryV2Impl animatedFactoryV2 = (AnimatedFactoryV2Impl) mAnimatedFactoryField.get(Fresco.getImagePipelineFactory());
Class animatedFactoryV2ImplClz = Class.forName("com.facebook.fresco.animation.factory.AnimatedFactoryV2Impl");
Field mBackingCacheField = animatedFactoryV2ImplClz.getDeclaredField("mBackingCache");
mBackingCacheField.setAccessible(true);
frescoAnimationCache = (CountingMemoryCache) mBackingCacheField.get(animatedFactoryV2);
} catch (Exception e) {
Log.e("FrescoUtil", e.getMessage(), e);
}
}
if (frescoAnimationCache != null) {
frescoAnimationCache.clear();
}
Fresco.getImagePipelineFactory().getBitmapCountingMemoryCache().clear();
Fresco.getImagePipelineFactory().getEncodedCountingMemoryCache().clear();
}

又一个兜底方案

为了防止峰值过高,我们还起了一个线程,定时的去监控实时的内存使用情况,如果内存紧急了,直接清空UIL/Fresco的内存缓存救急

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
scss复制代码    private static Handler lowMemoryMonitorHandler;
private static final int MEMORY_MONITOR_INTERVAL = 1000 * 60;
/**
* 开启低内存监测,如果低内存了,作出相应的反应
*/
public static void startMonitorLowMemory() {
HandlerThread thread = new HandlerThread("thread_monitor_low_memory");
thread.start();
lowMemoryMonitorHandler = new Handler(thread.getLooper());
lowMemoryMonitorHandler.postDelayed(releaseMemoryCacheRunner, MEMORY_MONITOR_INTERVAL);
}

/**
* 低内存时清空Fresco、UIL的内存缓存
* 如果已用内存达到了总的 80%时,就清空缓存
*/
private static Runnable releaseMemoryCacheRunner = new Runnable() {
@Override
public void run() {
long alreadyUsedSize = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
long maxSize = Runtime.getRuntime().maxMemory();
if (Double.compare(alreadyUsedSize, maxSize * 0.8) == 1) {
BitmapUtil.clearMemoryCaches();
}
lowMemoryMonitorHandler.postDelayed(releaseMemoryCacheRunner, MEMORY_MONITOR_INTERVAL);
}
};

五、特大图排查优化

我想大家都不会想到,在我们app的登录注册页,会有一个图片轮播控件,它轮播着五六张单张6M+的Bitmap。。。当然,特大图不仅限于此,还有其他地方会有相同情况,我们通过Profiler找出那些大的bitmap对象,然后预览之后确定是哪里在用的。

直接优化掉。最不济 8888 -> 565就少一半内存占用

怎么讲呢,,OOM这个东西,还没咋僵持呢,就没了。。

六、总结

深夜一时兴起想分享和记录一些什么,就随便写了这一篇博客,写的不详细,没有排版和良好的语言组织,单纯的就是想分享

总结一下吧,我们为了fix OOM所做的事情:

  1. 检查内存泄漏,包括常见的Context泄漏、单例泄漏、EditText的TextWatcher泄漏等等,找到并fix他们,最简单的例子,能传application的地方就不要硬传个activity过去
  2. 兜底方案:
  • 在Activity onDestory的时候,遍历View树,清空backGround、Drawable、EditText的TextWatcher等
  1. 内存峰值的优化。内存泄漏会导致内存峰值,内存峰值是OOM的大锅,举个例子当可用内存不够分配一个Bitmap对象时,就会OOM,Android上大多数的内存峰值都是图片的加载带来的。现在许多的app中都有信息流的展现,可能会有许多的九宫格展示图片,且Bitmap对象本身就可以非常大。
  • 优化UIL的使用
  • memoryCache选用,不是所有的图片加载都需要UIL去塞一份内存缓存的,比如闪屏图
  • ImageLoader.getInstance().displayImage()的时候,传进去的Option不要无脑ARGB_8888,讲道理来说,无脑RGB_565都是没啥问题的。。
  • 调用displayImage的时候,最好传一个ImageSize作为targetSize,这个size可以是你的ImageView的尺寸,当View尺寸本身不确定的时候,可以传一个大概值,比如我们app中有好些个的头像标准尺寸,为了偷懒,直接传MaxAvatarSize就ok
  • Fresco的优化
  • RN中使用Fresco加载图片,在RN Activity销毁的时候,会将Fresco默认的memory cache清空,但是动图的缓存没有清。手动清一下。我们项目中每个RN页面都会带一个Loading动图,所以吃了大亏。。
  1. 持续的后台监控内存,起一个HandlerThread,一直在后台拿内存使用的状态,达到了危险警戒线就清空一把UIL、Fresco的memory cache,先让世界安静一下
  2. 需要对内存泄漏、OOM、Crash、ANR进行监控

一些其他的细节暂时想不起来了,凌晨四点脑子不清醒了

后续关于这里面涉及到的Fresco的部分源码分析、Profiler的最佳使用姿势(经过这一次的折腾,总结出来一句话,Profiler真香)、以及前段时间在做的App的启动速度优化等等等等等都会单独拎文章去分享,后续也会带来更多,涉及的内容包括但不限于:

  • 主流框架的一些设计思想的分享
  • 工作项目中遇到的麻烦和坑
  • 工作中蹚坑的一些经验
  • 好代码
  • 坏代码
  • 坏的设计
  • 程序员从头发浓密到成为下雨天报警员的心路历程
  • 。。。

我的简书 邹啊涛涛涛的简书

我的CSDN 邹啊涛涛涛的CSDN

我的掘金 邹啊涛涛涛的掘金

你可能感兴趣

Android QUIC 实践 - 基于 OKHttp 扩展出 Cronet 拦截器 - 掘金 (juejin.cn)

Android启动优化实践 - 秒开率从17%提升至75% - 掘金 (juejin.cn)

如何科学的进行Android包体积优化 - 掘金 (juejin.cn)

Android稳定性:Looper兜底框架实现线上容灾(二) - 掘金 (juejin.cn)

基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)

记 AndroidStudio Tracer工具导致的编译失败 - 掘金 (juejin.cn)

Android 启动优化案例-WebView非预期初始化排查 - 掘金 (juejin.cn)

chromium-net - 跟随 Cronet 的脚步探索大致流程(1) - 掘金 (juejin.cn)

Android稳定性:可远程配置化的Looper兜底框架 - 掘金 (juejin.cn)

一类有趣的无限缓存OOM现象 - 掘金 (juejin.cn)

Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载) - 掘金 (juejin.cn)

Android - 彻底消灭OOM的实战经验分享(千分之1.5 -> 万分之0.2) - 掘金 (juejin.cn)

本文转载自: 掘金

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

Socks5代理协议

发表于 2019-08-24

或许你没听说过socks5,但你一定听说过SS,SS内部使用的正是socks5协议。

socks5是一种网络传输协议,主要用于客户端与目标服务器之间通讯的透明传递。

该协议设计之初是为了让有权限的用户可以穿过防火墙的限制,访问外部资源。

  1. RFC地址

  1. socks5协议规范rfc1928
  2. socks5账号密码鉴权规范rfc1929
  1. 协议过程

image.png

  1. 客户端连接上代理服务器之后需要发送请求告知服务器目前的socks协议版本以及支持的认证方式
  2. 代理服务器收到请求后根据其设定的认证方式返回给客户端
  3. 如果代理服务器不需要认证,客户端将直接向代理服务器发起真实请求
  4. 代理服务器收到该请求之后连接客户端请求的目标服务器
  5. 代理服务器开始转发客户端与目标服务器之间的流量
  1. 认证过程

3.1 客户端发出请求

客户端连接服务器之后将直接发出该数据包给代理服务器

VERSION METHODS_COUNT METHODS…
1字节 1字节 1到255字节,长度由METHODS_COUNT值决定
0x05 0x03 0x00 0x01 0x02
  • VERSION SOCKS协议版本,目前固定0x05
  • METHODS_COUNT 客户端支持的认证方法数量
  • METHODS… 客户端支持的认证方法,每个方法占用1个字节

METHOD定义

  • 0x00 不需要认证(常用)
  • 0x01 GSSAPI认证
  • 0x02 账号密码认证(常用)
  • 0x03 - 0x7F IANA分配
  • 0x80 - 0xFE 私有方法保留
  • 0xFF 无支持的认证方法

3.2 服务端返回选择的认证方法

接收完客户端支持的认证方法列表后,代理服务器从中选择一个受支持的方法返回给客户端

3.2.1 无需认证

VERSION METHOD
1字节 1字节
0x05 0x00
  • VERSION SOCKS协议版本,目前固定0x05
  • METHOD 本次连接所用的认证方法,上例中为无需认证

3.2.2 账号密码认证

VERSION METHOD
1字节 1字节
0x05 0x02

3.2.3 客户端发送账号密码

服务端返回的认证方法为0x02(账号密码认证)时,客户端会发送账号密码数据给代理服务器

VERSION USERNAME_LENGTH USERNAME PASSWORD_LENGTH PASSWORD
1字节 1字节 1-255字节 1字节 1-255字节
0x01 0x01 0x0a 0x01 0x0a
  • VERSION 认证子协商版本(与SOCKS协议版本的0x05无关系)
  • USERNAME_LENGTH 用户名长度
  • USERNAME 用户名字节数组,长度为USERNAME_LENGTH
  • PASSWORD_LENGTH 密码长度
  • PASSWORD 密码字节数组,长度为PASSWORD_LENGTH

3.2.4 服务端响应账号密码认证结果

收到客户端发来的账号密码后,代理服务器加以校验,并返回校验结果

VERSION STATUS
1字节 1字节
  • VERSION 认证子协商版本,与客户端VERSION字段一致
  • STATUS 认证结果
    • 0x00 认证成功
    • 大于0x00 认证失败
  1. 命令过程

认证成功后,客户端会发送连接命令给代理服务器,代理服务器会连接目标服务器,并返回连接结果

4.1 客户端请求

VERSION COMMAND RSV ADDRESS_TYPE DST.ADDR DST.PORT
1字节 1字节 1字节 1字节 1-255字节 2字节
  • VERSION SOCKS协议版本,固定0x05
  • COMMAND 命令
    • 0x01 CONNECT 连接上游服务器
    • 0x02 BIND 绑定,客户端会接收来自代理服务器的链接,著名的FTP被动模式
    • 0x03 UDP ASSOCIATE UDP中继
  • RSV 保留字段
  • ADDRESS_TYPE 目标服务器地址类型
    • 0x01 IP V4地址
    • 0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组
    • 0x04 IP V6地址
  • DST.ADDR 目标服务器地址
  • DST.PORT 目标服务器端口

4.2 代理服务器响应

VERSION RESPONSE RSV ADDRESS_TYPE BND.ADDR BND.PORT
1字节 1字节 1字节 1字节 1-255字节 2字节
  • VERSION SOCKS协议版本,固定0x05
  • RESPONSE 响应命令
    • 0x00 代理服务器连接目标服务器成功
    • 0x01 代理服务器故障
    • 0x02 代理服务器规则集不允许连接
    • 0x03 网络无法访问
    • 0x04 目标服务器无法访问(主机名无效)
    • 0x05 连接目标服务器被拒绝
    • 0x06 TTL已过期
    • 0x07 不支持的命令
    • 0x08 不支持的目标服务器地址类型
    • 0x09 - 0xFF 未分配
  • RSV 保留字段
  • BND.ADDR 代理服务器连接目标服务器成功后的代理服务器IP
  • BND.PORT 代理服务器连接目标服务器成功后的代理服务器端口
  1. 通信过程

经过认证与命令过程后,客户端与代理服务器进入正常通信,客户端发送需要请求到目标服务器的数据给代理服务器,代理服务器转发这些数据,并把目标服务器的响应转发给客户端,起到一个“透明代理”的功能。

  1. 实际例子

上文详细讲解了协议规范,下面来一个实例的通信过程范例。

6.2中无需认证和需要账号密码认证是互斥的,同一请求只会采取一种,本文都列在下面。

6.1 客户端发送受支持的认证方法

1
复制代码0x05 0x02 0x00 0x02
  • 0x05 SOCKS5协议版本
  • 0x02 支持的认证方法数量
  • 0x00 免认证
  • 0x02 账号密码认证

6.2 服务端响应选择的认证方法

6.2.1 无需认证

以下是无需认证,客户端收到该响应后直接发送需要发送给目标服务器的数据给到代理服务器,此时进入通信错过程

1
复制代码0x05 0x00
  • 0x05 SOCKS5协议版本
  • 0x00 免认证

6.2.2 需要账号密码认证

1
复制代码0x05 0x02
  • 0x05 SOCKS5协议版本
  • 0x02 账号密码认证

6.2.3 客户端发送账号密码

1
复制代码0x01 0x04 0x61 0x61 0x61 0x61 0x04 0x61 0x61 0x61 0x61
  • 0x01 子协商版本
  • 0x04 用户名长度
  • 0x61 0x61 0x61 0x61 转换为ascii字符之后为”aaaa”
  • 0x04 密码长度
  • 0x61 0x61 0x61 0x61 转换为ascii字符之后”aaaa”

6.2.4 代理服务器响应认证结果

1
复制代码0x01 0x00
  • 0x01 子协商版本
  • 0x00 认证成功(也就是代理服务器允许aaaa账号以aaaa密码登录)

6.3 客户端请求代理服务器连接目标服务器

以127.0.0.1和80端口为例

1
复制代码0x05 0x01 0x01 0x01 0x7f 0x00 0x00 0x01 0x00 0x50
  • 0x05 SOCKS协议版本
  • 0x01 CONNECT命令
  • 0x01 RSV保留字段
  • 0x01 地址类型为IPV4
  • 0x7f 0x00 0x00 0x01 目标服务器IP为127.0.0.1
  • 0x00 0x50 目标服务器端口为80

6.4 代理服务器连接目标主机,并返回结果给客户端

1
复制代码0x05 0x00 0x01 0x01 0x7f 0x00 0x00 0x01 0x00 0xaa 0xaa
  • 0x05 SOCKS5协议版本
  • 0x00 连接成功
  • 0x01 RSV保留字段
  • 0x01 地址类型为IPV4
  • 0x7f 0x00 0x00 0x01 代理服务器连接目标服务器成功后的代理服务器IP, 127.0.0.1
  • 0xaa 0xaa 代理服务器连接目标服务器成功后的代理服务器端口(代理服务器使用该端口与目标服务器通信),本例端口号为43690

6.5 客户端发送请求数据给代理服务器

如果客户端需要请求目标服务器的HTTP服务,就会发送HTTP协议报文给代理服务器,代理服务器将这些报文原样转发给目标服务器,并将目标服务器的响应发送给客户端,代理服务器不会对客户端或者目标服务器的报文做任何解析。

  1. 结尾

SOCKS5协议的讲解到此结束,后续会使用GOLANG实现一个SOCKS5服务器来讲述TCP协议服务器的开发。

关注公众号

本文转载自: 掘金

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

项目-日志分析平台 日志分析平台(练手项目) 数据源 数据采

发表于 2019-08-23

日志分析平台(练手项目)

练习hdfs mr hive hbase

  • 各种公司都需要,例如电商、旅游(携程)、保险种种。
  • 数据收集-数据清洗-数据分析-数据可视化。
  • 数据:用户的行为日志,不是系统产生的日志。

数据量

如何谈数据量

  • 站长工具:PV(页面访问量) UV(日均IP访问)
  • 说条数。
  • 大小慎重说不要瞎说。

技术选型

  • 存储引擎:hbase/hdfs
  • 分析引擎(计算):mr/hive 为了练手用MR
  • 可视化:不做。

模块

用户基本信息分析模块

  • 分析新增用户,活跃用户,总用户,新增会员,活跃会员,会话分析等。
  • 公司开始的钱都花在推广上。
  • 所有指标值都是离线跑批处理。而且没必要做实时,每天早上来看指标就好了。

浏览器来源分析

时间和浏览器两个维度

地域分析模块

调整仓库,根据IP定位

用户访问深度分析模块

某一个会话、某个用户访问的页面个数。业务强相关。

外链数据分析模块

广告投放。拼多多砍一刀

数据源

使用nginx的log module

nginx

upstream 中 下划线 坑?!

log module

  • 内嵌变量
    • $remote_host 远程IP地址
    • $request_uri 完整的原始请求行(带参数)
  • log module
    • $mesc 产生时间。单位有意思。

location

location文档
location有精准匹配>正则匹配>前缀匹配

js发送日志

  • 用图片发数据。请求一个图片资源,里面有参数给nginx抓到。
1
2
3
4
5
6
7
8
9
10
11
12
复制代码sendDataToServer : function(data) {

alert(data);

// 发送数据data到服务器,其中data是一个字符串
var that = this;
var i2 = new Image(1, 1);// <img src="url"></img>
i2.onerror = function() {
// 这里可以进行重试操作
};
i2.src = this.clientConfig.serverUrl + "?" + data;
},

java代码发送(订单的成功或失败)

发送日志到nginx,如果出现网络延迟等问题,不能让后面的业务受到影响

  • 开启一个阻塞队列,开一个线程从里面一直取然后发送。
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
复制代码// 只负责扔到队列中。
public static void addSendUrl(String url) throws InterruptedException {
getSendDataMonitor().queue.put(url);
}

// 第一次发送 开启一个线程 监听队列。
public static SendDataMonitor getSendDataMonitor() {
if (monitor == null) {
synchronized (SendDataMonitor.class) {
if (monitor == null) {
monitor = new SendDataMonitor();

Thread thread = new Thread(new Runnable() {

@Override
public void run() {
// 线程中调用具体的处理方法
SendDataMonitor.monitor.run();
}
});
// 测试的时候,不设置为守护模式
// thread.setDaemon(true);
thread.start();
}
}
}
return monitor;
}

数据采集

将nginx的日志通过flumesink到hdfs

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
复制代码a1.sources = r1
a1.sinks = k1
a1.channels = c1

a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100


a1.sources.r1.type = exec
a1.sources.r1.command = tail -F /opt/data/access.log
a1.sources.r1.channels = c1


a1.sinks.k1.channel = c1
a1.sinks.k1.type = hdfs
# hdfs 中的目录
a1.sinks.k1.hdfs.path = /project/events/%Y-%m-%d/
a1.sinks.k1.hdfs.filePrefix = events-
a1.sinks.k1.hdfs.useLocalTimeStamp = true
# 10k 滚动一个文件
a1.sinks.k1.hdfs.rollSize = 10240
a1.sinks.k1.hdfs.rollInterval = 10
a1.sinks.k1.hdfs.rollCount = 0
# 默认是SequenceFile
a1.sinks.k1.hdfs.fileType = DataStream

数据清洗

上MR代码。将hdfs->hbase

  • 具体需不需要reducer是看需不需要。差别还是很大的。从map->reduce中间需要落一次盘。差别很大。

本文转载自: 掘金

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

Java内存缓存-通过Map定制简单缓存

发表于 2019-08-23

缓存

在程序中,缓存是一个高速数据存储层,其中存储了数据子集,且通常是短暂性存储,这样日后再次请求此数据时,速度要比访问数据的主存储位置快。通过缓存,可以高效地重用之前检索或计算的数据。

为什么要用缓存

场景

在Java应用中,对于访问频率高,更新少的数据,通常的方案是将这类数据加入缓存中,相对从数据库中读取,读缓存效率会有很大提升。

在集群环境下,常用的分布式缓存有Redis、Memcached等。但在某些业务场景上,可能不需要去搭建一套复杂的分布式缓存系统,在单机环境下,通常是会希望使用内部的缓存(LocalCache)。

方案

  • 基于JSR107规范自研
  • 基于ConcurrentHashMap实现数据缓存

JSR107规范目标

  • 为应用程序提供缓存Java对象的功能。
  • 定义了一套通用的缓存概念和工具。
  • 最小化开发人员使用缓存的学习成本。
  • 最大化应用程序在使用不同缓存实现之间的可移植性。
  • 支持进程内和分布式的缓存实现。

JSR107规范核心概念

  • Java Caching定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, Entry 和 Expiry。
  • CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
  • CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于- CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
  • Cache是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
  • Entry是一个存储在Cache中的key-value对。
  • 每一个存储在Cache中的条目有一个定义的有效期,即Expiry Duration。
    一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

小例子

使用Map来实现一个简单的缓存功能

MapCacheDemo.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
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
复制代码package me.xueyao.cache.java;

import java.lang.ref.SoftReference;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;


/**
* @author simon
* 用map实现一个简单的缓存功能
*/
public class MapCacheDemo {

/**
* 使用 ConcurrentHashMap,线程安全的要求。
* 我使用SoftReference <Object> 作为映射值,因为软引用可以保证在抛出OutOfMemory之前,如果缺少内存,将删除引用的对象。
* 在构造函数中,我创建了一个守护程序线程,每5秒扫描一次并清理过期的对象。
*/
private static final int CLEAN_UP_PERIOD_IN_SEC = 5;

private final ConcurrentHashMap<String, SoftReference<CacheObject>> cache = new ConcurrentHashMap<>();

public MapCacheDemo() {
Thread cleanerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(CLEAN_UP_PERIOD_IN_SEC * 1000);
cache.entrySet().removeIf(entry ->
Optional.ofNullable(entry.getValue())
.map(SoftReference::get)
.map(CacheObject::isExpired)
.orElse(false));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
cleanerThread.setDaemon(true);
cleanerThread.start();
}

public void add(String key, Object value, long periodInMillis) {
if (key == null) {
return;
}
if (value == null) {
cache.remove(key);
} else {
long expiryTime = System.currentTimeMillis() + periodInMillis;
cache.put(key, new SoftReference<>(new CacheObject(value, expiryTime)));
}
}

public void remove(String key) {
cache.remove(key);
}

public Object get(String key) {
return Optional.ofNullable(cache.get(key)).map(SoftReference::get).filter(cacheObject -> !cacheObject.isExpired()).map(CacheObject::getValue).orElse(null);
}

public void clear() {
cache.clear();
}

public long size() {
return cache.entrySet().stream().filter(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(cacheObject -> !cacheObject.isExpired()).orElse(false)).count();
}

/**
* 缓存对象value
*/
private static class CacheObject {
private Object value;
private long expiryTime;

private CacheObject(Object value, long expiryTime) {
this.value = value;
this.expiryTime = expiryTime;
}

boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}

public Object getValue() {
return value;
}

public void setValue(Object value) {
this.value = value;
}
}
}

代码测试类MapCacheDemoTests.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码package me.xueyao.cache.java;

public class MapCacheDemoTests {
public static void main(String[] args) throws InterruptedException {
MapCacheDemo mapCacheDemo = new MapCacheDemo();
mapCacheDemo.add("uid_10001", "{1}", 5 * 1000);
mapCacheDemo.add("uid_10002", "{2}", 5 * 1000);
mapCacheDemo.add("uid_10003", "{3}", 5 * 1000);
System.out.println("从缓存中取出值:" + mapCacheDemo.get("uid_10001"));
Thread.sleep(5000L);
System.out.println("5秒钟过后");
System.out.println("从缓存中取出值:" + mapCacheDemo.get("uid_10001"));
// 5秒后数据自动清除了~
}
}

本文转载自: 掘金

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

SSM框架是什么?或许这篇这里可以帮助你!

发表于 2019-08-22

SSM是什么?或许这篇这里可以帮助你!

SSM框架简介

  • 对于许多学习JAVA或者是希望成为全栈程序员来说SSM框架是他们最开始遇到的框架。对于SSM其本质就是Spring + Spring MVC + MyBatis的。对于SSM地位,也算是在继SSH之后,目前市场比较主流的Java EE企业级框架,适用于搭建各种大型的企业级应用系统。 因此对于SSM的学习是程序员需要掌握的东西,而今天Damon就跟大家说说,SSM到底是一个什么东西。

解析Spring

  • 对于SPring来说,他一个开源框架,Spring是于2003年兴起的一个轻量级的Java开发框架,其存在的必然,就是为了解决企业应用开发的复杂性而创建的。
  • 而且对于Spring使用基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅限于服务器端的开发。
  • 从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。 简单来说,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
那么控制反转是什么东西呢?
  • IOC:控制反转也叫依赖注入。利用了工厂模式将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关的属性,让spring容器来生成类的实例对象以及管理对象。
  • 在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类(假设这个类名是A),分配的方法就是调用A的setter方法来注入,而不需要你在A里面new这些bean了。
面向切面(AOP)又是什么呢?
  • 首先,需要说明的一点,AOP只是Spring的特性,它就像OOP一样是一种编程思想,并不是某一种技术,AOP可以说是对OOP的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。
  • 当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。
  • 在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。
实现AOP的技术,主要分为两大类:
  • 一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
  • 二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。
[简单点解释],比方说你想在你的biz层所有类中都加上一个打印‘你好,AOP’的功能这你经可以用aop思想来做,你先写个类写个方法,方法经实现打印‘你好,AOP’让后你Ioc这个类 ref=“biz.*”让每个类都注入。

Spring MVC是什么东西

  • 其实Spring MVC属于Spring Framework的后续产品,已经融合在Spring Web Flow里面,它原生支持的Spring特性,让开发变得非常简单规范。Spring MVC 分离了控制器、模型对象、分派器以及处理程序对象的角色,这种分离让它们更容易进行定制。

最后就是MyBati

  • 其实MyBatis本是apache的一个开源项目iBatis,后来由于种种原因迁移到了google code,并且改名为MyBatis 。MyBatis是一个基于Java的持久层框架。
  • iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAO)MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。
  • MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。可以这么理解,MyBatis是一个用来帮你管理数据增删改查的框架。

总结:

作为一个新生程序猿,Damon希望能够与大家一同进步。文章或者描述有所不足的地方,希望大家多多提出来,一同进步。

Damon会继续发掘一些有用的咨询,知识以及新工具,与大家一同分享,谢谢!

过去文章都上传到github,有兴趣的小伙伴可以Star下:github.com/xxxyyh/Fron…

本文转载自: 掘金

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

Spring Boot 初探 - 接口和数据库操作

发表于 2019-08-22

之前一直使用 Python ,学习 Java 基本语法后也一直没能实践,正好部门的测试平台是使用的 Spring Boot 框架开发,借此机会学习一下,未来也能体会一下与 Python 的差异。

带着目标

首先明确我们在本次体验 Spring Boot 中想要实现的功能:

  • 一个 Web 接口
  • 数据写入数据库中

之所以是这两者,是因为我们在 Web 开发中最遇到的操作就是它们。

而能够实现写入数据库后,其他查、改、删的操作都是类似的。

基础环境

工具 版本
IDEA 2019.2
JDK 11
MySQL 5.7

此处只是个人环境,非推荐环境,环境不一致没太大问题

Spring Boot 插件安装

专业版自带插件,无需安装。

社区版需要在 IDEA 插件中心中搜索 “Spring Assistant” 安装。

项目配置

新建项目

  1. 选择项目类型 “Spring Initializr”
  2. 选择对应 SDK,”Initializr Service URL” 选择默认即可,下一步
  3. 输入项目名称等信息(我保持默认,因此项目为 com.example.demo),我选择 Maven 作为构建工具
  4. 选择需要加入的依赖,推荐使用:
    • Developer Tools:
      • Spring Boot DevTools (修改代码后能自动重启服务)
      • Lombok (Java优秀的注解库,可以减少Setter/Getter等代码的编写)
    • Web:
      • Spring Web Starter (包含Spring项目所需要的组件与容器)
    • SQL:
      • Spring Data JPA (遵循JPA规范,使用Hibernate持久化数据的库)
      • MySQL Driver (数据库驱动)

以上即创建成功一个新项目。

pom.xml

因为使用的 Spring Initializr 创建项目,pom.xml 中的依赖坐标都已经配置好,无需修改。

添加数据库配置

修改 src/main/resouces/application.properties 文件,添加下面内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码# 数据库配置
# 数据库连接地址,jpa库需要提前创建好
spring.datasource.url=jdbc:mysql://localhost:3306/jpa?useSSL=false
# 数据库账号
spring.datasource.username=root
# 数据库密码
spring.datasource.password=123456
# 数据库驱动
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# JPA 配置
# 自动处理对象关系映射,允许自动建表等操作
spring.jpa.hibernate.ddl-auto=update
# 是否在控制台显示数据库语句
spring.jpa.show-sql=true

关于 spring.jpa.hibernate.ddl-auto 的配置,有如下几个选项:

  • validate: 加载 Hibernate 时,验证创建数据库表结构
  • create: 每次加载 Hibernate ,重新创建数据库表结构
  • create-drop: 加载 Hibernate 时建表,退出时删除表结构
  • update: 加载 Hibernate 自动更新数据库表结构

代码实现

数据库操作

实体类

实体类是对一个实体对象的代码描述。

在实体类中建立与数据库表和字段的关系映射(ORM),使得通过操作实体类就完成对数据库的增删改查操作。

以创建一个 “Book” 实体类为例,假设我们期望操作的表名为 “t_book”:

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
复制代码// 标记此类为实体类
@Entity
// 设置操作的数据库表
@Table(name="t_book")
public class Book{
// 设置主键
@Id
// 字段值的生成策略,暂不展开说明,可以私下查一下
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
// 配置字段属性,可以不需要
@Column(length = 32)
private String name;

// 添加字段的 getter/setter 方法
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

代码有些长,可以使用 Lombok 进行简化。

*使用 Lombok 简化代码

Lombok 是一个 Java 实用工具,可以通过注解来帮助开发人员消除冗长的 Java 代码,应用在我们的实体类上的效果如下:

1
2
3
4
5
6
7
8
9
复制代码@Entity
@Table(name="t_book")
@Data
public class Book{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
}

可以看到,在使用 lombok 的 Data 注解后,我们的代码简化了很多,不需要再编写字段的 Getter/Setter 和 toString 方法,并且不需要为字段注明数据库Column,只需要保留主键的 @Id 注解即可。

DAO 类

实体类 Book 只是建立了对象关系映射,还需要创建一个 DAO 类简化持久化过程(save/delete等)。

1
2
3
复制代码import org.springframework.data.jpa.repository.JpaRepository;
public interface BookDao extends JpaRepository<Book, Integer> {
}

方法在 JpaRepository 中已经实现,不需要额外编写。

测试用例

创建一个测试用例,用来测试数据的写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class BookTests {
@Autowired
public BookDao bookDao;

@Test
public void testBook() {
Book book = new Book();
book.setName("book1");
bookDao.save(book);
}
}

测试用例通过,查询数据库的 t_book 表可以看到已经插入一条数据。

Web 接口

还是从最简单的开始实现,先写一个接收 GET 请求的接口,返回 “Hello, Spring Boog!” 响应内容:

1
2
3
4
5
6
7
8
复制代码@RestController
@RequestMapping("/")
public class HelloController {
@RequestMapping("/hello")
public String index(){
return "Hello, Spring Boot!";
}
}

右键运行项目的 DemoApplication ,启动后,访问 0.0.0.0:8080/hello 看到浏览器输出 “Hello, Spring Boot!” ,一个最简单的接口就创建成功了。

总结

之前对 Java 的印象是繁琐,声明多、配置多。但是在体验了 Spring Boot 后,特别是使用了一些初始化插件进行创建项目后可以发现,其实最基础的代码框架 Spring Boot 已经为我们生成好,不过是需要理解一些框架或者库的用法,大体 Web 开发的思想还是一致。接下来在业务代码中继续学习了!

本文转载自: 掘金

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

我的Github开源项目,从0到20000 Star!

发表于 2019-08-21

摘要

最近,我在Github上面开源的项目mall已经突破了20000 Star,这个项目是2018年3月份开始开发的,耗时9个月,发布了第一个版本,一直维护至今。回想起来,还是有诸多感慨的,下面我就谈谈我的项目发展的整个历程。

项目发展历程

为什么要写这个项目

2018年3月的时候,我在Github上面闲逛,想要找一个业务和技术相结合的项目,但是发现很多项目都是以技术为主,业务都比较简单。于是我产生了自己写一个业务与技术相结合的项目的想法,业务选择了电商,因为这是一个大家比较容易理解的业务场景,同时有很多成熟的系统可以借鉴。技术选择了SpringBoot全家桶,因为当时SpringBoot比较火,同时自己也想学习并实践下。

明确项目需求

划分项目模块

当时有了解到一个最小精益产品的概念,就是把一个复杂的产品进行简化,简化到一个只保留核心功能的产品。使用这种方法,我对一些成熟的系统功能进行简化,最后确定了管理后台需要开发的功能为商品管理、订单管理、运营管理、促销管理、内容管理、会员管理等功能,移动端需要开发的功能为首页推荐、首页内容、我的、购物车、商品展示、订单等功能。

项目业务架构图:

使用工具整理需求

当时整理需求用到了一个叫MindMaster思维导图工具,首先划分功能模块,之后划分每个模块中的功能,最后对每个功能所要处理的数据字段进行标注。形成了一套明确需求的思维导图,有了它,之后的数据库设计就容易多了!

当时设计的思维导图可以查看这里:mall数据库表结构概览

数据库表设计

有了上面整理需求的思维导图以后,就可以开始设计数据库了。刚开始设计数据库的时候,并不需要把数据库设计的特别完善,因为等到你编码用到时,总是要改的,只需要满足当前功能的数据存储需求即可。说说数据库的外键,数据库表之间建议做逻辑关联,不要设置外键。比如说我的项目里面的商品表,和十几张表都是有关联的,要是我用外键的话,当商品表被锁死了,其他外键关联的表也会被锁死,这样小半个数据库都会被锁死。再说说刚开始设计的时候是否需要添加索引,个人建议暂时不要加,等编码的时候再加。个人推荐数据库表使用PowerDesigner等设计工具来设计,效率高,可以保留表与表之间的依赖关系。

当时设计的数据库可以查看这里:mall数据库表结构概览

后端功能开发

技术选型

技术 说明 官网
Spring Boot 容器+MVC框架 spring.io/projects/sp…
Spring Security 认证和授权框架 spring.io/projects/sp…
MyBatis ORM框架 www.mybatis.org/mybatis-3/z…
MyBatisGenerator 数据层代码生成 www.mybatis.org/generator/i…
PageHelper MyBatis物理分页插件 git.oschina.net/free/Mybati…
Swagger-UI 文档生产工具 github.com/swagger-api…
Hibernator-Validator 验证框架 hibernate.org/validator/
Elasticsearch 搜索引擎 github.com/elastic/ela…
RabbitMq 消息队列 www.rabbitmq.com/
Redis 分布式缓存 redis.io/
MongoDb NoSql数据库 www.mongodb.com/
Docker 应用容器引擎 www.docker.com/
Druid 数据库连接池 github.com/alibaba/dru…
OSS 对象存储 github.com/aliyun/aliy…
JWT JWT登录支持 github.com/jwtk/jjwt
LogStash 日志收集 github.com/logstash/lo…
Lombok 简化对象封装工具 github.com/rzwitserloo…

个人对学习后端技术的心得

当然这一堆技术,我也不是刚开发这个项目的时候就会的,有很多都是开发过程中学会的,当时也看了很多资料,我看过的资料如下:mall学习所需知识点(推荐资料)。如果是系统学习某个技术,我推荐看书,因为书的知识体系是比较全面的,而且里面几乎没有啥错误。但是看书也有个缺点,有些书里面某些技术版本比较老旧,不过不用太过担心,因为一个流行的技术的核心不会因为版本的迭代发生太大的变化,老版本的使用方式到了新版本,绝大多数都依旧适用。

前端功能开发

技术选型

技术 说明 官网
Vue 前端框架 vuejs.org/
Vue-router 路由框架 router.vuejs.org/
Vuex 全局状态管理框架 vuex.vuejs.org/
Element 前端UI框架 element.eleme.io/
Axios 前端HTTP框架 github.com/axios/axios
v-charts 基于Echarts的图表框架 v-charts.js.org/
Js-cookie cookie管理工具 github.com/js-cookie/j…
nprogress 进度条控件 github.com/rstacruz/np…

个人对学习前端技术的心得

说起前端技术,很多后端开发都不怎么擅长。其实前端技术学习并不是那么难,因为现在的前端技术已经发展的很成熟了,比如Vue、React、Angular都是比较成熟的前端技术。当你前端写多了之后,你就会发现写前端也无非是使用使用框架,用js写写前端逻辑,和后端的写逻辑没啥大的区别。下面我来说说我是怎么学习前端的吧,首先我确定了我要学习的是Vue,大概花了一周看了一遍Vue的官方文档,毕竟是国人开源的框架,文档对国人还是很友好的。之后选择了一个脚手架vue-element-admin,然后大概看了一遍里面使用的技术,对这些技术都到官方网站上面看了一遍文档,主要还是看到Element的文档。之后我就拿着这个脚手架开始写我的项目实战了。学习编程,光靠看效果并不好,还是要多实践,学以致用才行!

设计移动端原型

为什么要设计一个移动端原型呢?主要是为了整个项目有个完整的业务流程,同时为下阶段移动端的开发做准备。目前我的原型有完整的移动端流程,可以完美对接后台管理。我觉得开发也需要有一定的产品设计能力,举个例子:要是某天老板有个演示产品要做,叫你去做怎么办?你要是会设计产品原型,就只要用工具做个就好了,就不用写一些临时的代码了,比开发个演示产品要省时省力的多。

移动端演示地址:http://39.98.190.128/mall-app/mainpage.html

项目部署

项目起初只有一套开发环境的windows部署方案,后来加入了linux部署方案,采用的docker容器化部署,之后又加入了更方便的docker-compose部署方案。

具体方案如下:

  • mall在Windows环境下的部署
  • mall在Linux环境下的部署(基于Docker容器)
  • mall在Linux环境下的部署(基于Docker Compose)

一个完整的流程

其实做这个项目,对我来说也是一个完善自身技术栈的过程。通过这个项目,我学习到了产品、开发、运维的一系列技术,虽然不精,但都是实用的。

项目框架升级

在2019年3月的时候,进行了一次框架升级,将SpringBoot从1.5.14版本升级到了2.1.3,同时将Elasticsearch从2.3.6版本升级到了6.2.2。

完善项目文档

我觉得一个好的项目,需要一份完善的项目文档,以便更多的人来学习,于是2019年5月的时候我开始完善整个项目的文档,对整个项目的架构、业务、技术要点进行全方位的解析。

项目文档地址:github.com/macrozheng/…

抽取项目骨架

为了方便只需要使用mall项目的技术栈来开发自己业务系统的朋友,我将mall项目中使用的技术栈抽取出来做成了一个项目骨架,对其中的业务进行精简,只保留了核心的12张表,方便开发使用,可以自由定制业务逻辑。

项目地址:github.com/macrozheng/…

项目Star增长历程

我的项目是从2018年12月,陆续有Star增长的,其实你只要用心去写一个开源项目,总是会有人来关注的,附上一张mall项目的Star增长图。

项目地址

mall项目地址:github.com/macrozheng/…

公众号

mall项目全套学习教程连载中,关注公众号第一时间获取。

公众号图片

本文转载自: 掘金

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

12个超好用的IntelliJ IDEA 插件!你用过几个?

发表于 2019-08-20

一、前言

IntelliJ IDEA如果说IntelliJ IDEA是一款现代化智能开发工具的话,Eclipse则称得上是石器时代的东西了。其实笔者也是一枚从Eclipse转IDEA的探索者,随着近期的不断开发实践和调试,逐步体会到这款智能IDE带来的巨大开发便利,在强大的插件功能支持下,诸如对Git和Maven的支持简直让人停不下来,各种代码提示,包括JS更是手到擒来,最终不得不被这款神奇的IDE所折服。为了让身边更多的小伙伴参与进来,决定写下这篇文章。二、IDEA VS Eclipse 核心术语比较

由下图可见:两者最大的转变就在于工作空间概念的转变,并且在IDEA当中,Project和 Module是作为两个不同的概念,对项目结构是具有重大意义的,这也恰恰是许多IDEA初学者觉得困扰的地方。1 为什么要取消工作空间?答:简单来说,IDEA不需要设置工作空间,因为每一个Project都具备一个工作空间!!对于每一个IDEA的项目工程(Project)而言,它的每一个子模块(Module)都可以使用独立的JDK和MAVEN配置。这对于传统项目迈向新项目的重构添加了极大的便利性,这种多元化的灵活性正是Eclipse所缺失的,因为开始Eclipse在初次使用时已经绑死了工作空间。2 此外,很多新手都会问,为什么IDEA里面的子工程要称为Module ?答:其实就是模块化的概念,作为聚合工程亦或普通的根目录,它称之为Project,而下面的子工程称为模块,每一个子模块之间可以相关联,也可以没有任何关联。三、IDEA的插件介绍

1.插件的安装 打开setting文件选择Plugins选项* Ctrl + Alt + S

  • File -> Setting

分别是安装JetBrains插件,第三方插件,本地已下载的插件包。详情见往期关于settings的文章。2.各种插件#1. activate-power-mode 和 Power mode II根据Atom的插件activate-power-mode的效果移植到IDEA上写代码是整个屏幕都在抖动,activate-power-mode是白的的,Power mode II色彩更酷炫点。#2.Background Image Plusidea背景修改插件,让你的idea与众不同,可以设置自己喜欢的图片作为code背景。安装成功之后重启,菜单栏的VIew标签>点击Set Background Image(没安装插件是没有这个标签的),在弹框中路由选择到本地图片,点击OK即可。#3.Grep console自定义日志颜色,idea控制台可以彩色显示各种级别的log,安装完成后,在console中右键就能打开。)并且可以设置不同的日志级别的显示样式。可以直接根据关键字搜索你想要的,搜索条件是支持正则表达式的。#4.Free Mybatis pluginmybatis 插件,让你的mybatis.xml像java代码一样编辑。我们开发中使用mybatis时时长需要通过mapper接口查找对应的xml中的sql语句,该插件方便了我们的操作。安装完成重启IDEA之后,我们会看到code左侧或多出一列绿色的箭头,点击箭头我们就可以直接定位到xml相应文件的位置。mapper)xml
#5.MyBatis Log PluginMybatis现在是java中操作数据库的首选,在开发的时候,我们都会把Mybatis的脚本直接输出在console中,但是默认的情况下,输出的脚本不是一个可以直接执行的。如果我们想直接执行,还需要在手动转化一下。MyBatis Log Plugin 这款插件是直接将Mybatis执行的sql脚本显示出来,无需处理,可以直接复制出来执行的,如图:执行程序后,我们可以很清晰的看到我们执行了哪些sql脚本,而且脚本可以执行拿出来运行。#6.String Manipulation强大的字符串转换工具。使用快捷键,Alt+m。* 切换样式(camelCase, hyphen-lowercase, HYPHEN-UPPERCASE, snake_case, SCREAMING_SNAKE_CASE, dot.case, words lowercase, Words Capitalized, PascalCase)

  • 转换为SCREAMING_SNAKE_CASE (或转换为camelCase)
  • 转换为 snake_case (或转换为camelCase)
  • 转换为dot.case (或转换为camelCase)
  • 转换为hyphen-case (或转换为camelCase)
  • 转换为hyphen-case (或转换为snake_case)
  • 转换为camelCase (或转换为Words)
  • 转换为camelCase (或转换为lowercase words)
  • 转换为PascalCase (或转换为camelCase)
  • 选定文本大写
  • 样式反转

#7.Alibaba Java Coding Guidelines阿里巴巴代码规范检查插件,当然规范可以参考《阿里巴巴Java开发手册》。)#8.LombokJava语言,每次写实体类的时候都需要写一大堆的setter,getter,如果bean中的属性一旦有修改、删除或增加时,需要重新生成或删除get/set等方法,给代码维护增加负担,这也是Java被诟病的一种原因。Lombok则为我们解决了这些问题,使用了lombok的注解(@Setter,@Getter,@ToString,@@RequiredArgsConstructor,@EqualsAndHashCode或@Data)之后,就不需要编写或生成get/set等方法,很大程度上减少了代码量,而且减少了代码维护的负担。安装完成之后,在应用Lombok的时候注意别忘了需要添加依,maven为例:
#9.Key promoterKey promoter 是IntelliJ IDEA的快捷键提示插件,会统计你鼠标点击某个功能的次数,提示你应该用什么快捷键,帮助记忆快捷键,等熟悉了之后可以关闭掉这个插件。#10.Gsonformat可根据json数据快速生成java实体类。自定义个javaBean(无任何内容,就一个空的类),复制你要解析的Json,然后alt+insert弹出如下界面或者使用快捷键 Alt+S,在里面粘贴刚刚复制的Json,点击OK即可。
#11.RestfultoolkitSpring MVC网页开发的时候,我们都是通过requestmapping的方式来定义页面的URL地址的,为了找到这个地址我们一般都是cmd+shift+F的方式进行查找,大家都知道,我们URL的命名一个是类requestmapping+方法requestmapping,查找的时候还是有那么一点不方便的,restfultookit就能很方便的帮忙进行查找。例如:我要找到/user/add 对应的controller,那么只要Ctrl+斜杠 ,(图片来自于网络))就能直接定位到我们想要的controller。这个也是真心方便,当然restfultookit还为我们提供的其他的功能。根据我们的controller帮我们生成默认的测试数据,还能直接调用测试,这个可以是解决了我们每次postman调试数据时,自己傻傻的组装数据的的操作,这个更加清晰,比在console找数据包要方便多了。(图片来自于网络)#12.JRebelJRebel是一种热部署生产力工具,修改代码后不用重新启动程序,所有的更改便可以生效。它跳过了Java开发中常见的重建、重新启动和重新部署周期。
四、最后


欢迎大家关注我的公众号【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

本文转载自: 掘金

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

Android ViewModel,再学不会你砍我

发表于 2019-08-19

之前工作用了很久MVP架构了,虽然很好的解决了M层与V层的耦合关系,但巨多的接口,难以复用、难以单测的问题一直萦绕心头,久久不能平复~,于是我将目光转向了MVVM。

首先声明,本篇不是ViewModel的使用介绍,更偏向底层原理,如果你还不清楚它的使用,不建议直接阅读~~

MVVM与MVP相比最大的区别就是用ViewModel(后文简称VM)代替了原来的P层,这里的VM就是ViewModel。一句话概括它的特点—对数据状态的持有和维护。换言之,它将原来P层关于数据的逻辑运算与处理统一放到了VM中,而剩余的V层的操作建议使用Databinding,从而形成最为简洁高效的MVVM架构。说到这呢,推荐一篇旧文DataBinding,再学不会你砍我(系兄弟就砍偶系列?)。

回到VM的特点—对数据状态的持有和维护。为什么需要做这些呢?事实上就是为了解决下面两个开发中常见的问题。

  • Activity配置更改重建时(比如屏幕旋转)保留数据
  • UI组件(Activity与Fragment、Fragment与Fragment)间实现数据共享。

对于第一条不用VM的情况下只能通过onSaveInstanceState保存数据,当activity重建后再通过onCreate或onRestoreInstanceState方法的bundle中取出,但如果数据量较大,数据的序列化和反序列化将产生一定的性能开销。

对于第二条如果不用VM,各个UI组件都要持有共享数据的引用,这会带来两个麻烦,第一,如果新增了共享数据,各个UI组件需要再次声明并初始化新增的共享数据;第二,某个UI组件对共享数据修改,无法直接通知其他UI组件,需手动实现观察者模式。而VM结合LiveData就可以很轻松的实现这一点。

LiveData作为数据变化的驱动器,VM借助它可以写出十分简洁的MVVM代码。

接下来我们来看一下VM到底是如何实现上述需求的,而事实上核心可以转化为下面两个问题。

问个正事.png

问题

  1. VM是如何解决Activity与Fragment、Fragment之间数据共享的问题?
  2. VM是如何在Activity发生旋转时保留数据的?不是也走onDestroy了吗?

ViewModel是什么?

回答上面问题之前我们要先了解一下VM到底是个啥。

文内源码来自,Android Architect Component(AAC)ViewModel组件1.1.1版本。

1
2
3
4
复制代码public abstract class ViewModel {
protected void onCleared() {
}
}

就是个抽象类甚至连抽象方法都没有,简单的令人发指。onCleared方法提供了释放VM中资源的一个机会,在Activity/Fragment的onDestroy生命周期中被调用。类库内部提供了一个实现类AndroidViewModel。

1
2
3
4
5
6
7
8
9
10
11
复制代码public class AndroidViewModel extends ViewModel {
private Application mApplication;

public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}

public <T extends Application> T getApplication() {
return (T) mApplication;
}
}

AndroidViewModel内持有Application的引用,所以通常可以做一些全生命周期的工作。为什么不能持有一个Activity呢?这与ViewModel的生命周期有关,我们稍后解释。

创建ViewModel

接下来看看VM的创建,通常我们是这样创建一个VM的。

1
复制代码val viewModel = ViewModelProviders.of(this).get(AndroidViewModel::class.java)

这里的this可以是FragmentActivity或Fragment,他们都是support-v4包中控件,为的是向下兼容。
我们以FragmentActivity为例看看of方法做了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码### 1.1 ViewModelProviders
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
return of(activity, null);
}

public static ViewModelProvider of(@NonNull FragmentActivity activity,
@Nullable Factory factory) {
Application application = checkApplication(activity);
if (factory == null) {
factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
}
//这里用的工厂模式,vm的创建交由工厂完成,默认使用AndroidViewModelFactory
return new ViewModelProvider(ViewModelStores.of(activity), factory);
}

这两个方法的返回值都是ViewModelProvider,ViewModelProviders是操作ViewModelProvider的工具类,内部都是获取ViewModelProvider的静态方法,而真正的VM的业务在ViewModelProvider中。

同理ViewModelStores和ViewModelStore的关系亦是如此。既然要保留VM的数据,必然要有个存储单元。ViewModelStore就是这个存储单元,因为一个Activity中可能有多个VM,所以需要一个Map来维护关系表,key为VM的名字,value为VM对象,简单的令人发指。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public class ViewModelStore {

private final HashMap<String, ViewModel> mMap = new HashMap<>();

final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}

final ViewModel get(String key) {
return mMap.get(key);
}

public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.onCleared();
}
mMap.clear();
}
}

ViewModelStores仅负责提供工具方法创建ViewModelStore。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码### 1.2
public class ViewModelStores {
private ViewModelStores() {
}

public static ViewModelStore of(@NonNull FragmentActivity activity) {
if (activity instanceof ViewModelStoreOwner) {
return ((ViewModelStoreOwner) activity).getViewModelStore();
}
return holderFragmentFor(activity).getViewModelStore();
}
...
}

ViewModelStoreOwner又是什么鬼?它是抽象了一个获取ViewModelStore的接口。常见的实现类有Fragment/FragmentActivity,这也不难理解,因为要想在配置改变时保留数据,首先得要将数据存储起来。

1
2
3
复制代码public interface ViewModelStoreOwner {
ViewModelStore getViewModelStore();
}

感觉这时需要一张类图了,不然你是不是想砍死我?

format.jpg

事实上目前提到的这几个类已经几乎涵盖了viewmodel库中所有的类。

我们回到1.1中创建ViewModelProvider的代码。

1
复制代码return new ViewModelProvider(ViewModelStores.of(activity), factory);

我们将activity中的VM存储单元和VM的创建工厂(AndroidViewModelFactory)传入得到一个ViewModelProvider对象,最终通过其get方法得到VM。

1
2
3
4
5
6
7
复制代码public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

以Default_Key作为前缀加上VM的完整类名为key,获取VM对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);

//先看ViewModelStore中是否存在,如果存了就直接返回
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
}
...
//如果不存在,创建一个新的VM并存入ViewModelStore
viewModel = mFactory.create(modelClass);
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}

默认情况下mFactory为AndroidViewModelFactory,来看下它是如何创建VM的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {
...
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
try {
//通过反射创建AndroidViewModel对象
return modelClass.getConstructor(Application.class).newInstance(mApplication);
} catch (NoSuchMethodException e) {
...
}
}
return super.create(modelClass);
}
}

至此VM的创建我们就讲完了,可以总结一下:

  1. ViewModelStore是存储VM的数据单元,存储结构为Map,Fragment/FragmentActivity持有其引用。
  2. ViewModelProvider通过get方法创建一个VM,创建之前会先检查ViewModelStore中是否存在,若不存在则通过反射创建一个VM。

UI组件间数据共享

讲到这我们回过头来看看开篇提的第一个问题:Activity与Fragment,Fragment之间是如何共享数据的。
数据要实现共享最简单的方式就是大家都读取一份数据源。
我们来看看数据源ViewModelStore具体是在哪里创建的。

上面讲的代码片段1.2可知,有两条路径创建。

1
2
3
4
5
6
复制代码public static ViewModelStore of(@NonNull FragmentActivity activity) {
if (activity instanceof ViewModelStoreOwner) { ①
return ((ViewModelStoreOwner) activity).getViewModelStore();
}
return holderFragmentFor(activity).getViewModelStore();②
}

①如果FragmentActivity是ViewModelStoreOwner类型通过activity的getViewModelStore方法创建。

②否则通过一个HolderFragment来创建。

先来看①

1
2
3
4
5
6
7
8
9
复制代码### FragmentActivity
private ViewModelStore mViewModelStore;
public ViewModelStore getViewModelStore() {
...
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
return mViewModelStore;
}

简单的令人发ck,既然是成员变量那每次调用必定返回同一个VM,只要保证调用ViewModelProviders.of(activity).get(AndroidViewModel::class.java)传递的activity是同一个对象。

再来看②,整体思路是添加一个不可见的HolderFragment到当前Activity中,再将数据源记录在这个fragment中。

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
复制代码### HolderFragment extends Fragment implements ViewModelStoreOwner
private ViewModelStore mViewModelStore = new ViewModelStore();

public static HolderFragment holderFragmentFor(FragmentActivity activity) {
return sHolderFragmentManager.holderFragmentFor(activity);
}

HolderFragment holderFragmentFor(FragmentActivity activity) {
FragmentManager fm = activity.getSupportFragmentManager();
//先查一下当前有没有这个HolderFragment
HolderFragment holder = findHolderFragment(fm);
if (holder != null) {
return holder;
}
...
//没有就创建一个
holder = createHolderFragment(fm);
...
return holder;
}

private static HolderFragment createHolderFragment(FragmentManager fragmentManager) {
//创建一个HolderFragment、打上tag,再添加到当前界面。
HolderFragment holder = new HolderFragment();
fragmentManager.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss();
return holder;
}

之所以不可见,是因为这个HolderFragment并未复写onCreateView。这么做的原因是早期的版本想利用Fragment的setRetainInstance()API接口,来实现当Activity因配置发生改变时保留这个不可见的Fragment,生命周期只走onDetach和onAttach。既然没有被重建,那么它持有的数据自然就会被保留,其他主动退出的情况走到onDestroy会清空数据。

1
2
3
4
5
6
7
8
9
复制代码### HolderFragment
public HolderFragment() {
setRetainInstance(true);
}
//被销毁时清除数据源
public void onDestroy() {
super.onDestroy();
mViewModelStore.clear();
}

那么在实际使用中会走哪条路径呢?我们反观源码FragmentActivity是实现了ViewModelStoreOwner接口的,那此处代码不是肯定走①吗?②是不是走错片场了?此时我感觉我的智商受到了侮辱。

智商不足.jpeg

原来在appcompat-v7版本在27.1.0之前FragmentActivity并没有实现ViewModelStoreOwner接口,也就是统一用HolderFragment实现。google大大可能是觉得这个HolderFragment有点太骚操作了就让后续版本的FragmentActivity直接支持了VM存储。因为添加的HolderFragment也是有维护成本的,且上层可以通过FragmentManager获取到它,然后对它进行其他骚操作,想想都刺激。。。

深井冰.jpg

上面我们分析了创建VM时传入FragmentActivity的情况,那传入Fragment的情况呢?实际上跟FragmentActivity几乎一样,在Fragment中也有一个mViewModelStore成员变量,注意这里哦,非常关键。也就是说如果我们通过下面的代码创建的是两个VM,是不能共享数据的。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码### MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
//传递activity
val vm = ViewModelProviders.of(this).get(AndroidViewModel::class.java)
}

### TestFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//传递fragment
val vm = ViewModelProviders.of(this).get(AndroidViewModel::class.java)
}

正确的写法应该是二者在of方法中传入相同的对象

1
2
3
4
5
6
复制代码### TestFragment.onCreate(savedInstanceState: Bundle?)
//通过getActivity方法取得Fragment所在的Activity
val vm = activity?.run {
//run函数 这里的this指代activity
ViewModelProviders.of(this)[AndroidViewModel::class.java]
}

Fragment之间同理,如果Fragment间同层级,可以统一通过Activity或共同的parentFragment(如果有)获取VM;如果有嵌套关系,可以使用parentFragment对象获取VM。

Activity配置发生变化时数据保持

其实在上面的内容中已经讲了HolderFragment是如何保留数据的,就是利用Fragment的setRetainInstance()API接口完成的。

google为了优化这点在新版本里直接让FragmentActivity支持了数据的保持,官方的配图很好的阐释了VM在Activity因配置变化销毁重建时的生命周期。

viewmodel-lifecycle.png

下面我们来分析一下是如何做到的,先看onDestroy的销毁逻辑。

1
2
3
4
5
6
7
8
9
复制代码### FragmentActivity
protected void onDestroy() {
super.onDestroy();
...
if (mViewModelStore != null && !mRetaining) {
mViewModelStore.clear();
}
...
}

mRetaining为true时将会保留VM数据,那它何时为true呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码### FragmentActivity
public final Object onRetainNonConfigurationInstance() {
if (mStopped) {
//方法内部会将mRetaining设置为true
doReallyStop(true);
}
...
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
//保存VM数据
nci.viewModelStore = mViewModelStore;
nci.fragments = fragments;
return nci;//数据返回会被记录
}

onRetainNonConfigurationInstance方法被retainNonConfigurationInstances方法调用,而它会被ActivityThread中performDestroyActivity方法调用,它执行在onDestroy生命周期之前。

1
2
3
4
5
6
7
8
9
复制代码### ActivityThread
performDestroyActivity(...boolean getNonConfigInstance,...) {
...
if (getNonConfigInstance) {
//如果配置发生改变记录下来
r.lastNonConfigurationInstances
= r.activity.retainNonConfigurationInstances();
}
}

这样VM数据就被封装到了NonConfigurationInstances一个对象中了。

那何时被还原呢?答案是Activity的attach时。

1
2
3
4
5
6
复制代码### Activity
final void attach(Context context, ... NonConfigurationInstances lastNonConfigurationInstances,...) {
...
mLastNonConfigurationInstances = lastNonConfigurationInstances;
...
}

VM数据源mViewModelStore在onCreate时被重新赋值。

1
2
3
4
5
6
7
8
9
10
11
复制代码### FragmentActivity
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//还原数据
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mViewModelStore = nc.viewModelStore;
}
...
}

最后我们整理一下整体流程图。

代码流程.png

从上面的流程可以看出VM在Activity因配置变化导致重建时会被保留,从生命周期的角度来说,ViewModel的生命周期可能会长于Activity的生命周期。

这说明我们在使用ViewModel时一定要注意,不能让其引用Activity或View,否则可能导致内存泄漏。

好了,整个ViewModel的分析就结束了,希望你在了解其工作原理的同时对MVVM模式也有一些新的认识。独立的看ViewModel没有什么实际意义,更像是数据容器,结合LiveData使用更容易理解,后续章节会继续分享LiveData的内容。

本文转载自: 掘金

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

10分钟快速掌握Docker必备基础知识

发表于 2019-08-18

原创作者,公众号【程序员读书】,欢迎关注公众号,转载文章请注明出处哦。

Docker是时下热门的容器技术,相信作为一名开发人员,你一定听说过或者使用过,很多人会把Docker理解为一个轻量级虚拟机,但其实Docker与虚拟机(VM)是两种不同的计算机虚拟化技术,也有很多人会觉得,有了虚拟机,那为什么还要使用Docker呢?

带着心里的一点点疑问,让我们一起来学习Docker吧。

没有虚拟化技术的原始年代

我们仔细想想,在没有计算虚拟化技术的“远古”年代,如果我们要部署一个应用程序(Application),一般的步骤是怎么样的?

第一步肯定是先要准备一台物理服务器,然后在物理服务器上安装一个操作系统(Operating System),有了操作系统之后,便在操作系统上安装运行我们的应用程序,这个过程可以用下面的图来表示:

物理服务器部署应用示意图
那么,这种方式有什么问题呢?其实,在物理机上部署应用有以下几个缺点:

  • 部署非常慢:因为我们得先准备硬件服务器,接着还要安装操作系统,然后再部署应用程序,而且应用程序还有很多的依赖软件,所以这个过程是比较慢的。
  • 成本非常高:主要是物理器成本太高,即使是部署一个简单的应用,也需要一台服务器。
  • 资源浪费:如果应用太简单,也容易浪费硬件资源,比如CPU和内存
  • 迁移和扩展太慢:如果需要迁移应用,或者扩展应用,都要再准备其他的物理服务器,过程很麻烦,也很慢。

那么有什么办法可以解决这些问题呢?答案便是虚拟化技术。

使用虚拟机部署应用程序的年代

什么是虚拟化技术

谈到计算机的虚拟化技术,我们直接想到的便是虚拟机,虚拟机允许我们在一台物理计算机模拟出多台机器,简单地理解,虚拟化技术就是在一台物理计算机上,通过中间虚拟软件层Hypervisor隔离CPU、内存等硬件资源,虚拟出多台虚拟服务器,这样做的话,一台物理服务器便可以安装多个应用程序,达到资源利用的最大化,而且多个应用之间相互隔离,如下图所示:

虚拟机上部署应用示意图

虚拟机的优点

  • 可以把资源分配到不同的虚拟机,达到硬件资源的最大化利用
  • 与直接在物理机上部署应用,虚拟机更容易扩展应用。
  • 云服务:通过虚拟机虚拟出不同的物理资源,可以快速搭建云服务。

虚拟机的不足之处

虚拟机的不足之处在于对物理服务器资源的消耗,当我们在物理服务器创建一台虚拟机时,便需要虚拟出一套硬件并在上面运行完整的操作系统,每台虚拟机都占用许多的服务器资源。

Docker是什么?

相对于虚拟机的笨重,Docker则更显得轻量化,因此不会占用太多的系统资源。

Docker是使用时下很火的Golang语言进行开发的,其技术核心是Linux内核的Cgroup,Namespace和AUFS类的Union FS等技术,这些技术都是Linux内核中早已存在很多年的技术,所以严格来说Docker并不是一个完全创新的技术,Docker通过这些底层的Linux技术,对Linux进程进行封装隔离,而被隔离的进程也被称为容器,完全独立于宿主机的进程。

所以Docker是容器技术的一种实现,也是操作系统层面的一种虚拟化,与虚拟机通过一套硬件再安装操作系统完全不同。

docker容器与系统关系示意图

Docker与虚拟机之间的比较

Docker是在操作系统进程层面的隔离,而虚拟机是在物理资源层面的隔离,两者完全不同,另外,我们也可以通过下面的一个比较,了解两者的根本性差异。

容器与虚拟机的比较【摘自《Docker-从入门到实践》】
从上面的容器与虚拟机的对比中,我们明白了容器技术的优势。

容器解决了开发与生产环境的问题

开发环境与生产环境折射的是开发人员与运维人员之间的矛盾,也许我们常常会听到开发人员对运维人员说的这样一句话:“在我的电脑运行没问题,怎么到了你那里就出问题了,肯定是你的问题”,而运维人员则认为是开发人员的问题。

开发人员需要在本机安装各种各样的测试环境,因此开发的项目需要软件越多,依赖越多,安装的环境也就越复杂。

同样的,运维人员需要为开发人员开发的项目提供生产环境,而运维人员除了应对软件之间的依赖,还需要考虑安装软件与硬件之间的兼容性问题。

就是这样,所以我们经常看到开发与运维相互甩锅,怎么解决这个问题呢?

容器就是一个不错的解决方案,容器能成为开发与运维之间沟通的语言,因为容器就像一个集装箱一样,提供了软件运行的最小化环境,将应用与其需要的环境一起打包成为镜像,便可以在开发与运维之间沟通与传输。

Docker的版本

Docker分为社区版(CE)和企业版(EE)两个版本,社区版本可以免费使用,而企业版则需要付费使用,对于我们个人开发者或小企业来说,一般是使用社区版的。

Docker CE有三个更新频道,分别为stable、test、nightly,stable是稳定版本,test是测试后的预发布版本,而nightly则是开发中准备在下一个版本正式发布的版本,我们可以根据自己的需求下载安装。

如何安装Docker?

好了,通过前面的介绍,我们应该对Docker有了初步的了解,下面开始进入Docker的学习之旅了。

而学习Docker的第一步,从安装Docker运行环境开始,我们以Docker的社区版本(CE)安装为例。

Docker社区版本提供了Mac OS,Microsoft Windows和Linux(Centos,Ubuntu,Fedora,Debian)等操作系统的安装包,同时也支持在云服务器上的安装,比如AWS Cloud。

在Windows系统上安装

Docker Desktop for Windows

Docker为Windows提供了一个桌面应用程序管理的安装包(Docker Desktop for Windows),不过对系统有以下几点要求:

  1. 必须是64位Windows10专业版,企业版,教育版,构建在15063或更高版本,
  2. 在BIOS中启用虚拟化。通常,默认情况下启用虚拟化。
  3. 至少有4GB内存。
  4. CPU支持SLAT。

如果操作系统满足上面的要求,则可以直接下载安装包直接安装,在安装成功后,Docker并不会自动启动,需要我们自己启动,我们可以在开始菜单中找到Docker,如下图,单击启动便可启动。

Docker Toolbox

如果系统达不到上面的要求,比如说你用的是Windows 7操作系统,这时候要想使用Docker,便需要借助Docker Toolbox,Docker Toolbox是Docker提供的在比较旧的Mac OS,Windows操作系统上安装Docker环境的工具集。

Docker Toolbox包括docker-cli(就是我们在终端使用的docker命令行工具),docker-compose(多容器管理工具),docker-mecahine,VirtualBox(虚拟机),Kitematic(docker的GUI管理工具)。

本质上使用Docker Toolbox安装Docker环境,实际上是在VirtualBox中创建一个Linux虚拟机,并在虚拟机上安装Docker。

另外,在安装过程中会开启Windows的Hyper-V模块(Windows操作系统实现虚拟化的一种技术),这里面有个要注意的点是如果开启了Hyper-V,则VirtualBox不再生效了。

在Mac OS上安装

如同Windows操作系统一样,Docker为Mac OS也一样提供一个桌面应用程序(Docker Desktop for Mac),比较简单,从docker官网上下载Dokcer.dmg安装,打开Docker.dmg,如下图所示:

直接拖动Docker图标便完成了安装。

对于比较老的Mac OS操作系统,也可以像Windows一样,使用Docker Toolbox,这点可以参考上面的介绍。

在Mac OS上安装完成之后,在Application中找到Docker图标,双击打开便可以启动Docker了,如下:

在Linux上安装

在Linux操作系统上的安装,主要以Centos7为例,其他Linux系统的发行版本,如Ubuntu,Debian,Fedora等,可以自行查询Docker的官方文档。

删除旧的docker版本

可能有些Linux预先安装Docker,但一般版本比较旧,所以可以先执行以下代码来删除旧版本的Docker。

1
2
3
4
5
6
7
8
9
10
复制代码$ sudo dnf remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine
指定安装版本
1
2
3
复制代码$ sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
使用yum安装docker
1
复制代码$ sudo yum install docker-ce docker-ce-cli containerd.io
启动docker服务器
1
2
复制代码# 启动docker守护进程
$ sudo systemctl start docker

测试安装是否成功

通过上面几种方式安装了Docker之后,我们可以通过下面的方法来检测安装是否成功。

打印docker版本
1
2
复制代码# 打印docker版本
$ docker version
拉取镜像并运行容器
1
2
3
4
5
复制代码# 拉取hello-world镜像
docker pull hello-world

# 使用hello-world运行一个容器
docker run hello-world

运行上面的命令之后,如果有如下图所示的输出结果,则说明安装已经成功了。

Docker的基本概念

镜像(Image)、容器(Container)与仓库(Repository),这三个是docker中最基本也是最核心的概念,对这三个概念的掌握与理解,是学习docker的关键。

镜像(Image)

什么是Docker的镜像?

Docker本质上是一个运行在Linux操作系统上的应用,而Linux操作系统分为内核和用户空间,无论是Centos还是Ubuntu,都是在启动内核之后,通过挂载Root文件系统来提供用户空间的,而Docker镜像就是一个Root文件系统。

Docker镜像是一个特殊的文件系统,提供容器运行时所需的程序、库、资源、配置等文件,另外还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。

镜像是一个静态的概念,不包含任何动态数据,其内容在构建之后也不会被改变。

下面的命令是一些对镜像的基本操作,如下:

查看镜像列表
1
2
复制代码# 列出所有镜像
docker image ls

由于我们前面已经拉取了hello-world镜像,所以会输出下面的内容:

1
2
复制代码REPOSITORY                                      TAG                 IMAGE ID            CREATED             SIZE
hello-world latest fce289e99eb9 7 months ago 1.84kB

下面的命令也一样可以查看本地的镜像列表,而且写法更简洁。

1
2
复制代码# 列表所有镜像
docker images
从仓库拉取镜像

前面我们已经演示过使用docker pull命令拉取了hello-world镜像了,当然使用docker image pull命令也是一样的。

一般默认是从Docker Hub上拉取镜像的,Docker Hub是Docker官方提供的镜像仓库服务(Docker Registry),有大量官方或第三方镜像供我们使用,比如我们可以在命令行中输入下面的命令直接拉取一个Centos镜像:

1
复制代码docker pull centos

docker pull命令的完整写法如下:

1
复制代码docker pull [选项] [Docker Registry 地址[:端口号]/]仓库名[:标签]

拉取一个镜像,需要指定Docker Registry的地址和端口号,默认是Docker Hub,还需要指定仓库名和标签,仓库名和标签唯一确定一个镜像,而标签是可能省略,如果省略,则默认使用latest作为标签名,另外,仓库名则由作者名和软件名组成。

那么,我们上面使用centos,那是因为省略作者名,则作者名library,表示Docker官方的镜像,所以上面的命令等同于:

1
复制代码docker pull library/centos:latest

因此,如果拉取非官方的第三方镜像,则需要指定完整仓库名,如下:

1
复制代码docker pull mysql/mysql-server:latest
运行镜像

使用docker run命令,可以通过镜像创建一个容器,如下:

1
复制代码docker run -it centos /bin/bash
删除镜像

当本地有些镜像我们不需要时,那我们也可以删除该镜像,以节省存储空间,不过要注意,如果有使用该镜像创建的容器未删除,则不允许删除镜像。

1
2
复制代码# image_name表示镜像名,image_id表示镜像id
dockere image rm image_name/image_id

删除镜像的快捷命令:

1
复制代码docker rmi image_name/image_id

好了,关于Docker镜像的相关知识,我们就简单地介绍到这里,有机会的话,我们单独写一篇文章来谈谈,特别构建Docker镜像部分的相关知识,有必要深入再学习一下。

容器(Container)

Docker的镜像是用于生成容器的模板,镜像分层的,镜像与容器的关系,就是面向对象编程中类与对象的关系,我们定好每一个类,然后使用类创建对象,对应到Docker的使用上,则是构建好每一个镜像,然后使用镜像创建我们需要的容器。

启动和停止容器

启动容器有两种方式,一种是我们前面已经介绍过的,使用docker run命令通过镜像创建一个全新的容器,如下:

1
复制代码docker run hello-world

另外一种启动容器的方式就是启动一个已经停止运行的容器:

1
2
复制代码# container_id表示容器的id
docker start container_id

要停止正在运行的容器可以使用docker container stop或docker stop命令,如下:

1
2
复制代码# container_id表示容器的id
docker stop container_id
查看所有容器

如果要查看本地所有的容器,可以使用docker container ls命令:

1
2
复制代码# 查看所有容器
docker container ls

查看所有容器也有简洁的写法,如下:

1
2
复制代码# 查看所有容器
docker ps
删除容器

我们也可以使用docker container rm命令,或简洁的写法docker rm命令来删除容器,不过不允许删除正在运行的容器,因此如果要删除的话,就必须先停止容器,

1
2
复制代码# container_id表示容器id,通过docker ps可以看到容器id
$ docker rm container_id

当我们需要批量删除所有容器,可以用下面的命令:

1
2
复制代码# 删除所有容器
docker rm $(docker ps -q)
1
2
复制代码# 删除所有退出的容器
docker container prune
进入容器
1
2
复制代码# 进入容器,container_id表示容器的id,command表示linux命令,如/bin/bash
docker exec -it container_id command

仓库(Repository)

在前面的例子中,我们使用两种方式构建镜像,构建完成之后,可以在本地运行镜像,生成容器,但如果在更多的服务器运行镜像呢?很明显,这时候我们需要一个可以让我们集中存储和分发镜像的服务,就像Github可以让我们自己存储和分发代码一样。

Docker Hub就是Docker提供用于存储和分布镜像的官方Docker Registry,也是默认的Registry,其网址为https://hub.docker.com,前面我们使用docker pull命令便从Docker Hub上拉取镜像。

Docker Hub有很多官方或其他开发提供的高质量镜像供我们使用,当然,如果要将我们自己构建的镜像上传到Docker Hub上,我们需要在Docker Hub上注册一个账号,然后把自己在本地构建的镜像发送到Docker Hub的仓库当中,Docker Registry包含很多个仓库,每个仓库对应多个标签,不同标签对应一个软件的不同版本。

Docker的组成与架构

在安装好并启动了Docker之后,我们可以使用在命令行中使用docker命令操作docker,比如我们使用如下命令打印docker的版本信息。

1
复制代码docker verion

其结果如下:

从上面的图中,我们看到打出了两个部分的信息:Client和Server。

这是因为Docker跟大部分服务端软件一样(如MySQL),都是使用C/S的架构模型,也就是通过客户端调用服务器,只是我们现在刚好服务端和客户端都在同一台机器上而已。

因此,我们可以使用下面的图来表示Docker的架构,DOCKER_HOST是Docker server,而Clinet便是我们在命令中使用docker命令。

Docker Engine

docker server为客户端提供了容器、镜像、数据卷、网络管理等功能,其实,这些功能都是由Docker Engine来实现的。

  1. dockerd:服务器守护进程。
  2. Client docker Cli:命令行接口
  3. REST API:除了cli命令行接口,也可以通过REST API调用docker

下面是Docker Engine的示例图:

小结

作为一名开发人员,在学习或开发过程中,总需要安装各种各样的开发环境,另外,一个技术团队在开发项目的过程,也常常需要统一开发环境,这样可能避免环境不一致引发的一些问题。

虽然使用虚拟机可以解决上面的问题,但虚拟机太重,对宿主机资源消耗太大,而作为轻量级容器技术,Docker可以简单轻松地解决上述问题,让开发环境的安装以及应用的部署变得非常简单,而且使用Docker,比在虚拟机安装操作系统,要简单得多。


欢迎扫码关注,共同学习进步

本文转载自: 掘金

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

1…860861862…956

开发者博客

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