微信号:jszj2014215

介绍:主要分享移动互联网的相关产品和资讯,关注你将学习到更多,在互联网的当下你会赚更多的钱...

Android从启动到程序运行发生的事情(二)

2016-04-15 10:31 Android技术之家

滑动冲突

顺带总结一下滑动冲突的解决吧

View的滑动冲突一般可以分为三种:

  • 外部滑动和内部滑动方向不一致

  • 外部滑动方向和内部滑动方向一致

  • 嵌套上面两种情况

比如说一个常见的,外部一个ListView,里面一个ScrollView。这个时候该怎么解决呢?其实这里想到了ViewPager,它里面实际上是解决了滑动冲突的,可以借鉴一下它的。

滑动处理规则

一般来说,我们可以根据用户手指滑动的方向以及角度来判断用户是要朝着哪个方向去滑动。而很多时候还可以根据项目的需求来指定一套合适的滑动方案。

外部拦截法

这种方法就是指所有的点击时间都经过父容器的拦截处理,如果父容器需要此时间就拦截,如果不需要此事件就不拦截。通过重写父容器的onInterceptTouchEvent方法:

case MotionEvent.ACTION_DOWN:
    intercepted = false;
break;

case MotionEvent.ACTION_MOVE:
if(父类容器需要) {
    intercepted = true;
} else {
    intercepted = false;
}
break;

case MotionEvent.ACTION_UP:
    intercepted = false;
break;

return intercepted;

这里有一点需要注意,ACTION_DOWN事件父类容器就必须返回false,因为如果父类容器拦截了的话,后面的Move等所有事件都会直接由父类容器处理,就无法传给子元素了。UP事件也要返回false,因为它本身来说没有太多的意义,但是对于子元素就不同了,如果拦截了,那么子元素的onClick事件就无法触发。

内部拦截法

这种方法指的是父容器不拦截任何时间,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理。它需要配合requestDisallowInterceptTouchEvent方法才能正常工作。我们需要重写子元素的dispatch方法

case MotionEvent.ACTION_DOWN:
    parent.requestDisallowInterceptTouchEvent(true);
break;

MotionEvent.ACTION_MOVE:
    if(父容器需要此类点击事件) {
    parent.requestDisallowInterceptTouchEvent(false);
    }
break;

return super.dispatchTouchEvent(event);

这种方法的话父类容器需要默认拦截除了ACTION_DOWN以外的其他时间,这样当子元素调用request方法的时候父元素才能继续拦截所需的事件。

其他的

如果觉得上面两个方式太复杂,看晕了,其实也可以自己根据项目的实际需要来指定自己的策略实现。例如根据你手指按的点的位置来判断你当前触碰的是哪个控件,以此来猜测用户是否是要对这个控件进行操作。如果点击的是空白的地方,就操作外部控件即可。

【等有时间了就把ViewPager的处理总结一下,挺重要的】

继续断点3

  • 当我们点击桌面的APP图标时,Launcher进程会采用Binder的方式向AMS发出startActivity请求

  • AMS在接收到请求之后,就会通过Socket向Zygote进程发送创建进程的请求

  • Zygote进程会fork出新的子进程(APP进程)

  • 之后APP进程会再向AMS发起一次请求,AMS收到之后经过一系列的准备工作再回传请求。

  • APP进程收到AMS返回的请求后,会利用Handler向主线程发送LAUNCH_ACTIVITY消息

  • 主线程在收到消息之后,就创建目标Activity,并回调onCreate()/onStart()/onResume()等方法,UI渲染结束后便可以看到App主界面 【断点4】

Handler/Looper/Message Queue/ThreadLocal机制

Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueue和Looper的支撑

虽然MessageQueue叫做消息队列,但是实际上它内部的存储结构是单链表的方式。由于Message只是一个消息的存储单元,它不能去处理消息,这个时候Looper就弥补了这个功能,Looper会以无限循环的形式去查找是否有新消息,如果有的话就处理消息,否则就一直等待(机制等会介绍)。而对于Looper来说,存在着另外的一个很重要的概念,就是ThreadLocal。

ThreadLocal

ThreadLocal它并不是一个线程,而是一个可以在每个线程中存储数据的数据存储类,通过它可以在指定的线程中存储数据,数据存储之后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到该线程的数据。

举个例子,多个线程通过同一个ThreadLocal获取到的东西是不一样的,就算有的时候出现的结果是一样的(偶然性,两个线程里分别存了两份相同的东西),但他们获取的本质是不同的。

那为什么有这种区别呢?为什么要这样设计呢?

先来研究一下为什么会出现这个结果。

在ThreadLocal中存在着两个很重要的方法,get和set方法,一个读取一个设置。

 /**
     * Returns the value of this variable for the current thread. If an entry
     * doesn't yet exist for this variable on this thread, this method will
     * create an entry, populating the value with the result of
     * {@link #initialValue()}.
     *
     * @return the current value of the variable for the calling thread.
     */
    @SuppressWarnings("unchecked")
    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }

 /**
     * Sets the value of this variable for the current thread. If set to
     * {@code null}, the value will be set to null and the underlying entry will
     * still be present.
     *
     * @param value the new value of the variable for the caller thread.
     */
    public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }

摘自源码

首先研究它的get方法吧,从注释上可以看出,get方法会返回一个当前线程的变量值,如果数组不存在就会创建一个新的。
这里有几个很重要的词,就是“当前线程”和“数组”。

这里提到的数组对于每个线程来说都是不同的,values.table,而values是通过当前线程获取到的一个Values对象,因此这个数组是每个线程唯一的,不能共用,而下面的几句话也更直接了,获取一个索引,再返回通过这个索引找到数组中对应的值。这也就解释了为什么多个线程通过同一个ThreadLocal返回的是不同的东西。

那这里为什么要这么设置呢?翻了一下书,搜了一下资料:

  • ThreadLocal在日常开发中使用到的地方较少,但是在某些特殊的场景下,通过ThreadLocal可以轻松实现一些看起来很复杂的功能。一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑使用ThreadLocal。例如在Handler和Looper中。对于Handler来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同的线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松的实现Looper在线程中的存取。如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定的Looper,这样就比较麻烦了,还需要一个管理类。

  • ThreadLocal的另一个使用场景是复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,就可能表现为函数调用栈比较深以及代码入口的多样性,这种情况下,我们又需要监听器能够贯穿整个线程的执行过程。这个时候就可以使用到ThreadLocal,通过ThreadLocal可以让监听器作为线程内的全局对象存在,在线程内通过get方法就可以获取到监听器。如果不采用的话,可以使用参数传递,但是这种方式在设计上不是特别好,当调用栈很深的时候,通过参数来传递监听器这个设计太糟糕。而另外一种方式就是使用static静态变量的方式,但是这种方式存在一定的局限性,拓展性并不是特别的强。比如有10个线程在执行,就需要提供10个监听器对象。

消息机制

上面提到了Handler/Looper/Message Queue,它们实际上是一个整体,只不过我们在开发中接触更多的是Handler而已,Handler的主要作用是将一个任务切换到某个指定的线程中去执行,而Android之所以提供这个机制是因为Android规定UI只能在主线程中进程,如果在子线程中访问UI就会抛出异常。

为什么Android不允许在子线程访问UI

其实这一点不仅仅是对于Android,对于其他的所有图形界面现在都采用的是单线程模式。

因为对于一个多线程来说,如果子线程更改了UI,那么它的相关操作就必须对其他子线程可见,也就是Java并发中很重要的一个概念,线程可见性,Happen-before原则【下篇博客总结一下自己对Java并发的理解吧,挺重要的,总结完后再把传送门贴过来】而一般来说,对于这种并发访问,一般都是采用加锁的机制,但是加锁的机制存在很明显的问题:让UI访问间的逻辑变得复杂,同时效率也会降低。甚至有的时候还会造成死锁的情况,这个时候就麻烦了。

而至于究竟能不能够实现这种UI界面的多线程呢?SUN公司的某个大牛(忘了是谁,很久之前看的,好像是前副总裁)说:“行肯定是没问题,但是非常考技术,因为必须要考虑到很多种情况,这个时候就需要技术专家来设计。而这种设计出来的东西对于广大普通程序员来说又是异常头疼的,就算是实现了多线程,普通人用起来也是怨声载道的。所以建议还是单线程”。

死锁

顺带着BB一下死锁。

死锁的四个必要条件

  1. 互斥条件:资源不能被共享,只能被同一个进程使用

  2. 请求与保持条件:已经得到资源的进程可以申请新的资源

  3. 非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺

  4. 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源

举个常见的死锁例子:进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

处理死锁的方法

  1. 忽略该问题,也就是鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它。

  2. 检测死锁并恢复

  3. 资源进行动态分配

  4. 破除上面的四种死锁条件之一

继续消息机制

MessageQueue主要包含两个操作:插入和读取,读取操作本身会伴随着删除操作,插入和读取对应的方法分别为enqueueMessage和next,其中enqueueMessage的作用是往消息队列中插入一条消息,而next的作用是从消息队列中取出一条消息并将其从消息队列中移除。这也就是为什么使用的是一个单链表的数据结构来维护消息列表,因为它在插入和删除上比较有优势(把下一个连接的点切换一下就完成了)。

而对于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) {
            return null;
        }

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

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

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

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

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

            // Run the idle handlers.
            // 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();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

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

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

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

源码有点长,总结一下就是:

next方法它是一个死循环,如果消息队列中没有消息,那么next方法就会一直阻塞在这里,当有新的消息来的时候,next方法就会返回这条信息并将其从单链表中移除。

而这个时候勒Looper就等着的,它也是一直循环循环,不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则就会一直阻塞在那里。而对于Looper来说,它是只能创建一个的,这个要归功与它的prepare方法。

 /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    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));
    }

从这里我们就可以看出该prepare方法会首先检测是否已经存在looper了,如果不存在,就创建一个新的;如果存在,就抛出异常。
而之后使用Looper.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;

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

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

从这里面我们可以看到它也是个死循环,会不停的调用queue.next()方法来获取信息,如果没有,就return,如果有就处理。

注意

当然了,这里有一个很重要的点,一般可能会忘,那就是在子线程中如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待状态,而如果退出Looper之后,这个线程就会立刻终止,所以建议不需要使用的时候终止Looper。

Handler

上面总结了Looper和MessageQueue,这里就对Handler进行一个总结吧。它的工作主要包含消息的发送和接受过程,消息的发送可以通过post的一系列方法以及send的一系列方法来实现,post的一系列方法最终是通过send的一系列方法来实现的。

实际上它发送消息的过程仅仅是向消息队列中插入了一条消息,MessageQueue的next方法就会返回这条消息给Looper,Looper在收到消息之后就会开始处理了。最后由Looper交给Handler处理(handleMessage()方法)。


 
Android技术之家 更多文章 自动展开标题通知栏,兼容各个版本,欢饮大家关注,并参与讨论。 minSdkVersion、targetSdkVersion、targetApiLevel的区别 ndk调用实战 GreenDao数据库操作 大家来找茬 看看下面这个单例模式有什么不妥的
猜您喜欢 MySQL for update 死锁案例 一维序列卷积之Python实现 Stackoverflow评选的10本最有影响力IT图书 40个Java多线程问题总结 【软件测试】软件测试模型综述