preface

This article is mainly to analyze the Handler message mechanism of the key source code, before reading the need to have some basic understanding of the Handler. Here’s a quick recap:

The basic composition of

A complete message processing mechanism consists of four elements:

  1. Message: The carrier of information
  2. MessageQueue: A queue used to store messages
  3. Looper(message loop) : Is responsible for checking if there are messages in the message queue and fetching messages
  4. Handler(sends and processes messages) : Adds messages to message queues and is responsible for distributing and processing messages

Basic usage

A simple use of Handler is as follows:

Handler handler = new Handler(){
    @Override
    public void handleMessage(@NonNull Message msg) {                     
        super.handleMessage(msg); }}; Message message =new Message();
handler.sendMessage(message);
Copy the code

Note that the looper.prepare () and looper.loop () methods are called in the non-main thread

The working process

Its working process is shown in the figure below:

The process from sending messages to receiving messages is summarized as follows:

  1. Send a message
  2. The message enters the message queue
  3. Retrieves a message from a message queue
  4. Processing of messages

Here is a fold of four steps to analyze the relevant source code:

Send a message

Handle has two classes of methods for sending messages, which are essentially indistinguishable:

  • sendXxxx()
    • boolean sendMessage(Message msg)
    • boolean sendEmptyMessage(int what)
    • boolean sendEmptyMessageDelayed(int what, long delayMillis)
    • boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
    • boolean sendMessageDelayed(Message msg, long delayMillis)
    • boolean sendMessageAtTime(Message msg, long uptimeMillis)
    • boolean sendMessageAtFrontOfQueue(Message msg)
  • postXxxx()
    • boolean post(Runnable r)
    • boolean postAtFrontOfQueue(Runnable r)
    • boolean postAtTime(Runnable r, long uptimeMillis)
    • boolean postAtTime(Runnable r, Object token, long uptimeMillis)
    • boolean postDelayed(Runnable r, long delayMillis)
    • boolean postDelayed(Runnable r, Object token, long delayMillis)

There is no analysis of the specific method of characteristics, they are ultimately by calling sendMessageAtTime () or sendMessageAtFrontOfQueue implementation news team operation, the only difference is the post series method calls the getPostMessage before messages:

private static Message getPostMessage(Runnable r) {
    Message m = Message.obtain();
    m.callback = r;
    return m;
}
Copy the code

Note that sendMessageAtTime() is typically used when called by another sendXxx:

sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
Copy the code

If the caller does not specify a delay time, the message is executed at the current time, immediately. All methods exposed by Handler follow this action, and unless specified, MSG message execution time is: current time plus delay time, essentially a timestamp. Of course, you can specify any time you want, which will be used later in the message insert. The code is simple: it simply assigns the Runnable callback passed by the caller to Message. SendMessageAtTime () and sendMessageAtFrontOfQueue methods through enqueueMessage method realizes the message into the stack:

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);
    }
Copy the code

The code is very simple, mainly have the following operations:

  • Let message hold a reference to the Handler that sent it (this is key to finding the corresponding Handler when processing messages)
  • Set whether the message is asynchronous (asynchronous message does not need to queue, through the synchronization barrier, queue execution)
  • callMessageQueuetheenqueueMessageMethod to queue a message

The message enters the message queue

Preparation before joining the team

The enqueueMessage method is the key to adding messages to MessageQueue.

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
	    throw new IllegalArgumentException("Message must have a target.");
    }
    / /... Omit the following code
}    
Copy the code

The code is simple: Determine if message’s target is null, and throw an exception if it is. The target is the Handler reference mentioned above in handler. enqueueMessage. The next step is to determine and process the message

boolean enqueueMessage(Message msg, long when) {
    / /... Omit the above code
    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;
        / /... Omit the following code
    }
    / /... Omit the following code
}
Copy the code

A synchronized lock is added, all subsequent operations are performed in synchronized blocks, and two if statements are used to handle two exceptions:

  1. Determine whether the current MSG has been used, if so, exclude exceptions;
  2. Determine whether the MessageQueue (MessageQueue) is closing, if so, reclaim the message, return join failed (false) to the caller, and print the relevant log

If all is well, mark that the message is in use by markInUse (corresponding to the exception of the first IF), and then set the time when the message is sent (machine system time). Next, perform the insert operations

Queues messages

Continue with the code implementation of enqueueMessage

boolean enqueueMessage(Message msg, long when) {
    / /... Omit the above code
    synchronized (this) {
            / /... Omit the above code
            / / step 1
            Message p = mMessages;
            boolean needWake;
            / / step 2
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                / / step 3
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p;
                prev.next = msg;
            }
            
            if(needWake) { nativeWake(mPtr); }}/ / step 4
    return true;
}
Copy the code

MessageQueue uses a one-way linked list to maintain the MessageQueue, following a first-in, first-out soft solution. Analyze the code above:

  • Step 1: mMessages is the header of the list.

  • Step 2: a judgment statement, if the three conditions are met, directly use MSG as the table header:

    1. If the header is empty, there is no message in the queue. MSG is directly used as the header of the linked list.
    2. when == 0Indicating that the message is to be executed immediately (e.gsendMessageAtFrontOfQueueMethod, but generally sent messages are sent at time plus delay unless specified), MSG is inserted as the linked list header;
    3. when < p.when, indicating that the message to be inserted is executed earlier than the table header, and MSG is inserted as the linked list header.
  • Step 3: The message is inserted into the appropriate location by repeatedly comparing the execution time of the message in the queue with the execution time of the inserted message, following the principle of small timestamp first.

  • Step 4: Return to the caller that message insertion is complete.

Note needWake and nativeWake in your code, which are used to wake up the current thread. Because at the message fetching side, the current thread will enter the blocking state according to the state of the message queue, and will decide whether to wake up according to the situation at the time of insertion.

The next step is to retrieve the message from the message queue

Retrieves a message from a message queue

Again, look at the preparatory work first

The preparatory work

To use a Handler in a non-main thread, you must do two things

  1. Looper.prepare(): Create a Loop
  2. Looper.loop(): Start the loop

Let’s ignore its creation and look at the code at the beginning of the loop in sections: first, some checking and judging work, the details of which are commented in the code

 public static void loop(a) {
 		// Get the loop object
        final Looper me = myLooper();
        if (me == null) {
        	// If loop is empty, an exception is thrown to terminate the operation
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        if (me.mInLoop) {
        	// start the loop repeatedly
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }
        // indicates that loop is enabled
        me.mInLoop = true;
        // Get the message queue
        final MessageQueue queue = me.mQueue;
        // Make sure that permission checks are based on local processes,
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow".0);
        boolean slowDeliveryDetected = false;
        / /... Omit the following code
}        

Copy the code

Operations in loop

The loop is now officially open, with key code reduced:

 public static void loop(a) {
 		/ /... Omit the above code
 		for (;;) {
 			/ / step one
            Message msg = queue.next();
            if (msg == null) {
                / / in step 2
                return;
            }
           / /... Omit non-core code
            try {
            	/ / step 3
                msg.target.dispatchMessage(msg);
                / /...
            } catch (Exception exception) {
                / /... Omit non-core code
            } finally {
                / /... Omit non-core code
            }
            / / step 4msg.recycleUnchecked(); }}Copy the code

Step by step analysis of the above code:

  • Step 1: From the message queueMessageQueueQueue.next () may block, as discussed below.
  • Step 2: If the message is NULL, end the loop (null is not returned when there are no messages in the message queue, but is returned when the queue is closed, as described below)
  • Step 3: Distribute the message after receiving it
  • Step 4: Recycle the messages that have been distributed, and then start a new cycle of fetching data
MessageQueue’s next method

We will look only at the first step of the message extraction, the rest will be seen in a later section, queue.next() has more code, so we will continue to look at the fragment

Message next(a) {
	/ / step one
	final long ptr = mPtr;
    if (ptr == 0) {
		return null;
	}
	/ / in step 2
	int pendingIdleHandlerCount = -1; 
	/ / step 3
	int nextPollTimeoutMillis = 0;
	/ /... Omit the following code
}	
Copy the code
  • Step 1: If the message Loop has exited and disposed, return NULL, corresponding to the Loop passed abovequeue.next()Retrieves the message to NULL and exits the loop
  • Step 2: InitializationIdleHandlercounter
  • The third part: the judgment conditions required for the initialization of native. The initial value is 0. If the value is greater than 0, it means that there are still messages to be processed (delayed messages have not reached the execution time), and -1 means that there is no message.

Continue analyzing the code:

Message next(a) {
	/ /... Omit the above code
	for(;;) {if(nextPollTimeoutMillis ! =0) {
			Binder.flushPendingCommands();
		}
		nativePollOnce(ptr, nextPollTimeoutMillis);
		/ /... Omit the following code}}Copy the code

This paragraph is easy:

  • Start an infinite loop
  • nextPollTimeoutMillis ! = 0Call Native if there are no messages in the message queue or all messages are not executedBinder.flushPendingCommands()Method to send a message to the kernel thread before entering the block so that the kernel can properly schedule and allocate resources
  • Call the native method again, according tonextPollTimeoutMillisWhen it is -1, the current thread will be blocked (it will re-enter the runnable state when the new message is queued); when it is greater than 0, it indicates that there are delayed messages.nextPollTimeoutMillisWill act as a blocking time, that is, the message will be executed after a long time.

Moving on to the code:

Message next(a) {
	/ /... Omit the above code
	for(;;) {/ /... Omit the above code
       // Enable synchronization lock
		synchronized (this) {
			final long now = SystemClock.uptimeMillis();     
                / / step one
                Message prevMsg = null;
                Message msg = mMessages;
                / / in step 2
                if(msg ! =null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while(msg ! =null && !msg.isAsynchronous());
                }
                / / step 3
                if(msg ! =null) {
                    / / step 4
                    if (now < msg.when) {
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        / / step 5
                        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();
                        returnmsg; }}else {
                    / / step 6
                    nextPollTimeoutMillis = -1; }}/ /... The IdleHandler code is omitted below}}Copy the code

Analyze the code:

  • Step 1: Get the queue header
  • Step 2: Determine whether the current message is a synchronous message (the target of an asynchronous message is null) and start the loop until the synchronous message is found
  • Step 3: Check whether the message is null, perform step 4 if it is not empty, and perform step 6 if it is empty.
  • Step 4: Determine the message execution time, if greater than the current time, to the previous mentionednextPollTimeoutMillisAssign a new value (the time difference between the current time and the message execution time), in this step basically completes all the fetch operations of the loop. If the current message does not reach the execution time, the loop ends and a new loop starts, the above mentioned will be usednativePollOnce(ptr, nextPollTimeoutMillis);The method enters the blocked state
  • Step 5: Retrieve the message that needs immediate execution from the message queue, end the loop and return.
  • Part 6: No message in message queue, flagnextPollTimeoutMillisSo that the next loop enters the blocking state

The rest of the code is basically handling and executing the IdleHandler, which will be explained in the IdleHandler section.

Processing of messages

Remember on the loop method of MSG. Target. DispatchMessage (MSG); ? Messages are distributed via the dispatchMessage method. Where target is a reference to the handler that MSG holds that sent it, which is assigned when the message is sent. DispatchMessage = dispatchMessage

public void dispatchMessage(@NonNull Message msg) { if (msg.callback ! = null) { handleCallback(msg); } else { if (mCallback ! = null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); }}Copy the code

The code is simple: call the callback or handleMessage method by checking whether the Message has a Runable, and hand it to the Handler you define. Note that although callback is a Runable, it does not call the run method, but executes it directly. This means that it does not start a new thread, but is used as a method (which would have been a higher-order function if Handler had been written using Kotlin in the first place).

Other Key points

The main flow of message processing is covered above. Next, the key source code outside the main flow is discussed

The creation of a Loop

Remember that looper.prepare () and looper.loop () are called on non-main threads? These two methods can be understood as initializing the Loop and starting the Loop, and we don’t need to do this in the main thread because the framework layer already does it for us in the main method of app startup. Let’s look at these two methods separately:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<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)); } private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }Copy the code

We first use a static ThreadLocal to ensure Loop uniqueness and thread isolation so that a thread has only one Loop instance. Then initialize the Loop and create MessageQueue (quitAllowed setting whether exit is allowed). The Loop is associated with the message queue in this step.

Note that the Loop construction is private and can only be created using the Prepare field and retrieved using the myLooper method.

public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
Copy the code

ThreadLocal. Get the source code:

public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map ! = null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e ! = null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }Copy the code

As you can see, each Thread holds a ThreadLocalMap, which uses the same data structure as a HashMap, using ThreadLocal as the key, and value as the Loop instance. Not hard to see: we can only get Loop instances of the current thread.

The Loop method also provides prepareMainLooper, an initialization method in the main thread, but this method explicitly states that it is not allowed to be called, only by the system itself. This is basically the key to creating the Loop, where the Loop is associated with message queues and lines.

The Handler to create

The constructor for Handler has the following functions:

  1. public Handler()
  2. public Handler(Callback callback)
  3. public Handler(Looper looper)
  4. public Handler(Looper looper, Callback callback)
  5. public Handler(boolean async)
  6. public Handler(Callback callback, boolean async)
  7. public Handler(Looper looper, Callback callback, boolean async)

The first and second of them have been abandoned, Public Handler(Callback Callback, Boolean async) or public Handler(Looper Looper, Callback Callback, Boolean async), their source code is as follows:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) { mLooper = looper; mQueue = looper.mQueue; mCallback = callback; mAsynchronous = async; } public Handler(@Nullable Callback callback, boolean async) { if (FIND_POTENTIAL_LEAKS) { final Class<? extends Handler> klass = getClass(); if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && (klass.getModifiers() & Modifier.STATIC) == 0) { Log.w(TAG, "The following Handler class should be static or leaks might occur: " + klass.getCanonicalName()); } } mLooper = Looper.myLooper(); If (mLooper == null) {looper.prepare (); throw new RuntimeException( "Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; mCallback = callback; mAsynchronous = async; }Copy the code

The main difference between the two methods is that one uses the passed loop, the other uses the loop directly from the current thread, and then the same initialization operations. The key point here is that the handler handles the message on a thread that has nothing to do with the thread that created it, but rather with the thread in the loop at which it was created.

This is why handlers can switch threads: Handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler handler Or sendMessage, the final Handle Message is executed in the main thread.

Message creation, recycling, and reuse mechanisms

We can create a Message directly using the new keyword:

Message message = new Message();This is not recommended, however, and there are several methods for creating messages:

These methods essentially create message by obtaining (), except for the parameters that are used to assign values to different member variables of message:

public static final Object sPoolSync = new Object(); Message next; private static Message sPool; 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(); }Copy the code

This side of the code is simple. Message maintains a single linked list inside, using sPool as the header to store Message entities. You can see that each time the caller needs a new message, it fetches it from the head of the list and returns it directly. A new message is created when there is no message.

So when does the linked list insert the message? Next, the Message collection:

public static final Object sPoolSync = new Object(); private static final int MAX_POOL_SIZE = 50; void recycleUnchecked() { flags = FLAG_IN_USE; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; sendingUid = UID_NONE; workSourceUid = UID_NONE; when = 0; target = null; callback = null; data = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; }}}Copy the code

This method is called every time a message is removed from the MessageQueue queue for distribution, as in the loop.loop () method mentioned above. The code is simple: restore the member variables of Message to their original state, and then insert the reclaimed Message into the linked list using header interpolation (limited to a maximum size of 50). In addition, the same lock is used to insert and take out the operation, which ensures security.

Note that inserts and retrieves are done at the head of the list, not the same as in the message queue. Although the use of one-way linked list, recycling using head plug and head out, in after out, is a stack. In MessageQueue, however, it is a queue, following the principle of first in, first out, and the position of insertion is determined according to the state of the message, and there is no fixed insertion node.

This is a typical share mode, the biggest characteristic is to reuse objects, avoid the memory waste caused by repeated creation. This is why Android officially recommends creating messages this way: to improve efficiency and reduce performance overhead.

IdleHandler

IdleHandler is simply defined as an interface defined in MessageQueue:

  public static interface IdleHandler {
        boolean queueIdle();
    }
Copy the code

In the Looper loop, the queueIdle method is executed whenever the message queue is idle: there is no message or no message execution time needs to be delayed. The returned Boolean indicates whether the IdleHandler is permanent or disposable: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler: IdleHandler

  • Ture: permanent, executed as soon as idle
  • False: One-off. The value is executed only when it is idle for the first time

It can be used as follows:

Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                return true;
            }
        });
Copy the code

Take a look at the implementation of addIdleHandler

private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>(); public void addIdleHandler(@NonNull IdleHandler handler) { if (handler == null) { throw new NullPointerException("Can't add a null IdleHandler"); } synchronized (this) { mIdleHandlers.add(handler); }}Copy the code

The code is simple, just a List to hold the implementation of the interface. So how does it implement the call when it’s idle?

Remember the code that was omitted from MessageQueue’s next method above?

Message next() { //... Int pendingIdleHandlerCount = -1; // -1 only exists in first iteration for (;;) {/ /... Omit no relevant code / / second step 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; Posthandlers == null {posthandlers = new posthandlers [math. Max (pendingIdleHandlerCount, pendingIdleHandlerCount); 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } for (int I = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; // Step 6 Try {keep = idler.queueidle (); } catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); } // Step 7 if (! keep) { synchronized (this) { mIdleHandlers.remove(idler); PendingIdleHandlerCount = 0; }}Copy the code

Let’s analyze the code step by step:

  • Step 1: Create local variables before the message fetch loop beginspendingIdleHandlerCountUsed to recordIdleHandlerThe number of, is -1 only at the beginning of the cycle;
  • Step 2: When not retrievedMessageThe message (no message or no message available for immediate execution, and no blocked state) or the message needs to be delayedpendingIdleHandlerCount Assignment recordIdleHandlerThe number of;
  • Step 3: JudgeIdleHandlerQuantity, if notIdleHandler, directly ends the current loop and marks the loop to enter the pending state.
  • Step 4: Determine if it is the first time and initializeIdleHandler The List of
  • Step 5: Start walking through all of themIdleHandler
  • Step 6: Do it one by oneIdleHandler thequeueIdlemethods
  • Part seven: According to eachIdleHandler thequeueIdleThe return value ofIdleHandler Permanent or one-time, remove non-permanent items from the array;
  • Step 8: ModifyIdleHandler Quantity information ofpendingIdleHandlerCount To avoidIdleHandler Repeat the execution.

This is the core principle of IdleHandler, which is fired only when the message queue is empty, or when the message queue header is delayed. When the message queue header is a delayed message, it fires only once. As we saw in the fetch message section, delayed messages that end the current loop and enter the next loop trigger blocking.

Handler is used in the Framework layer

Have you ever wondered why Android calls looper.prepare () and looper.loop () on the main thread for you? Isn’t that a bit of an overkill?

In fact, it’s not that simple. If you look at the source code for the Framework, you’ll see that the entire Android app runs on Handler. The operation of the four major components, their life cycle is also based on the Handler event model, and click events. All these are generated by the Android system framework layer corresponding message and handed to a Handler for processing. This Handler is the ActivityThread inner class H. Post a screenshot of its code

As you can see, handlers are involved in the life cycle of all four components, even when they are out of memory.

This also explains why performing time-consuming tasks on the main thread can result in UI stuttering or ANR: Because all the main thread, that is, the logic code of the UI thread, is executed in the life cycle of the component, and the life cycle is controlled by the event system of the Handler. When a time-consuming operation is executed in any life cycle, the subsequent messages in the MessageQueue MessageQueue cannot be processed in time, resulting in delay. Until the visual blockage, severe cases will further trigger the occurrence of ANR.

If you’re interested, take a look at the code.

Related series of articles recommended:

  • Blog.csdn.net/qingtiantia…
  • Blog.csdn.net/qingtiantia…
  • Blog.csdn.net/qingtiantia…
  • Blog.csdn.net/qingtiantia…