美文网首页
讲讲Handler、MessageQueue和Looper

讲讲Handler、MessageQueue和Looper

作者: 5afd372c86ba | 来源:发表于2017-07-19 21:29 被阅读39次
    前言

    首先得说下为什么写这篇文章。因为学习了Handler,MessageQueue与Looper后,感觉三者的关系是越学越乱,有时看一下这个人写的东西,感觉明白了,然后再看下另外一个人写的,感觉又有点不一样,大体是相同,但是总是会找出那么一两个矛盾点,也许是我个人的理解能力不行导致理解偏差吧,总之是我对不起那些辛苦写博客的博主。毕竟学习光看别人的也没用,还是得自己动手去验证,更何况看别人的还看得那么不解,所以我决定还是自己看API文档和SDK的源码研究下。

    摘要

    本文主要从读API文档开始,进行我对Handler、MessageQueue和Looper的推断,得出推断后我再跟踪SDK各个类的源码验证我的判断,进一步得到推论结果,最后利用代码验证我们关于三者关系的推论,同时介绍了如何使用Handler和Message。

    目录

    • 1.Handler、MessageQueue和Looper的关系:
      • 1.1 通过查API文档推断三者关系:
      • 1.2 通过SDK源码验证推断
      • 1.3 三者关系总结
    • 2.代码实践、验证结论
      • 2.1 消息的创建方式
      • 2.2 消息接收与处理的代码演示
      • 2.3 利用代码验证我们的推断结果:

    1. Handler、MessageQueue和Looper的关系:

    要知道如何使用这三个对象,首先,我们得明白这三者一个大概的关系。其实三个对象用起来并不会太难,但是他们的关系如果不好好研究下很容易理解错了。

    1.1 通过查API文档推断三者关系:

    首先看下官网对关于Handler的描述:

    Class Overview
    A Handler allows you to send and process Message
    and Runnable objects associated with a thread's MessageQueue
    . Each Handler instance is associated with a single thread and that thread's message queue.When you create a new Handler, it is bound to the thread / message queue of the thread that is creating it -- from that point on, it will deliver messages and runnables to that message queue and execute them as they come out of the message queue.
    来源: Android Developer - Handler

    意思就是Handler允许你发送消息或使用post的方法把Runnable对象添加到Handler依附的消息队列、线程上执行。我们根据此OverView做出推断1:每一个Handler都对应一个简单线程和消息队列,在哪个线程实例化,则绑定该线程以及其消息队列。Handler能发送消息到消息队列,同时也是由Handler把消息从消息队列取下来在对应的线程上处理。
    上述文字提到了消息(Message)和消息队列(MessageQueue),关于消息,大家都不陌生,顾名思义则可知消息是封装了某些数据的对象,而消息队列呢?现在的操作系统基本都有消息队列这个机制,Windows有两种线程,分别是GUI线程和Worker线程,只有GUI才有消息队列,而Worker线程是没有的,但是当Worker线程产生了一个窗口,那么它也会产生一个消息队列。安卓下不同,安卓是默认主线程即UI线程会有消息队列,其他线程创建是没有的,但是当它启用了Looper.prepare()方法,不管该线程是否有GUI,都会在该线程创建一个消息队列。消息队列主用用来存放线程内以及线程间各个组件的相互通信的消息,他们通信的方式是通过Handler互相发送消息。
    通过上述,我们可以初步得出线程间的组件如果需要通信可以选择通过通信目的线程内的Handler对象把本进程的消息发送到通信目的线程的消息队列,再由通信目的线程的Handler对象把该消息从消息队列取下,并在通信目的线程处理消息。前提是发送消息前,该Handler必须由通信目的线程完成实例化。那么上述说到Looper又是怎么回事?来看API文档关于Handler是怎么说的。

    Class Overview
    Class used to run a message loop for a thread. Threads by default do not have a message loop associated with them; to create one, call [prepare()](file:///D:/adt-bundle/sdk/docs/reference/android/os/Looper.html#prepare())
    in the thread that is to run the loop, and then [loop()](file:///D:/adt-bundle/sdk/docs/reference/android/os/Looper.html#loop())
    to have it process messages until the loop is stopped.
    Most interaction with a message loop is through the [Handler](file:///D:/adt-bundle/sdk/docs/reference/android/os/Handler.html)
    class.

    原来它是通过与Handler的互动控制消息队列的循环,再看看消息队列的OverView:

    Class Overview
    Low-level class holding the list of messages to be dispatched by a [Looper](file:///D:/adt-bundle/sdk/docs/reference/android/os/Looper.html)
    . Messages are not added directly to a MessageQueue, but rather through [MessageQueue.IdleHandler](file:///D:/adt-bundle/sdk/docs/reference/android/os/MessageQueue.IdleHandler.html)
    objects associated with the Looper.
    You can retrieve the MessageQueue for the current thread with [Looper.myQueue()](file:///D:/adt-bundle/sdk/docs/reference/android/os/Looper.html#myQueue())
    .

    可以看出Looper负责分配消息队列中的消息。Message也不会直接加到消息队列,而是通过MessageQueue.IdleHandler来与Looper互动。
    推断2:上面我们说Handler把消息发到消息队列,再由Handler从消息队列取下消息进行处理,而这里说Looper是分配消息的,消息也不是直接加到消息队列,而是通过MessageQueue.IdleHandler与Looper互动添加的。那么到这里可以推断他们存在这样一层关系——Handler封装消息,把消息发给Looper,由Looper与MessageQueue进行交互,把消息添加到消息队列,同样由Looper把消息从消息队列上取下,再交由Handler处理。
    到底是不是如我们推断那样,下面通过读SDK源码来验证推断。

    1.2 通过SDK源码验证推断

    首先我们必须先看下Handler源码,看它是否真的发送消息是发给了Looper,首先得先看看Handler的构造函数:

    /**
    * Default constructor associates this handler with the queue for the
    * current thread.
    *
    * If there isn't one, this handler won't be able to receive messages.
    */
    /*无参构造函数*/
    public Handler() {
        if(FIND_POTENTIAL_LEAKS) {
            finalClass<? extendsHandler> 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) {
            thrownewRuntimeException("Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = null;
    }
    
    /**
    * Use the provided queue instead of the default one.
    */
    /*有参构造函数*/
    public Handler(Looper looper) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = null;
    }
    

    从两个构造函数我们都可以看出想要构造一个Handler对象,都离不开Looper,无参构造函数是通过Looper.myLooper()来获取Looper对象,并绑定Looper对象对应的MessageQueue。这一点验证推断1——在哪个线程实例化,该Handler绑定了哪个线程以及其消息队列,绑定哪个线程,说白了就是绑定该线程的Looper。
    那么发送消息是怎么发送的呢?

    public final boolean sendMessage(Message msg) {
        returnsendMessageDelayed(msg, 0);
    }
    public final boolean sendMessageDelayed(Message msg, long delayMillis) {
        if(delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        boolean sent = false;
        MessageQueue queue = mQueue;
        if(queue != null) {
            msg.target = this;
            sent = queue.enqueueMessage(msg, uptimeMillis);  
            //Handler发送消息实际是通过Looper获得了消息队列,使用消息队列的enqueueMessage方法来发送
        } else {
            RuntimeException e = newRuntimeException(
            this+ " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
        }
        return sent;
    }
    

    从Handler的sendMessage方法一步一步跟踪下去,发现其实到最后是调用先前通过mLooper.mQueue获取到的消息队列的enqueueMessage方法来添加消息。
    那么,是如何处理消息的呢?我们从API文档里面得知Looper的一个写法是:

    class LooperThread extends Thread {
        public Handler mHandler;
        public void run() {
            Looper.prepare();
            mHandler = newHandler() {
                public void handleMessage(Message msg) {
                      // process incoming messages here
                }
            };
            Looper.loop();
        }
    }
    

    handleMessage是Handler如何处理消息的回调函数,我们可以通过重写该函数实现我们自己定义的处理消息的方法。那它是怎么被触发的呢?Looper.loop()又是干嘛的?
    不妨看下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) {   //判断是否有Looper,没有抛异常
            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
            Printer logging = me.mLogging;
            if(logging != null) {
                logging.println(">>>>> Dispatching to "+ msg.target + " "+ msg.callback + ": "+ msg.what);
            }
            msg.target.dispatchMessage(msg);  //可以看到这边调用了Handler的dispatchMessage
            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.recycle();
        }
    }
    

    可以看到源码比较关键的一步是调用Handler的dispatchMessage方法,而且是传入了Message对象作为参数,我们可以猜测该函数可能涉及到处理消息函数,因为处理消息函数(handleMessage)也是在Handler中的。接下来看dispatchMessage的源码。

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if(msg.callback != null) {  //callback是一个Runnable变量,如果为传入则不执行
            handleCallback(msg);
        } else{
            if(mCallback != null) {
                if(mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);//果不其然,这边调用了handleMessage
        }
    }
    

    由此到这里,对于Handler如何建立如何发消息,如何处理消息,这个过程如何与MessageQueue和Looper互动我们比较清楚了,所以接下来做个总结。

    1.3 三者关系总结

    由此,到这里可以得出我们的初步结论,想要利用Handler完成发送消息并处理消息的过程大概是这样的(以线程A发消息给线程B为例):
    (1)实例化与线程A绑定的Handler(前提是该线程已经有Looper,没有可通过prepare方法获得):
    两种方法,一种是在线程A直接调用无参构造器实例化,使得Handler与线程A的Looper、MessageQueue绑定;
    另一种是在其他线程使用Handler带Looper参数的构造器,传入线程A的Looper进行实例化,同样可得与线程A的Looper、MessageQueue相绑定的Handler对象;
    (2)重写处理消息的函数handleMessage
    (3)在线程B使用线程A的Handler对象发送一个消息,该Handler在实例化时已经通过Looper获得了线程A的MessageQueue,Handler使用获得的MessageQueue对象的enqueueMessage把消息添加到队列以完成消息的发送;
    (4)使用Looper的loop函数运行消息队列,这个过程是loop把消息从消息队列取下,传给Handler的dispatchMessage,判断该如何处理消息,如果没有其他callback则调用到了Handler的handleMessage处理消息。
    这里可以验证我们的推断2,但是推断2有一点是讲错了,handler并没有把消息发给Looper,由Looper去处理,而是从Looper获取了与MessageQueue的“话语权”,Handler通过使用MessageQueue的enqueueMessage方法进行消息的发送。而处理消息则是由Looper执行loop去循环MessageQueue,并调用Handler的dispatchMessage去处理消息。所以Handler是发送、处理消息的,Looper是管理MessageQueue与Handler通信的机制,而MessageQueue是负责保存、管理Message对象的。
    三者的关系图为:


    Handler原理图

    2. 代码实践、验证结论

    2.1 消息的创建方式

    Message一般创建对象使用Message m = Message.obtain(); 之所以这样操作时因为系统的消息池可能存在消息未被使用,我们可通过该方法去获取未被使用的消息,这样就能够避免一直新建消息对象而又存在很多未被使用消息存在内存,达到节省内存目的。这是sdk关于Message.obtain函数的源码:

    private static final Object  sPoolSync  =  newObject();
    private static Message  sPool ;
    private statici ntsPoolSize  = 0;
    private static final int MAX_POOL_SIZE  = 50;
    /**
    * 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;
                sPoolSize --;
                returnm;
             }
        }
        return new Message();    
    }
    

    可以看出obtain会先去判断消息池是否为空,如果为空则new Message(),如果不是则从消息池取出消息返回。

    2.2 消息接收与处理的代码演示

    理解完原理后,我们来使用Handler发送与处理消息就感觉明朗多了,主要使用方法是:
    (1) 建立Handler对象,重写handleMessage方法,Handler类里面该方法内容是空,需要开发者根据不同情况自己重写;
    (2)获取消息对象:可以通过Message的obtain方法获取消息对象,其实获取消息的方法多种多样,可以通过Message,也可以通过Handler,但是看了一下源码,各种消息获取的方法归根到底都是调用了Message的obtain方法。
    (3)设置Message所携带的各种值,设置值的方法也是多种多样,设置的办法根据获取消息方法的不同而不同,但是归根到底各个方法都是先obtian获取Message对象,然后再对其各个属性进行赋值,这就是面向对象的好处之一;
    (4)发送消息;
    下面以同个线程和不同线程利用Handler发送、处理消息为例子:
    (1)同线程:

    package cth.android.handlerexer;
    import android.app.Activity;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;
    import android.util.Log;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    public class MainActivity extends Activity implements OnClickListener {
        private Button btn_sendMsg1;
        private Button btn_sendMsg2;
        private Button btn_sendMsg3;
        private Button btn_sendMsg4;
                  
        private Handler handler = new Handler(){
            public void handleMessage(android.os.Message msg) {
                Log.i("cth","--arg1-->" + msg.arg1);
                Log.i("cth","--arg2-->" + msg.arg2);
                Log.i("cth","--what-->" + msg.what);
                Log.i("cth","--obj-->" + msg.obj);
                Log.i("cth","--getWhen-->" + msg.getWhen());
                Log.i("cth","--getTarget-->" + msg.getTarget());
                          
            };
        };
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            btn_sendMsg1 = (Button) findViewById(R.id.btn_sendMsg1);
            btn_sendMsg2 = (Button) findViewById(R.id.btn_sendMsg2);
            btn_sendMsg3 = (Button) findViewById(R.id.btn_sendMsg3);
            btn_sendMsg4 = (Button) findViewById(R.id.btn_sendMsg4);
        }
        @Override
        public void onClick(View v) { //各个函数的点击事件
            switch (v.getId()) {
            case R.id.btn_sendMsg1:
                Message msg = Message.obtain();  //通过obtain方法获取一个消息对象
                msg.arg1 = 1;                   //设定各种值
                msg.arg2 = 2;
                msg.what = 3;
                msg.obj = "Message.obtain()";
                handler.sendMessage(msg);       //利用Handler把消息发送出去
                //obtain的重载方法,直接设置message的值
                Message msg1 = Message.obtain(handler, 3, 1, 2, "Message.Obtain(handler,what,arg1,arg2,obj)");
                msg1.sendToTarget();  //原理还是利用Handler发送
                break;
            case R.id.btn_sendMsg2:
                handler.sendEmptyMessage(3);    //发送一个只带what=3的空消息,虽说是空消息,但实际还是有利用Message.obtain()获取。
                handler.sendMessage(Message.obtain()); //发送空消息
                break;
            case R.id.btn_sendMsg3:
                handler.sendEmptyMessageDelayed(4, 5000);  //发送一个延时5秒的消息
                break;
            case R.id.btn_sendMsg4:
                handler.sendEmptyMessageAtTime(3, 9000);  //发送一个消息,在9秒内发送出去
                break;
            }
        }
    }
    

    (2)不同线程:
    设立设立三个按钮,一个是主线程向两个子线程发送消息1、消息2。另外两个按键是启动两个子线程,接收主线程利用子线程的Handler对象h1、h2发的消息,两个子线程收到消息后,还利用主线程的Handler对象h3发回确认消息。 (注意:主线程发送消息前一定得先启动两个子线程)

    package cth.android.handlerlooper;
    import android.app.Activity;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Looper;
    import android.os.Message;
    import android.util.Log;
    import android.view.Menu;
    import android.view.MenuItem;
    import android.view.View;
    import android.view.View.OnClickListener;
    import android.widget.Button;
    public class MainActivity extends Activity implements OnClickListener {
        private Button btn_sendMsg, btn_receiveMsg1, btn_receiveMsg2;
                
        private Handler h1 = null;
        private Handler h2 = null;
        private Handler h3 = new Handler(){
            public void handleMessage(Message msg) {  //主线程实例化h3,用以接受子线程发回的确认消息
                Log.i("cth", "主线程接收消息中...");
                super.handleMessage(msg);
                Log.i("cth", "--主线程收到的obj-->" + msg.obj);
                Log.i("cth","该消息队列是" + Looper.myQueue().toString());
                msg.recycle();
            }
        };
                
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            btn_sendMsg = (Button) findViewById(R.id.btn_sendMsg);
            btn_sendMsg.setOnClickListener(this);
            btn_receiveMsg1 = (Button) findViewById(R.id.btn_receiveMsg1);
            btn_receiveMsg1.setOnClickListener(this);
            btn_receiveMsg2 = (Button) findViewById(R.id.btn_receiveMsg2);
            btn_receiveMsg2.setOnClickListener(this);
        }
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
            case R.id.btn_sendMsg:
                if (h1 != null) {
                    Message msg = Message.obtain(h1, 1);
                    msg.sendToTarget();  //发到子线程1的消息队列
                    Log.i("cth", "已发送消息1");
                }
                        
                if (h2 != null) {
                    Message msg = Message.obtain(h2, 2);
                    msg.sendToTarget();  //发到子线程2的消息队列
                    Log.i("cth", "已发送消息2");
                }
                break;
            case R.id.btn_receiveMsg1:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Log.i("cth", "新线程1开启");
                        Looper.prepare();
                        h1 = new Handler() {
                            public void handleMessage(Message msg) {  //子线程1实例化h1
                                Log.i("cth", "子线程1接收消息中...");
                                super.handleMessage(msg);
                                Log.i("cth", "--子线程1收到的what-->" + msg.what);
                                Log.i("cth","该消息队列是" + Looper.myQueue().toString());
                                        
                                        
                                Message msg3 = h3.obtainMessage();
                                msg3.obj = "子线程1收到what=" + msg.what;
                                msg3.sendToTarget();
                                msg.recycle();
                                        
                            }
                        };
                        Looper.loop();
                                
                    }
                }).start();
                break;
            case R.id.btn_receiveMsg2:
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Log.i("cth", "新线程2开启");
                        Looper.prepare();
                        h2 = new Handler() {
                            public void handleMessage(Message msg) {  //子线程2实例化h2
                                Log.i("cth", "子线程2接收消息中...");
                                super.handleMessage(msg);
                                Log.i("cth", "--子线程2收到的what-->" + msg.what);
                                Log.i("cth","该消息队列是" + Looper.myQueue().toString());
                                        
                                Message msg3 = h3.obtainMessage();
                                msg3.obj = "子线程2收到what=" + msg.what;
                                msg3.sendToTarget();
                                msg.recycle();
                            }
                        };
                        Looper.loop();
                    }
                }).start();
                break;
            default:
                break;
            }
        }
    }
    

    运行结果:

    运行结果
    2.3 利用代码验证我们的推断结果:

    首先我们的创建一个类,继承Handler,重写我们前面推断所涉及到的函数,所涉及到的包括sendMessage、sendMessageDelayed和sendMessageAtTime、dispatchMessage和handleMessage,其中sendMessage和sendMessageDelayed是final的,无法重写,所以我们只重写后三个,如果sendMessageAtTime被调用到就能证明前两个被调用到了。以下是Handler子类的源代码:

    class MyHandler extends Handler {
        @Override
        public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
            Log.i("cth","Handler的sendMessageAtTime方法被调用");
            return super.sendMessageAtTime(msg, uptimeMillis);
        }
        @Override
        public void dispatchMessage(Message msg) {
            Log.i("cth","Handler的dispatchMessage方法被调用");
            super.dispatchMessage(msg);
        }
        @Override
        public void handleMessage(Message msg) {
            Log.i("cth","主线程收到消息");
            Log.i("cth","Handler的handleMessage方法被调用");
            Log.i("cth","--arg1-->"+ msg.arg1);
            Log.i("cth","--arg2-->"+ msg.arg2);
            Log.i("cth","--what-->"+ msg.what);
            Log.i("cth","--obj-->"+ msg.obj);
            Log.i("cth","--getWhen-->"+ msg.getWhen());
            Log.i("cth","--getTarget-->"+ msg.getTarget());
        }
    }
    

    如果均被调用且调用顺序和我们推断的相同,则可验证我们的推断结论是正确的。
    接下来,创建一个按钮,点击按钮建立一个子线程,在子线程使用该Handler子类的对象发送消息:

    private Button btn_sendMsg;
    private myHandler handler = new MyHandler();
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn_sendMsg = (Button) findViewById(R.id.btn_sendMsg);
        btn_sendMsg.setOnClickListener(newOnClickListener() {
        
            @Override
            public void onClick(View v) {
                newThread(newRunnable(){
                    @Override
                    public void run() {
                        Message msg = Message.obtain();  //通过obtain方法获取一个消息对象
                        msg.arg1 = 1;                   //设定各种值
                        msg.arg2 = 2;
                        msg.what = 3;
                        msg.obj = "来自子线程的Message。";
                        Log.i("cth","子线程发送消息");
                        handler.sendMessage(msg);       //利用Handler把消息发送出去
                    }
                }).start();
            }
        });
    }
    

    运行后我们观看日志,可以看到和我们推断结果完全相同结果:

    运行结果

    其中MessageQueue的enqueueMessage方法和Looper的loop方法都无法重写,所以Log打不出来,但是通过Handler各个函数的输出顺序已经可以验证我们之前的推断结论了。
    总之,看了API、源代码后,自己再好好总结了一下,感觉思路清晰多了,以后遇到问题,建议还是不要急着百度看各种各样的资料,先结合API文档尝试自己推断一下,然后再看SDK分析,进一步推断,最后再用代码论证,个人觉得这种办法是比较实际的,而且更能学到东西。

    相关文章

      网友评论

          本文标题:讲讲Handler、MessageQueue和Looper

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