Android-异步消息处理机制

作者: Android_Jian | 来源:发表于2018-07-31 22:05 被阅读45次

    关于Android异步消息处理机制的文章网上已经有很多了,笔者当时还是跟随郭神的博客来学习的,在此先放上郭神的文章来镇楼哈哈,链接:https://blog.csdn.net/guolin_blog/article/details/9991569 。这两天有点闲,拿出源码重新翻阅了下,做下笔记。

    异步消息处理机制主要涉及到的类有四个,分别为:Handler、Message、MessageQueue、Looper。一般情况下提及Android异步消息处理,大家首先肯定会想到“更新UI操作”,如下所示:

    public class MainActivity extends AppCompatActivity {
    
        private static final int WHAT_UI_UPDATE = 1;
    
        private TextView mTextView;
    
        Handler mHandler = new Handler(){
    
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
    
                switch (msg.what){
    
                    case WHAT_UI_UPDATE:
                        mTextView.setText("成功接收到消息");
                        break;
    
                    default:
                        break;
                }
    
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mTextView = findViewById(R.id.mTextView);
        }
    
        public void handlerTest(View view){
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Message message = Message.obtain();
                    message.what = WHAT_UI_UPDATE;
                    mHandler.sendMessage(message);
    
                    //可以使用 mHandler.sendEmptyMessage(WHAT_UI_UPDATE)代替,这里只是为了引出Message
                }
            }).start();
        }
    }
    

    这里我们先忽略掉Handler引起的内存泄漏问题,有关Handler内存泄漏的解决方案下文会一并给出。上述代码相信大家一眼就能看懂,下面我们从源码的角度来看一下。

    大家初学Java的时候都知道,Java程序的入口是main方法,那我们Android应用程序的入口在哪呢?Android应用程序的入口同样也是main方法,它就是ActivityThread类的main方法,我们一起去看下:

    # ActivityThread类
    public static void main(String[] args) {
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
            SamplingProfilerIntegration.start();
    
            // CloseGuard defaults to true and can be quite spammy.  We
            // disable it here, but selectively enable it later (via
            // StrictMode) on debug builds, but using DropBox, not logs.
            CloseGuard.setEnabled(false);
    
            Environment.initForCurrentUser();
    
            // Set the reporter for event logging in libcore
            EventLogger.setReporter(new EventLoggingReporter());
    
            // Make sure TrustedCertificateStore looks in the right place for CA certificates
            final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
            TrustedCertificateStore.setDefaultUserDirectory(configDir);
    
            Process.setArgV0("<pre-initialized>");
     
            // 1.调用Looper的prepareMainLooper方法,创建主线程Looper对象以及MessageQueue对象
            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);
    
            // 2.调用 Looper的loop()方法进行消息处理操作
            Looper.loop();
    
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    

    上述代码需要我们注意的地方有两处。1 处调用了Looper的prepareMainLooper方法,创建主线程Looper对象以及MessageQueue对象。2 处调用了Looper的loop()方法进行消息处理操作。我们首先分析下 1处,点进去看一下:

        /**
         * Initialize the current thread as a looper, marking it as an
         * application's main looper. The main looper for your application
         * is created by the Android environment, so you should never need
         * to call this function yourself.  See also: {@link #prepare()}
         */
        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对象的,让它作为应用程序的主线程looper,这个方法是Android系统调用的,不允许我们自己调用。方法的第一行首先调用到prepare方法,我们跟进去看下:

        // static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
        private static void prepare(boolean quitAllowed) {
            if (sThreadLocal.get() != null) {
                throw new RuntimeException("Only one Looper may be created per thread");
            }
            sThreadLocal.set(new Looper(quitAllowed));
        }
    

    可以看到方法中首先调用到了sThreadLocal的get方法,如果不为null,直接抛出异常。如果为null,则会调用sThreadLocal的set方法。sThreadLocal是什么东东?代码中已经标明了,它就是一个静态的ThreadLocal对象。ThreadLocal可以简单理解为线程本地存储,ThreadLocal为变量在每个线程中都创建了一个副本,每个线程都可以独立访问相应的副本变量的值,各个线程之间的访问互不影响。关于ThreadLocal的分析,可以看下我的另一篇文章:https://www.jianshu.com/p/d55c96029667。在这里通过sThreadLocal的get()方法获取到的肯定为null,程序会走sThreadLocal的set方法,新创建一个looper对象作为参数传递过去。下面我们进入到Looper的构造方法看一下:

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

    可以看到在looper的构造方法中创建了一个MessageQueue对象,并获取到当前线程对象赋值给mThread。
    prepare方法分析完毕,我们在prepareMainLooper方法中接着往下看,程序会调用到myLooper方法,将返回值赋值给sMainLooper。我们点进去myLooper方法去看一下:

        /**
         * Return the Looper object associated with the current thread.  Returns
         * null if the calling thread is not associated with a Looper.
         */
        public static @Nullable Looper myLooper() {
            return sThreadLocal.get();
        }
    

    可以看到方法中直接将sThreadLocal.get() return掉。这个时候调用sThreadLocal的get方法获取到的就是我们刚才通过sThreadLocal的set方法设置的looper对象。也就是说程序将我们刚才通过sThreadLocal的set方法设置的looper对象赋值给sMainLooper变量。

    就这样,Looper的prepareMainLooper方法就分析完毕了, 2 处的loop方法我们等下再分析。

    在文章开头的Demo,我们首先在主线程中创建了一个handler实例,并重写了它的handleMessage方法,然后在子线程中通过handler的sendMessage方法发送了一条消息,handleMessage方法中接收到消息进行相应处理。我们首先看下handler的构造方法。

        public Handler() {
            this(null, false);
        }
    

    简单一句话最终调用到如下两个参数的构造方法:

        public Handler(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());
                }
            }
    
            // 1. 通过looper的myLooper方法获得当前线程对应的looper对象,并赋值给mLooper
            mLooper = Looper.myLooper();
            if (mLooper == null) {
                throw new RuntimeException(
                    "Can't create handler inside thread that has not called Looper.prepare()");
            }
            
            //2. 在这里通过mLooper.mQueue获取到MessageQueue对象,并赋值给mQueue
            mQueue = mLooper.mQueue;
            
            // 3.将Callback类型的参数赋值给成员变量mCallback
            mCallback = callback;
            mAsynchronous = async;
        }
    

    在这里,由于handler实例是在主线程中创建的,1处通过looper的myLooper方法获得的looper对象其实就是主线程looper,也就是我们应用程序启动时调用prepareMainLooper方法设置的looper对象。由于我们的应用程序启动时创建了主线程looper对象,在Looper的构造方法中创建了MessageQueue对象,并赋值给Looper的成员变量mQueue,所以 2 处mLooper.mQueue获取到的就是主线程Looper对象对应的messageQueue对象。3 处,将Callback类型的参数赋值给成员变量mCallback,在这里mCallback为null。

    接下来我们看下Handler的sendMessage方法,该方法最终会调用到sendMessageAtTime方法:

        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    
            // 1. 将mQueue赋值给queue
            MessageQueue queue = mQueue;
            if (queue == null) {
                RuntimeException e = new RuntimeException(
                        this + " sendMessageAtTime() called with no mQueue");
                Log.w("Looper", e.getMessage(), e);
                return false;
            }
            
            // 2.调用enqueueMessage方法
            return enqueueMessage(queue, msg, uptimeMillis);
        }
    

    接下来我们看下enqueueMessage方法:

        private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
          
            // 1. 将当前handler对象赋值给msg的成员变量target
            msg.target = this;
            if (mAsynchronous) {
                msg.setAsynchronous(true);
            }
    
            // 2.调用messageQueue的enqueueMessage进行消息入队操作
            // 其中msg为我们通过handler发送的消息,uptimeMillis为消息截止时间
            return queue.enqueueMessage(msg, uptimeMillis);
        }
    

    接下来我们进入messageQueue的enqueueMessage方法看一下:

        boolean enqueueMessage(Message msg, long when) {
            if (msg.target == null) {
                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) {
                    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();
    
                // 1. 将消息时间when赋值给msg的成员变量when
                msg.when = when;
    
                // Message mMessages;      mMessages表示当前待处理消息,由于消息链表是按照消息时间从小到大排序的,mMessages也为链表首部消息
                Message p = mMessages;
                boolean needWake;
                if (p == null || when == 0 || when < p.when) {
                    // 如果当前没有消息 或者 新入队的消息时间为0 或者 新入队的消息时间小于当前待处理消息时间
                    // 则将新入队的消息插入到消息链表的首部作为当前待处理消息
                    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;
          
                    // 2. 将新入队的消息按照时间从小到达的顺序插入到消息链表中正确位置
                    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;
        }
    

    MessageQueue意为“消息队列“,而message在MessageQueue中却以单链表的形式存储,有没有一种被欺骗的感觉哈哈。

    到此为止,我们的消息入队操作就完成了。现在我们可以分析ActivityThread中 2 处 Looper的Loop方法了。注意:ActivityThread中的loop方法运行在先,应用程序启动时,ActivityThread中的loop方法就执行了,这里我们只是为了便于理解,先行分析了消息入队操作。下面我们一起看下:

        /**
         * Run the message queue in this thread. Be sure to call
         * {@link #quit()} to end the loop.
         */
        public static void loop() {
    
            // 1. 通过myLooper方法获取到looper对象,进而获取到messageQueue对象
            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;
    
            // 2. 无限循环操作,不断从消息队列中取出消息
            for (;;) {
                Message msg = queue.next(); // might block
                if (msg == null) {
                    // 如果消息msg为null,直接return掉
                    return;
                }
    
                final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
                final long end;
                try {
                    
                    // 3. 如果消息msg不为null,则调用msg.target.dispatchMessage方法
                    msg.target.dispatchMessage(msg);
                    end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
                } finally {
                    if (traceTag != 0) {
                        Trace.traceEnd(traceTag);
                    }
                }
        
                // 4. 将msg消息回收掉
                msg.recycleUnchecked();
            }
        }
    

    在这里我为了分析方便,对loop方法中的代码进行了删减操作。loop方法中的要点部分已经在代码中进行标注了,这里我们重点看下 3 处,如果消息msg不为null,则调用msg.target.dispatchMessage方法。msg.target ? ? ? 有没有很熟悉,我们在消息入队时,不是将当前handler对象赋值给msg.target了嘛。所以这里当然是调用当前handler的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);
            }
        }
    

    在handler的dispatchMessage方法中,首先对msg的callback字段进行null判断,如果msg.callback不等于null,则调用handleCallback方法,将当前msg作为参数传递过去。如果msg.callback为null,则对mCallback进行null判断,如果mCallback不等于null,则调用mCallback的handleMessage方法,当该方法返回true时,直接return掉,否则接着调用handleMessage方法。如果mCallback为null,则直接调用handleMessage方法。

    我们先来看下最简单的handleMessage方法:

        /**
         * Subclasses must implement this to receive messages.
         */
        public void handleMessage(Message msg) {
        }
    

    可以看到handleMessage方法是一个空实现,注释信息告诉我们,我们的子类必须实现这个方法用来接收消息。正所谓我们在日常开发工作中创建handler对象时,通常会覆写handleMessage方法,用来进行消息处理操作。

    关于handler的消息发送,我们还会有post方法,而msg.callback的赋值,正是通过handler post方法入队时设置的。我们先来看下post方法的使用:

    public class MainActivity extends AppCompatActivity {
    
        private TextView mTextView;
    
        Handler mHandler = new Handler();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mTextView = findViewById(R.id.mTextView);
        }
    
        public void handlerTest(View view){
    
            new Thread(new Runnable() {
                @Override
                public void run() {
    
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                 
                            mTextView.setText("成功接收到消息");      //更新UI操作
                        }
                    });
                }
            }).start();
    
        }
    }
    

    我们点进去handler的post方法去看下:

        public final boolean post(Runnable r)
        {
           return  sendMessageDelayed(getPostMessage(r), 0);
        }
    

    可以看到方法中直接调用到了sendMessageDelayed方法,将getPostMessage方法的返回值作为第一个参数,msg.callback的赋值就是在getPostMessage这个方法中完成的。我们先来看下getPostMessage方法。

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

    getPostMessage方法中对我们传入的Runnable对象进行了封装操作,将Runnable对象赋值给message的callback字段,最后将message对象返回。
    接着我们看下sendMessageDelayed方法:

        /**
         * Enqueue a message into the message queue after all pending messages
         * before (current time + delayMillis). You will receive it in
         * {@link #handleMessage}, in the thread attached to this handler.
         *  
         * @return Returns true if the message was successfully placed in to the 
         *         message queue.  Returns false on failure, usually because the
         *         looper processing the message queue is exiting.  Note that a
         *         result of true does not mean the message will be processed -- if
         *         the looper is quit before the delivery time of the message
         *         occurs then the message will be dropped.
         */
        public final boolean sendMessageDelayed(Message msg, long delayMillis)
        {
            if (delayMillis < 0) {
                delayMillis = 0;
            }
            return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
        }
    

    可以看出,sendMessageDelayed方法最后还是调用到sendMessageAtTime方法,后续的入队操作和之前我们分析的sendMessage方法一致。那通过handler的post方法发送的消息是怎么处理的呢?我们回过头看下handler的dispatchMessage方法,这个时候msg.callback不等于null,程序会调用到handleCallback方法,将我们的message对象作为参数传入,我们一起看下handleCallback方法:

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

    可以看到handleCallback方法中就一行代码,直接调用到message.callback.run(); message.callback我们刚才已经判断过了,就是我们之前发送post消息传入的Runnable对象,所以这里会直接调用到我们发送post消息传入的Runnable对象的run方法。

    上面我们分析了msg.callback的赋值,正是通过handler post方法入队时设置的,那dispatchMessage方法中mCallback的赋值是通过什么实现的呢?其实mCallback的赋值是在handler的构造方法中实现的,不信你看:

        public Handler(Callback callback) {
            this(callback, false);
        }
    
        public Handler(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) {
                throw new RuntimeException(
                    "Can't create handler inside thread that has not called Looper.prepare()");
            }
            mQueue = mLooper.mQueue;
            mCallback = callback;                          //mCallback 在此赋值
            mAsynchronous = async;
        }
    
    

    所以我们通过handler发送消息还可以这么写:

    public class MainActivity extends AppCompatActivity {
    
        private static final int WHAT_UI_UPDATE = 1;
    
        private TextView mTextView;
    
        Handler mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message message) {
    
                switch (message.what){
    
                    case WHAT_UI_UPDATE:
                        mTextView.setText("成功接收到消息");
                        break;
    
                    default:
                        break;
                }
    
                return true;
            }
        });
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mTextView = findViewById(R.id.mTextView);
        }
    
        public void handlerTest(View view){
    
            new Thread(new Runnable() {
                @Override
                public void run() {
    
                    Message message = Message.obtain();
                    message.what = WHAT_UI_UPDATE;
                    mHandler.sendMessage(message);
                }
            }).start();
    
        }
    }
    

    上述对Handler异步消息处理的分析基本上就结束了。

    下面我们一起来学习下有关Message的一个小知识点,不知道大家创建Message对象的时候是怎么做的,是直接 new Message() 的形式还是通过Message.obtain()方法来获取呢?老司机肯定知道,Android推荐我们通过Message.obtain()方法来获取到Message对象。但是你有没有想过,为什么需要这样子做呢?下面我们一起来看下它的源码,揭开我们心中的疑惑:

        // private static final Object sPoolSync = new Object();
        // private static Message sPool;
    
        /**
         * Return a new Message instance from the global pool. Allows us to
         * avoid allocating new objects in many cases.
         */
        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实例,避免我们在代码中大量创建Message对象。原来Android为了减少创建大量Message对象造成的内存开支,对我们的Message对象进行了缓存复用,其实Message的缓存原理就是一条单链表。代码内部首先进行了同步处理,sPoolSync就是一个Object对象。sPool是个什么东东?其实sPool就是链表的头指针。当我们调用obtain方法的时候,首先会判断sPool是否为null,也就是判断缓存链表中是否存在Message对象。如果存在的话,就从缓存链表首部取出一个Message对象,将sPoolSize减1,sPoolSize就是缓存链表的长度。如果缓存链表中不存在Message对象,则进行new操作,创建Message实例,并将新创建的Message对象返回掉。我们接下来看下Message的构造方法:

       /** Constructor (but the preferred way to get a Message is to call {@link #obtain() Message.obtain()}).
        */
        public Message() {
        }
    

    原来构造方法的实现为空哈哈。上面我们分析了从缓存链表中将Message对象取出来,那Message对象是什么时候放入缓存链表的呢?我们猜想下,肯定是当前Message对象不再使用,进行回收操作的时候放入我们的缓存链表中的,也就是在Looper的loop方法中,调用完msg.target.dispatchTouchMessage方法后对当前Message对象进行回收操作。到底是不是这样子呢?我们进入loop方法看一下:

    /**
         * Run the message queue in this thread. Be sure to call
         * {@link #quit()} to end the 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;
    
            for (;;) {
                Message msg = queue.next(); // might block
                if (msg == null) {
                    // No message indicates that the message queue is quitting.
                    return;
                }
    
                final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
                final long end;
                try {
                    msg.target.dispatchMessage(msg);
                    end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
                } finally {
                    if (traceTag != 0) {
                        Trace.traceEnd(traceTag);
                    }
                }
              
                msg.recycleUnchecked();
            }
        }
    

    为了方便大家观看,我对代码进行了删减。我们看下代码,调用完msg.target.dispatchTouchMessage方法后在代码最后一行调用了 msg.recycleUnchecked()方法,这个方法就是用来回收当前Message对象的,我们跟进去看下:

        /**
         * Recycles a Message that may be in-use.
         * Used internally by the MessageQueue and Looper when disposing of queued Messages.
         */
        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++;
                }
            }
        }
    

    在上述方法中首先对当前Message对象的成员变量的赋值进行清空操作,并将当前Message对象标记为FLAG_IN_USE状态,最后在一个同步代码块中判断缓存链表中的Message对象的个数是否小于最大限制个数,如果存链表中的Message对象的个数小于最大限制个数,则将当前Message对象添加到缓存链表首部,并将sPoolSize个数加1。

    有关Message的分析就结束了,下面我们扩展一下,看下Handler引发的内存泄漏问题。

    相关文章

      网友评论

        本文标题:Android-异步消息处理机制

        本文链接:https://www.haomeiwen.com/subject/dluhvftx.html