编译内联导致内存泄漏的问题定位修复

news2024/11/26 23:39:03

作者:0x264

问题

线上长时间存在一个跟异步 inflate 相关的量级较大的内存泄漏,如下所示:

第一次分析

从内存泄漏粗略看有几个信息:

  1. 被泄漏的Activity有很多,所以可能跟某个具体业务的关系不大
  2. 引用链特别短,并且可以看出 gc root 是 Java Frame 中的BasicInflater实例,然后它通过 mContext 字段持有了 Activity

从上面的这个信息推测导致内存泄漏的原因是:

  1. 业务代码触发了一些布局的异步 Inflate
  2. 当前页面退出,destroy
  3. 之前发出的异步 Inflate 请求还没有执行完,还在子线程中inflate,所以会短暂的持有 context
  4. 如果这个时候dump hprof 分析,就会发现 context(activity)泄漏了
public void runInner() {  
    InflateRequest request;  
    try {  
        request = mQueue.take();  
    } catch (InterruptedException ex) {  
        // Odd, just continue  
        Log.w(TAG, ex);  
        return;  
    }  

// 子线程需要把 mQueue 里面的 request 处理完,而request 持有 inflater,inflater 持有 context

    try {  
        request.view = request.inflater.mInflater.inflate(  
                request.resid, request.parent, false);  
    } catch (RuntimeException ex) {  
        // Probably a Looper failure, retry on the UI thread  
        Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"  
                + " thread", ex);  
    }  
    Message.obtain(request.inflater.mHandler, 0, request)  
            .sendToTarget();  
}  
  
@Override  
public void run() {  
    while (true) {  
        runInner();  
    }  
}

根据上面的分析,这种泄漏理论上是存在的,但是 inflate 一个layout一般很快,几毫秒、几十毫秒、最多几百毫秒,这种属于短时泄漏,而且时间特别短,影响不大,所以第一次简单看了下这个问题后觉得影响不大不必处理😂

第二次分析

虽然上面判断这个“影响不大,且泄漏时间很短”,但是每个版本都会触发报警,而且是量级很大的泄漏,于是继续排查一下这个泄漏是否有其他原因~

从内存泄漏的量级看,之前的判断似乎有点说不通:

  1. 泄漏时间窗口这么短,hprof 刚好就在这期间dump的概率极低,量怎会这么大?
  2. 短时泄漏问题其实很多,比如业务 postDelay 一个几秒的Runnable(持有外部类引用),在这期间 activity destroy了,也会出现短时泄漏,这个时间几秒,比 inflate 要长多了,而且业务上这类情况很多,但是线上抓到的这类情况很少。

因此第一次分析的情况似乎不太对,于是捞个 hprof 再分析一下看看:

我们先通过内存信息来验证下我们第一次分析的猜测原因(activity destroy的时候,异步 inflate 任务还没执行完成)是否正确:

  1. 看下 InflateThread 的 mQueue 中还有多少 InflateRequest 待处理:

捞了很多个hprof,结果都是如此令人惊讶:InflateRequest 队列都是空的,里面没有任务待处理!!!

  1. 再一次猜想:会不会是每次dump hprof的时候,刚好最后一个 InflateRequest 被从队列中取出来了,但是还没有执行完成呢?其实想想这种概率已经不能再低了,但是目前也没有其他怀疑点,换个角度看,假设是这种情况,会不会是某个布局有点问题,导致 inflate 耗时特别长,然后增加了被抓到的概率呢?

先来看下那个持有activity的 BasicInflater 信息:

我们知道LayoutInflater在inflate开始前会把当前要用的context存到他的 mConstructorArgs[0] 中,inflate 完成后再把 mConstructorArgs[0] 恢复,可以参考如下代码:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {  
    synchronized (mConstructorArgs) {  
        // ......
         
        Context lastContext = (Context) mConstructorArgs[0];  
        mConstructorArgs[0] = inflaterContext;  

		try {
		
         // ...... do inflate
         
        } finally {  
            // Don't retain static reference on context.  
            mConstructorArgs[0] = lastContext;  
            mConstructorArgs[1] = null;  
  
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);  
        }  
  
        return result;  
    }  
}

也就是说如果正在 inflate,mConstructorArgs[0]应该持有context,但是我们看到这个时候 BasicInflater 中的 mConstructorArgs 的2个element都是 null,也就是说当前它处于空闲状态,并非正在 inflate!!!

初步结论

根据目前的信息来看,泄漏的Activity是被一个空闲的 BasicInflater 持有的。

进一步排查,发现一个更奇怪的现象:在dump出来的 hprof中,AsyncLayoutInflater$BasicInflater 的实例数始终比 AsyncLayoutInflater 刚好多一个,而多出来的那一个就是导致 activity 泄漏的那个实例

从代码上看,AsyncLayoutInflater$BasicInflater 都是在 AsyncLayoutInflater的构造函数中创建的,按理说AsyncLayoutInflater$BasicInflater不会比AsyncLayoutInflater更多才对🤔️

public AsyncLayoutInflater(@NonNull Context context) {  
    mInflater = new BasicInflater(context);  
    mHandler = new Handler(mHandlerCallback);  
    mInflateThread = InflateThread.getInstance();  
}

难道导致泄漏的这个 BasicInflater 是其他地方创建出来的? 首先反射创建不大可能,因为这个类没有keep,那么最有可能的就是下面这个路径了:

@Override  
public LayoutInflater cloneInContext(Context newContext) {  
    return new BasicInflater(newContext);  
}

而这个方法似乎只在ViewStub中使用:

// layoutinflater
public final View createView(@NonNull Context viewContext, @NonNull String name,  
        @Nullable String prefix, @Nullable AttributeSet attrs)  
        throws ClassNotFoundException, InflateException {  
    // ... 
                final ViewStub viewStub = (ViewStub) view;  
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));  
    // ...
}

这个clone出来的 inflater 会被 ViewStub.mInflater 持有,但是从内存数据来看,它自己是gc root,并且没有其他对象持有它

而这个 BasicInflater 之所以是 gc root,是因为它是在当前Java frame的本地变量表中,再回头看一下相关代码:

public void runInner() {  
    InflateRequest request;  
    try {  
        request = mQueue.take();  
    } catch (InterruptedException ex) {  
        // Odd, just continue  
        Log.w(TAG, ex);  
        return;  
    }  
  
    try {  
        request.view = request.inflater.mInflater.inflate(  
                request.resid, request.parent, false);  
    } catch (RuntimeException ex) {  
        // Probably a Looper failure, retry on the UI thread  
        Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"  
                + " thread", ex);  
    }  
    Message.obtain(request.inflater.mHandler, 0, request)  
            .sendToTarget();  
}  
  
@Override  
public void run() {  
    while (true) {  
        runInner();  
    }  
}
第二次猜想

倘若 runInner()被内联到 run()中,那么正好就是这个情况,并且能解释上面的所有现象,于是找了个线下包看了下,结果惊呆了:没有内联。。。

无奈,似乎没有思路了,但是也不能不处理啊,翻了下业务代码后发现有个LiveAsyncLayoutInflater 它就是从 AsyncLayoutInflater中copy出来的,唯一的改动就是在 runInner() 中,取出 request 执行 inflate之前先判断一下 context(activity)是否已经destroy,如果是的话,直接丢弃这个 request。跟这个代码的作者聊了下,他这样做也是因为担心Activity销毁后还有没处理完的request。

第一次尝试修复

虽然我们上面分析过导致这个内存泄漏的原因似乎不是 因为“Activity销毁后还有没处理完的request”,但是我搜了一下发现却真没有跟 LiveAsyncLayoutInflater 相关的泄漏,业务用法也都一样。。。 没有其他思路,于是也这样改一下试试吧,起码不会有坏处。并且我们还加了一个逻辑: 监听Activity destroy,然后遍历 request 队列,把和其关联的 request 移除。这样的目的是因为有些 request 是用的 Application context 来处理的,只在 runInner 中判断,可能会影响后面 request 处理的及时性。

尝试修复上线后发现跟预期的基本“一致”,可以说是毫无作用 😂😂😂

第三次分析

尝试一下看看线下能不能复现,看看复现的场景是什么。

目前我们线下包在 activity destroy的时候都会去分析泄漏,于是改了下代码,当发现跟当前问题引用链一样的时候就将额外补充的一些信息一起上报上来排查。

然而用这个包测试了一段时间,没复现。。。然后又看了下线下的内存泄漏监控,这个每天也会上报不少的泄漏问题,结果这个问题一个都没有。。。

线上量很大,线下一个都没有,难道是某处逻辑线上包跟线下包不一样触发了这个问题?

回想一开始分析的时候有个判断:如果runInner被内联到run里面,那问题就可以解释,当时反编译已经排除了,但是忽然想到当时包用错了。。。拿的是线下包,线下包都是 fast 模式,是不会走 optimized 的,所以肯定不会内联。相关配置如下:

if (BuildContext.isFast) {
    // ...
    proguardFile 'proguard_not_opt.pro'
    // ...
}

// ... proguard_not_opt.pro
-dontoptimize
// ...

因为 optimize 特别耗时,线下包关闭也是很合理的。但是线上包是没有这个配置的,也就是打开了 optimize。另外混淆配置中没有关闭 unique method inline:

# 下面的没有配:
# -optimizations !method/inlining/unique

runInner 也只有一处调用,在打开optimize且没有禁止 unique method inline 时是可能inline的。于是找了个线上包再来看看,果然 内 联 了 !!!

既然内联了,那么 runInner 的本地变量表中的对象就被合到了 run 中,而 run 里面是个 while (true) 死循环,生命周期无限长,所以如果这里面的本地变量表中持有 BasicInflater 那么它就是gc root,并且进一步导致它的 context 泄漏,我们来看下AsyncLayoutInflater的这段代码:

.method public final run()V
.registers 6
.prologue
:catch_0
:goto_0
:try_start_0
iget-object v0, p0, LX/pJX;->LIZIZ:Ljava/util/concurrent/ArrayBlockingQueue;
invoke-virtual {v0}, Ljava/util/concurrent/ArrayBlockingQueue;->take()Ljava/lang/Object;
move-result-object v4
check-cast v4, LX/pJZ;
const/4 v3, 0x0
:try_end_9
.catch Ljava/lang/InterruptedException; {:try_start_0 .. :try_end_9} :catch_0
:try_start_9
iget-object v0, v4, LX/pJZ;->LIZ:LX/pJW;
iget-object v2, v0, LX/pJW;->LIZ:Landroid/view/LayoutInflater;
iget v1, v4, LX/pJZ;->LIZJ:I
iget-object v0, v4, LX/pJZ;->LIZIZ:Landroid/view/ViewGroup;
invoke-virtual {v2, v1, v0, v3}, Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;Z)Landroid/view/View;
move-result-object v0
iput-object v0, v4, LX/pJZ;->LIZLLL:Landroid/view/View;
:try_end_17
.catch Ljava/lang/RuntimeException; {:try_start_9 .. :try_end_17} :catch_17
:catch_17
iget-object v0, v4, LX/pJZ;->LIZ:LX/pJW;
iget-object v0, v0, LX/pJW;->LIZIZ:Landroid/os/Handler;
invoke-static {v0, v3, v4}, Landroid/os/Message;->obtain(Landroid/os/Handler;ILjava/lang/Object;)Landroid/os/Message;
move-result-object v0
invoke-virtual {v0}, Landroid/os/Message;->sendToTarget()V
goto :goto_0
.end method
  1. iget-object v2, v0, LX/pJW;->LIZ:Landroid/view/LayoutInflater; 知道 BasicInflater 被赋值到了 v2寄存器
  2. invoke-virtual {v2, v1, v0, v3}, Landroid/view/LayoutInflater;->inflate(ILandroid/view/ViewGroup;Z)Landroid/view/View; 通过 v2中的BasicInflater去inflate 布局
  3. v2寄存器没有复用,当通过 goto :goto_0 进行下一次循环,并且取出下一个 request,v2 被赋予下一个 BasicInflater 实例引用之前,v2 一直持有者上一个 BasicInflater 引用,而这个就是导致泄漏的引用。 当前处于while循环中,等待下一个request,跟我们上面分析的“导致泄漏的BasicInflater处于空闲状态一致”,并且长时间处于这个状态,所以抓到的概率就很大了。

为什么AsyncLayoutInflater$BasicInflater 的实例数始终比 AsyncLayoutInflater多一个呢?,原因是:AsyncLayoutInflater 的引用是存在 v0 寄存器中的,而v0寄存器被多次复用,所以AsyncLayoutInflater的引用并没有被一直持有。

到此问题基本就分析清楚了,但是还有一个遗留问题,上面提到的业务中copy出来的 LiveAsyncLayoutInflater为什么没有泄漏呢?原因是:

  1. 他的 BasicInflater 引用也是放到 v2寄存器中的
  2. 但是这个类中的方法有插桩,runInner被内联之后,插桩代码也被内联了,在进行while(true)的下一轮循环时,首先会去执行插桩代码,而插桩代码复用了v2寄存器,因此就不再持有BasicInflater的引用了,因此没有泄漏

修复

问题已经明确,修复就比较简单了,只要让 runInner 不内联就行了。而之所以它会被内联,是因为 proguard/R8 有个优化,如果某个方法只有一处调用(当然还要满足很多其他条件),那么就将它内联,并删除原方法。因此我们改一下,找一个其他地方调一下就可以规避,比如:

if (/* 此处返回 false,让if block不执行就行 */) {  
    runInner();// Make sure never reach here  
}

这样静态分析不是一处调用,不会内联,实际上也不会走到,也不影响逻辑。

不过线上我没有这样改,因为还有其他办法可以不用改代码:给 runInner keep 一下就也不会内联了,原因也好理解:如果方法被keep了,那么原方法不能删,而这个又不是个小方法,要是内联的话,字节码变大了,方法数也没少,必然负向了,那还内联干啥。

-keepclassmembers class androidx.asynclayoutinflater.view.AsyncLayoutInflater$InflateThread {  
    public void runInner();  
}

改了之后,泄漏解决了~😊

顺便提一句

runInner 里面判断context(activity)是否destroy,如果destroy的话,就拦截不处理这个 request还有个小麻烦:onInflateFinished 这个回调是否要触发?

public interface OnInflateFinishedListener {
    void onInflateFinished(@NonNull View view, @LayoutRes int resid,
                           @Nullable ViewGroup parent);
}
  1. 如果要回调onInflateFinished,那么 view 如何获取?null 肯定不行,因为原本接口中有 @NonNull,导致很多业务代码不会判空
  2. 不回调也不行,因为有些业务有个“优化”逻辑,如果上一个 inflate 没有回来,后续就走同步 inflate,所以如果不回调,相当于关闭了异步 inflate 功能。。。

所以当时我们加了个 onCancel 回调,业务可以在这里处理被拦截的情况:

public interface OnInflateFinishedListener {
    void onInflateFinished(@NonNull View view, @LayoutRes int resid,
                           @Nullable ViewGroup parent);

    /**
     * if context (activity) destroyed, InflateRequest will be cancel, and this method will be invoked.
     *
     * It can be invoked on different thread
     * @param resid
     */
    default void onCancel(@LayoutRes int resid) {}
}

比如:

override fun onCancel(resid: Int) {
    isAsyncInflating = false
}

但是这样也不优雅,而且也不是所有业务都知道有这么个 onCancel api,如果改成非default接口,又要改动很多地方的代码。

好在上面也看到了这个泄漏并非因为 “Activity destroy后还有没处理完的 InflateRequest,可能导致短暂泄漏”,拦截的必要性也不大。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/780427.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SkyWalking链路追踪中span全解

基本概念 在SkyWalking链路追踪中,Span(跨度)是Trace(追踪)的组成部分之一。Span代表一次调用或操作的单个组件,可以是一个方法调用、一个HTTP请求或者其他类型的操作。 每个Span都包含了一些关键的信息&am…

yaml语法详解

#kv #对空格的严格要求十分高 #注入到我们的配置类中 #普通的keyvalue name: qinjiang#对象 student:name: qingjiangage: 3#行内写法 student1: {name: qinjiang,age: 3}#数组 pets:- cat- dog- pigpet: [cat,dog,pig]yaml可以给实体类赋值 person:name: kuangshenage: 19happ…

css——box-sizing属性

含义 盒子模型由四部分构成,外边距(margin), 边框(border),内边距(padding), 内容content box-sizing 就是指定盒子的大小和结构的。 box-sizing: content-box; //默认值 内容真正宽度 设置的宽度box-sizing: border-box; // 内容真正宽度width 设置的width- 左右p…

LabVIEW可重入VI,VI模板和动态VI之间的差异

LabVIEW可重入VI,VI模板和动态VI之间的差异 应该在何时使用可重入VI、模板VI和动态调用VI?这三种类型之间有什么区别? 可重入VI 当想要同时运行同一VI的多个实例时,将使用可重入VI。当VI不可重入时,VI只有一个数据空…

浏览器对跨域请求携带Cookie的方法

文章目录 一、前后端协商配置1.1 前端页面搭建1.2后端服务器搭建 二、配置允许跨域浏览器三、Chrome浏览器安装ModHeader插件 企业开发时会分开发环境、测试环境以及生产环境,但是有的企业开发只有真正发布到线上的生产环境的流程才会严格配置,有的项目开…

C++线性技巧,STL

例题1&#xff1a;字串计算 样例输入 10101 样例输出 0 2 01 2 1 3 10 2 101 2 直接上代码&#xff1a; #include<iostream> #include<string> #include<map> using namespace std; map<string,int>mp;//用map存储每一个子串出现的次数 string str…

漏洞复现-yapi远程执行命令漏洞复现

目录 漏洞原理漏洞发现漏洞描述影响范围 yapi学习漏洞复现环境搭建exp 入侵检测与防御参考 漏洞原理 漏洞发现 查看issue2229 漏洞描述 网站开放注册功能时可随意注册&#xff0c;设置全局mock脚本可执行任意代码。 影响范围 Yapi < 1.9.2 yapi学习 YApi 是高效、易…

Docker(四)

文章目录 1. docker其他命令补充2. docker-registry使用3. docker-hub的使用4. 企业级私有仓库harbor4.1 harbor安装4.2 harbor配置https4.3 harbor常见使用4.3.1 harbor新建项目仓库4.3.2 harbor创建用户4.3.3 harbor仓库管理4.3.4 harbor复制管理4.3.5 harbor删除镜像 5. doc…

【JavaEE】Spring中注解的方式去获取Bean对象

【JavaEE】Spring的开发要点总结&#xff08;3&#xff09; 文章目录 【JavaEE】Spring的开发要点总结&#xff08;3&#xff09;1. 属性注入1.1 Autowired注解1.2 依赖查找 VS 依赖注入1.3 配合Qualifier 筛选Bean对象1.4 属性注入的优缺点 2. Setter注入2.1 Autowired注解2.2…

【漏洞复现】​金蝶云星空管理中心反序列化命令执行漏洞(RCE)

文章目录 前言声明一、系统简介二、漏洞描述三、影响版本四、漏洞复现五、整改意见 前言 ​金蝶云星空管理中心存在反序列化命令执行,攻击者可通过该漏洞获取敏感信息&#xff0c;进而接管服务器。 声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文…

Mybatis-plus 配置自定义sql(.xml文件)查询语句的步骤

这是使用Mybatis-plus 的自动生成实体类代码生成.xml文件&#xff0c; 所以他会在java目录下&#xff0c;不在resources目录下 如果在java目录下的xml文件&#xff0c;需要分别配置application.yml和pom.xml文件 application.yml 文件进行以下配置&#xff1a; mybatis-plus…

视频增强技术-对比度增强

在图像处理中&#xff0c;由于获取的图像质量不好&#xff0c;需要通过对比度增强来提升图片质量&#xff0c;主要解决的是由于图像灰度级范围较小造成的对比度较低的问题&#xff0c;作用是使图像的灰度级范围放大&#xff0c;从而让图像更加清晰。主要对比度增强方法包括线性…

CentOS 7.9 安装 mydumper(RPM方式)

链接&#xff1a;https://pan.baidu.com/s/1sGhtiKPOmJw1xj0zv-djkA?pwdtaoz 码&#xff1a;taoz 开始正文啦&#xff1a; rpm -ivh mydumper-0.14.5-3-zstd.el7.x86_64.rpm 问题如下&#xff1a; 解决&#xff1a; yum -y install epel-release yum install -y libzstd …

分布式消息流处理平台kafka(一)-kafka单机、集群环境搭建流程及使用入门

1.kafka概述 1.1 kafka的前世今生 kafka最初是LinkedIn的一个内部基础设施系统。最初开发的起因是&#xff0c;LinkedIn虽然有了数据库和其他系统可以用来存储数据&#xff0c;但是缺乏一个可以帮助处理持续数据流的组件。 所以在设计理念上&#xff0c;开发者不想只是开发一…

通过 EXPLAIN 分析 SQL 的执行计划

通过 EXPLAIN 分析 SQL 的执行计划 EXPLAIN SELECTleave_station_area_id,ROUND( ( SUM( station_dist ) / 1000 ) / ( SUM( station_travel_time ) / 60 ), 2 ) evnPeakAvgSpeedFROMV3_SHIFT_ANALYSISWHERESTAT_DATE DATE_SUB( CURRENT_DATE, INTERVAL 1 DAY )AND LEAVE_STA…

【计算机视觉 | 图像分割】arxiv 计算机视觉关于图像分割的学术速递(7 月 20 日论文合集)

文章目录 一、分割|语义相关(11篇)1.1 Two Approaches to Supervised Image Segmentation1.2 Boundary-Refined Prototype Generation: A General End-to-End Paradigm for Semi-Supervised Semantic Segmentation1.3 Source-Free Domain Adaptive Fundus Image Segmentation w…

STL:vector的使用(初识迭代器迭代器失效)

vector也是动态类型的顺序表&#xff0c;可以存储任意类型的元素 string是动态类型顺序表&#xff0c;只能存储char vector< char >字符数组 string 字符串字符串结尾有\0&#xff0c;而vector是一个泛型类型&#xff0c;不能因为字符串需要\0&#xff0c;而对每个类型最…

分布式光伏并网防孤岛保护装置AM5SE-IS

分布式光伏并网防孤岛保护装置AM5SE-IS 应用场景 防孤岛原理&#xff1a;防孤岛保护装置检测到并网点有逆功率、频率突变、 等异常数据时&#xff0c;即发生孤岛现象时&#xff0c;装置可配合断路器快速切除并网点&#xff0c;使本站与电网侧快速脱离&#xff0c;保证整个电站…

jmeter随记3:常用jmeter功能(附带场景)

常用jmeter功能&#xff08;附带场景&#xff09; 一、jmeter其他特性1、请求的接口有多个 且 域名相同2、 jmeter支持统一管理参数的设置a、创建HTTP Header Managerb、用户定义参数c、csv数据文件设置 3、接口a的返回值作为 接口b的入参a、 json提取器b、 正则表达式 4、if c…

【密码学】二、古典密码

古典密码 1.置换密码1.1列置换密码1.2周期置换密码 2.代换密码2.1单表代换密码2.1.1凯撒密码2.1.2仿射密码 2.2多表代换密码2.2.1维吉尼亚密码2.2.2普莱费尔密码 3.轮转密码3.1恩尼格玛密码机Enigma3.1.1Enigma加密3.1.1Enigma解密 4.古典密码的分类5.古典密码的统计分析5.1单表…