android 消息机制详解

栏目: 后端 · 发布时间: 5年前

内容简介:相信于此,绝大多数同学都会回答消息机制是android 为了线程间通信而引入的工具。可以轻松的将一个任务切换到handler所在线程执行。android开发规范有规定,不允许于子线程更新ui,这样会触发异常;我们平时使用handler主要都是将子线程切换到主线程中去执行;因此从本质上来来说,Handler并不是专门用于更新UI的,它只是常被开发者用来更新UI。A:因为Android的UI线程是非A:我们都知道在java中,线程存在以下几种基本状态:

相信于此,绝大多数同学都会回答消息机制是android 为了线程间通信而引入的工具。可以轻松的将一个任务切换到handler所在线程执行。android开发规范有规定,不允许于子线程更新ui,这样会触发异常;我们平时使用handler主要都是将子线程切换到主线程中去执行;因此从本质上来来说,Handler并不是专门用于更新UI的,它只是常被开发者用来更新UI。

Q?为何不能在主线程外更新ui呢?

A:因为Android的UI线程是非 线程安全 的,应用更新UI,是调用 invalidate() 方法来实现界面的重绘,而 invalidate() 方法是 非线程安全 的,也就是说当我们 在非UI线程来更新UI时,可能会有其他的线程或UI线程也在更新UI,这就会导致界面更新的不同步 。因此我们不能在非UI主线程中做更新UI的操作。也就是说我们在使用Android中的线程时,要保证: 更新ui都在UI主线程执行.

Q:那为何不将需要更新ui的操作放在UI线程执行呢?

A:我们都知道在 java 中,线程存在以下几种基本状态: 创建 , 就绪运行阻塞 , 死亡 。我们的应用启动后,所有的交互都是在 UI 线程完成的;如果在 UI 执行延时操作,如常见的 网络请求UI 线程就会进入 阻塞 状态;此时用户就无法响应任何操作了;如果此过程超过5秒,就会让程序处于 ANR(application not response) ,这时用户就可能想要和你的应用说声 gg 了。

Q:Android提供了哪几种线程间通信方式?

A: AsyncTask? , Handler 。为什么 AsynTask 打了个 ? 呢,我们可以简单看下 AsynTask 源码,他内部也是接住handler来进行线程间通信的。

Q:MessageQueue存在Targer对象的消息,那和我们正常流程中,由handler传递的消息有什么出入呢?

A:其实平时我们使用的 Message ,都是通过 Handler 发送的,有一些系统消息,他们会直接通过调用 MessageQueue 发送一个屏障消息,这类消息没有 Target ,然后配合 Handler 发送异步消息来使用;当 MessageQueue 读取到屏障消息后,他们会直接在链表中找到最近的 异步消息 ,直接执行。

feature-要素

  • Message(消息单元)定义一个可以发送到 Handler 的消息;它定义了 消息Id ,两个额为的int字段和一个额外的 object字段 (消息处理对象),它们可以不被初始化;虽然它的构造方法是public,但是还是建议我们通过obtain系列函数进行定义。

  • MessageQueue(消息队列)存放所有发送的消息队列,单链表结构,供Looper从中读取数据;延时消息是怎么存取的,这个很有趣;

  • Looper(消息读取者)永动机;其中有个死循环函数 Loop() ,不断读取 MessageQueue 中的消息,交给目标处理;问题来了,既然是个死循环,那不是始终会阻塞 Looper 所在线程吗。这又是如何解决的。

  • Handler(消息分发以及处理者)通过 sendMessage 系列函数,会将 Message 传入 MessageQueue 中; Looper.loop() 读取到消息传递给 Handler 处理。

desc

  1. handler 创建前, Looper.loop() 执行前;需要保证当前线程 Looper 有创建,而这个保证即 Looper.prepare() ;主线程由于在1ActivityThread1创建时,已经做过,所以无需执行;
  2. Looper.loop() 中有一个死循环,所以线程资源不会释放;在线程运行结束时调用 MessageQueue 中的 quit 函数,我们才能释放资源;
  3. Java 中,所有非静态成员变量会持有当前对象的引用(不然你又是怎么引用外部类的各种成员变量和函数等);那样我们在 Activity 中通过 new Handler() , 创建的对象会持有当前页面的引用;而我们发送的每个消息不能保证是立即执行,以及迅速执行结束的, handler.sendEmptyMessageDelayed ;消息是会持有 handler 做为他的 target ,那在这个 message 在通过 msg.target.dispatchMessage(msg); 会一直被持有;这样会导致 messageQueue->message->handler->activity|fragment ;在页面被销毁,声明周期执行到 desatory 时, activity 不会得到释放,从而 内存泄漏handler 得到消息处理时,如果当前页面已经被销毁,执行 Ui 更新,又会导致难以预料的问题。
  4. 针对 3 所提的我们可以按以下两种处理: 1:页面 destory 销毁时,调用 handler.removeCallbacksAndMessages(null); 2:通过软引用创建静态Handler对象;

流程解析

android handler流程分析晚上有很多资料;我们这儿简单介绍下:

  • Looper , MessageQueue 就绪;调用 Looper.prepare() ,其间会向 Looper 静态线程变量 sThreadLocal 插入一个当前线程的 Looper ;在调用 Looper 构造函数时,我们会初始化 MessageQueue ,并将 mThread 设置为当前 Thread.currentThread();

  • Looper.prepare() 代码块如下:

    public static void prepare() {
            prepare(true);
        }
    
    private static void prepare(boolean quitAllowed) {
        	//sThreadLocal->ThreadLocal对象,里面封装了一个map逻辑,key是线程hash值;static 类变量
           if (sThreadLocal.get() != null) {//不允许多次prepare
                throw new RuntimeException("Only one Looper may be created per thread");
           }
           sThreadLocal.set(new Looper(quitAllowed));//设置当前线程的Looper
     }
    复制代码

    Looper 构造函数,以及 MessageQueue 构造函数如下:

    private Looper(boolean quitAllowed) {
            mQueue = new MessageQueue(quitAllowed); //初始化消息池
            mThread = Thread.currentThread();
     }
    //是否允许退出
    MessageQueue(boolean quitAllowed) {
            mQuitAllowed = quitAllowed; 
            mPtr = nativeInit(); //线程id
      }
    复制代码
  • 接下来我们看下数据插入

    sendMessage(Message msg)
    sendEmptyMessage(int what)
    sendEmptyMessageDelayed(int what, long delayMillis)
    sendEmptyMessageAtTime(int what, long uptimeMillis)
    sendMessageDelayed(Message msg, long delayMillis)
    sendMessageAtTime(Message msg, long uptimeMillis)
    sendMessageAtFrontOfQueue(Message msg)
    

    这以上七个方法,可以通过handler向handler所在线程发送消息;其中 1,2,3,4,5 都是调用方法 6 进行执行的;其中方法 6 中的 uptimeMillis 取的是系统非休眠时间 SystemClock.uptimeMillis()

    我们接下来看下 sendMessageAtTime(Message msg, long uptimeMillis) :

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue; //将线程变量Looper中的queue取出使用
        if (queue == null) { //queue判空,其实创建handler时,也是必须要Looper初始化结束;queue创建后的
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);//简单判断,交给enqueueMessage函数执行;
    }
    复制代码

    7.sendMessageAtFrontOfQueue(Message msg) 调用的函数如下:

    public final boolean sendMessageAtFrontOfQueue(Message msg) {
            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, 0);//简单判断,交给enqueueMessage函数执行;设置执行时间0
        }
    复制代码

    据此,我们发现所有消息的发送都是通过 MessageQueueenqueueMessage(Message msg, long when) 方法;

  • 我们来看下 enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this; //将msg的target设置为当前handler;这儿可以看出msg和handler是 n:1的关系
        if (mAsynchronous) {//handler是否是异步?默认false;将值赋予msg
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);//可以看到,此处最终调用了eqeue的enqueueMessage方法
    }
    复制代码

    那我们看下 enqueueMessage 函数(handler的消息基本都是通过该函数放入线程 MessageQueue 中):

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {//handler不能为空
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {//消息是否在被使用处理
            throw new IllegalStateException(msg + " This message is already in use.");
        }
    
        synchronized (this) {//同步锁
            if (mQuitting) {//mQuitting;子线程消息池我们在线程即将结束时,调用这个mQuitting退出;之后发送的消息都是不会被收入消息池的;所以如果遇到消息没有发送成功,我们可能需要判断是不是looper已经退出了;
                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;
                //表头消息替换为放入的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.
                //入队列中,默认不唤醒,仅当头部msg是屏障消息,当前msg是异步消息
                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插入对应节点
                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;
    }
    复制代码
  • 于此我们所有的消息入栈已经看完了;那消息是怎么获取的呢; mBlocked 是不是真的代表线程阻塞呢?根据前面的图形介绍,我们知道, Looper 中有一个 loop 函数,他是一个死循环,负责向 MessageQueue 读取数据,接下来我们来看下这个函数;

    /**
         * Run the message queue in this thread. Be sure to call
         * {@link #quit()} to end the loop. 在线程结束时,要调用looper.quit()退出
         */
        public static void loop() {
            final Looper me = myLooper(); //静态方法,获取当前线程的Looper;两个工作线程进行通信,需要先在补获线程调用prepare(),并在其run()结束时,调用quit()
            if (me == null) {
                throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
            }
            final MessageQueue queue = me.mQueue; //获取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();  //清空远程调用端的uid和pid,用当前本地进程的uid和pid替代
            final long ident = Binder.clearCallingIdentity();
    
            // Allow overriding a threshold with a system prop. e.g.
            // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
            final int thresholdOverride =
                    SystemProperties.getInt("log.looper."
                            + Process.myUid() + "."
                            + Thread.currentThread().getName()
                            + ".slow", 0);
    
            boolean slowDeliveryDetected = false;
    
            for (;;) {
                Message msg = queue.next(); // might block 获取消息可能阻塞线程;我们先分析它,后面的代码下面分析
               ……
            }
        }
    复制代码

    MessageQueue.next():

    Message next() {
            // Return here if the message loop has already quit and been disposed.
            // This can happen if the application tries to restart a looper after quit
            // which is not supported. 
            final long ptr = mPtr;
            if (ptr == 0) { //在调用quit结束loop后,又一次尝试调用prepare后,此时ptr会为0,不支持
                return null;
            }
    
            int pendingIdleHandlerCount = -1; // -1 only during first iteration 默认pendingIdleHandler为0
            int nextPollTimeoutMillis = 0; //需要阻塞时间,-1表示无限阻塞,直到消息入栈调用nativeWake唤醒
            for (;;) {
                if (nextPollTimeoutMillis != 0) {//time不为0存在阻塞
                    Binder.flushPendingCommands();  //native方法,看注释是配合线程长时间阻塞使用,用于释放任何的挂起对象
                }
    
                nativePollOnce(ptr, nextPollTimeoutMillis);//线程阻塞,time阻塞时长
    			
                //同步锁
                synchronized (this) {
                    // Try to retrieve the next message.  Return if found.
                    final long now = SystemClock.uptimeMillis();
                    Message prevMsg = null;
                    Message msg = mMessages;
                    //当前消息的目标为屏障消息(消息无target),找寻下一个异步消息执行
                    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) {
                        //when比当前时间大;需要阻塞
                        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 {
                            //返回这个将要执行的消息;将mBlocked阻塞置false;将当前message置为执行消息后一个
                            // 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();
                        dispose();
                        return null;//返回null作为next()执行结果,注意,此时Looper.loop()也会执行结束
                    }       
                    //idleHandlers->idleHandler是指一个线程当前没有需要立即执行的消息,(延时执行or无消息)时,会执行的一个callback;根据上面的分析,只有在next()执行,且没有需要返回消息时执行         
                    if (pendingIdleHandlerCount < 0 
                            && (mMessages == null || now < mMessages.when)) {//延时执行or无消息
                        pendingIdleHandlerCount = mIdleHandlers.size(); //只有调用addIdleHandler加入idle时,count才会增加
                    }
                    //默认0;无idle时,mBlocked阻塞置为true,执行循环 for (;;) {}内部内容
                    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.
                // We only ever reach this code block during the first iteration.
                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();//根据queueIdle返回值,决定是否需要执行后移除该idle
                    } catch (Throwable t) {
                        Log.wtf(TAG, "IdleHandler threw exception", t);
                    }
    
                    if (!keep) {
                        synchronized (this) {
                            mIdleHandlers.remove(idler);
                        }
                    }
                }
    			
                //执行完PendingIdleHandler后,我门将count置为0,不再执行他
                // Reset the idle handler count to 0 so we do not run them again.
                pendingIdleHandlerCount = 0;
    			//执行完idle后,可能有消息准备就绪,我们重新计算阻塞时间
                // 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;
            }
        }
    复制代码

    总结一下, MessageQueue 不断获取待执行消息,并可能阻塞线程(没有message or 待执行 messagewhen 比当前时间晚);而 MessageQueue 提供了一个 Idle机制 ,用于在当前线程没有由于没有待执行 Message 或者 延时Message 时执行,而 addIdleHandler 就是用于添加 Idle

    我们再回头看下 Looper.loop() :

    for (;;) {
                Message msg = queue.next(); // might block 刚刚分析上文,当前消息是延时消息或者消息队列为空时,会进行阻塞
                if (msg == null) { //没有消息退出循环,loop结束工作 ; next控制 ;即Mq执行quit退出后,不在执行任何消息
                    // 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);
                }
    			//Trace用于追踪一个Message执行的;可以结合TraceView等 工具 查看,具体请百度吧;而我们的Ui线程所有的ui绘制,事件流执行,等都属于一个消息,可以通过Trace进行跟踪;
                final long traceTag = me.mTraceTag;
                long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
                long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
                if (thresholdOverride > 0) {
                    slowDispatchThresholdMs = thresholdOverride;
                    slowDeliveryThresholdMs = thresholdOverride;
                }
                final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
                final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);
    
                final boolean needStartTime = logSlowDelivery || logSlowDispatch;
                final boolean needEndTime = logSlowDispatch;
    		
                if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                    Trace.traceBegin(traceTag, msg.target.getTraceName(msg));//跟踪起始
                }
    
                final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
                final long dispatchEnd;
                try {
                    msg.target.dispatchMessage(msg); //分发消息
                    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; //记录执行结束时间
                } finally {
                    if (traceTag != 0) {
                        Trace.traceEnd(traceTag);//跟踪结束
                    }
                }
         		//Slow?没研究,但也是打印相关日志信息的。。
                if (logSlowDelivery) {
                    if (slowDeliveryDetected) {
                        if ((dispatchStart - msg.when) <= 10) {
                            Slog.w(TAG, "Drained");
                            slowDeliveryDetected = false;
                        }
                    } else {
                        if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                                msg)) {
                            // Once we write a slow delivery log, suppress until the queue drains.
                            slowDeliveryDetected = true;
                        }
                    }
                }
                if (logSlowDispatch) {
                    showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
                }
    
                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();//msg结合obtion()实现对象复用
            }
    复制代码

    结合代码来看 Looper.loop() 所做的事情不多,主要都是用于记录分析 Message 信息的:

  • 开启一个死循环,将消息读取交给 MessageQUeuenext() 函数,该函数可能导致线程阻塞;

  • 提供一个 Printer 接口,记录打印每个 Message 的执行开始和结束信息;

  • 提供的 Trace 函数用来记录每个消息的处理信息;

  • 通过 msg.target.dispatchMessage(msg) 执行消息

  • 通过 msg.recycleUnchecked() 回收消息,使得 Message 消息池得到复用; Message 是一个链表结构,提供了 Message.obtion() 方法,用于不断的取链表头对象;在表头空时新建;消息执行完调用的 recycleUnchecked 会将 Message 相关消息情况,插入链表头

    至此我们对 Android 的消息机制发送和读取有了一个完整的了解:下面附上一个简单的流程图(md的流程图绘制,真心累啊。)

对于一个消息创建流程,加入消息队列, MessageQueue 简单通过 Mq 代表了:

android 消息机制详解

接下来是消息读取的流程图:

android 消息机制详解

上面流程图中有涉及到一个新的消息概念 屏障消息(无target的消息)

我们向消息队列 MessageQueue 发送一个屏障消息,然后再发送一个异步消息;在我们读取到这个屏障消息的时候,我们会找到链表后的第一个异步消息;这样就能快速执行该异步消息了;

系统有一个 postSyncBarrier() 用来发送屏障消息,但是被隐藏了;我们可以反射调用或者直接向 MessageQueue 表头反射插入一个 Message ,但是不建议这样做;

发送异步消息可以通过:创建 Handler 时,传入异步参数:

public Handler(boolean async);
public Handler(Callback callback, boolean async);
public Handler(Looper looper, Callback callback, boolean async);
复制代码

这样就能发送屏障消息和异步消息了;

在系统源码 ViewRootImpl.scheduleTraversals 中,为了更快响应 UI刷新事件时

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //设置同步障碍,确保mTraversalRunnable优先被执行
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //内部通过Handler发送了一个异步消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
复制代码

mTraversalRunnable 调用了 performTraversals 执行 measure、layout、draw

为了让 mTraversalRunnable 尽快被执行,在发消息之前调用 MessageQueue.postSyncBarrier 设置了同步屏障


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

驯服烂代码

驯服烂代码

伍斌 / 机械工业出版社 / 2014-11 / 69.00

Kent Beck、Martin Fowler、Michael C. Feathers、Robert C. Martin、Joshua Kerievsky、Gerard Meszaros等大师们的传世著作为如何提升编程技艺和代码质量提供了思想和原则上的指导,本书则为实践和融合这些思想、原则提供了过程和方法上指导。本书通过编程操练的方式讲述了如何用TDD(测试驱动开发)的方法来驯服烂代码,通过结对编......一起来看看 《驯服烂代码》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

MD5 加密
MD5 加密

MD5 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具