FyListen 在 MVP 架构中的内存优化表现

news2024/11/17 15:45:34

FyListen 在 MVP 中的内存优化表现

本文只是分享个人开源框架的内存优化测试,你可以直接跳到最后,参考内存泄漏的分析过程!

项目地址:
https://github.com/StudyNoteOfTu/fylisten2-alpha1

由于使用到 AOP,所以直接导入依赖是是用不了的。需要将该模块和主模块进行合并,并做AspectJ的配置。

FyListen是否能够应用到项目中,需要测试。本文测试的结果为:【可以使用,而且符合开闭原则】

本次实验分析,我选择分析它在MVP架构中的内存优化表现,我将通过以下场景过程进行模拟:

  1. Activity 实现了 LifecyclePublisher 接口,并将 onDestroy() 生命周期暴露出去

  2. Presenter 层使用 RxJava 来控制 Model层和View层

  3. Presenter 层的 Rxjava 利用 FyListen 对传入的 View层进行生命周期监听

    1. compose()操作符,发现生命周期结束,立马执行 dispose(),停止上游的异步任务,最快速度释放资源
  4. View的任务是:从网络上下载图片,并保存到本地

    1. Model层1:retrofit从网络下载图片(模拟卡顿10s)
    2. Model层2:引用Context,进行本地文件写入
  5. RxJava 极大地化简了 Model 层代码

    1. 如果让Model层自行做异步,再通过接口回调,则需要在Model层的接口中加上一个类似:

      public interface IDownloadModel{
          public interface OnDownloadListener{
              void onFinish(String path);
          }
      }
      
    2. 由于使用RxJava,自动切换到子线程执行,所以完全可以让 Model 层的方法进行同步的返回,只需要由 Presenter 层将最后的结果回调给 View 层(如果View层没有销毁的话/dispose()没有被调用)

同时,为了尽可能地简化代码,以表现出 FyListen 生命周期监听的优越性,我们不做:

  1. 不在View层对Presenter进行解绑 - 减少了 View 层 onDestroy() 中的代码量
  2. 不在Presenter层对FyListen做发布者释放
    1. 和 unregisterPublisher() 方法的设计初衷保持一致:这个方法只用来发布者主动要求不再发布,不代表发布者生命周期结束!

1. 代码结构:

请添加图片描述

Model层

  1. FileStoreModelImpl - 本地文件写入工具,负责将下载图片的byte[]写入本地文件 - 模拟本地io

    public class FileStoreModelImpl implements IFileStoreModel {
    
        FileOutputStream fos;
    
        /**
         * 取消文件存储或者读取的工作
         */
        @Override
        public void cancel() {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 开始文件存储
         */
        @Override
        public String storeFile(Context context, String filename, String filepath, byte[] filebytes) {
            try {
                //由于读写暂时没用到context,我们使用是GCROOT的局部变量,来对context作一个强引用
                Context c = context;
                File file = new File(filepath+File.separator+filename);
                if (file.exists()){
                    file.delete();
                }
                File parentFile = file.getParentFile();
                if (!parentFile.exists()){
                    parentFile.mkdirs();
                }
    			//本地文件写入
                file.createNewFile();
                fos = new FileOutputStream(file);
                fos.write(filebytes);
                fos.flush();
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return filepath+File.separator+filename;
        }
    }
    
    
  2. PictureDownloadImpl - 网络图片下载工具 - 模拟耗时的网络请求

    public class PictureDownloadImpl implements IPictureDownloadModel {
    	//将 Observable交出去,让presenter来调度该异步任务
        @Override
        public Observable<ResponseBody> download(String url) {
            //使用retrofit进行下载
            Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl("https://img-blog.csdnimg.cn/")
                    .addConverterFactory(GsonConverterFactory.create())
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .build();
            IPictureDownloadService downloadServiceProxy = retrofit.create(IPictureDownloadService.class);
    
            //模拟网络卡顿,等待时间长达1000秒
            try {
                Thread.sleep(1000_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            return downloadServiceProxy.download(url);
    
        }
    	//由于将异步任务交出有 Presenter层来管理,所以observable任务的停止有rxjava来控制,这里不作实现
        @Override
        public void cancel() {
        }
    }
    

Presenter层

  1. BasePresenter - 绑定的同时开启生命周期监听,当LifecyclePublisher生命周期来到 onDestroy()的时候,进行解绑:

    public class BasePresenter<T> implements Listener {
    
        //所有presenter都需要传入IView 所以我们抽出Base层 并且用弱引用来绑定IView
    
        //解决内存泄漏问题,我们暂时采用弱引用方式
        // 还是得等到内存不足的时候才让gc来收
        protected WeakReference<T> mViewRef;
    
        //根除内存泄漏 --- 绑定
        //进行绑定
        public void attachView(T view){
            mViewRef = new WeakReference<T>(view);
            //进行监听,监听目标退出时,自动回调并进行解绑!!!
            if (view instanceof LifecyclePublisher){
                //如果是个生命周期发布者,就监听它
                //如果项目中并没有使用LifecyclePublisher,即view没有实现LifecyclePublisher,就不会进到这里
                FyListen.registerListenerAnyway((LifecyclePublisher)view,this);
            }
        }
        
        //进行解绑
        public void detachView(){
            //手动地打断弱引用,而非让gc来
            mViewRef.clear();
        }
    
        /**
         * 监听到onDestroy()的时候,自动解绑
         * @param p 生命周期发布者
         */
        @Override
        public void onDestroy(LifecyclePublisher p) {
            detachView();
        }
    
        @Override
        public void onError(LifecyclePublisher p, String error) {
    		//一般不会使用到,这是LifecyclePublisher绑定出问题时候的回调
        }
    }
    
  2. PictureDownloadPresenter - 具体表现层,负责控制Model层进行图片下载与保存,并将结果回调给View层。

    1. Model层分别为下载器和文件存储工具,将任务细化解耦,便于后期维护
    2. 使用 RxJava 按顺序执行下载和存储这两个步骤,如果任务成功完成,将回调,如果任务中断,则直接结束方法
    public class PictureDownloadPresenter<T extends IPictureDownloadView> extends BasePresenter<T>  {
    
        //下载器
        IPictureDownloadModel pictureDownloadModel = new PictureDownloadImpl();
    
        //文件工具-存储byte[]或者stream到本地,再次之前,请自行处理权限申请
        IFileStoreModel fileModel = new FileStoreModelImpl();
    
        //presenter发起电影下载以及文件存储工作,任务分工明确,使用两个model
        //rxjava将两个先后执行的异步任务整合起来,得到最终结果后,回到主线程,回调UI
        public void downloadPicture(String url) {
            if (mViewRef.get() != null) {
                mViewRef.get().beginDownload();
                if (pictureDownloadModel != null && fileModel != null) {
                    //开启监听,以及线程切换
                    //1.执行下载任务
                    //2.下载任务执行之后,保存文件
                    //3.主线程回调任务结果
                    // compose(CommonTransformer.listen(mViewRef.get())):
                    // 传入的mViewRef.get()如果是LifecyclePublisher,就会进行生命周期监听注册
                    //当view的onDestroy()被监听到时,会自动调用dispose()停止rxjava的工作
                    //如果不是,就自行根据原项目结构进行内存泄漏处理
                    Disposable subscribe = Observable
                        .create((ObservableOnSubscribe<String>) emitter -> emitter.onNext(url))
                        .flatMap(new Function<String, Observable<String>>() {
                            @Override
                            public Observable<String> apply(String s) throws Exception {
                                return pictureDownloadModel.download(s)
                                    .map(new Function<ResponseBody, String>() {
                                        @Override
                                        public String apply(ResponseBody responseBody) throws Exception {
                                            //获取返回结果中的文件,通过filemodel进行存储,分工明确
                                            byte[] bytes = responseBody.bytes();
                                            //这些model都同步执行任务,提交异步交给rxjava来完成
                                            String filePath = Environment.getExternalStorageDirectory().getPath();
                                            String fileName = "test.jpg";
                                            return fileModel.storeFile((Context) mViewRef.get(), fileName, filePath, bytes);
                                        }
                                    });
                            }
                        }).compose(CommonTransformer.listen(mViewRef.get()))
                        .subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread())
                        .subscribe(new Consumer<String>() {
                            @Override
                            public void accept(String s) throws Exception {
                                if (mViewRef.get() != null) {
                                    //主线程回调下载结果(返回文件下载路径)
                                    mViewRef.get().pictureDownloaded(s);
                                }
                            }
                        });
                }
            }
        }
    
    
    }
    
  3. RxJava使用 compose()操作符来插入生命周期监听

    public class CommonTransformer<T> implements Listener, ObservableTransformer<T,T> {
        CompositeDisposable compositeDisposable = new CompositeDisposable();
        //rxjava监听到生命周期发布者发起了 onDestroy
        //就会回调到这里,将rxjava的任务取消!
        @Override
        public void onDestroy(LifecyclePublisher p){
            Log.e("TAG","publisher ondestroyed");
            if (!compositeDisposable.isDisposed()){
                compositeDisposable.dispose();
            }
        }
    
        @Override
        public void onError(LifecyclePublisher p, String error) {
            Log.e("TAG","publisher error");
        }
    
        @Override
        public ObservableSource<T> apply(Observable<T> upstream) {
            return upstream.doOnSubscribe(new Consumer<Disposable>() {
                @Override
                public void accept(Disposable disposable) throws Exception {
                    compositeDisposable.add(disposable);
                }
            });
        }
        public static <T> CommonTransformer<T> listen(Object o){
            CommonTransformer<T> transformer = new CommonTransformer<>();
            if (o instanceof LifecyclePublisher){
                //如果object是lifecyclePublisher,就注册,否则什么也不做
                Log.e("TAG","listen to lifecycle publisher");
                FyListen.registerListenerAnyway((LifecyclePublisher) o,transformer);
            }
            return transformer;
        }
    }
    

View层

常见MVP架构,通常会使用BaseActivity来进行Presenter的绑定和解绑,由于 FyListen 符合设计思想:对拓展开放,对修改关闭。这表现在:

  1. 只需要在原项目的BaseActivity中实现LifecyclePublisher这个空接口
  2. 在onDestry()上填上@LifecycleTrace(Status.ON_DESTROY) 注解

对原本项目几乎没有任何修改!

  1. BaseActivity - MVP架构下的Activity基类

    public abstract class BaseLifecyclePublisherActivity<V, T extends BasePresenter<V>>  extends AppCompatActivity implements LifecyclePublisher {
    
        //表示层的引用
        public T mPresenter;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //由于监听者使用的是 registerListenerAnyway()
            // 所以甚至不需要发布者主动进行注册
            // 更加符合了对修改关闭,对拓展开放的设计原则
    //        FyListen.registerPublisher(this);
            mPresenter = createPresenter();
            mPresenter.attachView((V)this);
        }
    
        protected abstract T createPresenter();
    
        //对外暴露onDestroy()的生命周期回调
        @LifecycleTrace(Status.ON_DESTROY)
        @Override
        protected void onDestroy() {
            super.onDestroy();
            //没有FyListen的时候,未解决内存泄漏
            //你需要手动在这里解绑
            //mPresenter.detachView();
        }
    
    }
    
  2. MainActivity - 下载图片并展示的活动

    一个View对应一个Presenter,一个Presenter可以根据业务数据要求,找到不同的Model层进行数据获取,并将获取到的数据处理完后,回调View层更新UI的接口。通过RxJava,我们不再需要在View层写类似 runOnUIThread()的代码。

    public class MainActivity extends BaseLifecyclePublisherActivity<IPictureDownloadView, PictureDownloadPresenter<IPictureDownloadView>> implements IPictureDownloadView,LifecyclePublisher {
    
        Button btnDownload;
    
        ImageView ivPicture;
    
        //图片地址
        String picUrl = "eb102bfc59ae457798bbf13c381a4af1.jpeg";
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initViews();
    
        }
    
        //绑定表示层
        //如果未来表示层逻辑需要替换,在这里替换需要的表示层即可
        @Override
        protected PictureDownloadPresenter<IPictureDownloadView> createPresenter() {
            return new PictureDownloadPresenter<>();
        }
    
        private void initViews() {
            btnDownload = findViewById(R.id.btn_download);
            btnDownload.setOnClickListener(v->{
                mPresenter.downloadPicture(picUrl);
            });
            ivPicture = findViewById(R.id.imageview);
        }
    
    
    
        //----------UI 回调---------------
        @Override
        public void pictureDownloaded(String filepath) {
            //假设我们下载了一个图片,我们把它展现出来
            Glide.with(this).load(filepath).into(ivPicture);
        }
    
        @Override
        public void beginDownload() {
            //通知用户开始下载了!
            Toast.makeText(this, "开始下载图片,并存储到本地", Toast.LENGTH_SHORT).show();
        }
    }
    

2. 测试开始 - 测试成功

我们从LauncherActivity进入到MainActivity后,点击按钮进行下载图片,如果一直等待,图片将会展示出来,这是正常运行的情况:

请添加图片描述

为了测试在图片下载过程中,点击返回键,Activity是否能够正常被释放,我将图片下载的延时设置为100s,我们来做以下操作:

  1. 点击下载图片
  2. 点击返回键
  3. 查看Profiler中内存情况:
    1. 点击下载图片时候的内存情况
    2. 点击返回后的内存情况
    3. 主动GC后的内存情况

1. 点击下载图片时候的内存情况:

由于MainActivity在前台运行,所以自然没有被释放

请添加图片描述

2. 在图片还在下载过程中(模拟1000秒等待),点击返回

我们观察到,虽然点了返回,但是MainActivity仍然没有被释放:

请添加图片描述

我们注意到,如果一个对象只被弱引用所持有的,也不会立即被释放,需要等到GC到来的时候,才被释放。我们来看一下 FyListen 是否成功将其他引用全部断开:

请添加图片描述

可以看到,现在剩下唯一的GCROOT只有 WeakHashMap 对它持有引用,而且是弱引用。我们可以等待GC的到来,也可以通过下面这个按钮让GC提前到来:(我在这里卡了很久,一直在想为什么WeakHashMap没有释放对MainActivity的引用,后来才想起来,这只是个弱引用,只是GC还没来而已!)

请添加图片描述

通知GC清理战场后,图片还没下载好,但是我们惊喜的发现,Activity被释放了!

请添加图片描述

本人又进行了额外强度大一点的测试:连续开关MainActivity多次,导致内存中有很多的MainActivity实例,通知GC时,全都被回收,过程同上,不贴图了。

3. 为什么会这样呢?

WeakHashMap的原理我后续会另外整理一份文档和大家分享。我们只需要知道,WeakHashMap对key是个弱引用,key被回收后,value会在 get()、put()等方法中被主动置空清除。

我们主要来分析 FyListen 作为核心角色,在这里面发挥的作用,具体使用参考FyListen 的接口文档:

1. 代码优化

FyListen 可以被用在原本就构建好的MVP工程,你只需要做两件事:

  1. 让BaseActivity或者Activity实现 LifecyclePublisher 接口
  2. 在Presenter层实现一个监听者,这里的可能性很多,我上面是在一个RxJava的项目中的表现,但这并不影响没有使用 RxJava 框架的项目,你只需要做到:实现一个Listener接口的实例,让它监听View层的LifecyclePublisher实例
    1. 例如本文分析时,通过Listener实例,通过dispose()让RxJava的异步任务停止,从而释放引用
    2. 你也可以在Presenter层任意一个合适的地方,对View层注册监听。
    3. 如果你的项目比较特殊,View和Presenter是多对多,FyListen的回调中也明确了是谁发来的生命周期通知。
  3. 你不再需要在 BaseActivity 的 onDestroy() 中对 Presenter层进行解绑,所有解绑工作都放在了 Presenter层的Listener实例的 onDestroy(LifecyclePublisher p)生命周期回调之中

2. 优化原理

首先,FyListen的监听者可以主动要求实现了 LifecyclePublisher接口的发布者进行生命周期发布注册,这使得你不用在 BaseActivity 的onCreate() 中加入registerPublisher()这串代码进行发布者注册。(对修改关闭设计)

//监听者在注册监听时,只需要有发布者的引用,就可以让发布者被动的去注册生命周期发布。
//如果这个发布者此时已经被释放了,将不会注册成功,并回调监听者的 onError()方法
FyListen.registerListenerAnyway((LifecyclePublisher) o,listener);

其次,FyListen由于监听到了LifecyclePublisher的onDestroy()生命周期,会通知所有仍然存活着的监听者。监听者们可以通 onDestroy(LifecyclePublisher p)的回调,判断是否需要退出,如果要退出,执行一些任务关闭的方法:

例如本例中,监听到onDestroy,主动调用dispose()关闭 RxJava 的工作

public class CommonTransformer<T> implements Listener, ObservableTransformer<T,T> {
    //作为Listener,发布者通知 ON_DESTROY 的时候,回调到这个方法
    @Override
    public void onDestroy(LifecyclePublisher p){
        //通过 dispose() 来关闭rxjava的工作
        if (!compositeDisposable.isDisposed()){
            compositeDisposable.dispose();
        }
    }
}

除了关闭执行的任务,还需要在FyListen管理着中,将View层的引用断开,但是这个步骤,由WeakHashMap帮我们完成了,因为本身就是弱引用。

需要注意的是,本项目中,认为Model层是长生命周期,View层是短生命周期,所以不需要处理View对Model层的引用(准确来说,是不需要处理 FyListen的map对Model层的强引用,Model层的引用会在View层释放之后的某一个时间被回收。如果有性能要求,你也可以在监听者的onDestroy()中,进行解绑监听,提前释放引用)。如果你的项目中,View层是长生命周期,而Model层工具是短声明周期,请注意调用 unregisterListener(LifecyclePublisher target, Listener listener),将Model层解放出来,使之可以被回收!

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

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

相关文章

【嵌入式开发】microcom安装与使用

microcom安装与使用1.安装2.使用3.用法4.测试三级目录1.安装 sudo apt-get install microcom -yQ&#xff1a;报错E: 在更改保留软件包的同时使用了 -y 选项&#xff0c;但没有搭配 --allow-change-held-packages. A&#xff1a;sudo apt-get install microcom -y --allow-cha…

软件测试面试复述,想知道你面试不过的原因吗?

最近有机会做一些面试工作&#xff0c;主要负责面试软件测试人员招聘的技术面试。 之前一直是应聘者的角色&#xff0c;经历了不少次的面试之后&#xff0c;多少也积累一点面试的经验&#xff0c;现在发生了角色转变。初次的面试就碰到个工作年限比我长的&#xff0c;也没有时…

PowerShell Install Office 2021 Pro Plus Viso Professional

前言 微软Office在很长一段时间内都是最常用和最受欢迎的软件。从小型创业公司到大公司,它的使用比例相当。它可以很容易地从微软的官方网站下载。但是,微软只提供安装程序,而不提供完整的软件供下载。这些安装文件通常比较小。下载并运行后,安装的文件将从后端服务器安装M…

5.1配置IBGP和EBGP

5.2.1实验1&#xff1a;配置IBGP和EBGP 实验目的 熟悉IBGP和EBGP的应用场景掌握IBGP和EBGP的配置方法 实验拓扑 实验拓扑如图5-1所示&#xff1a; 图5-1&#xff1a;配置IBGP和EBGP 实验步骤 IP地址的配置 R1的配置 <Huawei>system-view Enter system view, return …

Python每日一练(20230218)

目录​​​​​​​ 1. 旋转图像 2. 解码方法 3. 二叉树最大路径和 1. 旋转图像 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在原地旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像…

[LeetCode 1237]找出给定方程的正整数解

题目描述 题目链接&#xff1a;[LeetCode 1237]找出给定方程的正整数解 给你一个函数 f(x, y) 和一个目标结果 z&#xff0c;函数公式未知&#xff0c;请你计算方程 f(x,y) z 所有可能的正整数 数对 x 和 y。满足条件的结果数对可以按任意顺序返回。 尽管函数的具体式子未知…

Vue:@font-face引入外部字体

在项目开发中&#xff0c;我们经常会遇到想要优化字体font-family的问题&#xff0c;如下为默认字体样式&#xff0c;在大屏项目中看起来似乎有些呆板。 默认字体效果默认font属性尽管我们可以使用web安全字体&#xff0c;但是大多数场景下&#xff0c;例如&#xff1a;对于电子…

IOT2.5|第1章嵌入式系统概论|操作系统概述|嵌入式操作系统

目录 第1章&#xff1a; 嵌入式系统概论 1.嵌入式系统发展史 2.嵌入式系统定义* 3.嵌入式系统特点* 4.嵌入式处理器的特点 5.嵌入式处理分类 6.嵌入式系统的应用领域及嵌入式系统的发展趋势 第8章&#xff1a;Linux内核配置 1.内核概述 2.内核代码结构 第1章&#xf…

Linux内核CPU可运行进程队列的负载均衡

前面主要是学习进程的调度管理&#xff0c;默认都是在单CPU上的调度策略&#xff0c;在O(1)调度后&#xff0c;为了减小CPU之间的干扰&#xff0c;就会为每个CPU上分配一个任务队列&#xff0c;运行的时候可能会出现有的CPU很忙&#xff0c;有的CPU很闲&#xff0c;为了避免这个…

Vue:filters过滤器

日期、时间格式化是Vue前端项目中较为常遇到的一个需求点&#xff0c;此处&#xff0c;围绕Vue的过滤器来介绍如何更为优雅的解决此类需求。 过滤器filters使用注意点 Vue允许开发者自定义过滤器&#xff0c;可以实现一些常见的文本格式化等需求。 使用时要注意的点在于&#…

[软件工程导论(第六版)]第1章 软件工程学概述(复习笔记)

文章目录1.1 软件危机1.1.1 软件危机的介绍1.1.2 产生软件危机的原因1.1.3 消除软件危机的途径1.2 软件工程1.2.1 软件工程的介绍1.2.2 软件工程的基本原理1.2.3 软件工程方法学1.3 软件生命周期组成1.4 软件过程概念1.4.1 瀑布模型1.4.2 快速原型模型1.4.3 增量模型1.4.4 螺旋…

Windows系统扩充C盘空间系列方法总结

目录前言方法一 使用自带的Windows的DiskPart扩充C盘1. 打开cmd2.三步命令方法二&#xff1a;使用Windows系统内置磁盘管理扩展C盘方法三. 使用专业磁盘分区工具总结前言 本教程是总结Windows系统进行C盘&#xff08;系统盘&#xff09;扩充空间的系列方法&#xff0c;一般来讲…

VSCode远程调试Linux代码,python解释器配置

安装插件并配置 安装后找到插件图标&#xff0c;点击 点击SSH上的 号 在弹出框中输入命令&#xff1a;ssh usernameip -p port username: 远程服务器的用户名 ip&#xff1a; 远程ip port&#xff1a;端口号&#xff0c;没有可以不用 输入完毕后点击enter 选择ssh配置文件保存…

AI_News周刊:第二期

2023.02.13—2023.02.17 1.ChatGPT 登上TIME时代周刊封面 这一转变标志着自社交媒体以来最重要的技术突破。近几个月来&#xff0c;好奇、震惊的公众如饥似渴地采用了生成式人工智能工具&#xff0c;这要归功于诸如 ChatGPT 之类的程序&#xff0c;它对几乎任何查询做出连贯&a…

ArcGIS:模型构建器实现批量按掩膜提取影像

用研究区域的矢量数据来裁剪栅格数据集时&#xff0c;一般我们使用ArcGIS中的【按掩膜提取工具】。如果需要裁剪的栅格数据太多&#xff0c;处理起来非常的麻烦&#xff0c;虽然ArcGIS中有批处理的功能&#xff0c;但是还是需要手动选择输入输出数据。 如下图&#xff0c;鼠标…

HTTPS协议原理---详解六个加密方案

目录 一、HTTPS 1.加密与解密 2.我们为什么要加密&#xff1f; 3.常见加密方式 ①对称加密 ②非对称加密 4.数据摘要 5.数字签名 二、HTTPS的加密方案 1.只是用对称加密​ 2.只使用非对称加密 3.双方都使用非对称加密 4.非对称加密&#xff0b;对称加密 中间人攻…

kubernetes教程 --Pod控制器详解

Pod控制器详解 介绍 Pod是kubernetes的最小管理单元&#xff0c;在kubernetes中&#xff0c;按照pod的创建方式可以将其分为两类&#xff1a; 自主式pod&#xff1a;kubernetes直接创建出来的Pod&#xff0c;这种pod删除后就没有了&#xff0c;也不会重建控制器创建的pod&am…

字母消消乐游戏(C语言版本_2023首篇新作)

上一篇: 2022圣诞树&#xff08;C语言摇钱树版本&#xff09; 逐梦编程&#xff0c;让中华屹立世界之巅。 简单的事情重复做,重复的事情用心做,用心的事情坚持做&#xff1b; 文章目录前言一、图形库准备1.EasyX绘图库下载2.EasyX作用二、游戏内画面展示1.游戏开场介绍2.游戏画…

5年软件测试工程师分享的自动化测试经验,一定要看

今天给大家分享一个华为的软件测试工程师分享的关于自动化测试的经验及干货。真的后悔太晚找他要了&#xff0c; 纯干货。一定要看完&#xff01; 1.什么是自动化测试&#xff1f; 用程序测试程序&#xff0c;用代码取代思考&#xff0c;用脚本运行取代手工测试。自动化测试涵…

0.2opencv库源码编译

如何编译opencv库源码 大家好&#xff0c;我是周旋&#xff0c;感谢大家学习【opencv源码解析】系列&#xff0c;本系列首发于公众号【周旋机器视觉】。 上篇文章我们介绍了如何配置opencv环境&#xff0c;搞清了opencv的包含目录include、静态库链接以及动态库链接的作用。 【…