Android Handler消息机制原理最全解读(持续补充中)

2023-11-02

 本文主要详细去解读Android开发中最常使用的Handler,以及使用过程中遇到的各种各样的疑问。

Handler


 在Android开发的过程中,我们常常会将耗时的一些操作放在子线程(work thread)中去执行,然后将执行的结果告诉UI线程(main thread),熟悉Android的朋友都知道,UI的更新只能通过Main thread来进行。那么这里就涉及到了如何将
子线程的数据传递给main thread呢?
 Android已经为我们提供了一个消息传递的机制——Handler,来帮助我们将子线程的数据传递给主线程,其实,当熟悉了Handler的原理之后我们知道,Handler不仅仅能将子线程的数据传递给主线程,它能实现任意两个线程的数据传递。
 接下来,我们便详细的了解下Handler的原理及其使用。
 首先看一下Handler最常规的使用方式:

private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MESSAGE_WHAT:
                    Log.d(TAG, "main thread receiver message: " + ((String) msg.obj));
                    break;
            }
        }
    };
    
    private void sendMessageToMainThreadByWorkThread() {
        new Thread(){
            @Override
            public void run() {
                Message message = mHandler.obtainMessage(MESSAGE_WHAT);
                message.obj = "I am message from work thread";
                mHandler.sendMessage(message);
            }
        }.start();
    }
    /*
    * 通常我们在主线程中创建一个Handler,
    * 然后重写该Handler的handlerMessage方法,可以看到该方法传入了一个参数Message,
    * 该参数就是我们从其他线程传递过来的信息。
    *
    * 我们在来看下子线程中如何传递的信息,子线程通过Handler的obtainMessage()方法获取到一个Message实例,
    * 我们来看看Message的几个属性:
    * Message.what------------------>用来标识信息的int值,通过该值主线程能判断出来自不同地方的信息来源
    * Message.arg1/Message.arg2----->Message初始定义的用来传递int类型值的两个变量
    * Message.obj------------------->用来传递任何实例化对象
    * 最后通过sendMessage将Message发送出去。
    *
    * Handler所在的线程通过handlerMessage方法就能收到具体的信息了,如何判断信息的来源呢?当然是通过what值啦。
    * 怎么样很简单吧
    */

 文章的开头说过,Handler不仅仅是能过将子线程的数据发送给主线程,它适用于任意两个线程之间的通信。
 下面我们来看下两个子线程之间如何进行通信的。
 很简单啊,在一个线程创建Handler,另外一个线程通过持有该Handler的引用调用sendMessage发送消息啊!
 写程序可不能关说不练啊,我们把代码敲出来看一下!

private Handler handler;
    private void handlerDemoByTwoWorkThread() {
        Thread hanMeiMeiThread = new Thread() {
            @Override
            public void run() {
//                Looper.prepare();
                handler = new Handler() {
                    @Override
                    public void handleMessage(Message msg) {
                        Log.d(TAG, "hanMeiMei receiver message: " + ((String) msg.obj));
                        Toast.makeText(MainActivity.this, ((String) msg.obj), Toast.LENGTH_SHORT).show();
                    }
                };
//                Looper.loop();
            }
        };
        Thread liLeiThread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Message message = handler.obtainMessage();
                message.obj = "Hi MeiMei";
                handler.sendMessage(message);
            }
        };
        hanMeiMeiThread.setName("韩梅梅 Thread");
        hanMeiMeiThread.start();
        liLeiThread.setName("李雷 Thread");
        liLeiThread.start();

        /*
        * 搞定,我们创建了两个Thread,liLeiThread和hanMeiMeiThread两个线程,很熟悉的名字啊!
        * 跟之前的代码没太大区别hanMeiMeiThread创建了Handler,liLeiThread通过Handler发送了消息。
        * 只不过此处我们只发送一个消息,所以没有使用what来进行标记
        * 运行看看,我们的李雷能拨通梅梅吗?
        * 啊哦,出错了
        * 05-13 17:08:17.709 20673-20739/? E/AndroidRuntime: FATAL EXCEPTION: 韩梅梅 Thread
                                                   Process: design.wang.com.designpatterns, PID: 20673
                                                   java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
                                                       at android.os.Handler.<init>(Handler.java:200)
                                                       at android.os.Handler.<init>(Handler.java:114)
        *Can't create handler inside thread that has not called Looper.prepare()
        * -----------》它说我们创建的handler没有调用Looper.prepare();
        * 好的,我们在实例化Handler之前调用下该方法,看一下。加上是不是没有报错了呢。
        * 等等,虽然没有报错,但是hanMeiMeiThread也没有接到消息啊,消息呢?别急。
        * 我们在Handler实例化之后加上Looper.loop();看一看,运行一下,是不是收到消息了呢。
        * 这是为什么呢?
        * 接下来我们就去看看Handler是怎么实现的发消息呢,弄清楚了原理,这里的原因也就明白了。
        */

    }

 好了,卖了半天的关子,终于要开始真正的主题了。
 首先我们来看下,为什么在子线程里实例化的时候不调用Looper.prepare()就会报错呢?

//我们先来看看new Handler();时出错的原因。后续讲解源码分析只贴出关键部分。
//如下是Handler构造函数里抛出上文异常的地方,可以看到,由于mLooper对象为空才抛出的该异常。
mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
/*
  异常的原因看到了,接下来我们看看Looper.prepare()方法都干了些什么?
*/
public static void prepare() {
    prepare(true);
}

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));
}
/*
 可以看到,该方法在当前thread创建了一个Looper(), ThreadLocal主要用于维护线程的本地变量,  
*/
 private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
//而Looper的构造函数里面又为我们创建了一个MessageQueue()对象。

 了解到此,我们已经成功引出了Handler机制几个关键的对象了,Looper、MessageQueue、Message。
 那么,肯定也有人又产生新的疑问了——为什么在主线程中创建Handler不需要要用Looper.prepare()和Looper.loop()方法呢?
 其实不是这样的,App初始化的时候都会执行ActivityThread的main方法,我们可以看看ActivityThread的main()方法都做了什么?

        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }
        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();
/*
 真相只有一个,是的在创建主线程的时候Android已经帮我们调用了Looper.prepareMainLooper()
 和Looper.loop()方法,所以我们在主线程能直接创建Handler使用。
*/

 我们接着来看Handler发送消息的过程:

//调用Handler不同参数方法发送Message最终都会调用到该方法
public boolean sendMessageAtTime(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);
    }

 sendMessage的关键在于enqueueMessage(),其内部调用了messageQueue的enqueueMessage方法

boolean enqueueMessage(Message msg, long when) {
        ...
        synchronized (this) {
            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;
                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;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }
    /*从代码可以看出Message被存入MessageQueue时是将Message存到了上一个Message.next上, 
      形成了一个链式的列表,同时也保证了Message列表的时序性。
    */

 Message的发送实际是放入到了Handler对应线程的MessageQueue中,那么,Message又是如何被取出来的呢?
 细心的朋友可能早早就发现了,之前抛出异常的地方讲解了半天的Loop.prepare()方法,一直没有说到Loop.loop()方法。同时,在之前的例子中也看到了,如果不调用Looper.loop()方法,Handler是接受不到消息的,所以我们可以大胆的猜测,消息的获取肯定和它脱不了关系!当然关怀疑还不行,我们还必须找出真相来证明我们的猜想?那还等什么,先看看loop()方法吧。

public static void loop() {
//可以看到,在调用Looper.prepare()之前是不能调用该方法的,不然又得抛出异常了
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            if (traceTag != 0) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }
/*
这里我们看到,mLooper()方法里我们取出了,当前线程的looper对象,然后从looper对象开启了一个死循环 
不断地从looper内的MessageQueue中取出Message,只要有Message对象,就会通过Message的target调用
dispatchMessage去分发消息,通过代码可以看出target就是我们创建的handler。我们在继续往下分析Message的分发
*/
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
/*好了,到这里已经能看清晰了
可以看到,如果我们设置了callback(Runnable对象)的话,则会直接调用handleCallback方法
*/
private static void handleCallback(Message message) {
        message.callback.run();
    }
//即,如果我们在初始化Handler的时候设置了callback(Runnable)对象,则直接调用run方法。比如我们经常写的runOnUiThread方法,由于Handler在主线程创建,所以最终得以在主线程执行:
runOnUiThread(new Runnable() {
            @Override
            public void run() {
                
            }
        });
public final void runOnUiThread(Runnable action) {
      if (Thread.currentThread() != mUiThread) {
          mHandler.post(action);
      } else {
          action.run();
      }
  }
  /*
而如果msg.callback为空的话,
存在一种情况,当创建Handler使用了带Callback的构造方法的话,将会执行Callback的handleMessgae方法,并且会根据其方法的返回值判断是否会被callback拦截,  
是否需要继续执行Handle的handlMessgae方法,如果不存在Callback的拦截则Handler本身的handleMessage方法得意执行。(Handler(Callback)的构造方法使用场景暂时未遇到,欢迎补充)
  */

 到这里,想必你应该清楚如何在不同的线程之间来使用Handler了吧。

最后总结一下:

  1. 在使用handler的时候,在handler所创建的线程需要维护一个唯一的Looper对象, 每个线程对应一个Looper,每个线程的Looper通过ThreadLocal来保证,如需了解ThreadLocal,点击查看详细讲解 ,
    Looper对象的内部又维护有唯一的一个MessageQueue,所以一个线程可以有多个handler,
    但是只能有一个Looper和一个MessageQueue。
  2. Message在MessageQueue不是通过一个列表来存储的,而是将传入的Message存入到了上一个
    Message的next中,在取出的时候通过顶部的Message就能按放入的顺序依次取出Message。
  3. Looper对象通过loop()方法开启了一个死循环,不断地从looper内的MessageQueue中取出Message,
    然后通过handler将消息分发传回handler所在的线程。

最后附上一张自己理解画出来的流程图:
这里写图片描述



Handler补充:

1. Handler在使用过程中,需要注意的问题之一便是内存泄漏问题。

为什么会出现内存泄漏问题呢?
首先Handler使用是用来进行线程间通信的,所以新开启的线程是会持有Handler引用的,
如果在Activity等中创建Handler,并且是非静态内部类的形式,就有可能造成内存泄漏。

  1. 首先,非静态内部类是会隐式持有外部类的引用,所以当其他线程持有了该Handler,线程没有被销毁,则意味着Activity会一直被Handler持有引用而无法导致回收。
  2. 同时,MessageQueue中如果存在未处理完的Message,Message的target也是对Activity等的持有引用,也会造成内存泄漏。
解决的办法:

 (1). 使用静态内部类+弱引用的方式:

  静态内部类不会持有外部类的的引用,当需要引用外部类相关操作时,可以通过弱引用还获取到外部类相关操作,弱引用是不会造成对象该回收回收不掉的问题,不清楚的可以查阅JAVA的几种引用方式的详细说明。

private Handler sHandler = new TestHandler(this);

static class TestHandler extends Handler {
    private WeakReference<Activity> mActivity;
    TestHandler(Activity activity) {
        mActivity = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Activity activity = mActivity.get();
        if (activity != null) {
            //TODO:
        }
    }
}

 (2). 在外部类对象被销毁时,将MessageQueue中的消息清空。例如,在Activity的onDestroy时将消息清空。

@Override
protected void onDestroy() {
    handler.removeCallbacksAndMessages(null);
    super.onDestroy();
}
2. 在使用Handler时,通常是通过Handler.obtainMessage()来获取Message对象的,而其内部调用的是Message.obtain()方法,那么问题来了,为什么不直接new一个Message,而是通过Message的静态方法obtain()来得到的呢?

下面就通过代码来一探究竟

public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

 其实在在Message中有一个static Message变量sPool,这个变量是用于缓存Message对象的,在obtain中可以看到当需要一个Message对象时,如果sPool不为空则会返回当前sPool(Message),而将sPool指向了之前sPool的next对象,(之前讲MessageQueue时讲过Message的存储是以链式的形式存储的,通过Message的next指向下一个Message,这里就是返回了sPool当前这个Message,然后sPool重新指向了其下一个Message),然后将返回的Message的next指向置为空(断开链表),sPoolSize记录了当前缓存的Message的数量,如果sPool为空,则没有缓存的Message,则需要创建一个新的Message(new Message)。
这里写图片描述
 接着看下sPool中缓存的Message是哪里来的?

public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

 recycle()是回收Message的方法,在Message处理完或者清空Message等时会调用。
recycleUnchecked()方法中可以看到,将what、arg1、arg2、object等都重置了值,如果当前sPool(Message缓存池)的大小小于允许缓存的Message最大数量时,将要回收的Message的next指向sPool,将sPool指向了回收的Message对象(即将Message放到了sPool缓存池的头部)
这里写图片描述

总结:

由此可见,使用obtain获取Message对象是因为Message内部维护了一个数据缓存池,回收的Message不会被立马销毁,而是放入了缓存池,
在获取Message时会先从缓存池中去获取,缓存池为null才会去创建新的Message。


3. Handler sendMessage原理解读。

 引入问题!

  1. sendMessageDelayed是如何实现延时发送消息的?
  2. sendMessageDelayed是通过阻塞来达到了延时发送消息的结果,那么会不会阻塞新添加的Message?
详细分析请移步下篇文章:Handler进阶之sendMessage原理探索

欢迎提供其他有关Handler的问题分享讨论

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android Handler消息机制原理最全解读(持续补充中) 的相关文章

  • 通知无法在 Android Oreo (API 26) 中显示

    当我尝试在 Android O 上显示通知时收到此消息 对于除卷之外的操作 不推荐使用流类型 控制 该通知直接来自示例文档 并且在 Android 25 上显示良好 根据评论这篇 Google 帖子 https plus google co
  • 在 Android 上解析查询字符串

    Java EE 有ServletRequest getParameterValues http java sun com j2ee sdk 1 3 techdocs api javax servlet ServletRequest html
  • DESFire EV1 卡模拟

    我需要在 Android 设备上模拟 DESFire EV1 卡 但是 我对如何将 Mifare SDK 精简版或高级版 与 HCE 结合使用感到困惑 这可能吗 我需要启动该项目 并且对如何在 DESFire 卡中存储和加密数据进行了大量研
  • Glide V4 加载 https 图像

    我知道这个link https stackoverflow com questions 41114569 glide javax net ssl sslhandshakeexception java security cert certpa
  • cordova - 错误:无法获取平台 android

    我安装了 cordova 并创建了一个新项目 但是当我使用这个命令添加android平台时 cordova平台添加android 出现此错误 Error Failed to fetch platform android Probably t
  • 在多台计算机上使用相同的调试密钥库

    我正在多台计算机上进行开发 如果我直接从一台计算机上的 Eclipse 部署到手机 则在不先卸载的情况下无法从另一台计算机进行部署 我假设这是因为有一个调试证书用于签署应用程序 并且它们在两台机器上并不相同 是否可以将一台机器复制到另一台机
  • Firestore:发现名称 isText 的 getter 冲突

    我试图查看类似的问题 但它们对我来说没有用 我有一堂课 data class TextMessage val text String override val time Date override val senderId String o
  • 了解 UUID 的长形式和短形式

    我正在尝试学习一些android中的蓝牙编程我已经能够制作一个基本程序 该程序可以发现并连接到具有内置蓝牙功能的开发板 目前只有一个 我希望能够使用 RFCOMM 向开发板发送命令并接收消息有人告诉我 如果不知道主板和手机的 UUID 我可
  • 使应用程序背景适合不同设备的最佳方法

    因此 我希望通过一些漂亮的背景图像等来为我的应用程序增添一点趣味 但我很好奇的是如何确保不同的机器人正确渲染 我将在文本后面设置背景 并且我需要确保它们对于每个不同的屏幕尺寸和分辨率具有相同的尺寸和位置 有这方面好的教程吗 Thanks E
  • NsdManager 不会停止服务发现

    In Android我目前正在开发的应用程序 我想使用连接零配置网络NsdManager 我设法运行网络服务发现并连接到所需的网络 但在停止发现后NsdManager线程仍在运行 这会导致屏幕旋转几次后出现很多的情况NsdManager正在
  • 阻止 Android WebView 尝试在 loadData() 上加载/捕获 CSS 等资源

    背景 这似乎与许多其他问题重复 相信我 事实并非如此 我正在尝试将 html 数据加载到WebView https developer android com reference android webkit WebView html 能够
  • Android Studio 2.3 Beta 1 + 支持 25.1.0 = 预览已损坏

    Android Studio 2 3 Canary 3 支持库25 1 0 布局预览完全损坏 任何包含支持相关视图的内容似乎都会在预览期间中断 在所有情况下 这都是堆栈的开头 Exception Details java lang NoSu
  • GCM 服务器以什么频率刷新注册 ID?如何在我的手机中获取 regId 更改事件?

    GCM 服务器刷新注册 ID 的时间频率 如果更改了如何在我的手机中获取 regId 更改事件 我的应用程序在首次启动时在 GCM 上注册 并将 regId Id 存储在 Sahred 首选项文件中 然后 当用户手动完成某些事件时 它将从共
  • android recycler 视图适配器、viewpagers、数据库、位图和内存不足错误

    hello im loading lots of little images ex 180x180 10 21kb from a LOCAL database into lots of different recycler views in
  • 我可以以编程方式更改 Xamarin.Forms 中的 styles.xml 吗?

    我们有一个可自定义颜色的应用程序 这使得列表视图中所选项目的橙色 Android 默认值有时看起来很糟糕 我们想要更改列表视图所选项目的颜色 我知道如何在我们页面的后台代码 xaml cs 中执行此操作 并且我知道您可以在 styles x
  • 风味 A 通过的 Robolectic 测试用例,风味 B 失败

    我的应用程序包含 A 和 B 两种风格 当我运行时 gradlew app test风味 A 的所有测试都通过 而风味 B 则失败 我也有两种构建类型debug and release 我的架构是这样的 my project app src
  • 没有 WindowManager.LayoutParams.TYPE_PHONE 的粘性覆盖

    我所说的粘性是指一个不会通过调用启动器意图而关闭的窗口 intent addCategory Intent CATEGORY HOME 以前这是用完成的WindowManager LayoutParams TYPE PHONE 但此类型现已
  • java.lang.IllegalStateException Butterknife

    我面临着java lang IllegalStateException Required view splash text 但我已将其包含在 xml 中 我在用Butterknife绑定视图 compile com jakewharton
  • 可以以编程方式打开“立即发言”对话框吗?

    是否可以通过编程方式打开 立即发言 对话框 目前 如果用户点击我的 搜索 按钮 则会打开一个对话框 并且我会自动打开软键盘 因此用户无需点击文本编辑字段 我想提供一个替代的 通过语音搜索 它将打开对话框并自动打开 立即发言 窗口 因此用户不
  • 如何使用NotificationCompat.Builder和startForeground?

    简短的问题 我正在尝试使用 NotificationCompat Builder 类来创建将用于该服务的通知 但由于某种原因 我要么看不到该通知 要么在该服务应该取消时无法取消它被破坏 或停止在前台 my code Override pub

随机推荐

  • Qt: error: C2001: 常量中有换行符

    这里简单记录在使用Qt Creator时遇到的一个问题 如下一行代码 没有任何问题 但编译运行时会报错 label gt setText label 我是一个窗口 当使用中文时 可能会习惯性的使用QObject tr 函数 label gt
  • 文件查重FindDupFile

    finddupfile是网上用的比较多的一款绿色查重软件 具体步骤请参考以下 1 下载软件 可从网上下载该软件 或者从以下地址进行下载 本软件从网上可自行下载 下载链接 https pan baidu com s 1KDJ9U4U 8HQa
  • 数学分析闭区间套定理_什么是区间套定理?

    什么是闭区间 数轴上任意两点和这两点间所有点组成的线段为一个闭区间 闭区间套定理 有无穷个闭区间 第二个闭区间被包含在第一个区间内部 第三个被包含在第二个内部 以此类推 后一个线段会被包含在前一个线段里面 这些区间的长度组成一个无穷数列 如
  • FineReportV10.0入门

    第三章 报表属性设计基础 第三课层次坐标 常用公式
  • gitlab CI/CD自动化部署

    文章目录 1 gitlab Runner 1 1 安装gitlab Runner 1 2 注册runner 2 gitlab ci yml的书写 3 部署 3 1 docker方式部署 3 2 OSS部署 CI CD 是一种持续开发软件的方
  • 针对QT——“在程序文件中(*ui,*cpp,*h)更改之后编译运行的程序结果无法更新”——解决方案

    本篇文章主要介绍在QT中 对程序文件 ui cpp h 更改之后编译运行的程序结果却无法更新的解决方案 问题描述 在设计QT的GUI用户界面时 我们需要不断对程序文件进行修改以优化用户体验 因此需要更新程序的生成文件 实际经历 笔者最近在一
  • IntelliJ Spring Configuration Check

    用IntelliJ 导入现有工程时 如果原来的工程中有spring 每次打开工程就会提示 Spring Configuration Check 开始不知道怎么回事 但工程不影响 首先到工程设置界面 工程设置 Project Structur
  • 1-APP启动源码分析-1

    桌面app也就是我们认识的launcher app 点击app icon启动到app内部的过程分为2种情况 一种是冷启动 一种叫热启动 冷启动 系统没有创建过app的进程 也就是后台没有此app进程 所以冷启动系统会创建一个新的进程分配给a
  • 高效阅读嵌入式源码系列一:静态分析神器understand软件基本操作

    系列文章目录 高效阅读嵌入式源码系列一 静态分析神器understand软件基本操作 高效阅读嵌入式源码系列二 understand阅读linux uboot等源码 高效阅读嵌入式源码系列三 understand阅读经keil MDK编译的
  • Celery + custom backend

    custom backend 以redis为例 本质是用自定义的backend替换RedisBackend https docs celeryq dev en stable getting started backends and brok
  • 盘点 10大 数据库!

    大家好 我是小猿 DB Engines 最近发布了 2021 年 9 月份的数据库排名 该网站根据数据库管理系统的受欢迎程度对其进行排名 实时统计了 378 种数据库的排名指数 前 30 名的排行情况详见下图 前10大数据库 用线段做了分割
  • Docker安装clickhouse

    目录 1 创建相关配置目录 2 拉取镜像 3 查看 Network ports ClickHouse Docs 中端口号配置 暂时只需要映射8123 9000 两个端口 4 创建临时容器 用以生成配置文件 5 将配置文件复制到 data c
  • 达梦数据库教程:DM8查看试用版证书到期时间SQL

    达梦数据库试用期限为一年 不是以本地安装时间算起的 是安装包已经把开始时间和截止时间内置好了 官网定期更新安装包版本 如果想看你安装的达梦啥时候到期 执行以下语句即可 select from v license 查询出记录后 看EXPIRE
  • MySQL 留存率和复购率的场景分析

    实际工作中常见的业务场景是求次日留存率 还有一些会对次日留存率增加限制 例如求新用户的次日留存率或者求活跃用户留存率 另外 留存率和复购率看起来都是统计重复出现的概率 但实际求解方法是不一样的 场景 次日留存率 复购率 知识点 留存率的定义
  • gdb如何调试动态链接库

    gdb file
  • 桌面右键加入“用管理员权限运行命令行”DOS

    在桌面右键和文件夹空白处右键中加入 用管理员权限运行命令行 DOS Windows Registry Editor Version 5 00 HKEY CLASSES ROOT Directory Background shell runa
  • Django图书商城系统实战开发-总结经验之后端开发

    Django图书商城系统实战开发 总结经验之后端开发 简介 在这篇博客中 我将总结经验分享后端开发Django图书商城系统的过程 在开发过程中 我遇到了各种挑战和问题 并且通过实践获得了宝贵的经验和教训 通过本文 我希望能帮助读者更好地了解
  • 抖音卡片/快手/小红书/h5浏览器/微博跳转微信/qq/微信公众号/指定链接

    首先说明 本文内容及教程均转载自 抖音私信卡片系统源码搭建 图文教程 已经本人允许 功能说明 抖音卡片跳转 微信 抖音卡片跳转 qq 抖音卡片跳转 微信公众号 抖音卡片跳转 指定网页链接 快手跳转 微信 快手跳转 qq 快手跳转 微信公众号
  • Jmeter(二十四) - 从入门到精通 - JMeter函数 - 中篇(详解教程)

    1 简介 在性能测试中为了真实模拟用户请求 往往我们需要让提交的表单内容每次都发生变化 这个过程叫做参数化 JMeter配置元件与前置处理器都能帮助我们进行参数化 但是都有局限性 为了帮助我们能够更好地进行参数化 JMeter提供了一组函数
  • Android Handler消息机制原理最全解读(持续补充中)

    本文主要详细去解读Android开发中最常使用的Handler 以及使用过程中遇到的各种各样的疑问 Handler 在Android开发的过程中 我们常常会将耗时的一些操作放在子线程 work thread 中去执行 然后将执行的结果告诉U