跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Java大前端java

Android Handler 消息机制深度解析

深入解析了 Android 中的 Handler 消息机制,涵盖了 Handler、Looper、MessageQueue 及 ThreadLocal 的核心原理。文章详细说明了为何 UI 操作需单线程处理,并剖析了消息的发送、入队、轮询及分发的完整流程。通过对关键源码如 enqueueMessage、next、loop 及 dispatchMessage 的分析,揭示了消息如何在不同线程间传递以及如何保证线程安全。同时解释了主线程 Looper 的特殊初始化过程以及 Handler 回调优先级的处理逻辑,帮助开发者全面理解 Android 异步通信模型。

怪力乱神发布于 2025/2/7更新于 2026/7/2047 浏览
Android Handler 消息机制深度解析

Android Handler 消息机制深度解析

Android 提供了 Handler 和 Looper 来满足线程间的通信。Handler 遵循先进先出原则,Looper 类用来管理特定线程内对象之间的消息交换。Handler 消息机制可以说是 Android 系统中最重要部分之一,下面我们将深入解析其工作原理。

Handler 的简单使用

为什么系统不允许子线程更新 UI

因为 UI 控件不是线程安全的。如果在多线程中并发访问可能会导致 UI 控件处于不可预期的状态。那为什么不对 UI 控件的访问加上锁机制呢?主要有两个缺点:

  1. 上锁会让 UI 控件变得复杂和低效。
  2. 上锁后会阻塞某些进程的执行。

对于手机系统来说,这两个缺点是不可接受的,所以最简单高效的方法就是采用单线程模型来处理 UI 操作。对开发者而言也不是很麻烦,只是通过 Handler 切换一下访问的线程即可。

Handler 的使用步骤

既然子线程不能直接更改界面,我们可以借助 Handler 来更新界面。主要步骤如下:

  1. new 出来一个 Handler 对象,复写 handleMessage 方法。
  2. 在需要执行更新 UI 的地方 sendEmptyMessage 或者 sendMessage。
  3. 在 handleMessage 里面的 switch 里面 case 不同的常量执行相关操作。
public class MainActivity extends ActionBarActivity {

    private TextView mTextView;
    private Handler mHandler;
    private static final int UI_UPDATE1 = 0;
    private static final int UI_UPDATE2 = 1;

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

        mHandler = new Handler() {
            @Override
            public void  {
                 (msg.what) {
                     UI_UPDATE1:
                        mTextView.setText();
                        ;
                     UI_UPDATE2:
                        mTextView.setText();
                        ;
                }
            }
        };
        mTextView = (TextView) findViewById(R.id.textview);
        mTextView.setOnClickListener( () {
            
               {
                Log.d(, );
                updateUi();
            }
        });
    }

       {
         ( () {
            
               {
                
                
                
                
                   Message.obtain();
                msg.what = UI_UPDATE2;
                mHandler.sendMessage(msg);
            }
        }).start();
    }
}
handleMessage
(Message msg)
switch
case
"通过 Handler 方法 1 修改 UI"
break
case
"通过 Handler 方法 2 修改 UI"
break
new
OnClickListener
@Override
public
void
onClick
(View v)
"Test"
"点击文字"
protected
void
updateUi
()
new
Thread
new
Runnable
@Override
public
void
run
()
// 方式一和方式二可以达到相同的效果,就是更改界面
// 方式一
//mHandler.sendEmptyMessage(UI_UPDATE1);
// 方式二
Message
msg
=

对应的布局文件 activity_main.xml 如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.handlerdemo3.MainActivity">

    <TextView
        android:id="@+id/textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world"
        android:layout_centerInParent="true"
        android:clickable="true" />

</RelativeLayout>

消息机制的分析理解

安卓的异步消息处理机制就是 handler 机制。主线程 ActivityThread 被创建的时候就会创建 Looper,Looper 被创建的时候创建 MessageQueue。也就是说主线程会直接或间接创建出来 Looper 和 MessageQueue。

Handler 的工作机制简单来说是这样的:

  1. Handler 发送消息仅仅是调用 MessageQueue 的 enqueueMessage 向插入一条信息到 MessageQueue。
  2. Looper 不断轮询调用 MessageQueue 的 next 方法。
  3. 如果发现 message 就调用 handler 的 dispatchMessage,dispatchMessage 被成功调用,接着调用 handleMessage()

Handler 消息机制的源码分析

ThreadLocal 工作原理

首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过 ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。

另外,说 ThreadLocal 使得各线程能够保持各自独立的一个对象,并不是通过 ThreadLocal.set() 来实现的,而是通过每个线程中的 new 对象的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过 ThreadLocal.set() 将这个新创建的对象的引用保存到各线程的自己的一个 map 中,每个线程都有这样一个 map,执行 ThreadLocal.get() 时,各线程从自己的 map 中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal 实例是作为 map 的 key 来使用的。

如果 ThreadLocal.set() 进去的东西本来就是多个线程共享的同一个对象,那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,还是有并发访问问题。

查看 ThreadLocal 的 set 方法:

public void set(T value) {
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values == null) {
        values = initializeValues(currentThread);
    }
    values.put(this, value);
}

查看 values 方法里面做了什么:

Values values(Thread current) {
    return current.localValues;
}

通过上面代码我们可以知道,当 values 为空的时候才调用 initializeValues 方法进行初始化。其实就是各种赋值 table 数组,进行初始化。最后才是调用 values.put(this, value) 把 ThreadLocal 和 value 一起保存。

可以看出,Threadlocal 的值在 table 数组的存储位置总是 reference 的下一个位置。

接下来,查看 Threadlocal 的 get 方法。Get 方法的逻辑是:通过 values 方法取出当前线程的 localValues 对象,如果为 null,就返回初始值。如果 localValues 不为 null,取出其 table 数组,如果 reference 等于 table 数组 index 角标的值,就在 table[index + 1] 取出其 Threadlocal 值。

MessageQueue 工作原理

MessageQueue 中文翻译就是消息队列,它内部存储了一组信息,存放的是 Message,以队列的形式对外提供了插入和删除的工作(虽然名字叫做队列,但是其内部的存储结构是单链表)。主要插入和读取两个操作,这两个操作对应着两个方法:

  • 插入(入队)enqueueMessage(Message msg, long when)
  • 读取(出队)next()

查看 enqueueMessage 的源码:

boolean enqueueMessage(Message msg, long when) {
    if (msg.isInUse()) {
        throw new AndroidRuntimeException(msg + " This message is already in use.");
    }
    if (msg.target == null) {
        throw new AndroidRuntimeException("Message must have a target.");
    }

    boolean needWake;
    synchronized (this) {
        if (mQuiting) {
            RuntimeException e = new RuntimeException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w("MessageQueue", e.getMessage(), e);
            return false;
        }

        msg.when = when;
        Message p = mMessages;
        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 {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            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;
        }
    }
    if (needWake) {
        nativeWake(mPtr);
    }
    return true;
}

从 enqueueMessage 的实现来看,主要操作就是单链表的插入操作。接下来查看 next 方法的实现:

Message next() {
    // Return here if the message loop has already quit and been disposed.
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // Stalled by a barrier.  Find the next asynchronous message in the queue.
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // Next message is not ready.  Set a timeout to wake up when it is ready.
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // Got a message.
                    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;
            }

            // Process the quit message now that all pending messages have been handled.
            if (mQuitting) {
                dispose();
                return null;
            }

            // If first time idle, then get the number of idlers to run.
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                mBlocked = true;
                continue;
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);

            // Run the idle handlers.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }
}

从上面代码可以得出以下结论:

  1. next 方法是一个无限循环的方法,如果消息队列中没有消息,那么 next 方法会一直阻塞。
  2. 当有新消息到来,next 方法会返回这条消息,并将其从单链表中移除。

Looper 的工作原理

Looper 是一个轮询器,它的作用不断轮询 MessageQueue,当如果有新的消息就交给 Handler 处理,如果轮询不到新的消息,那就自身就处于阻塞状态。

查看 Looper 类的源码,可以发现他的构造方法里面创建了一个 MessageQueue,然后将当前线程的对象保存起来:

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

在 Looper 的构造方法中初始化了一个消息队列 MessageQueue 和一个线程 Thread。从这可看出:一个 Looper 对应着一个消息队列以及当前线程。当收到消息 Message 后系统会将其存入消息队列中等候处理。至于 Looper,它在 Android 的消息机制中担负着消息轮询的职责,它会不间断地查看 MessageQueue 中是否有新的未处理的消息;若有则立刻处理,若无则进入阻塞。

相信大家一定有遇到过,在子线程中创建 Handler 会报错。解决办法就是 new Handler 的时候加上 Looper.prepare()。而 Looper.prepare() 的内部实现逻辑就是创建一个 Looper:

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));
}

线程默认是没有 Looper 的,但是为什么在主线程没有创建的 Looper 就可以使用 Handler?主线程是特别的。主线程,也就是 ActivityThread,当主线程被创建的时候,会调用 Looper 内的 prepareMainLooper 方法,创建 Looper,该方法是专门给主线程创建 Looper 用的。也正因为这点,所以我们在主线程创建了 Handler 就直接能用了。

public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}

由于这个特殊性,Looper 还提供了一个 getMainLooper 方法,使得可以在任何地方获取主线程的 Looper。

public static Looper getMainLooper() {
    synchronized (Looper.class) {
        return sMainLooper;
    }
}

接下来,我们看一下 Looper 的退出。Looper 提供了两个方法进行退出操作,分别是 quit 和 quitSafely,他们调用的是 MessageQueue 的 quit 方法。

public void quit() {
    mQueue.quit(false);
}

public void quitSafely() {
    mQueue.quit(true);
}

MessageQueue 的 quit 方法:

void quit(boolean safe) {
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }

    synchronized (this) {
        if (mQuitting) {
            return;
        }
        mQuitting = true;

        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }

        // We can assume mPtr != 0 because mQuitting was previously false.
        nativeWake(mPtr);
    }
}

如果调用 Looper.quit 方法,最终会调用 removeAllMessagesLocked 方法,该方法逻辑:直接遍历所有的消息,并将消息强制回收。

private void removeAllMessagesLocked() {
    Message p = mMessages;
    while (p != null) {
        Message n = p.next;
        p.recycleUnchecked();
        p = n;
    }
    mMessages = null;
}

如果调用 Looper.quitSafely 方法,最终会调 removeAllFutureMessagesLocked 方法,该方法逻辑:

void removeCallbacksAndMessages(Handler h, Object object) {
    if (h == null) {
        return;
    }

    synchronized (this) {
        Message p = mMessages;

        // Remove all messages at front.
        while (p != null && p.target == h
                && (object == null || p.obj == object)) {
            Message n = p.next;
            mMessages = n;
            p.recycleUnchecked();
            p = n;
        }

        // Remove all messages after front.
        while (p != null) {
            Message n = p.next;
            if (n != null) {
                if (n.target == h && (object == null || n.obj == object)) {
                    Message nn = n.next;
                    n.recycleUnchecked();
                    p.next = nn;
                    continue;
                }
            }
            p = n;
        }
    }
}

一个无限 for 循环,只有 n.when > now 和 n == null 才会跳出循环,说明是等消息队列中已有的消息处理完毕后,才会跳出,然后执行回收。

Looper 这个类里面最重要的方法就是 loop() 开启消息循环这个方法了,看一下 loop 代码的实现逻辑:

public static void loop() {
    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();
    }
}

通过代码我们知道:looper 方法是一个死循环,唯一跳出的循环的方式是 MessageQueue 的 next 方法返回 null,但这几乎不可能,因为在 MessageQueue 的 next 方法中,假如没有消息加入队列,next 方法会一直阻塞,不会返回 null。如果我们不手动调用 quit 或者 quitSafely 方法的话,MessageQueue 的 next 方法是不可能返回 null 的。当 MessageQueue 没有消息时,next 方法会一直阻塞在那里,因为 MessageQueue 的 next 方法阻塞了,就导致 Looper 的 loop 方法也一直在阻塞了。

这里我们那一分为二的谈,loop 轮询不到消息:那么处于阻塞状态,然后就没有然后了,除了又轮询到了新的消息。loop 轮到了新的消息:Looper 就会处理消息。

  1. msg.target.dispatchMessage(msg),这里的 msg.target 就是指 Handler 对象。
  2. 到了最后,Handler 发送的消息又交给了自己的 dispatchMessage 方法来处理了。(这个 dispatchMessage 方法不是 Handler 自己调用时,是与 Handler 相相关的 Looper 间接调用的),这样下来,就成功地将逻辑切换到指定的线程当中去了。

Handler 的工作原理

Handler 的主要工作:消息的发送和接收。Handler 消息发送的形式有 post 和 send 两种。

查看 Handler 中 sendMessage 代码:

public final boolean sendMessage(Message msg) {
    return sendMessageDelayed(msg, 0);
}

sendMessage 调用 sendMessageDelayed:

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

sendMessageDelayed 调用 sendMessageAtTime:

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);
}

sendMessageAtTime 调用 enqueueMessage:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

得出结论:Handler 的发送消息仅仅是调用 MessageQueue 的 enqueueMessage 向插入一条信息到 MessageQueue,MessageQueue 就会返回这条消息给 Looper,Looper 会交给 msg.target.dispatchMessage(msg) 方法处理,就进入消息处理阶段。

查看 dispatchMessage 方法:

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

首先,判断 Message.Callback 是否为 null,不为 null,就调用 handleCallback 方法:

private static void handleCallback(Message message) {
    message.callback.run();
}

Callback 是一个 Runnable 对象,实际上就是 post 方法传递的 Runnable 参数。

其次,检查 mCallback 是否为 null,如果不为 null 就调用 mCallback.handleMessage 方法,查看 mCallback.handleMessage 方法:

public interface Callback {
    public boolean handleMessage(Message msg);
}

源码中注释已经对 Callback 进行了解释:可以用来创建一个 Handler 的实例但不需要派生 Handler 的子类。在日常开发中,创建 Handler 最常见的方式就是派生一个 Handler 的子类并重写 handleMessage 方法来处理具体的消息,而 Callback 给我们提供了另外一种方式,不需要派生 Handler 的子类。

最后,调用 Handler 的 handleMessage 方法,这就是我们平时写 Handler 要实现的方法。

主线程的消息循环

Android 的主线程就是 ActivityThread,主线程的入口方法是 main 方法,在 main 中系统会通过 Looper.prepareMainLooper() 来创建主线程的 Looper 以及 MessageQueue,并通过 Looper.loop() 开启主线程消息循环。

public static void main(String[] args) {
    SamplingProfilerIntegration.start();

    CloseGuard.setEnabled(false);

    Environment.initForCurrentUser();

    EventLogger.setReporter(new EventLoggingReporter());

    Security.addProvider(new AndroidKeyStoreProvider());

    Process.setArgV0("<pre-initialized>");

    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    AsyncTask.init();

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    Looper.loop();

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

主线程开始循环后,ActivityThread 还需要一个 Handler 来和消息队列进行交互,这个 Handler 就是 ActivityThread.H。

ApplicationThread 通过 binder 与 Ams 通信,并将 Ams 的调用,通过 H 类(也就是 Handler)将消息发送到消息队列,然后进行相应的操作,H 收到消息后,就会将 ApplicationThread 中逻辑切换到 ActivityThread 中执行,也就是主线程中执行,这个过程就是主线程的消息循环。

至此,Handler 消息机制就分析完毕。如有错漏,欢迎指正。

目录

  1. Android Handler 消息机制深度解析
  2. Handler 的简单使用
  3. 为什么系统不允许子线程更新 UI
  4. Handler 的使用步骤
  5. 消息机制的分析理解
  6. Handler 消息机制的源码分析
  7. ThreadLocal 工作原理
  8. MessageQueue 工作原理
  9. Looper 的工作原理
  10. Handler 的工作原理
  11. 主线程的消息循环
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 数据结构核心:链表详解与实现
  • 数据结构——图详解
  • GitHub Copilot 配置性能优化与开发环境调优指南
  • 国内 Stable Diffusion 与 LLaMA 模型镜像站推荐及训练实践
  • 前端开发实战:一天通常能完成多少个页面?
  • Python GUI 开发:主流框架对比与实战选择
  • 毕业论文全流程通用 AI 指令清单(21 条)
  • Python 核心语法详解:变量、流程控制与函数基础
  • C++ 网络编程中的 Protobuf 消息分发 (Dispatcher) 设计模式
  • Virt-A-Mate 虚拟实境交互软件技术特性解析
  • 链表数据结构详解:定义、操作与对比
  • 一次预测多个 token:Meta 的新训练方法在编码上表现亮眼
  • StructBERT WebUI 实战:万级评论去重脚本实现
  • C++ 类与对象进阶:初始化列表、静态成员与编译器优化实战
  • Java 调用百度地图 API 实现长沙热门路段与景点实时路况检索
  • 无需部署服务器:利用内网穿透实现本地服务远程访问
  • 普通产品经理转型 AI 产品经理:必备准备与技能提升
  • Python 自动化采集音乐数据实战:解决反爬与代理配置
  • Java SE 文件 IO 基础
  • Spring IOC 注解进阶:@Bean 管理第三方 Bean、@Import 拆分配置与@Value 注入

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online