Android消息机制与源码剖析(Looper,Message,MessageQueue以及Handler)

news2024/11/22 16:10:00

Android消息机制

文章目录

  • Android消息机制
    • 消息传递与处理的流程(配图示)
      • 1.【入口】 在 ActivityThread.class 的 main() 方法,为主线程创建 Looper,并开启 loop() 循环
      • 2.【创建Looper】通过 Looper.prepareMainLooper() 创建主线程的 Looper,并将主线程的 Looper 存放在 ThreadLocal 中。
      • 3. 【开启loop循环】通过 Looper.loop() 来启动 Looper 的死循环,不断接收 Message消息,可能阻塞。
      • 4. 【获取消息】Looper.loop() 的每次循环都需要从 MessageQueue消息队列 中获取消息
      • 5. 【消息入队】通过MessageQueue.class的enqueueMessage() 让Message消息入队
      • 6.【消息处理】 通过Handler.handleMessage() 来进行Message消息处理
      • 7. 【实践】如何使用Handler来处理来自其他线程的消息事件?
      • 8. 【常见疑问】为什么Looper.loop()在主线程中死循环不会让程序卡死?
      • 9. 【常见疑问】Activity的生命周期如何表现?
      • 10.【常见疑问】 和ANR的区别

Android的消息传递机制主要由: Looper, Message, MessageQueue, Handler完成。

Looper:负责不断获取消息队列中的消息。

MessageQueue:消息队列,单链表形式存放Message队列

Message: 消息实体

Handler: 其中的handleMessage()用来处理那些交给它处理的消息。也可以通过其中的sendMessage()来将自定义消息加入到消息队列中。系统中可以有多个Handler,包括系统本身设计的用来处理系统消息的,也包括用户自定义实现的用来处理自定义消息的。

消息传递与处理的流程(配图示)

请添加图片描述

1.【入口】 在 ActivityThread.class 的 main() 方法,为主线程创建 Looper,并开启 loop() 循环

在ActivityThread中的main()方法准备Looper。

public static void main(String[] args) {
        //initial...
        Looper.prepareMainLooper();
    
    	//...
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

    	//...
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

2.【创建Looper】通过 Looper.prepareMainLooper() 创建主线程的 Looper,并将主线程的 Looper 存放在 ThreadLocal 中。

进入 Looper.prepareMainLooper() 之后,就是创建主线程的Looper。

public static void prepareMainLooper() {
    //在ThreadLocal中设置主线程Looper
    prepare(false);
    synchronized (Looper.class) {
        //确保在此之前主线程Looper没有任何引用实例
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        //将主线程Looper引用为ThreadLocal中设置的Looper
        sMainLooper = myLooper();
    }
}

/**
 * 1. 确保ThreadLocal中主线程没有设置过Looper
 * 2. 在主线程中,调用ThreadLocal的set方法设置Looper
 */
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

其中,上述代码最后一行的 new Looper()构造函数中,为该 Looper 实例设置了消息队列与对应的当前线程:

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

3. 【开启loop循环】通过 Looper.loop() 来启动 Looper 的死循环,不断接收 Message消息,可能阻塞。

将主线程的Looper设置好之后,在ActivityThread.main()的最后通过 Looper.loop() 开启Looper的死循环。

public static void loop() {
        final Looper me = myLooper();
        //...
        for (;;) {
            if (!loopOnce(me, ident, thresholdOverride)) {
                return;
            }
        }
    }

死循环中,每一轮都调用了loopOnce(),它的主要任务通过:

Message msg = me.mQueue.next();

来从消息队列中获取消息,并加以处理:

private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
        Message msg = me.mQueue.next(); // might block
        if (msg == null) {
            // 如果拿到的消息是null了,说明退出了messageQueue,也说明程序结束了。
            return false;
        }
		//...
        try {
            msg.target.dispatchMessage(msg);
            
        } finally {
            
        }
        //...
        msg.recycleUnchecked();

        return true;
    }

4. 【获取消息】Looper.loop() 的每次循环都需要从 MessageQueue消息队列 中获取消息

MessageQueue消息队列是由单链表实现的,它将通过:

Message next(){...}

next() 方法获取消息的逻辑:

  1. 判断是否有消息
  2. 其次判断是否有人处理该消息
  3. 消息是否到了处理时机
Message next() {
       	//空闲的,原先就由系统框架设计好的Handler的个数
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
    	//延迟时间
        int nextPollTimeoutMillis = 0;
        for (;;) {
            //native方法,用于判断是否需要阻塞,
            //nextPollTimeoutMillis为-1,将阻塞,
            //nextPollTimeoutMillis为0,将跳过,
            //nextPollTimeoutMillis为数值,将延迟对应的时间
            //若是阻塞,可以在有消息入队时由nativeWake()方法唤醒。
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                //获取消息队头元素
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // 如果有消息,但是没有对应处理它的handler,就先跳过它
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                //如果找到了首个有handler可以处理的msg
                if (msg != null) {
                    //但是时间还没到,则设置延迟时间,等到下一个循环开始的时候,阻塞响应时间。
                    if (now < msg.when) {
                        // 下一个消息还没准备好。设置一个延迟时间,当它准备好的时候,唤醒。
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //如果条件都满足,立即取出消息
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    //没有更多消息了,一般只发生在第一次。
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

            //没有消息的时候,空闲Handles将会运行,如果没有空闲handler,将会设置为阻塞(等待消息入队时被唤醒)。这只会在第一次循环的时候发生。
                
            // 在调用空闲处理程序时,可能已经传递了一条新消息,因此无需等待就可以返回并再次查看挂起的消息
            nextPollTimeoutMillis = 0;
        }
    }

5. 【消息入队】通过MessageQueue.class的enqueueMessage() 让Message消息入队

入队的消息必须有处理它的handler,(即msg.target不为空)。
入队的消息根据延迟时间判断是插入队头,还是插入队中首个合适的位置(判断依据:延迟时间)。
这个方法除了通过MessageQueue本身实例来调用,也可以通过自定义的HandlersendMessage()来间接调用。

boolean enqueueMessage(Message msg, long when) {
    	//入队的消息必须由处理它的handler
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        synchronized (this) {
            //各类错误判断
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }

            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }
			//入队后,设置标记,不允许再次入队。
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            //如果原先队列中没有消息,说明原先是阻塞的,需要唤醒
            //插入队头有条件的:可以立即执行,或者延迟时间比原先队头更短
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;//插入到队头
                mMessages = msg;//设置当前消息为队头
                needWake = mBlocked;
            } else {
               	//是否需要唤醒,要看阻塞状态与队头的消息是否有人处理
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
              	//将消息msg插入到首个合适的位置
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
			//唤醒MessageQueue.next()中的nativePollOnce()
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

6.【消息处理】 通过Handler.handleMessage() 来进行Message消息处理

在 3. 启动Loop 中有这样一段代码:

private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
        Message msg = me.mQueue.next(); // might block
        if (msg == null) {
            // 如果拿到的消息是null了,说明退出了messageQueue,也说明程序结束了。
            return false;
        }
		//...
        try {
            msg.target.dispatchMessage(msg);
            
        } finally {
            
        }
        //...
        msg.recycleUnchecked();

        return true;
    }

拿到需要处理的消息msg之后,将会交给 msg.target 也就是对应的处理该msg的handler来处理消息。

这里注意,msg.recycleUnchecked()方法是msg回收的一个方法,Message中维护着一个sPool,消息池,可以将 Message 实例回收后重复利用。所以当我们需要自定义一个新的 Message 时,推荐使用Message.obtain()从而获取来自消息池中的 Message 实例,而不是直接new Message()创建一个新实例。

Handler中处理消息的代码如下,它是待具体实现的:

/**
  * Subclasses must implement this to receive messages.
  */
public void handleMessage(@NonNull Message msg) {
    }
    
/**
  * Handle system messages here.
  */
public void dispatchMessage(@NonNull Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

7. 【实践】如何使用Handler来处理来自其他线程的消息事件?

下面是一个例子:

public class MainActivity extends AppCompatActivity {

	//自定义一个Handler用来处理消息
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            Log.e("TAG","msg: "+msg);
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

	//一个按钮触发发送消息事件
    public void click(View view) {
        new Thread(()->{
            Message message = new Message();
            message.what = 1;//这个属于用户自定义TAG,让使用者知道这个msg是关于什么的,或者属于什么分类标签的
            handler.sendMessage(message);
        }).start();
    }
}

handler.sendMessage(msg) 实际就是让message入队,其中的消息队列mQueue在Handler初始化的时候就在构造方法中拿到了:

public Handler(@Nullable Callback callback, boolean async) {
       //...
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
       //...
    }

由handler发起的消息入队调用链如下:

//1. handler.sendMessage
public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }

//2. 判断延迟时间
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

//3. 设置消息的延迟时间
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

//4. 设置msg的属性,并让消息入队
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

8. 【常见疑问】为什么Looper.loop()在主线程中死循环不会让程序卡死?

Android是事件驱动的,在Looper.loop()调用之前,就通过thread.attach(false) 创建了Binder线程(具体是指 ApplicationThreadBinder 的服务端,用于接收系统服务 AMS 发送来的事件).该 Binder 线程通过 Handler 将 Message 发送给主线程。 Binder用于进程间通信。

thread.attach()中完成AMS与App进程通讯的绑定的代码:

final IActivityManager mgr = ActivityManager.getService();
try {
    mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
    throw ex.rethrowFromSystemServer();
}

这是来自系统服务发来的事件,用于进程间通信。而 ActivityThread 的内部类 H 继承于Handler,用于同一进程的线程间通信。来自不同地方的消息事件驱动着Android。

9. 【常见疑问】Activity的生命周期如何表现?

Activity的生命周期依靠着主线程的 Looper.loop(),由 Looper.loop()不断接收消息事件,再通过 ActivityThread 中的 H.handleMessage(msg)来处理消息,例如处理 Activity 的请求onCreate事件。我们关注一下创建 Activity 实例的事件在Handler中是怎么处理的。

 class H extends Handler {
     //...
     public static final int RECEIVER                = 113;
     //...
     
     String codeToString(int code) {
        if (DEBUG_MESSAGES) {
            switch (code) {
               	//...
                case RECEIVER: return "RECEIVER";
                //...
            }
        }
        return Integer.toString(code);
     }
     
     //处理消息
     public void handleMessage(Message msg) {
            //...
            switch (msg.what) {
                case RELAUNCH_ACTIVITY:
                    handleRelaunchActivityLocally((IBinder) msg.obj);
                 break;
                //...
            }
            //...
        }
     
 }

首先 H 的handleMessage() 接收到了 RELAUNCH_ACTIVITY 事件,通过一系列调用;最后调到handleLaunchActivity():

@Override
public Activity handleLaunchActivity(ActivityClientRecord r,PendingTransactionActions pendingActions, Intent customIntent) {
        //...
        final Activity a = performLaunchActivity(r,customIntent);

        if (a != null) {
            //...
        } else {
            //如果创建出错,让activity manager终止。
        ActivityClient.getInstance().finishActivity(...);
        }
        return a;
}

看到其中真正开启 Activity 的是 performLaunchActivity()

它首先通过反射构建了一个 Activity,然后通过Instrumentation.callActivityOnCreate()来主动调用 Activity 的onCreate()

/**  Core implementation of activity launch. */
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;
        //获取包信息
        //...

        //获取组件(Activity Service Broadcast ContentProvider)
       	//...
		
        //创建上下文
        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        //通过反射构建activity
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            //...
        } catch (Exception e) {
            //...
        }

        //...
        synchronized (mResourcesManager) {
            //Maps from activity token to local record of running activities in this process
            mActivities.put(r.token, r);
        }
		//...
        if(activity != null){
            //...
            activity.attach(...);
            //...
            r.activity = activity;
            if (r.isPersistable()) {
                mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                mInstrumentation.callActivityOnCreate(activity, r.state);
            }
        }

        return activity;
    }

step1:实例化Activity

Instrumentation.newActivity()通过工厂来实例化一个 Activity:

public Activity newActivity(ClassLoader cl, String className,Intent intent) throws InstantiationException, IllegalAccessException,ClassNotFoundException {
    	String pkg = intent != null && intent.getComponent() != null
                ? intent.getComponent().getPackageName() : null;
    	return getFactory(pkg).instantiateActivity(cl, className, intent);
}

其中由AppComponentFactory来实例化一个Activity:

public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    	//通过反射来实例化Activity
        return (Activity) cl.loadClass(className).newInstance();
}

step2: 通过Instrumentation调用Activity的onCreate()

public void callActivityOnCreate(Activity activity, Bundle icicle,
            PersistableBundle persistentState) {
        prePerformCreate(activity);
        activity.performCreate(icicle, persistentState);
        postPerformCreate(activity);
    }

Activity 中的performCreate()方法,真正调用了 Activity 的 onCreate()

final void performCreate(Bundle icicle, PersistableBundle persistentState) {
        
        //...
        if (persistentState != null) {
            onCreate(icicle, persistentState);
        } else {
            onCreate(icicle);
        }
		//...
    	//FragmentController的这个方法为FragmentManager提供了一个fragment host(一般这个host是activity)的集成点,该集成点主要是为了提供host(activity)的回调信息。
        mFragments.dispatchActivityCreated();
    	//...
    }

10.【常见疑问】 和ANR的区别

ANR: 5s内没有响应输入事件,比如按键、屏幕触摸

ANR本质原因是消息队列中其他消息太耗时,导致按键或者广播消息没有及时处理。

也就是ANR的本质原因不是线程被等待消息入队的阻塞,而是消息队列被其他耗时消息阻塞,导致按键或者广播消息没有及时处理。所以主线程中不推荐进行耗时操作。所以许多数据库框架也都不允许把增删改查操作放在主线程中进行。

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

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

相关文章

MySQL如何快速恢复单表(物理方式)

GreatSQL社区原创内容未经授权不得随意使用&#xff0c;转载请联系小编并注明来源。GreatSQL是MySQL的国产分支版本&#xff0c;使用上与MySQL一致。作者&#xff1a;xryz文章来源&#xff1a;社区投稿 使用方法 1、首先创建一个测试表test1&#xff0c;并插入几条数据&#x…

DataParallel里为什么会显存不均匀以及如何解决

作者&#xff1a;台运鹏 (正在寻找internship...)主页&#xff1a;https://yunpengtai.top鉴于网上此类教程有不少模糊不清&#xff0c;对原理不得其法&#xff0c;代码也难跑通&#xff0c;故而花了几天细究了一下相关原理和实现&#xff0c;欢迎批评指正&#xff01;关于此部…

商品上下游第六讲-交易中心-商品秒杀

交易中心-商品秒杀设计 文章目录 交易中心-商品秒杀设计1、项目背景2、主要技术3、项目职责4、项目实现4.1、需求分析4.2、核心流程4.3、关键链路技术方案4.4、库存处理方式1、库存超卖问题订单层面的控制4.5、限流,熔断,降级4.6、超职购小程序—接口梳理4.7、缓存的设计4.8、…

mybatis入门(一)

什么是 MyBatis &#xff1f; MyBatis 是一款优秀的持久层框架&#xff0c;它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息&#xff0c;将接口和 Java 的…

如何使用匈牙利算法解决多维度的约束条件问题

&#x1f37f;*★,*:.☆欢迎您/$:*.★* &#x1f37f; 正文 假设 一个项目 有三个 维度的参数 A B C 都要 组合后最小 分别求解 a b c 三个维度的最优组合 如果三个组合方案刚好 重叠 那么说明有一个使得三个方案最优的 解 如果没有 那么若选择某个方案 其他维度的参数 的值 是…

直播倒计时 2 天 | SOFAChannel#31 RPC 框架设计的考和量

SOFARPC 是蚂蚁集团开源的一款基于 Java 实现的 RPC 服务框架&#xff0c;为应用之间提供远程服务调用能力&#xff0c;具有高可伸缩性&#xff0c;高容错性&#xff0c;目前蚂蚁集团所有的业务的相互间的 RPC 调用都是采用 SOFARPC。SOFARPC 为用户提供了负载均衡&#xff0c;…

Android -- 每日一问:回调函数和观察者模式的区别?

知识点 观察者模式 网上很容易查到观察者模式的定义&#xff1a; 观察者模式定义了对象间的一种一对多依赖关系&#xff0c;使得每当一个对象改变状态&#xff0c;则所有依赖于它的对象都会得到通知并被自动更新。 Android中大量的使用了观察者模式。你可能已经用过ListView…

基于51单片机的舞蹈机器人步进机仿真设计

程序运行图&#xff1a; 仿真原理图&#xff1a; 部分程序&#xff1a; #include "reg51.h" #include "intrins.H" //8步式步进电机脉冲序列 //unsigned char steps[8] {0x77,0x33,0xbb,0x99,0xdd,0xcc,0xee,0x66}; unsigned char steps[8] {0x2,0x…

Vue2快速入门

Vue 介绍 Vue 是一套构建用户界面的渐进式前端框架只关注视图层&#xff0c;并且非常容易学习&#xff0c;还可以很方便的与其它库或已有项目整合通过尽可能简单的API来实现响应数据的绑定和组合的视图组件特点易用&#xff1a;在有HTML CSS JavaScript的基础上&#xff0c;快速…

拓扑排序(数据结构之图的应用)

我们先搞清楚一个概念&#xff1a; 什么是出度与入度&#xff1f; 在有向图中&#xff0c;箭头是具有方向的&#xff0c;从一个顶点指向另一个顶点&#xff0c;这样一来&#xff0c;每个顶点被指向的箭头个数&#xff0c;就是它的入度。从这个顶点指出去的箭头个数&#xff0c…

不锈钢风淋室的使用需要注意哪些事项

风淋室的使用需要注意哪些事项 一、风淋室的操作说明&#xff1a; 1) 接通380V&#xff0c;50HZ电源(L1、L2、L3-火线&#xff0c;N-零线&#xff0c;E-接地线)&#xff0c;打开工作、照明开关&#xff0c;确认风机与照明工作正常&#xff0c;此时&#xff0c;风/货淋室处于初…

原创 | Attention is all you need 论文解析(附代码)

作者&#xff1a;杨金珊审校&#xff1a;陈之炎本文约4300字&#xff0c;建议阅读8分钟“Attention is all you need”一文在注意力机制的使用方面取得了很大的进步&#xff0c;对Transformer模型做出了重大改进。目前NLP任务中的最著名模型&#xff08;例如GPT-2或BERT&#x…

【数集项目之 MCDF】(四) 整形器 formatter

根据上一章的arbiter结构图&#xff0c;结合设计文档中MCDF的整体结构图&#xff0c;可以发现formatter整形器模块是arbiter的上级&#xff0c;负责最终的数据输出&#xff0c;与外界数据接收端相连。 第一节 fromatter文档理解 设计文档formatter的部分时序介绍如下 如图所示…

钡铼技术S274数据遥测终端机

钡铼技术S274数据遥测终端机功能特点&#xff1a; 内置 2 路 DC 直流电源输出&#xff0c;无需单独额外增加变送器的电源适配器&#xff0c;节省布线成本&#xff1b;  采用完备的防掉线机制&#xff0c;保证数据终端永远在线&#xff0c;掉线重发数据以及掉线短信通知用户…

第38篇 网络(八)TCP(二)

导语 在上一节里我们使用TCP服务器发送一个字符串&#xff0c;然后在TCP客户端进行接收。在这一节将重新写一个客户端程序和一个服务器程序&#xff0c;这次实现客户端进行文件的发送&#xff0c;服务器进行文件的接收。有了上一节的基础&#xff0c;这一节的内容就很好理解了…

postgresql_internals-14 学习笔记(三)冻结、rebuild

一、 Freezing 冻结 1. 引入原因 简单说来就是目前pg事务id只有32位&#xff0c;大业务量下很可能用完&#xff0c;触发事务id回卷&#xff08;循环使用&#xff09;。而pg是根据事务id大小判断可见性的&#xff0c;如果新事务却使用了小id&#xff0c;旧事务将可以看到新事务…

win下 conda 虚拟环境没有名字怎么进入

本文主要介绍windows下&#xff0c;在conda 虚拟环境名字消失后的解决办法。主要介绍两种解决方案。 文章目录前言解决方案一&#xff1a;往.condarc文件中添加envs_dirs1. 设置envs_dirs2. 重新查看虚拟环境解决方案二&#xff1a;直接通过path 激活虚拟环境总结前言 我们都知…

Grafana 监控大屏可视化图表

Grafana 系列文章&#xff0c;版本&#xff1a;OOS v9.3.1 Grafana 的介绍和安装Grafana监控大屏配置参数介绍&#xff08;一&#xff09;Grafana监控大屏配置参数介绍&#xff08;二&#xff09;Grafana监控大屏可视化图表 前面我们以Time series 图表为例&#xff0c;学习了面…

每天投递一两个公司,我连续投了三个月

作者&#xff1a;阿秀校招八股文学习网站&#xff1a;https://interviewguide.cn你好&#xff0c;我是阿秀。阿秀以前在秋招的时候投递过八九十份简历&#xff0c;当时还没有简历一键上传功能&#xff0c;很多时候都需要自己去那些公司注册账号&#xff0c;然后找到校园招聘模块…

手机备忘录误删恢复的操作方法

手机备忘录在使用的过程中&#xff0c;会有多种不同的操作&#xff0c;通过不同的操作来实现不同的效果。对于有的内容来说&#xff0c;是可以过期删除的&#xff0c;但是在删除这个操作的过程当中&#xff0c;如果不小心把有用的东西误删了&#xff0c;那么恢复误删内容的操作…