Yun's Blog

  • Home

  • Archives

服务的性能概述、优化及测试工具

Posted on 2019-04-15 | Edited on 2019-05-24 | In java

服务的性能概述、优化及测试工具

一、服务性能及优化

一般服务器的组成结构主要包括以下5部分:客户端、网关、服务、数据库以及各个系统之间的宽带。

1、客户端

1.1. 减少请求频率和次数

  • 配置数据做本地缓存,避免多次请求。对实时性要求高的缓存数据,采用增量更新。
  • 提交修改前作检查,避免没有修改内容,也提交到服务器。
  • 业务非必须的情况下,避免并发请求,采用懒加载。(如:多页数据分别加载、图片显示时才加载等)

1.2. 减少带宽占用

  • 请求数据进行压缩(json 压缩),减少带宽占用。

2、网关

  • 采用高性能网关(nginx 等)

  • 启用数据压缩(如果服务端没有实现)

  • 分流,限流

3、服务

3.1. 服务架构

单服务性能瓶颈后,可以做集群,也可以做微服务。

  • 单服务:如果能满足正常业务,可以不调整。
  • 集群:单服务不能满足时,可采用多个单服务的集群。实现初级扩展,可以快速提高服务处理能力。
  • 微服务:需要服务拆分,可能涉及夸数据库事务处理等。

3.2. 服务性能

提高单个服务的性能

  • 基础框架:JAVA 的springboot,go等
  • Web应用服务器:tomcat、undertow 等
  • 各应用组件的选择:json、日志等

3.3. 缓存能力

1、热点数据做缓存,利用缓存、缓解数据库压力,提高性能

3.4. 异步消息

1、并发高的业务,可采用消息队列缓冲;

2、与主体逻辑不相关的业务,可以采用消息进行通知。(如邮件、短信等)

4、数据库

4.1. 数据库性能匹配

设计的数据库类型,id 等,需要符合数据引擎的特性,提高性能。

  • MySql的 InnoDb最好采用自增 id
  • 使用索引,提高查询效率,索引使用遵循原则

4.2. 数据库集群

  • 对于读性能要求高的业务,可以采用读写分离
  • 对于数据量大的业务,可以考虑分库分表

5、带宽

  • 网关-服务-数据库 尽量在同一内网中。
  • 提高客户端-网关的宽带。
  • 分离对象业务:将对象数据,存储于专门的对象服务器。

二、优化方案

1、对象资源内容

对于图片、视频、音频等资源,不应该放在服务中。占用服务带宽和CPU资源。

对象应该存放在专门的对象服务器中。

1、七牛对象服务。

2、自建服务器。

2、热点资源缓存

做缓存,一般可采用 redis

1)用户数据和认证数据 token

2)接口验证数据(公司信息,项目信息),配置数据。

3)最近都热点数据(日报、组织架构等),设置过期时间

4)持久化数据(验证通过的单子,数据永远不会变),消息(生成就不会变)。设置过期时间

5)树结构设计 idPath(路径:|1.2.3),可以按照 like 搜索所有子节点。更新修改的次数比较多。

3、提高硬件配置

CPU、内存、宽带

4、jvm 优化

工作日志:

Xms Xmx 可设置成一样,避免内容波动。16G设置为8G。

Xmn 新生代:2G

-Xss128k 线程大小

-XX:newRatio = 2;

1)4调整到2,响应时间快,老年代的减少,由缓存弥补。
2)吞吐量大,老年代大。

整合服务:

Xms Xmx 根据业务大小设置。

5、mysql

1)设计原则

  • 索引,最左原则。最多没有超过3个。消息,架构,日志。

  • like 最好不要用左匹配,不能使用到索引

  • id 自增,聚镞 不适合 uuid

2)连接池

1)durid 阿里开源,性能好,有监控

2)HikariCP 速度比 durid 快,小巧

6、传输格式

一般采用 REST 接口,数据格式为json

1)json 压缩,去掉换行,空格等;

2)容器压缩,tomcat 进行压缩。

3)nginx 进行 gzip 压缩。

如果对数据大小要求更高,可采用 gRPC。

三、性能测试工具

性能测试工具

1、wrk

https://github.com/wg/wrk

wrk是轻量化的http性能测试工具,采用线程+网络异步IO模型,网络异步IO可以使得系统使用很少的线程模拟大量的网络连接以增大并发量、提高压力。

  • 操作简单、易于使用
  • 只支持简单测试、只支持单机测试
  • 适用于初期测试,评判系统的 QPS 水平

使用

1
./wrk  -t 8 -c 1000 -d 10s http://www.baidu.com
  • -t(–thread) 需要模拟的线程数;不宜过大,
  • -c(connection) 需要模拟的连接数;
  • –timeout 超时的时间;-d(–duration) 测试的持续时间

2、jmeter

jmeter同样采用线程并发机制,但其主要依靠增加线程数提高并发量,当单机模拟数以千计的并发用户时,对于CPU和内存的消耗比较大。与上述wrk相比,jmeter本身具有以下优点和缺点:

优点

①界面可视化操作,可以使用录制脚本方式对较为复杂的用户流建模,还可以创建断言来验证测试行为是否通过;

②表格、图形、结果树等多类可视化数据分析和报告输出,举例如下;

③支持http、ftp、tcp等多种协议类型测试;

④支持分布式压力测试,但对于上万的用户并发测试需要多台测试机支持,资源要求比较大;

⑤可以用于测试固定吞吐量下的系统性能,例如在100QPS(QPS:每秒查询率)下系统的响应时间和资源消耗;

缺点

jmeter的GUI模式消耗资源较大,当需要测试高负载时,需要先使用GUI工具来生成XML测试计划,然后在非GUI模式下导入测试计划运行测试,并且关闭不需要的侦听器(收集数据与展示测量的组件),因为侦听器也会消耗掉本用于生成负载的大量资源。测试结束后后,需要将原始结果数据导入GUI以才能查看结果。

3、locust

locust是一个的简单易用的分布式负载测试工具,主要用来对网站进行负载压力测试。locust使用python语言开发,测试资源消耗远远小于java语言开发的jmeter。且其支持分布式部署测试,能够轻松模拟百万级用户并发测试。

与jmeter和wrk相比,locust具有以下优缺点:

优点

①不同与wrk和jmeter使用线程数提高并发量,locust借助于协程实现对用户的模拟,相同物理资源(机器cpu、内存等)配置下locust能支持的并发用户数相比jmeter可以提升一个数量级;

②相比wrk对复杂场景测试的捉襟见肘和jmeter需要界面点击录制复杂场景的麻烦,locust只需用户使用python编写用户场景完成测试;

③不同与jmeter复杂的用户使用界面,locust的界面干净整洁,可以实时显示测试的相关细节(如发送请求数、失败数和当前发送请求速度等);

④locust虽然是面向web应用测试的,但是它可以用来测试几乎所有系统。给locust编写一个客户端,可以满足你所有的测试要求;

缺点

同wrk一样,locust测试结果输出不如jmeter的测试结果展示类型多;

消息队列概述

Posted on 2019-04-03 | Edited on 2019-05-24 | In java

消息队列概述

消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ

1、应用场景

消息队列的主要应用场景有:异步处理,应用解耦,流量削锋、日志处理和消息通讯。

1.1 异步处理

将不是业务的必须逻辑,采用消息进行异步处理:

如:用户注册后,需要发注册成功邮件和注册成功短信。邮件和短信可以写入消息队列,异步执行。

1.2 应用解耦

将接口调用,解耦为消息通知。

用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如果库存系统失败,那么整个订单将失败。

如果订单完成后,采用将后续处理写入队列,由其他系统自行处理。以及解耦。

1.3 流量削锋

流量过大,导致流量暴增,应用挂掉,可以用消息队列缓冲,一般在秒杀或团抢活动中使用广泛。

用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。

秒杀业务根据消息队列中的请求信息,再做后续处理。

1.4 日志处理

日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下

  • 日志采集客户端,负责日志数据采集,定时写受写入Kafka队列
  • Kafka消息队列,负责日志数据的接收,存储和转发
  • 日志处理应用:订阅并消费kafka队列中的日志数据

1.5 消息通讯

消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等

  • 客户端A和客户端B使用同一队列,进行消息通讯。

  • 客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。

2、MQ 框架对比

https://blog.csdn.net/u010486495/article/details/80179504

  • 在面向服务架构中,推荐 RabbitMQ
  • 日志消息,推荐 Kafka,高吞吐量。

1)Kafka

Kafka是linkedin开源的MQ系统,主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,0.8开始支持复制,不支持事务,适合产生大量数据的互联网服务的数据收集业务。

2)RabbitMQ

RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。

RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。

3)ZeroMQ

ZeroMQ只是一个网络编程的Pattern库,将常见的网络请求形式(分组管理,链接管理,发布订阅等)模式化、组件化,简而言之socket之上、MQ之下。对于MQ来说,网络传输只是它的一部分,更多需要处理的是消息存储、路由、Broker服务发现和查找、事务、消费模式(ack、重投等)、集群服务等。

4)区别

RabbitMQ/Kafka/ZeroMQ 都能提供消息队列服务,但有很大的区别。
在面向服务架构中通过消息代理(比如 RabbitMQ / Kafka等),使用生产者-消费者模式在服务间进行异步通信是一种比较好的思想。

因为服务间依赖由强耦合变成了松耦合。消息代理都会提供持久化机制,在消费者负载高或者掉线的情况下会把消息保存起来,不会丢失。就是说生产者和消费者不需要同时在线,这是传统的请求-应答模式比较难做到的,需要一个中间件来专门做这件事。其次消息代理可以根据消息本身做简单的路由策略,消费者可以根据这个来做负载均衡,业务分离等。

缺点也有,就是需要额外搭建消息代理集群(但优点是大于缺点的 ) 。

ZeroMQ 和 RabbitMQ/Kafka 不同,它只是一个异步消息库,在套接字的基础上提供了类似于消息代理的机制。使用 ZeroMQ 的话,需要对自己的业务代码进行改造,不利于服务解耦。

RabbitMQ 支持 AMQP(二进制),STOMP(文本),MQTT(二进制),HTTP(里面包装其他协议)等协议。

Kafka 使用自己的协议。
Kafka 自身服务和消费者都需要依赖 Zookeeper。

RabbitMQ 在有大量消息堆积的情况下性能会下降,Kafka不会。毕竟AMQP设计的初衷不是用来持久化海量消息的,而Kafka一开始是用来处理海量日志的。

总的来说,RabbitMQ 和 Kafka 都是十分优秀的分布式的消息代理服务,只要合理部署,不作,基本上可以满足生产条件下的任何需求。

3、示例

2.1 电商系统

消息队列采用高可用,可持久化的消息中间件。比如Active MQ,Rabbit MQ,Rocket Mq。

(1)应用将主干逻辑处理完成后,写入消息队列。消息发送是否成功可以开启消息的确认模式。(消息队列返回消息接收成功状态后,应用再返回,这样保障消息的完整性)

(2)扩展流程(发短信,配送处理)订阅队列消息。采用推或拉的方式获取消息并处理。

(3)消息将应用解耦的同时,带来了数据一致性问题,可以采用最终一致性方式解决。比如主数据写入数据库,扩展应用根据消息队列,并结合数据库方式实现基于消息队列的后续处理。

2.2 日志收集系统

分为Zookeeper注册中心,日志收集客户端,Kafka集群和Storm集群(OtherApp)四部分组成。
Zookeeper注册中心,提出负载均衡和地址查找服务
日志收集客户端,用于采集应用系统的日志,并将数据推送到kafka队列
Kafka集群:接收,路由,存储,转发等消息处理
Storm集群:与OtherApp处于同一级别,采用拉的方式消费队列中的数据

参考

https://blog.csdn.net/HD243608836/article/details/80217591

https://blog.csdn.net/u010486495/article/details/80179504

jvm 调优

Posted on 2019-03-17 | Edited on 2019-05-24 | In java

jvm 调优

1、查看进程 jvm 状态

查看进程 pid

1
jps

查看 jvm 分布

1
jmap -heap <pid>

2、参数设置

  • -Xms 初始内存
  • -Xmx 最大内存
  • -Xmn 新生代内存

Xms与Xmx 最好一样,避免内存波动。单击可设置为物理内存的80%
Xmn 最好不要太大,会影响老生代,Sun官方推荐配置为整个堆的3/8

  • -Xss128k 每个线程大小

  • -XX:NewRatio=4 年轻与年老大比值。4为 1:4,表示年轻代占比1/5。

1)年轻代大小选择

  • 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。

在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

  • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。

因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

2)年老代大小选择

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

  • 并发垃圾收集信息
  • 持久代并发收集次数
  • 传统GC信息
  • 花在年轻代和年老代回收上的时间比例
  • 减少年轻代和年老代花费的时间,一般会提高应用的效率

3)吞吐量优先的应用

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

参考

https://fjding.iteye.com/blog/2319808

https://github.com/crossoverJie/JCSprout/blob/master/MD/MemoryAllocation.md

jvm 内存 概述

Posted on 2019-03-16 | Edited on 2019-05-24 | In java

jvm 内存 概述

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

一、运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 堆
  • 方法区
  • 直接内存

1、程序计数器 Program Counter Register

Register 的命名源于CPU的寄存器,CPU只有把数据装载到寄存器才能够运行

寄存器存储指令相关的现场信息,由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。

另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

主要的两个作用

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

如果当前线程正在执行的是

  • Java方法

计数器记录的就是当前线程正在执行的字节码指令的地址

  • 本地方法

那么程序计数器值为undefined

注意:程序计数器是唯一不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

特点

  • 一块较小的内存空间
  • 线程私有。每条线程都有一个独立的程序计数器。
  • 是唯一一个不会出现OOM的内存区域。
  • 生命周期随着线程的创建而创建,随着线程的结束而死亡。

2、Java 虚拟机栈 VM Stack

相对于基于寄存器的运行环境来说,JVM是基于栈结构的运行环境

栈结构移植性更好,可控性更强

JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的

栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程

在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧

正在执行的方法称为当前方法

栈帧是方法运行的基本结构

在执行引擎运行时,所有指令都只能针对当前栈帧进行操作

StackOverflowError表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中

JVM能够横扫千军,虚拟机栈就是它的心腹大将,当前方法的栈帧,都是正在战斗的战场,其中的操作栈是参与战斗的士兵

局部变量表

  • 存放方法参数和局部变量
  • 相对于类属性变量的准备阶段和初始化阶段来说,局部变量没有准备阶段,必须显式初始化
  • 如果是非静态方法,则在index[0]位置上存储的是方法所属对象的实例引用,随后存储的是参数和局部变量
  • 字节码指令中的STORE指令就是将操作栈中计算完成的局部变量写回局部变量表的存储空间内

操作栈

  • 操作栈是一个初始状态为空的桶式结构栈
  • 在方法执行过程中,会有各种指令往栈中写入和提取信息
  • JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈
  • 字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的stack属性中

Java虚拟机栈是描述Java方法运行过程的内存模型

Java虚拟机栈会为每一个即将运行的Java方法创建“栈帧”

用于存储该方法在运行过程中所需要的一些信息

  • 局部变量表

    存放基本数据类型变量、引用类型的变量、returnAddress类型的变量

  • 操作数栈

  • 动态链接
  • 当前方法的常量池指针
  • 当前方法的返回地址
  • 方法出口等信息

每一个方法从被调用到执行完成的过程,都对应着一个个栈帧在JVM栈中的入栈和出栈过程

注意:人们常说,Java的内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。
这句话不完全正确!这里的“堆”可以这么理解,但这里的“栈”就是现在讲的虚拟机栈,或者说Java虚拟机栈中的局部变量表部分.
真正的Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息.

特点

  • 局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建.
  • 而且表的大小在编译期就确定,在创建的时候只需分配事先规定好的大小即可.
  • 在方法运行过程中,表的大小不会改变

异常

Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
  • OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。

其他

Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

虚拟机栈通过压/出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上
在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定
栈帧在整个JVM体系中的地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等

Java 内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack)其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法出口信息)

局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

3、本地方法栈 Native Method Stack

和虚拟机栈所发挥的作用非常相似,区别是:

虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。

4、Java堆 Java Heap

Heap是OOM故障最主要的发源地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用

通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗完所有的空间

堆的内存空间既可以固定大小,也可运行时动态地调整,通过如下参数设定初始值和最大值,比如

1
-Xms256M. -Xmx1024M

其中-X表示它是JVM运行参数

  • ms是memory start的简称 最小堆容量
  • mx是memory max的简称 最大堆容量

但是在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM的Xms和Xmx设置成一样大小,避免在GC后调整堆大小时带来的额外压力

堆分成两大块:新生代和老年代

对象产生之初在新生代,步入暮年时进入老年代,但是老年代也接纳在新生代无法容纳的超大对象

新生代= 1个Eden区+ 2个Survivor区

绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发Young GC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区,这个区真是名副其实的存在

Survivor 区分为S0和S1两块内存空间,送到哪块空间呢?每次Young GC的时候,将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态

如果YGC要移送的对象大于Survivor区容量上限,则直接移交给老年代

假如一些没有进取心的对象以为可以一直在新生代的Survivor区交换来交换去,那就错了。每个对象都有一个计数器,每次YGC都会加1。

1
-XX:MaxTenuringThreshold

参数能配置计数器的值到达某个阈值的时候,对象从新生代晋升至老年代。如果该参数配置为1,那么从新生代的Eden区直接移至老年代。默认值是15,可以在Survivor 区交换14次之后,晋升至老年代

若Survivor区无法放下,或者超大对象的阈值超过上限,则尝试在老年代中进行分配;

如果老年代也无法放下,则会触发Full Garbage Collection(Full GC);

如果依然无法放下,则抛OOM.

GC分类

  • Partial GC:并不收集整个GC堆的模式

    • Young GC:只收集young gen的GC,eden区分配满的时候触发
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

特点

  • Java虚拟机所需要管理的内存中最大的一块.

  • 堆内存物理上不一定要连续,只需要逻辑上连续即可,就像磁盘空间一样.

  • 堆是垃圾回收的主要区域,所以也被称为GC堆.

  • 堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的(通过-Xmx和-Xms控制),因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError.

  • 线程共享

  • 整个Java虚拟机只有一个堆,所有的线程都访问同一个堆.
  • 它是被所有线程共享的一块内存区域,在虚拟机启动时创建.
  • 而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个

其他

  1. JAVA对象优先在Eden区分配,当Eden区没有足够的空间时触发一次Minor GC ,触发Minor GC时,Eden和from区中的存活对象会被复制到to区,然后from和to交换指针,以保证下次Minor GC时,to区还是空的,如果survival区无法容纳的对象将通过分配担保机制直接进入老年区

  2. 分配担保机制可以通过HandlePromotionFailure配置,如果不允许的话,则直接发生FULL GC

  3. 新生代(Young Generation)的最大大小将根据总堆的最大大小和NewRatio参数的值来计算。参数的“不受限制”默认值MaxNewSize意味着计算值不受限制,MaxNewSize除非MaxNewSize在命令行中指定了值

  4. 一般情况下,不允许-XX:Newratio值小于1,即Old要比Young大

  5. 大对象直接进入老年区的判断是根据PretenureSizeThreshold设置的阈值,所谓大对象时指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象)

  6. 发生full GC的条件是:

  • (1)调用System.gc时,系统建议执行Full GC,但是不必然执行
  • (2)老年代空间不足
  • (3)方法区空间不足
  • (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • (5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  1. 对象存活判断
  • 引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题
  • 可达性分析:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象
  1. GC Roots对象包括
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象
  • 已启动且未停止的java线程

5、方法区 Method Area

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

特点

线程共享

  • 方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的.整个虚拟机中只有一个方法区.

永久代

  • 方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为永久代.

内存回收效率低

  • Java虚拟机规范对方法区的要求比较宽松,可以不实现垃圾收集.
  • 方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效.
  • 对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载

和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。

当方法区内存空间无法满足内存分配需求时,将抛出OutOfMemoryError异常.

5.1、运行时常量池 Runtime Constant Pool

运行时常量池是方法区的一部分。

方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码.其中常量存储在运行时常量池中.

java文件被编译之后生成的.class文件中除了包含:类的版本、字段、方法、接口等信息外,还有一项就是常量池

常量池中存放编译时期产生的各种字面量和符号引用,.class文件中的常量池中的所有的内容在类被加载后存放到方法区的运行时常量池中。

1
2
int age = 21; //age是一个变量,可以被赋值;21就是一个字面值常量,不能被赋值;
int final pai = 3.14; //pai就是一个符号常量,一旦被赋值之后就不能被修改。

Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池( Constant pool table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放。运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。

在近三个JDK版本(6、7、8)中, 运行时常量池的所处区域一直在不断的变化,

  • JDK6时它是方法区的一部分
  • 7又把他放到了堆内存中
  • 8之后出现了元空间,它又回到了方法区。

其实,这也说明了官方对“永久代”的优化从7就已经开始了

特性

  • class文件中的常量池具有动态性.
  • Java并不要求常量只能在编译时候产生,Java允许在运行期间将新的常量放入方法区的运行时常量池中.
  • String类中的intern()方法就是采用了运行时常量池的动态性.当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串,则返回池中的字符串.否则,将此 String 对象添加到池中,并返回此 String 对象的引用.

异常

运行时常量池是方法区的一部分,所以会受到方法区内存的限制,因此当常量池无法再申请到内存时就会抛出OutOfMemoryError异常.

我们一般在一个类中通过public static final来声明一个常量。这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。

当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如:String类的intern()方法就能在运行期间向常量池中添加字符串常量。

当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。

6、直接内存 Direct Memory

直接内存不是虚拟机运行时数据区的一部分,也不是JVM规范中定义的内存区域,但在JVM的实际运行过程中会频繁地使用这块区域.而且也会抛OOM

在JDK 1.4中加入了NIO(New Input/Output)类,引入了一种基于管道和缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆里的DirectByteBuffer对象作为这块内存的引用来操作堆外内存中的数据.
这样能在一些场景中显著提升性能,因为避免了在Java堆和Native堆中来回复制数据.

综上看来

程序计数器、Java虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的程序计数器、Java虚拟机栈、本地方法区。并且他们的生命周期和所属的线程一样。
而堆、方法区是线程共享的,在Java虚拟机中只有一个堆、一个方法栈。并在JVM启动的时候就创建,JVM停止才销毁。

7、元空间 Metaspace

在JDK8,元空间的前身Perm区已经被淘汰,在JDK7及之前的版本中,只有Hotspot才有Perm区(永久代),它在启动时固定大小,很难进行调优,并且Full GC时会移动类元信息

在某些场景下,如果动态加载类过多,容易产生Perm区的OOM.

为解决该问题,需要设定运行参数

1
-XX:MaxPermSize= l280m

如果部署到新机器上,往往会因为JVM参数没有修改导致故障再现。不熟悉此应用的人排查问题时往往苦不堪言,除此之外,永久代在GC过程中还存在诸多问题

所以,JDK8使用元空间替换永久代,区别于永久代,元空间在本地内存中分配.
也就是说,只要本地内存足够,它不会出现像永久代中java.lang.OutOfMemoryError: PermGen space

默认情况下,“元空间”的大小可以动态调整,或者使用新参数MaxMetaspaceSize来限制本地内存分配给类元数据的大小.

在JDK8里,Perm 区所有内容中

  • 字符串常量移至堆内存

  • 其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间

特点

  • 充分利用了Java语言规范:类及相关的元数据的生命周期与类加载器的一致
  • 每个类加载器都有它的内存区域-元空间
  • 只进行线性分配
  • 不会单独回收某个类(除了重定义类 RedefineClasses 或类加载失败)
  • 没有GC扫描或压缩
  • 元空间里的对象不会被转移
  • 如果GC发现某个类加载器不再存活,会对整个元空间进行集体回收

GC

  • Full GC时,指向元数据指针都不用再扫描,减少了Full GC的时间
  • 很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了
  • 元空间只有少量的指针指向Java堆

    这包括:类的元数据中指向java.lang.Class实例的指针;数组类的元数据中,指向java.lang.Class集合的指针。

  • 没有元数据压缩的开销

  • 减少了GC Root的扫描(不再扫描虚拟机里面的已加载类的目录和其它的内部哈希表)
  • G1回收器中,并发标记阶段完成后就可以进行类的卸载

元空间内存分配模型

  • 绝大多数的类元数据的空间都在本地内存中分配
  • 用来描述类元数据的对象也被移除
  • 为元数据分配了多个映射的虚拟内存空间
  • 为每个类加载器分配一个内存块列表

    • 块的大小取决于类加载器的类型
    • Java反射的字节码存取器(sun.reflect.DelegatingClassLoader )占用内存更小
  • 空闲块内存返还给块内存列表

  • 当元空间为空,虚拟内存空间会被回收
  • 减少了内存碎片

最后,从线程共享的角度来看

  • 堆和元空间是所有线程共享的
  • 虚拟机栈、本地方法栈、程序计数器是线程内部私有的

从这个角度看一下Java内存结构

8、从GC角度看Java堆

堆和方法区都是线程共享的区域,主要用来存放对象的相关信息。我们知道,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行期间才能知道会创建哪些对象,因此, 这部分的内存和回收都是动态的,垃圾收集器所关注的就是这部分内存(本节后续所说的“内存”分配与回收也仅指这部分内存)。而在JDK1.7和1.8对这部分内存的分配也有所不同,下面我们来详细看一下

二、JVM关闭

  • 正常关闭:当最后一个非守护线程结束或调用了System.exit或通过其他特定于平台的方式,比如ctrl+c。
  • 强制关闭:调用Runtime.halt方法,或在操作系统中直接kill(发送single信号)掉JVM进程。
  • 异常关闭:运行中遇到RuntimeException 异常等

在某些情况下,我们需要在JVM关闭时做一些扫尾的工作,比如删除临时文件、停止日志服务。为此JVM提供了关闭钩子(shutdown hocks)来做这些事件。

Runtime类封装java应用运行时的环境,每个java应用程序都有一个Runtime类实例,使用程序能与其运行环境相连。

关闭钩子本质上是一个线程(也称为hock线程),可以通过Runtime的addshutdownhock (Thread hock)向主jvm注册一个关闭钩子。hock线程在jvm正常关闭时执行,强制关闭不执行。

对于在jvm中注册的多个关闭钩子,他们会并发执行,jvm并不能保证他们的执行顺序。

参考

https://www.nowcoder.com/discuss/151138?type=1

http://www.importnew.com/31126.html/jvm-1

https://blog.csdn.net/yingziisme/article/details/82946084

使用 spring boot jpa 的坑(unique 不起作用)

Posted on 2019-03-11 | Edited on 2019-04-23 | In SpringBoot

使用 spring boot jpa 的坑(unique 不起作用)

在使用JPA 时,定义@Entity对象时,有些字段需要唯一(如账号名),但是我发现,自动生成的数据库中,对该字段并没有设置为唯一约束

对象如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
@EntityListeners(AuditingEntityListener.class)
@Data
public class AdminUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
// @NotEmpty
// @Length(min = 4, max = 20)
// @ApiModelProperty("账户名(4-20)")
private String acctName;

@Column
// 转成32位 md5密码
private String password;
}

查看 console 输出,确实有修改命名

1
2
3
4
5
6
7
8
9
10
11
12
13
Hibernate: 

create table admin_user (
...
) engine=InnoDB
Hibernate:

alter table admin_user
drop index UK_5gnafmd0cjdf2txrbtigl1fsa
Hibernate:

alter table admin_user
add constraint UK_5gnafmd0cjdf2txrbtigl1fsa unique (acct_name)

反复测试后,发现,Long对象的 unique 可以添加成功,String 对象的不行,然后我把 String 对象加上length限制

1
2
@Column(unique = true, nullable = false, length = 20)
private String acctName;

结果就成功了。

原因

然后查找原因,手动创建表,添加索引,出现如下错误

Specified key was too long; max key length is 767 bytes

原来是因为指定字段长度过长。未指定长度的字符串,默认为255 varchar,utf8mb4字符集每个 varchar 为4bytes,即为总长255x4=1020bytes,大于了767bytes。

因此,unique 字段的最大长度为767/4 = 191 varchar。(注:utf8mb4字符集)

Hackintosh 黑苹果总结

Posted on 2019-01-13 | Edited on 2019-05-24 | In Hackintosh

Hackintosh 黑苹果总结

个人用黑苹果开发iOS、java多年。之所以选择黑苹果,还是因为 Macbook 性能太弱,而且发热严重。这几年,基本随着 Mac系统更新,也会更新系统(不然无法更新最新的Xcode,纯 java 开发倒是无所谓)。只有一次打开【文件保险箱】功能挂了(下面有说明,千万不要打开该选项),其他时候都没问题。分享其中的一些经验,给愿意尝试黑苹果的朋友。

  • 个人黑苹果电脑的配置:

    电脑一:台式机(i7-87000k 华擎 Z370M-ITX 16G GT750ti)

    电脑二:台式机(i5-4590 华硕 b85m 8G)

    电脑三:联想 Y700笔记本(i5-6300 8G GT960M)

  • 帮人配的电脑:

    电脑一:台式机(i7-77000 华擎 deskmini 16G)

    电脑二:台式机(i5-7500 华硕 B150 8G)

    EIF 分享-包括以上提到的几种配置机型

一、哪些电脑可以安装黑苹果

1、笔记本

如何确定笔记本可以安装:

1)论坛(tonymacx86、pcbeta)搜索笔记本型号,看有安装成功的案例没。

2)看CPU,如果 CPU 的型号与苹果已经发布的笔记本相同或类似(类似定义为同代 CPU),那么 CPU 应该没问题。

3)看显卡:如果笔记本有独显,那么独显基本是不能使用的,只能使用集显。偶尔少数笔记本有独显,主板不能屏蔽独显,导致无法安装。

4)以上满足的话,可以尝试安装。

2、台式机

如何确定台式机可以安装:

1)论坛(tonymacx86、pcbeta)搜索台式机配置,看有安装成功的案例没。

2)看CPU,如果CPU的型号与苹果已经发布的iMac或 Mac mini相同或类似(类似定义为同代 CPU),那么 CPU 应该没问题。

基本都常见的 intel 台式机 CPU 都可以安装。最新的 AMD 都 CPU 也有大神放出内核,可以安装。

3)看显卡:如果有独显,比较新的 AMD 显卡都可以支持,N 卡一般也支持,有 WebDriver(目前10.14还没有 webdriver)。

强烈建议入 AMD 显卡,原生支持,升级无忧。

特别注意 RX580(2048P)不能驱动,慎入。

4)主板:一般都支持,技嘉的一般支持原生电源管理,比较好。华擎的支持比较到位,曾经几块主板专门出过安装黑苹果的 BIOS,良心。

5)网卡:一般 intel 的有线网卡都支持,无线的话,选择 苹果电脑用过的型号(BCM94352Z,淘宝上买),容易驱动。

3、建议

1)如果买新电脑

笔记本可以买没有独显的,因为有也一版用不上。先查下哪些比较好安装的机型,照着买就行。

台式机可以参考 tonymacx86上的配置,都很容易安装。

2)最好配2块以上硬盘。

一块安装 Mac。另外一块安装 Windows,或者作为备份盘(TimeMachine)。如果作为生成环境使用,建议单独配置一块硬盘作为 TimeMachine 的备份盘。

3)如果要独显,最好选 AMD 的卡,可以很好的原生驱动。

4)如果配无线,最好选 Mac 电脑上用过的型号,容易驱动。

二、安装流程

最简方式:

1、Mac 上 AppStore 中下载 Mac 系统

2、制作安装镜像到 U 盘,可以借助工具 DiskMaker

3、制作 EFI 启动分区,可以制作在 U 盘上,也可以制作在硬盘上。

4、放入 EFI 分区启动文件(kext 和 config 配置文件等)

5、设置好 BIOS 选项

6、从 EFI 启动安装

7、完善安装(各硬件驱动)

8、完善 EFI,可将稳定的 EIF 文件,放入 Mac 分区的 EFI 分区。从 Mac 分区启动。

注意事项

1、不要随意升级 Mac 新版本,可能造成 kext 不兼容。

2、最好配一块备份盘,用 TimeMachine 备份。TimeMachine 确实好用。

3、一般不要去动 SLE 下的系统kext,也尽量不要把 kext 放入 SLE 下面,补丁 kext 都可以放在 EFI 下的 kext 中调试。

4、千万不要打开『安全与隐私』中的 【文件保险箱】功能,该功能与硬件相关,打开后,黑苹果就GG。

5、config 中的硬件 ID 尽量用同一个(同一台机子),新机子第一次安装时,随机生成一个。频繁更改 ID,会让你重新登录 AppleId。

可以参考的网站

tonymacx86 - 国外很活跃度黑苹果网站,还有配置推荐

pcbeta - 国内活跃度黑苹果网站

cloverefiboot - clover项目现在地址

RehabMan bitbucket - RehabMan的 kext下载

RehabMan git - RehabMan的 kext下载和一些配置信息

acidanthera git - 作者写了很多有用的 kext

SpringCloud 概述

Posted on 2019-01-11 | Edited on 2019-04-23 | In SpringCloud

SpringCloud 概述

微服务

微服务是一个小的、松耦合的分布式服务。

1、微服务的基础问题

服务粒度

  • 服务职责单一
  • 服务无状态话
  • 操作表不能太多(3-5个)
  • 避免简单的 CRUD服务

通信协议

接口设计

  • RESTful 风格接口
  • 内容使用 JSON
  • HTTP 状态码表示结果

服务的配置管理 – Config

统一配置服务器,避免配置与服务硬绑定。

服务之间的事件处理 – Stream

使用事件解耦微服务,最小化服务之间的硬编码依赖。

2、微服务路由模式

负责处理客服端的服务请求,使其到达特定实例。

服务路由(网关) – Netflix Zuul

为所有服务提供单个入口点,可整合安全策略、路由规则等。

服务发现 – Netflix Eureka

微服务注册中心、管理服务。

3、客服端弹性模式

避免单个服务影响整个系统,提高服务稳定性。

客服端负载均衡 – Neflix Ribbon

负载均衡,避免单个服务过载

断路器模式 – Netflix Hystrix

阻止客户继续调用出故障/有性能问题的服务

后备模式 – Netflix Hystrix

服务调用失败后,提供后备方案

舱壁模式 – Netflix Hystrix

避免个别微服务故障影响整个服务。

4、微服务安全

验证 – Security/OAuth2

身份验证

授权 – Security/OAuth2

权限验证

凭据管理和传播 – Security/OAuth2 JWT

验证的模式 -JWT

5、微服务日志记录与跟踪模式

日志关联 – Sleuth

关联请求在所有微服务中的调用链。

日志聚合 – Sleuth、Papertrail

将所有微服务的日志聚合到一起

微服务跟踪 – Sleuth/Zipkin

跟踪微服务的事物流程,以及其性能。

6、微服务构建和部署

构建和部署管道

可重复的构建和部署过程。

基础设施即代码

不可变服务器

部署之后永远不会改变

凤凰服务器(Phoenix server)

服务长期一致性。

我的 iOS 框架简介

Posted on 2019-01-01 | Edited on 2019-04-23 | In iOS

我的 iOS 框架简介

在平时 iOS 开发中,自己积累了一些功能框架,现在年前,正好有时间,把各个框架的功能和使用说明写了一下。希望能帮助到有用的人。

如果在使用中有什么BUG,疑问或者建议,都可以联系我:email:wangjr@mail.tsinghua.edu.cn

1、YunBaseApp

github

自己开发的 iOS 应用开发的基本框架,涉及 App 中等各种功能:UIViewController 的封装、主题管理、帐号管理、日志管理、、加载页、提示信息、错误封装等。

主要模块:

  • Account (用户信息管理)

  • ActionListView (Action选择控件)

  • AlertView (提示控件)

  • Cache (缓存管理)

  • Error (Error管理)

  • HudView (HudView 基类)

  • Log (日志封装)

  • Rqt (网络请求封装)

  • Theme (主题管理)

  • View (UIView 和 UIViewController 的封装)

  • ViewCategory (UIView 和 UIViewController的扩展)

2、YunKits

github

iOS 基本库的一些封装扩展。主要包括:

  • BaseView (对UIView、UIViewController、UITableView 的一些扩展)

  • Categories (分类扩展)

  • Factory (一些对象和控件的工厂方法)

  • Macro (一些常用宏。建议少用宏,尽量用静态变量或者实例变量。)

  • Tools (工具类)

3. YunImgView

github

封装的 iOS 图片列表控件,用 Objective-C 编写

该库主要包括两部分:1)YunImgView 图片列表库。2)YunSelectImgHelper 图片视频选择库。

4. YunQiniuHelper

github

自己封装的 iOS 端的七牛上传工具,使用 Objective-C。

可以上传单个文件,多个文件,指定 key。

5. YunWebView

github

使用Objective-C 实现的自定义 WebView,封装了 App 接口提供给 Web 前端使用,适用于 App 内嵌功能网页。

6. YunImageBrowser

github

基于MWPhotoBrowser,进行修改的图片浏览控件

  • 修改依赖库,支持最新的 SDWebImage
  • 修改样式,支持 iPhone X
  • 修复一些 BUG

iOS 实现AOP编程(Objective-C)

Posted on 2018-11-23 | In iOS

iOS 实现AOP编程(Objective-C)

一、AOP与OOP

  • OOP(Object Oriented Programming,面向对象编程)

OOP比较经典的程序设计思想,面向对象的特点是封装、多态和继承。面向对象设计时,每个对象职责不同,封装的功能也不同。这样就进行了解耦,增加了代码的重用性、灵活性和扩展性。

但这种方式也存在一个问题,比如,我们在两个类中,可能都需要在每个方法中进行日志记录(功能完全一样)。按OOP 方式,需要两个类的方法中都加入日志功能。这样就会有很多重复代码,当需要更改日志记录功能时,每个实现的类都需要更改。

一种解决方法:将日志功能写在一个独立的类中,然后再在这两个类中调用该类的日志记录功能。修改日志功能只需要修改单独的类即可。但是各个类与独立类有耦合,当有一个类需要增加或移除日志记录功能时,需要修改该类。另一种方法就是 AOP。

  • AOP(Aspect Oriented Program,面向切面编程)

AOP 思想是一种在不修改源代码的情况下给程序动态统一添加功能的一种技术。一般通过预编译方式和运行期动态代理实现程序功能的统一维护。

一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。

AOP 与 OOP 配合,可以很好的分离应用的业务逻辑与系统级服务。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。

二、 iOS实现 AOP

实现 AOP 需要语言支持对对象的动态扩展,正好 Objective-C的 Runtime 特性可以实现。现在有两种实现方式:

  • 1. Method Swizzling
  • 2. 消息转发

1. Method Swizzling 实现 AOP

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。

利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现。

每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。

  • 每个类(Class)维护一张调度表(dispatch table)用于解析运行时发送的消息;
  • 调度表中的每个实体(entry)都是一个方法(Method),其中key值是一个唯一的名字——选择器(SEL),它对应到一个实现(IMP - 实际上就是指向标准C函数的指针)。

Method Swizzling就是改变类中SEL 的具体实现函数IMP。

1
2
3
4
5
6
7
8
9
10
11
12
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE; // selector 名字
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // IMP 实现方法,运行时可更改
}

// 常用函数
- method_exchangeImplementations // 交换2个方法中的IMP

- class_replaceMethod // 会调用class_addMethod和method_setImplementation,先实现方法,再设置IMP

- method_setImplementation // 直接设置某个方法的IMP

可参考EffectiveObjective-C2.0 笔记 - 第二部分

示例 - 日志打印

  • 封装的 Swizzling 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ (void)swizzClass:(Class)classItem originSel:(SEL)originSel newSel:(SEL)newSel {
Method orgMd = class_getInstanceMethod(classItem, originSel);
Method newMd = class_getInstanceMethod(classItem, newSel);

IMP newImp = method_getImplementation(newMd);

// 检查源方法有没有实现
// 如果是YES,表示originSel没有实现,则需要先实现,然后再设置Imp
// 如果是NO,表示originSel已经有存在的实现方法,此时,只需要将orgMd和newMd互换就好
BOOL isAddMdSuccess = class_addMethod(classItem, originSel, newImp, method_getTypeEncoding(newMd));

if (isAddMdSuccess) {
// 会调用class_addMethod和method_setImplementation,先实现方法,再设置IMP
class_replaceMethod(classItem, originSel, newImp, method_getTypeEncoding(newMd));
}
else {
// orgMd和newMd互换
method_exchangeImplementations(orgMd, newMd);
}
}
  • 注意classItem,看你是替换类的方法,还是实例对象的放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (Class)getClassItem {
Class classItem = nil;

//要特别注意你替换的方法到底是哪个性质的方法
// When swizzling a Instance method, use the following:
// 仅替换本实例方法,子类方法不变
classItem = [self class];

// When swizzling a class method, use the following:
// 替换类方法
classItem = object_getClass((id) self);

return classItem;
}
  • 在 load 中交换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// load 中执行 Swizzling
+ (void)load {
static dispatch_once_t onceToken;

// dealloc是关键字,不能使用@selector(dealloc)
SEL orgSel = NSSelectorFromString(@"dealloc");

SEL newSel = @selector(swizzing_dealloc);

// 保证仅执行一次
dispatch_once(&onceToken, ^{
[self swizzClass:[self class]
originSel:orgSel
newSel:newSel];
});
}

- (void)swizzing_dealloc {
NSLog(@" ** %@ 释放了 %s", NSStringFromClass([self class]), __func__);

// 交换后,就不能用 [self dealloc]
[self swizzing_dealloc];
}
  • 为什么在 load 中交换

+(void)load 方法只要类所在文件被引用就会被调用,在程序运行后立即执行(在main()之前执行),这样就可以在执行方法前,完成方法的替换。

另外 +(void)initialize 方是在类或者其子类的第一个方法被调用前调用,因为method swizzling会影响全局,+load能够保证在类初始化的时候就会被加载,这为改变系统行为提供了一些统一性。 但+initialize并不能保证在什么时候被调用——事实上也有可能永远也不会被调用,例如应用程序从未直接的给该类发送消息。

使用注意点:

  1. Method Swizzling 需要在 + (void)load{}中使用

  2. Method Swizzling 需要保证只执行一次。 需要使用 dispatch_once;

  3. 注意Class的选择,类对象还是实例对象

  4. Method Swizzling 是以替换 IMP 来实现动态修改代码,这样实现的 AOP 不优雅,使用消息转发可以更优雅。

2. 消息转发 实现 AOP

Aspects是一个已经实现的 AOP 轮子。下面结合Aspects对消息转发的实现进行分析。

2.1 实例方法的执行

Objective-C 中执行实例方式,其实是给对象发送一个消息(id objc_msgSend ( id self, SEL cmd, ... )),执行流程如下:

  • 对象实例(instance)收到消息(selector 选择子+参数)
  • 根据对象实例的ISA找到类对象,在类对象中找与选择子名称相符的方法,如果找到,就调至执行代码
  • 如果找不到,则根据类对象中的super_class指针找到父类的Class对象。一直找到NSObject的类对象
  • 如果NSObject也无法找到这个选择子,则进入消息转发机制(message forwarding)
  • 如果消息转发机制无法处理,则抛出异常: doesNotRecognizeSelector:

2.2 消息转发机制

在Objective C的方法调用过程中,当无法响应一个selector时,在抛出异常之前会先进入消息转发机制。这里来详细讲解消息转发的过程:

关于消息转发,官方文档在这里: Message Forwarding

其他参考二、 消息转发

在触发消息转发机制即forwardInvocation:之前,Runtime提供了两步来进行轻量级的动态处理这个selector.

  • 1. 动态方法 resolveInstanceMethod:

Dynamically provides an implementation for a given selector for an instance method.

这个方法提供了一个机会:为当前类无法识别的SEL动态增加IMP。

比如:可以通过class_addMethod增加 IMP

1
2
3
4
5
6
7
8
9
10
11
12
void dynamicMethodIMP(id self, SEL _cmd) {/*...implementation...*/}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSel];
}


// "v@:"表示方法参数编码,v表示Void,@表示OC对象,:表示SEL类型。

如果resolveInstanceMethod返回NO,则表示无法在这一步动态的添加方法,则进入下一步:

  • 2. 备援接收者 forwardingTargetForSelector:

Returns the object to which unrecognized messages should first be directed.

这个方法提供了一个机会:把这个SEL转给其他接收者来处理。

比如

1
2
3
4
5
6
7
8
9
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(dynamicSelector) &&
[self.myObj respondsToSelector:@selector(dynamicSelector)]) {
return self.myObj;
}
else {
return [super forwardingTargetForSelector:aSelector];
}
}
  • 3. 消息转发 message forwarding

如果上述两步都无法完成这个SEL的处理,则进入消息转发机制,消息转发机制有两个比较重要的方法:

  • forwardInvocation: 具体的NSInvocaion
  • methodSignatureForSelector: 返回SEL的方法签名

这里不得不提一下两个类:

  • NSMethodSignature 用来表示方法的参数签名信息:返回值,参数数量和类型
  • NSInvocaion SEL + 执行SEL的Target + 参数值

通常,拿到NSInvocaion对象后,我们可选择的进行如下操作

  • 修改执行的SEL
  • 修改执行的Target
  • 修改传入的参数

然后调用:[invocation invoke],来执行这个消息。

_objc_msgForward

我们知道,正常情况下SEL背后会对一个IMP,在OC中有一个特殊的IMP就是:_objc_msgForward。当执行_objc_msgForward时,会直接触发消息转发机制,即forwardInvocation:。

2.3 Method Swizzling

上一节已经介绍了Method Swizzling,可以替换SEL 对应的IMP。

2.4 Aspect 实现

使用Aspect,可以在一个OC方法执行前/后插入代码,也可以替换这个OC方法的实现。通过作者暴露的2个接口可以实现对实例和类的 Hook:

1
2
3
4
5
6
7
8
9
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;

- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;

下面以在ViewControler的viewWillAppear:方法之后插入一段代码为例,来讲解hook前后的变化

1) 在没有hook之前,ViewController的SEL与IMP关系如下

2) 调用以下aspect来Hook viewWillAppear:后

1
2
3
4
5
[ViewController aspect_hookSelector:@selector(viewWillAppear:)
withOptions:AspectPositionAfter
usingBlock:^{
NSLog(@"Insert some code after ViewWillAppear");
} error:&error];

  • 最初的viewWillAppear: 指向了_objc_msgForward
  • 增加了aspects_viewWillAppear:,指向最初的viewWillAppear:的IMP
  • 最初的forwardInvocation:指向了Aspect提供的一个C方法ASPECTS_ARE_BEING_CALLED
  • 动态增加了aspects_forwardInvocation:,指向最初的forwardInvocation:的IMP

3) hook后,一个viewWillAppear:的实际调用顺序:

  • object收到selector(viewWillAppear:)的消息
  • 找到对应的IMP:_objc_msgForward,执行后触发消息转发机制。
  • object收到forwardInvocation:消息
  • 找到对应的IMP:ASPECTS_ARE_BEING_CALLED,执行IMP
    • 向object对象发送aspects_viewWillAppear:,执行最初的viewWillAppear方法的IMP
    • 执行插入的block代码
    • 如果ViewController无法响应aspects_viewWillAppear,则向object对象发送__aspects_forwardInvocation:来执行最初的forwardInvocation IMP

所以,Aspects是采用了集中式的hook方式,所有的调用最后走的都是一个C函数ASPECTS_ARE_BEING_CALLED。

2.4.1 核心类/数据结构

1) Aspects 内部定义了两个协议:

  • AspectToken

AspectToken 协议旨在让使用者可以灵活的注销之前添加过的 Hook

1
2
3
4
5
/// 用于注销 Hook
@protocol AspectToken /// 注销一个 aspect.
/// 返回 YES 表示注销成功,否则返回 NO
- (BOOL)remove;
@end
  • AspectInfo

AspectInfo 协议旨在规范对一个切面,即 aspect 的 Hook 内部信息的纰漏,在 Hook 时添加切面的 Block 第一个参数就遵守此协议。

1
2
3
4
5
6
7
8
/// AspectInfo 协议是嵌入 Hook 的Block的第一个参数。
@protocol AspectInfo /// 当前被 Hook 的实例
- (id)instance;
/// 被 Hook 方法的原始 invocation
- (NSInvocation *)originalInvocation;
/// 所有方法参数(装箱之后的)惰性执行
- (NSArray *)arguments;
@end

2) Aspects 内部还定义了 4 个类:

  • AspectInfo

切面信息:NSInvocation的容器,表示一个执行的Command,遵循 AspectInfo 协议。AspectInfo 扮演了一个提供 Hook 信息的角色。

1
2
3
4
5
@interface AspectInfo : NSObject <AspectInfo>
@property (nonatomic, unsafe_unretained, readonly) id instance;
@property (nonatomic, strong, readonly) NSArray *arguments;
@property (nonatomic, strong, readonly) NSInvocation *originalInvocation;
@end
  • AspectIdentifier

切面 ID:代表一个Aspect的具体信息,包括被Hook的对象,SEL,插入的block等具体信息,遵循 AspectToken 协议。

1
2
3
4
5
6
7
@interface AspectIdentifier : NSObject
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end
  • AspectContainer

AspectIdentifier的容器:以SEL合成key,然后作为关联对象存储到对应的类/对象里。包括beforeAspects,insteadAspects,afterAspects

1
2
3
4
5
@interface AspectsContainer : NSObject
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end
  • AspectTracker

切面跟踪器:跟踪一个类的继承链中的hook状态:包括被hook的类,哪些SEL被hook了。

1
2
3
4
5
6
@interface AspectTracker : NSObject
@property (nonatomic, strong) Class trackedClass;
@property (nonatomic, readonly) NSString *trackedClassName;
@property (nonatomic, strong) NSMutableSet *selectorNames;
@property (nonatomic, strong) NSMutableDictionary *selectorNamesToSubclassTrackers;
@end

其原理大致为

1
2
3
4
5
6
7
8
9
10
11
12
13
// Add the selector as being modified.
currentClass = klass;
AspectTracker *parentTracker = nil;
do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if (!tracker) {
tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
swizzledClassesDict[(id)currentClass] = tracker;
}
[tracker.selectorNames addObject:selectorName];
// All superclasses get marked as having a subclass that is modified.
parentTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));

AspectTracker 是从下而上追踪,最底层的 parentEntry 为 nil,父类的 parentEntry 为子类的 tracker。

3)一个结构体:

  • AspectBlockRef - 即 _AspectBlock,充当内部 Block

4)两个内部静态全局变量:

  • static NSMutableDictionary *swizzledClassesDict;
  • static NSMutableSet *swizzledClasses;
2.4.2 hook过程

1. 对Class和MetaClass进行进行合法性检查,判断能否hook,规则如下

  • retain,release,autorelease,forwoardInvocation:不能被hook
  • dealloc只能在方法前hook
  • 类的继承关系中,同一个方法只能被hook一次

2. 创建AspectsContainer对象,以aspects_ + SEL为key,作为关联对象依附到被hook 的对象上

1
objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);

3. 创建AspectIdentifier对象,并且添加到AspectsContainer对象里存储起来。这个过程分为两步

  • 生成block的方法签名NSMethodSignature
  • 对比block的方法签名和待hook的方法签名是否兼容(参数个数,按照顺序的类型)

4. 根据hook实例对象/类对象/类元对象的方法做不同处理。其中,对于上文以类方法来hook的时候,分为两步

  • hook类对象的forwoardInvocation:方法,指向一个静态的C方法,并且创建一个aspects_ forwoardInvocation:动态添加到之前的类中
1
2
3
4
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
  • hook类对象的viewWillAppear:方法让其指向_objc_msgForward,动态添加aspects_viewWillAppear:指向最初的viewWillAppear:实现
2.4.3 Hook实例的方法

Aspects支持只hook一个对象的实例方法

只不过在第4步略有出入,当hook一个对象的实例方法的时候:

  • 新建一个子类,_Aspects_ViewController,并且按照上述的方式hook forwoardInvocation:
  • hook _Aspects_ViewController的class方法,让其返回ViewController
  • hook 子类的类元对象,让其返回ViewController
  • 调用objc_setClass来修改ViewController的类为_Aspects_ViewController

这样做,就可以通过object_getClass(self)获得类名,然后看看是否有前缀类名来判断是否被hook过了

2.4.4 其他

1) object_getClass/与self.class的区别

  • object_getClass获得的是isa的指向
  • self.class则不一样,当self是实例对象的时候,返回的是类对象,否则则返回自身。

比如:

1
2
3
4
5
6
7
8
TestClass * testObj = [[TestClass alloc] init];
//Same
logAddress([testObj class]);
logAddress([TestClass class]);

//Not same
logAddress(object_getClass(testObj));
logAddress(object_getClass([TestClass class]));

输出

1
2
3
4
2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930
2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930
2017-05-22 22:41:48.216 OCTest[899:25934] 0x107d10930
2017-05-22 22:41:49.061 OCTest[899:25934] 0x107d10908

2) Block签名

block因为背后其实是一个C结构体,结构体中存储着着一个函数指针来指向实际的方法体

Block的内存布局如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef NS_OPTIONS(int, AspectBlockFlags) {
AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25),
AspectBlockFlagsHasSignature = (1 << 30)
};
typedef struct _AspectBlock {
__unused Class isa;
AspectBlockFlags flags;
__unused int reserved;
void (__unused *invoke)(struct _AspectBlock *block, ...);
struct {
unsigned long int reserved;
unsigned long int size;
// requires AspectBlockFlagsHasCopyDisposeHelpers
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
// requires AspectBlockFlagsHasSignature
const char *signature;
const char *layout;
} *descriptor;
// imported variables
} *AspectBlockRef;

对应生成NSMethodSignature的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
AspectBlockRef layout = (__bridge void *)block;
if (!(layout->flags & AspectBlockFlagsHasSignature)) {
NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
void *desc = layout->descriptor;
desc += 2 * sizeof(unsigned long int);
if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
desc += 2 * sizeof(void *);
}
if (!desc) {
NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
const char *signature = (*(const char **)desc);
return [NSMethodSignature signatureWithObjCTypes:signature];
}

3) 效率

消息转发机制相对于正常的方法调用来说是比较昂贵的,所以一定不要用消息转发机制来处理那些一秒钟成百上千次的调用。

iOS12 XCode10 适配

Posted on 2018-11-03 | Edited on 2018-11-23 | In iOS

iOS12 XCode10 适配

1. libstdc++弃用 报错Undefined symbols

XCode10编译报错ndefined symbols for architecture XXX,如果你的工程中有libstdc++依赖(可从Linked Frameworks and Libraries 项查看),那么就会出现这类错误。

因为苹果在XCode10和iOS12中移除了libstdc++这个库,由libc++这个库取而代之,苹果的解释是libstdc++已经标记为废弃有5年了,建议大家使用经过了llvm优化过并且全面支持C++11的libc++库。

libstdc++.dylib是C++98版本的标准库实现动态库,而libc++.dylib是C++11版本的标准库实现动态库。libc++是一个更加新的C++标准库实现,它完全支持C++11标准。因此苹果弃用了libstdc++.dylib,这符合苹果一贯的作风。

解决方法

  • 最直接的是修改依赖库,支持libc++.dylib

  • 临时方法

    将libstdc++.dylib拷贝到 XCode中,共四个地方

    libstdc++.dylib下载地址

1
2
3
4
sudo cp CoreSimulator/* /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/
sudo cp MacOSX/* /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib/
sudo cp iPhoneOS/* /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/
sudo cp iPhoneSimulator/* /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/

2. UICollectionViewCell 高度计算不正确

更新 iOS12后,一定要检查所有用到UICollectionViewCell的界面,因为UICollectionViewCell可能出现高度计算不正确的现象。

iOS12对AutoLayout做出了性能优化,但是更新 iOS12后,发现一些UICollectionViewCell的高度不正确,一时间也调试不出什么问题,因此就采用手动计算高度暂时解决。

这里有一篇同样的问题,解决思路可供参考链接

解决方法

  • 1. 手动计算高度

  • 2. 忽略 contentView,直接把 subView 加到 cell 上

3. StatusBar 网络状态

如果app通过状态栏的网络状态指示器去判断手机当前联网状态,修改进行修改,因为iOS12 更改了StatusBar内部结构。

参考链接

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
+ (NSString *)getIphoneXNetWorkStates {    
UIApplication *app = [UIApplication sharedApplication];
id statusBar = [[app valueForKeyPath:@"statusBar"] valueForKeyPath:@"statusBar"];
id one = [statusBar valueForKeyPath:@"regions"];
id two = [one valueForKeyPath:@"trailing"];
NSArray *three = [two valueForKeyPath:@"displayItems"];
NSString *state = @"无网络";
for (UIView *view in three) {
//alert: iOS12.0 情况下identifier的变成了类"_UIStatusBarIdentifier"而不是NSString,所以会在调用“isEqualToString”方法时发生crash,
//修改前
// NSString *identifier = [view valueForKeyPath:@"identifier"];
//修改后
NSString *identifier = [[view valueForKeyPath:@"identifier"] description];
if ([identifier isEqualToString:@"_UIStatusBarWifiItem.signalStrengthDisplayIdentifier"]) {
id item = [view valueForKeyPath:@"_item"];

//alert: 这个问题和上边一样itemId是_UIStatusBarIdentifier 类型,不是string
NSString *itemId = [[item valueForKeyPath:@"identifier"] description];
if ([itemId isEqualToString:@"_UIStatusBarWifiItem"]) {
state = @"WIFI";
}
state = @"不确定";

} else if ([identifier isEqualToString:@"_UIStatusBarCellularItem.typeDisplayIdentifier"]) {
UIView *statusBarStringView = [view valueForKeyPath:@"_view"];
// 4G/3G/E
state = [statusBarStringView valueForKeyPath:@"text"];
}

}

return state;
}

iOS12新功能

1. 刘海屏判断

1
#define isNotchMobile ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? (CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size)||CGSizeEqualToSize(CGSizeMake(1242, 2688), [[UIScreen mainScreen] currentMode].size)||CGSizeEqualToSize(CGSizeMake(828, 1792), [[UIScreen mainScreen] currentMode].size)) : NO)
123

Yun

25 posts
7 categories
20 tags
© 2019 Yun
Powered by Hexo v3.7.1
|
Theme – NexT.Gemini v6.4.1