Android Handler用法

Android Handler用法

  • 为什么要设计Handler机制?
  • Handler的用法
    • 1、创建Handler
    • 2、Handler通信
      • 2.1 sendMessage 方式
      • 2.2 post 方式
  • Handler常用方法
    • 1、延时执行
    • 2、周期执行
  • HandlerThread用法
    • 主线程-创建Handler
    • 子线程-创建Handler
  • FAQ
    • Message是如何创建
    • 主线程中Looper的轮询死循环为何没有阻塞主线程
    • Handler内存泄漏问题及解决方案
    • Handler为什么会持有Activity的引用?

参考文档:https://developer.android.google.cn/guide/components/processes-and-threads?hl=zh-cn#java
在这里插入图片描述

Android UI操作并非线程安全。因此,请不要在工作线程(即子线程)中操纵界面。您可以通过界面线程对界面进行所有操作。Android 的单线程模型有以下两条规则:

  • 请勿阻塞UI线程
  • 请勿从UI线程以外的线程进行UI操作

为什么要设计Handler机制?

一个耗时的操作,比如需要联网读取数据或者读取本地较大的一个文件或者数据库查询,如果把这些操作放在主线程中,界面会出现假死现象, 如果5秒钟还没有完成的话,阻塞了UI线程会收到Android系统的一个错误提示 “强制关闭”,这糟糕的体验会造成严重的损失,所以不能阻塞UI线程,以确保应用界面的响应能力。
这时候就需要把这些耗时的操作,放到子线程中去执行,但是在子线程执行完以后,又需要将结果更新到UI页面,此时就涉及到子线程UI更新的操作。那为什么安卓规定不能在子线更新UI? 最根本的原因是多线程并发的问题,假设在一个Activity中,有多个线程去更新UI,并且都没有加锁机制,就会产生更新界面错乱,所以子线程中更新UI是不安全的,而在一个线程中更新UI是变得比较合理,那自然就是主线程了,当然主线程也可以叫UI线程了。
综上所述,所以安卓应用需要这样的机制:

  1. 只能在主线程更新UI,并且所有更新UI的操作,都要在主线程的消息队列当中去轮询处理。
  2. 耗时的操作在子线程,更新UI的操作在主线程,他们之间的交互需要实现线程间通信。

Handler用于实现多线程通信和管理UI线程的消息处理。它为开发者提供了一种简单有效的方法来处理异步任务和更新UI界面。
当然,Android已经提供了多种从其他线程访问UI线程的方式。以下列出了几种有用的方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • Handler.post(Runnable)

Activity.runOnUiThread(Runnable)方法:如果在UI线程,直接更新UI;如果非UI线程,使用的是post。

new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();runOnUiThread(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}});}
}).start();

View.post(Runnable) 方法:

new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();findViewById(R.id.button_send).post(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}});}
}).start();

Handler.post(Runnable)方法

private Handler mHandler =new Handler();  //默认主线程
...
new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();mHandlers.post(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}});}
}).start();

随着操作变得越来越复杂,这种代码也会变得复杂且难以维护。为了处理与工作线程(子线程)的更复杂的交互,建议在UI线程中使用 Handler 处理从子线程传送的消息。

工作原理:Handler运行在主线程(UI线程)中, 它与子线程可以通过Message对象来传递数据和消息,然后把这些消息放入主线程队列中,按照先进先出的原则配合主线程逐个进行更新UI的操作。另外,驱动这套机制运行的核心是Looper.loop() 里的死循环。

@Override
public void run() {mTid = Process.myTid();Looper.prepare(); // 创建Looper的子线程,然后创建MessageQueue,最后进行绑定。synchronized (this) {mLooper = Looper.myLooper();  // 获取准备好的Looper对象notifyAll();}Process.setThreadPriority(mPriority);onLooperPrepared();Looper.loop();  // 启动LoopermTid = -1;
}

Handler的用法

下面的这种写法是可以实现刷新UI的功能,但是它违背了单线程模型:Android UI操作并不是线程安全的,并且这些操作必须在UI线程中执行。但是如果与UI无关的操作如上传/下载,数据库,可以使用此种写法。

new Thread(new Runnable() {@Overridepublic void run() {textviewCurrentStatus.invalidate();}
}).start();

优点:避免了创建新线程带来的线程切换开销。
缺点:Handler发送的消息会保存在消息队列中,如果一直发送大量的消息,将可能导致消息队列过长,影响应用的响应能力。LiveData和RxJava等现在比较流行的框架,能够替代Handler实现更优异的异步编程和UI更新。

1、创建Handler

一般在主线程中创建Handler如下:

private Handler mHandler = new Handler(Looper.getMainLooper()) {public void handleMessage(Message msg) {switch (msg.what) {case 1:// todo 在主线程更新UIdoSomethingsAboutUI();break;}super.handleMessage(msg);}
};

在子线程中创建Handler,最好使用HandlerThread。如果不使用HandlerThread,必须要手动启动Looper,具体如下:

private Handler mHandler;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程进行耗时操作doSomethings();Looper.prepare();  // 准备LoopermHandler= new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {// todo 实际还是在主线程更新UIdoSomethingsAboutUI();return false;}});Looper.loop();    // 启动Looper}}).start();
}

2、Handler通信

使用Handler通信,有两种方法将消息加入消息队列中:post()方法和sendMessage()方法。
– sendMessage()方法是异步方式。即加入消息到消息队列中后,不会立即执行此消息,而是等待消息阻塞的处理程序返回。 — 存疑
– post()方法是同步方式。即加入消息到消息队列中后,会直接处理此消息,不必等待消息阻塞的处理程序返回。— 存疑

2.1 sendMessage 方式

首先,需要定义好handler需要处理的业务。

private Handler myHandler = new Handler(Looper.getMainLooper()) {public void handleMessage(Message msg) {switch (msg.what) {case 1:// todo 在主线程更新UIdoSomethingsAboutUI();break;}super.handleMessage(msg);}
};

在需要的时机,发送消息触发handler调用业务。

new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();Message message = new Message(); //或者Message msg = mHandler.obtainMessage();message.what = 1;msg.arg1 = 100;msg.obj = "message content";myHandler.sendMessage(message);}
}).start();

另外一种常见写法,本质都是一样的:

private Handler mHandler = new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {switch (msg.what){case 1:// todo 在主线程更新UIdoSomethingsAboutUI();break;}return false;}
});
....
new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();Message message = mHandler.obtainMessage(1);mHandler.sendMessage(message);}
}).start();

2.2 post 方式

通过调用 Handler 的 post() 方法,将 Runnable 对象通过 MessageQueue 发送到消息队列中,即可让主线程处理相应的操作。这种方式可以用于解决在子线程中不能进行 UI 操作的问题,例如我们可以在子线程中通过 post 方式将更新 UI 的任务传递到主线程来完成,这样就不会因为在非 UI 线程中更新 UI 而导致 ANR(Application Not Responding)了。

private Handler mHandler =new Handler();
...
mHandler.post(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}
});

注意:post方法虽然发送的是一个实现了Runnable接口的类对象,但是它并非创建了一个新线程,而是执行了该对象中的run方法。也就是说,整个run中的操作和主线程处于同一个线程。这样对于那些简单的操作,似乎并不会影响。但是对于耗时较长的操作,就会出现“假死”。为了解决这个问题,就需要使得handler绑定到一个新开启线程的消息队列上,在这个处于另外线程的上的消息队列中处理传过来的Runnable对象和消息。

在主线程中使用Handler,可以直接使用getMainLooper()获取主线程Looper对象,并创建Handler实例。例如,在Activity中实现在子线程中更新UI:

private Handler mHandler = new Handler(Looper.getMainLooper());
...
new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomething();mHandler.post(new Runnable() {@Overridepublic void run() {// todo 在主线程更新UIdoSomethingsAboutUI();}});}
}).start();

Handler常用方法

1、延时执行

3秒后执行UI更新的代码。

mHandler.postDelayed(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}
},3000);

2、周期执行

有时候需要按时反复周期性的执行一些任务
2.1 使用Timer和TimerTask 实现

private Timer mTimer = new Timer();
private TimerTask mTimerTask = new TimerTask() {@Overridepublic void run() {Message message = new Message();message.what = 1;mHandler.sendMessage(message);}
};private Handler mHandler = new Handler() {public void handleMessage(Message msg) {switch (msg.what) {case 1:// todo 处理定时或周期性的业务doSomethings();break;}super.handleMessage(msg);}
};

在需要触发的时机,调用即可

mTimer.schedule(mTimerTask, 10000);

2.2 使用postDelayed和sendMessage实现

private int index = 0;private Handler mHandler = new Handler(Looper.getMainLooper()) {public void handleMessage(Message msg) {switch (msg.what) {case 1:updateOnTick();break;}super.handleMessage(msg);}
};private void updateOnTick(){mHandler.postDelayed(new Runnable() {@Overridepublic void run() {// todo 周期性执行doSomethings();Log.d(TAG, "run: " + (index++));mHandler.sendMessage(Message.obtain(mHandler,1));}}, 2000);  // 2秒执行一次
}

HandlerThread用法

HandlerThread常用于需要在后台执行耗时任务,并与UI线程进行交互的场景。比如,每隔6秒需要切换一下TextView的显示数据,虽然可以在UI线程中执行,但是这样的操作长时间占用UI线程,很容易让UI线程卡顿甚至崩溃,所以最好在子线程HandlerThread中调用这种业务。
HandlerThread能新建一个拥有Looper的线程。这个Looper能够用来新建其他的Handler。但需要注意的是,新建的HandlerThread需要及时回收,否则容易内存泄露。

非UI线程的业务也可以使用HandlerThread消息机制,因为不会干扰或阻塞UI线程,而且通过消息可以多次重复使用当前线程,也可以多个Handler也可以共享一个Looper,节省开支。

一个线程只能创建一个Looper,一个Looper个创建多个Handler。

使用HandlerThread可以实现以下功能和优势:

  1. 后台线程执行任务:HandlerThread在后台创建一个工作线程,可以在该线程中执行耗时任务,而不会阻塞UI线程,保证了应用的响应性和流畅性。
  2. 消息处理和线程间通信:HandlerThread内部封装了Looper和Handler,可以轻松地实现消息的发送和处理,以及线程间的通信。通过HandlerThread,可以将耗时任务的结果发送到UI线程进行更新,或者接收UI线程发送的消息进行处理。
  3. 简化线程管理:HandlerThread将线程的创建和管理进行了封装,开发人员只需要关注业务逻辑的实现,而无需手动创建和管理线程,减少了线程管理的复杂性。

主线程-创建Handler

private Handler mHandler;
private HandlerThread mHandlerThread;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mHandlerThread = new HandlerThread("子线程HandlerThread");mHandlerThread.start();mHandler= new Handler(mHandlerThread.getLooper()) {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);switch (msg.what){case 1:Log.d(TAG, "handleMessage: " + mHandlerThread.getName());// todo 在主线程更新UIdoSomethingsAboutUI();break;}}};
}
...
private void sendMessages(){new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();Message message = mHandler.obtainMessage(1);mHandler.sendMessage(message);}}).start();
}@Override
protected void onPause() {super.onPause();// 防止退出界面后Handler还在执行mHandler.removeMessages(1);
}@Override
protected void onDestroy() {super.onDestroy();// 防止退出界面后Handler还在执行mHandler.removeMessages(1);// 释放资源mHandlerThread.quit();
}

子线程-创建Handler

private Handler mHandler;
private HandlerThread mHandlerThread;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);new Thread(new Runnable() {@Overridepublic void run() {mHandlerThread = new HandlerThread("子线程HandlerThread");mHandlerThread.start();mHandler= new Handler(mHandlerThread.getLooper()) {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);switch (msg.what){case 1:Log.d(TAG, "handleMessage: " + mHandlerThread.getName());// todo 在主线程更新UIdoSomethingsAboutUI();break;}}};}}).start();
}
...
private void sendMessages(){new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();Log.d(TAG, "sendMessages: 耗时操作");Message message = mHandler.obtainMessage(1);mHandler.sendMessage(message);}}).start();
}@Override
protected void onPause() {super.onPause();// 防止退出界面后Handler还在执行mHandler.removeMessages(1);  // 删除所有消息 mHandler.removeCallbacksAndMessages(null);
}@Override
protected void onDestroy() {super.onDestroy();// 防止退出界面后Handler还在执行mHandler.removeMessages(1);   // 删除所有消息 mHandler.removeCallbacksAndMessages(null);// 释放资源mHandlerThread.quit();
}

注意:1、子线程中创建了Looper,当没有消息的时候子线程将会被block,无法被回收,所以我们需要手动调用quit 方法将消息删除并且唤醒looper,然后next方法返回null退出loop。
2、在主线程和子线程中,使用HandlerThread创建Handler,基本没有区别。但如果没有使用HandlerThread,在子线程中需要先创建Looper,再创建Handler。具体如下:

private Handler mHandler ;
...
new Thread(new Runnable() {@Overridepublic void run() {Looper.prepare(); // 创建Looper的子线程,然后创建MessageQueue,最后进行绑定。mHandler = new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {return false;}});Looper.loop();  // 启动Looper}
}).start();

FAQ

Message是如何创建

首先考虑一个问题,屏幕刷新率 60Hz(即每秒刷新60次),每次刷新要用到 3 个 Message,也就是每秒钟至少要创建180个Message。这样不断的创建回收,就会出现内存抖动的问题,从而导致 GC、屏幕卡顿等问题。
为了解决上面的问题,采用 Message 了享元的设计模式,使用 obtain() 方法创建。在Handler 中创建两个线程池队列,一个是我们比较熟悉的 MessageQueue,另一个就是回收池 sPool(最大长度是 50 复用池)。MessageQueue 中 Message 回收时,我们将清空数据的 Message 放回到 sPool 队列中。创建 Manager,我们直接从 sPool 池中取出来就可以了。
应用场景:地图、股票、RecyclerView复用等对数据的处理都使用了享元模式。

主线程中Looper的轮询死循环为何没有阻塞主线程

Looper 轮询是死循环,但是当没有消息的时候他会 block(阻塞, 阻塞代码没有执行计时操作),ANR 是当我们处理点击事件的时候 5s 内没有响应,我们在处理点击事件的时候也是用的 Handler,所以一定会有消息执行,并且 ANR 也会发送 Handler 消息,所以不会阻塞主线程。Looper 是通过 Linux 系统的 epoll 实现的阻塞式等待消息执行(有消息执行无消息阻塞),而 ANR 是消息处理耗时太长导致无法执行剩下需要被执行的消息触发了 ANR。Handler底层为什么用epoll,为什么不用select和poll? Socket 非阻塞 IO 中 select 需要全部轮询不适合,poll 也是需要遍历和 copy,效率太低了。epoll 非阻塞式 IO,使用句柄获取 APP 对象,epoll 无遍历,无拷贝。还使用了红黑树(解决查询慢的问题)。

Handler内存泄漏问题及解决方案

内部类持有外部类的引用导致了内存泄漏,如果 Activity 退出的时候,MessageQueue中还有一个 Message 没有执行,这个 Message 持有了 Handler 的引用,而 Handler 持有了 Activity 的引用,导致 Activity 无法被回收,导致内存泄漏。这种问题可以使用 static 关键字修饰,在 onDestory 的时候将消息清除。
简单理解:当 Handler 为非静态内部类时,其持有外部类 Activity 对象,所以导致 static Looper -> mMainLooper -> MessageQueue -> Message -> Handler -> MainActivity,这个引用链中 Message 如果还在 MessageQueue 中等待执行,则会导致 Activity 一直无法被释放和回收。
根本原因:因为Looper需要循环处理消息,但一个线程只有一个Looper,而一个线程中可以有多个Handler,MessageQueue中消息Message 执行时不知道要通知哪个Handler执行任务,所以在Message创建时target引用了Handler对象,用于回调执行的消息。
如果Handler是Activity这种短生命周期对象的非静态内部类时,则创建出来的Handler对象会持有该外部类Activity的引用,当页面销毁时,还在队列的Message持有着Handler对象,而Handler正持有着外部类Activity,就会导致 Activity无法被gc回收,从而导致内存泄漏。
解决办法:
1、Handler不能是Activity这种短生命周期的对象类的内部类;
2、在 Activity销毁时,将创建的 Handler中的消息队列清空并结束所有任务。
3、将handler设置成static,static变量是全局变量,不能够自动引用外部类变量,这时Handler 就不再持有 Activity,Activity就可以正常释放。

Handler为什么会持有Activity的引用?

创建Handler时,采用的是匿名内部类或者成员内部类的方式,而内部类会默认持有外部类的引用,也就是Handler对象会默认持有Activity的引用。

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

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

相关文章

云端芳华、运维之美

今天,在我们享受互联网服务带来的便利与高效的同时,有一群人默默地在幕后为我们提供支持,他们就是云端运维人员。 值此五一国际劳动节来临之际,我们要真诚感谢他们辛勤的劳动和奉献!

vue2集成ElementUI编写登录页面

目录 1. 整理目录文件: a. app.vue文件如下: b. Login.vue文件如下: c. router/index.js文件如下: d. 删除components中的文件: e. 最终项目目录整理如下: 2. 集成ElementUI编写登录页面 a. 安装ElementUI: b. 在main.js

springcloud自定义全局异常

自行创建一个实体类 /*** 全局异常处理类**/ ControllerAdvice public class GlobalExceptionHandler {ExceptionHandler(Exception.class) ResponseBody public Result error(Exception e){e.printStackTrace(); return Result.fail();}/*** 自定义异常处理方法* param e * re…

MyBatis-plus笔记——分页插件

插件配置 插件配置类,拦截器添加PaginationInnerInterceptor Configuration MapperScan("com.zxb.mp.mapper") // 扫描指定mapper接口路径 public class MyBatisConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisP…

2023-2024年汽车行业报告/方案合集(精选345份)

汽车行业报告/方案(精选345份) 2023-2024年 来源:2023-2024年汽车行业报告/方案合集(精选345份) 【以下是资料目录】 2023中国汽车科技50强 2023中国智能汽车产业发展与展望 2023比亚迪海豹汽车拆解报告 2023新能…

unity制作app(3)--gps定位

1.unity中定位Unity之GPS定位(高德解析)_unity gps定位-CSDN博客 代码需要稍微修改一下,先把脚本绑到一个button上试一试! 2.先去高德地图认证(app定位) 创建应用和 Key-Web服务 API | 高德地图API (ama…

[C++基础学习]----03-程序流程结构之跳转语句详解

前言 在C程序中,跳转语句break和continue是两种用于控制程序流程的关键字,常用于循环语句(如for循环、while循环)中。 正文 01-简介 1、break关键字: 当程序执行到break语句时,会立即跳出当前所在的循环&…

C#知识|汇总方法重载与静态方法应用技巧

哈喽,你好,我是雷工! 今天学习C#方法重载与静态方法应用技巧的相关内容。 01 方法重载有什么好处? 1.1、可以有效的减少类的对外接口(只显示一个方法比较简洁),从而降低类的复杂度。 1.2、方便…

【Vue 2.x】学习vue之二组件

文章目录 Vue二组件第五章es6文件导入出1、导出export 组件(component)1、定义2、模块化与组件化3、组件的分类1、非单文件组件非单文件三步骤创建组件标准写法简化写法组件的嵌套非单文件的不足之处 2、单文件组件vue单文件组件的使用脚手架创建项目重点…

C++@vscode配置C++开发环境常见问题和实践经验

文章目录 abstractvscode配置C/C开发环境常见问题 FAQC/C共用一组tasks.json/launch.json文件?关于配置文件中的注释更快地编译运行调试时调用外部终端控制台二次编译失败问题编译多个源文件😊源文件组织 编译出的可执行文件名中文乱码😊修改tasks.json…

Django框架之视图层

一、三板斧的原理介绍 1、HttpResponse 在Django中,HttpResponse是一个类,用于构建HTTP响应并返回给客户端。当视图函数处理完请求后,需要返回一个响应时,就会使用HttpResponse对象。 (1)创建HttpRespon…

C语言随笔集

注意 strlen 和 sizeof 的区别 strlen计算的是第一个 ‘\0’ 前面的字符的个数sizeof计算的是占用的内存空间的大小只和定义时有关C语言中,输出double类型(双精度)和float(单精度)时, 编译器默认精确到小数点后六位输出 默认输出的是6位小数,不足6位,以0补齐,超过6位按…

leetcode1329--将矩阵按对角线排序

1. 题意 对角线排序 2. 题解 2.1 直接模拟 跟螺旋输出数组实际上有点像&#xff0c;这里需要枚举对角线数组的起始值。 class Solution { public:void sort_dia(int br, int bc, vector<vector<int>> &mat) {vector<int> tmp;int r mat.size();int…

Java解决除自身以外数组的乘积

Java解决除自身以外数组的乘积 01 题目 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 **不要…

数据库的嵌套查询(子查询),分组查询和统计查询—学生选课

学生选课数据库中的表的结构参考学生选课数据库的定义和维护 1.嵌套查询 1.1查询选修了数据库原理与应用课程的学生学号和姓名 SELECT sno 学号, sname 姓名 FROM student WHERE sno IN(SELECT snoFROM SCINNER JOIN Course c ON c.cno SC.cno AND c.cname 数据库原理与应…

qml之text

显示字体 Text {text: "The quick brown fox"color: "#303030"font.family: "Ubuntu"font.pixelSize: 28x:200y:200}Text {width:84elide: Text.ElideMiddle y:text1.ytext1.height20text: "一个很长很长的句子 !!!!!!!!!!"style…

全新神经网络架构KAN一夜爆火!200参数顶30万,MIT华人一作 | 最新快讯

白交衡宇发自凹非寺 量子位公众号 QbitAI 一种全新的神经网络架构 KAN&#xff0c;诞生了&#xff01; 与传统的 MLP 架构截然不同&#xff0c;且能用更少的参数在数学、物理问题上取得更高精度。 比如&#xff0c;200 个参数的 KANs&#xff0c;就能复现 DeepMind 用 30 万参数…

Linux 进程间通信之匿名管道

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;Linux知识分享⏪   &#x1f69a;代码仓库:Linux代码练习&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Linux知识   &#x1f51d; 目录 前言 一. 进程间通信介绍 1.进程间通…

C#知识|WinForm项目结构Partial部分类与窗体控件介绍

哈喽,你好啊,我是雷工! 在我国上位机开发,医院里的HIS开发、ERP开发、很多二次开发、GIS开发等,相当一部分都是由C#开发的。 目前很多企业应用是C/S+B/S架构,WinForm作为经典的框架,还是很有必要学习的,特别是对于初学者比较友好。 要想学好WinForm需要着重以下几个方面…

遍历JavaScript对象(字典)

在JavaScript中&#xff0c;对象通常被用来作为字典使用&#xff0c;因为它们是由键值对组成的。当我们需要遍历这些键值对时&#xff0c;有几种常用的方法。 使用for...in循环 for...in循环是JavaScript中用于遍历对象属性的常用方式。它会遍历对象所有可枚举的属性&#xf…