从源码一次彻底理解Android的消息机制

情景重现

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    Thread.sleep(5 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                textView.setText("after changed");
            }
        });

开头我们就看到了如上的一段简单的伪码。因为这里我试图去还原一种场景,一种可能我们不少人最初接触Android时可能都会遇到的错误场景。
这种场景的逻辑很简单:在程序运行中,某个控件的值会被动态的改变。这个值通过某种途径获取,但该途径是耗时的(例如访问网络,文件读写等)。
上面的伪码中的逻辑是:点击按钮将会改变文本框的值,且这个值是通过某种耗时的操作获取到,于是我们通过将线程休眠5秒来模拟这个耗时操作。

好的,现在我们通过编译正式开始运行类似的代码。那么,我们首先会收到熟悉的一个错误,即“Application No Response(ANR)”。
接着,通过查阅相关的资料,我们明白了:原来我们像上面这样做时,耗时操作直接就是存在于主线程,即所谓的UI线程当中的。
那么这就代表着:这个时候的UI线程会因为执行我们的耗时操作而被堵塞,也自然就无法响应用户其它的UI操作。于是,就会引起ANR这个错误了。
现在我们了解了ANR出现的原因,所以我们自然就会通过一些手段来避开这种错误。我们决定将耗时的操作从UI线程拿走,放到一个新开的子线程中:

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(5 * 1000);
                            textView.setText("after changed");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        });

好的,现在我们模拟的耗时操作已经被我们放到了UI线程之外的线程。当我们信心十足的再次运行程序,确得到了如下的另一个异常信息:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

从异常信息中,我们看到系统似乎是在告诉我们一个信息,那就是:只有创建一个视图层次结构的原始线程才能触摸到它的视图
那么,我们似乎就能够理解这种异常出现的原因了:我们将耗时操作放在了我们自己创建的分线程中,显然它并非原始线程,自然就不能够去访问View。
这样设计的初衷实际上是不难猜想的,如果任何线程都能去访问UI,请联想一下并发编程中各种不可预知且操蛋的问题,可能我们的界面最终就热闹了。
但是,现在我们针对于这一异常的解决方案似乎也不难给出了。既然只有主线程能够访问View,那么我们只需要将更新UI的操作放到主线程就OK了。
那么,这里就顺带一提了。不知道有没有人和我曾经一样,想当然的写出过类似下面一样的代码:

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(5 * 1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
                // 这是在主线程里执行的
                textView.setText("after changed");
            }
        });

是的,如上的代码十分的,非常的“想当然”。这当然是因为对Java多线程机制理解不够所造成的。更新UI的操作确实是放到了主线程,但是!!!:
这并不代表着,UI更新一定会在分线程的耗时操作全部完成后才会执行,这自然是因为线程执行权是随机切换的。也就是说,很可能出现的情况是:
分线程中的耗时操作现在并没有执行完成,即我们还没有得到一个正确的结果,便切换到了主线程执行UI的更新,这个时候自然就会出现错误。

Handler粉墨登场

这个时候,作为菜鸟的我们有点不知所措。于是,赶紧上网查查资料,看看有没有现成的解决方案吧。这时,通常“Handler”就会进入我们的视线了:

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case 0x0001:
                    textView.setText("after changed");
            }
        }
    };
    //===============================================
    new Thread(new Runnable() {
        @Override
        public void run() {
           try {
              Thread.sleep(5 * 1000);
              mHandler.sendEmptyMessage(0x0001);
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
             }
     }).start();

我们发现关于Handler的使用似乎十分容易不过,容易到当我们认为自己掌握了它的时候似乎都没有成就感:

  • 首先,我们只需要建立一个Handler对象。
  • 接着,我们会在需要的地方,通过该Handler对象发送指定的Message。
  • 最后,该Handler对象通过handleMessage方法处理接收到的Message。

但我们沉下心来想一想:Handler为什么能够解决我们之前碰到的非原始线程不能更新UI的错误呢?它的实现原理如何?它能做的就只是更新UI吗?
掰扯了这么多,带着这些疑问,我们终于来到了我们这篇blog最终的目的,那就是搞清楚Android的消息机制(主要就是指Handler的运行机制)。

从构造函数切入

就像医生如果要弄清楚人体构造,方式当然是通过解剖来进行研究。而我们要研究一个对象的实现原理,最好的方式就是通过分析它的源码。
个人的习惯是,当我们没有一个十分明确的切入点的时候,选择构造函数切入通常是比较合适的,那我们现在就打开Handler的构造函数来看一下:

    // 1.
    public Handler() {
        this(null, false);
    }
    // 2.
    public Handler(Callback callback) {
        this(callback, false);
    }
    // 3.
    public Handler(Looper looper) {
        this(looper, null, false);
    }
    // 4.
    public Handler(Looper looper, Callback callback) {
        this(looper, callback, false);
    }
    // 5.
    public Handler(boolean async) {
        this(null, async);
    }
    // 6.
    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;
        mAsynchronous = async;
    }
    // 7.
    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

好的,分析一下我们目前所看的,我觉得我们至少可以很容易的分析并掌握两点:

  • Handler自身提供了7种构造器,但实际上只有最后两种提供了具体实现。
  • 我们发现各种构造器最终围绕了另外两个类,即Callback与Looper。我们推测它们肯定不是做摆设的。

现在我们来分别看一下唯一两个提供了具体实现的构造器,我们发现:
除了 ”if (FIND_POTENTIAL_LEAKS) “这一段看上去和反射有关的if代码块之外,这两个构造器剩下的实现其实基本上是完全一致的,即:

        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;

唯一的不同在于mLooper这一实例变量的赋值方式:

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        //==========================================
        mLooper = looper;

“mLooper = Looper.myLooper();”这种方式究竟有何神奇,我们这里暂且不提。我们的注意力聚焦在以上看到的几个实例变量,打开源码看看:

    final MessageQueue mQueue;
    final Looper mLooper;
    final Callback mCallback;
    final boolean mAsynchronous;

mAsynchronous是一个布尔型的变量,并且我们看到默认情况它的构造值是false,从命名我们就不难推测到,它多半与异步有关。除此之外:
其余类型分别是”MessageQueue,Looper,Callback“。一定记住它们!!!正是它们配合HandlerMessage完成了整个消息传递的架构。

OK,首先我们来看Callback这个东西,从命名来看绝逼与回调有关系,打开源码,果不其然正是定义在Handler内部的一个接口:

    public interface Callback {
        public boolean handleMessage(Message msg);
    }

我们看到了其中唯一声明的一个方法接口,看上去似乎有点眼熟。是的,那么它与Handler自身的handleMessage有何联系?我们暂且提不提。
现在,我们再接着看Looper和MessageQueue两个类型。很遗憾的是,这里我们发现:这是另外单独定义的两个全新的类。也就是说:
目前我们似乎无法在逻辑上将其与Handler联系起来。我们现在只知道从命名上来说,它们似乎分别代表着“循环”与“消息队列”的意思。

post()与sendMessage系列函数

那么,到了这一步似乎情况有点糟糕,因为似乎失去了下一步的切入点。没关系,这个时候我们回忆一下我们通常怎么样使用Handler:

  • mHandler.post();
  • mHandler.sendMessage();

没错,我们基本上就是通过以上两种方式去使用Handler。所以现在我们打开这两个方法相关的源码来看看:

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

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

    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

由此我们发现的是:post函数最终调用的仍是send系列的函数;而sendMessage底部也依然是通过sendMessageDelayed调用的。
并且!查看一系列的send方法源码发现:它们最终都将通过sendMessageAtTime来完成整个调用。所以显然这将是我们下一个关注点。

先分析一下post方法的实现,我们看到其实Handler内部是通过getPostMessage对我们传入的Runnable对象进行了一次封装。
当我们看到getPostMessage方法的实现,我们会发现没什么大不了的,只是将传入Runnable对象赋值给了一个Message对象而已。
但我们也可能会观察到另外一点。就是我们可能会在使用Message时会通过构造器得到消息对象,而这里是通过静态方法obtain。

使用obtainMessage而非构造器

这二者有什么不同呢?我们先打开Message的构造器的方法来看一下:

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

好的,我们发现构造器实际上没有任何实现内容。而注释告诉我们:更推荐使用obtain系列的方法来获取一个Message对象。
那么我们就好奇了?为什么更推荐使用obtain呢?我们以无参的obtain方法为例,打开源码瞧一瞧:

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

从以上代码我们可以看到的是:obtain最终的本质仍是产生Message对象。关键在于一个叫sPool的东西,这兄弟到底是个什么鬼呢?
实际上是Handler内部会通过这个叫做sPool的静态全局变量构建一个类似“池”的东西,而通过next属性我们不难推断”池”应该是以单链表来实现的。
再查看方法的注释:从全局池中返回一个新的消息实例。使我们能够避免在许多情况下分配新的对象。由此我们好像已经知道为何推荐obtain了。
包括网上很多打着类似“new message与obtainMessage之间区别”的资料里,一大段的文字之后,我们会发现最终实际有用的就类似一句话:
obtainMessage可以从池中获取Message对象,从而避免新的对象创建,达到节约内存的效果。但这样当然还是熄灭不了一颗好奇的心:
究竟为什么这个“池”能够避免新的对象创建呢?要解开这个疑问,我们还需要关注Handler类中的另一个方法“recycleUnchecked”的如下代码:

 void recycleUnchecked() {
        // 第一部分
        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对象。而这个回收动作发生的时机是什么呢?
通常来说,我们可以通过人为调用msg.recycle()来完成回收;另一种更常见的回收时机发生在MessageQuene当中,我们稍后会看到。
接下来我们该方法中的回收工作都做了什么,代码中注释为第一部分的代码做的工作很易懂,就是将回收的message对象的各个属性清空。
第二部分其实就是将回收的对象向“池”内添加的过程,而之前说到的obtain方法,其一旦判断sPoll不为null,就直接从池内获取闲置对象,不再创建。

到此实际上我们就已经分析了,为什么obtain能够节约内存开销的原理了。但如果你的数据结构和我一样渣,可能还会有点晕。没关系,看如下代码:

Message msg1 = Message.obtain();
msg1.recycle();
Message msg2 = Message.obtain();

我们对应于这三行简单的代码,来有代入感的分析一下它们运行的过程,相信就会有个比较清晰的理解了。

  • 首先获取msg1的时候,这个时候sPool肯定是为null的。所以它的工作实际与直接通过构造器创建对象没有区别。
  • 通过msg1对象调用recycle方法,最终进入之前所说的回收工作的第二部分执行。此时的结果为:msg1.next = sPoll(即null,没有next节点);sPoll = msg1;
  • 这时我们再通过obtain去获取对象msg2,进入方法后,判断sPoll不为null。于是, Message m = msg1;注意:
    这代表我们已经从池中取出了msg1,于是执行sPool = m.next时,我们说到msg1.next是null,所以sPool再次等于null,逻辑完全正确。
    与此同时,我们也可以想得到,假设m.next不等于null时:sPool = m.next的逻辑实际上就转换成了,将sPool指向next节点,即代表我们已经取走一个对象了,池将指向下一个节点,即为我们下次要获取的消息对象。

MessageQuene 消息队列的工作机制

好了,现在相信我们都清楚以上的概念了。我们的关注点将回到我们之前提到的关键位置,即sendMessageAtTime方法,打开其源码:

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        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, uptimeMillis);
    }

我们发现该方法的实现很简单,但最终会调用另一个方法“enqueueMessage”,赶紧打开这个方法看一看:

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

我们发现该方法显然使用了委托设计模式,将最终的方法实现委托了给了quene对象,即MessageQuene来实现。
对于MessageQuene中的enqueueMessage方法,该方法的源码个人觉得没必要全部关注。我们先看下面这小段代码:

            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.recycle方法,记得我们之前说过的回收工作吗?这里正是另一种发生时机,这个时机的标准如上所示,正是:
“mQuitting”为true,而在什么时候mQuitting会被设置为true,我们稍后将会看到,这里先暂且一提。接着看另一端更为关键的代码:

                Message prev;
                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;

有了之前的基础,我们发现该方法所做的工作实际上很简单,它仍然是以单链表的形式,通过不断追加next节点达到向队列中添加Message的效果。
由此,我们发现:当我们通过handler对象post或send了一条消息,其实最终的工作很简单,就是向MessageQuene即消息队列中追加一条消息而已。
那么,接下来呢?自然的,消息追加到了队列当中。我们则需要从队列中依次取出消息对象,才能对其作出处理。苦苦寻觅一番之后:
我们发现了next()方法,该方法的实现归根结底是通过循环来不断的从队列中拉取消息,考虑到篇幅,我们不再贴出源码。唯一注意:

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

当没有新的消息来临之前,如上的代码将能够确保队列“阻塞”而一直等待新的消息对象来临。好了,我们总结一下:
MessageQuene将通过enqueueMessage方法向队列中插入消息,而通过next方法取出消息。但现在的关键点在:
关于enqueueMessage方法我们已经知道它在Handler当中被调用,而next方法目前我们只看到声明,还没看到调用的产生。

Looper – 消息的拉取者

以next()方法的调用为关键字按图索骥,我们最终发现它在我们之前提到的另一个关键的东西”Lopper”中产生了调用,具体是Lopper中的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;
            }

            ...

            msg.target.dispatchMessage(msg);

            ...

            ...

            msg.recycleUnchecked();
        }
    }

我们只保留了关于loop方法最为关键的部分,我们依次来分析一下,首先我们注意到的,肯定是一个名为“myLooper()”的方法调用:

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

ThreadLocal显神通

我们从代码中看到逻辑十分简单清晰,就是通过myLooper()来获取looper对象,而最终的方式则是通过sThreadLocal来获取。
这里,就不得不提到一个功能强大的东西ThreadLocal。我们来看一下Looper的源码当中关于sThreadLocal的定义:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

这个东西的作用究竟如何?简单来说,我们知道普通的定义一个实例变量,它将创建在“堆”上。
而“堆”并非线程私有的,所以实例变量也将被线程共享。而ThreadLocal则是将变量的作用域限制为线程私有。举例来说:

       static final ThreadLocal<String> sThreadLocal = new ThreadLocal<String>();

       sThreadLocal.set("1");

        new Thread(new Runnable() {
            @Override
            public void run() {
                sThreadLocal.set("2");
            }
        }).start();

上面的代码通过sThreadLocal.get()来获取string,在主线程中和我们new的线程当中获取的值是独立的,分别是“1”和“2”。

接下来,我们看到的就是将会在一个无限循环中一直通过调用MessageQuene的next()方法来获取消息对象。
假设此次获取到了msg对象,则会通过msg.target调用dispatchMessage方法来分发消息。问题在于target是个什么东西?
在Message类中查看源码,我们可以知道target自身是一个Handler类型的对象。但通常我们都没有人为的去为这个变量赋值。
那么这个变量通常默认是什么呢?回到之前Handler类的enqueneMessage方法当中,看到如下代码:

 msg.target = this;

也就是说,如果我们没有明确的去为Message对象的target域赋值,它将被默认赋值为发送这条Message的Handler对象自身。
那么,我们先要做的就简单了,回到Handler类当中,查看dispatchMessage方法的源码如下:

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

这个方法的实现逻辑也很清晰,它的具体分发过程如下:

  • 如果msg.callback不为null,那么将通过handleCallback方法来处理消息(实际上就是msg.callback.run());
  • 否则进一步判断mCallback是否为null,不为null则通过mCallback.handleMessage来处理消息。
  • 最后如果mCallback也为null,则会调用Handler自身的handleMessage方法来处理消息。

逻辑很简单,我们唯一注意的就是msg.callback和mCallback这两兄弟是指什么东西?很简单:
msg.callback是我们通过Message.obtain(Handler h, Runnable callback)或者通过Handler.post(Runnable r)传入的Runnable对象。
而mCallback就更熟悉了,回想我们之前查看Handler的构造器时看到的东西。
mCallback的本质就是Handler内部定义的接口Callback,所以通过它实际就是通过回调接口处理消息。

而这里,我觉得值得一说的是msg.callback这个东西。我们知道当它不为null,最终实际将通过message.callback.run()来处理消息。
也就是说最终实际上是调用了Runnable对象的run方法,但有Java的基础就会知道这样的调用实际与线程无关,只相当于普通的调用一个实例方法而已。
对于这点,我们一定要有清楚的认识,否则可能会因为使用不当造成一些想不到的错误。具体的例子我们暂且不说,放在最后的总结部分来看。

实际上到了这里,我们就已经把构建Android消息机制的四个关键,即Handler,Message,MessageQuene及Looper给联系起来了。简单总结一下:

  • 通过调用Handler的post或者send系列的方法来发送一条Message
  • 这一条Message最终会加入到链表结构的MessageQuene当中存放。
  • Looper会通过内部的loop方法不断调用MessageQuene的next()方法获取下一条Message
  • Looper获取到Message方法后,又会通过Handler的dispatchMessage来分发并处理消息。

Looper的正确创建

我相信到了这里,我们或多或少都会有些收获。但对于刚接触Andoid消息机制的朋友来说,还可能存在一个疑问,那就是:
通过之前我们的分析与理解,我们知道了对于Handler处理消息的机制来说,Lopper的参与是至关重要的。
但与此同时,我们发现之前我们似乎并没有创建Looper。我们不免会考虑,是系统帮助我们创建了吗?答案是肯定的。
回忆一下之前的代码,我们是通过无参的构造器来创建Handler对象的。我们也可以看到,该构造器最终会调用我们之前说到的第6个构造器。
然后我们发现在第6种构造器当中,是通过“mLooper = Looper.myLooper();”的方式来获取looper对象的。
这时我们就想起了之前的ThreadLocal,但即使是使用ThreadLocal,也起码得有一个ThreadLocal.set(Looper)的过程吧。
这个过程是在什么时候完成的呢?正常来说,我们推断这个过程很可能发生在Looper的构造器中。但一番查找我们发现Looper压根没提供公有的构造器。
经过苦苦的寻觅之后,最终我们会发现在Looper的静态方法“prepare”中,终于找到了我们想要看见的代码:

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

好了,现在我们回想一下,我们之前的代码中创建的Handler,是一个属于当前Activity的实例域。这代表它的创建是在主线程中完成的。
而关于Looper的创建的确也是在Android的主线程,即ActivityThread中完成创建的。具体的调用位于主线程的入口main方法中。
并且,主线程里的Looper,是通过调用Looper类专门为主线程创建Looper对象封装的方法“prepareMainLooper()”来创建的。

现在我们再看关于”Can’t create handler inside thread that has not called Looper.prepare()”的这个异常,我们就很容易找到原因了。
因为通过源码我们知道这个异常就是在第6个构造器中,当通过Looper.myLooper()获取结果为null时报告的。
同时,我们知道我们在主线程创建Handler的时候,没有问题是因为主线程默认就创建了Looper对象。那么:
当我们要在主线程以外的线程中创建Handler对象的时候,只要我们也为它创建对应的Looper对象就行了。示例如下:

        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Handler handler = new Handler();
                Looper.loop();
            }
        }).start();

是的,在创建完对象后,别忘了调用loop()方法,因为这才是开启循环从消息队列中获取消息的关键。

好了,现在我们正式接着看loop()方法中接下来的代码msg.recycleUnchecked();。是的,我们很熟悉的”回收”,由此我们可以知道:
回收Message的另一个时机实际就是,当Message对象从队列中取出处理完成之后,就会进行回收,放入池内。
如果具体阅读MessageQuene的源码,我们会发现还有多种不同的回收时机,我们简单总结几种常见的时机:

  • 人为调用message.recycle()来回收对象。
  • message从队列中取出被处理完成后会自动回收。
  • 调用Lopper.quit()/quitSafely(),该方法最终会调用MessageQuene的quit()方法。
  • MessageQuene调用quit在合适的时机将自身队列中的Message对象进行回收。
  • MessageQuene的quit方法还会将我们之前谈到的mQuitting设置为true,这代表着当调用了quit之后,再通过handler来send任何message,都将被直接回收。

总结

到这里,对于Android的消息机制我们也就研究的差不多了。虽然我总觉得在写的过程中,我本来还有个别想总结的点,但最后似乎忘了,也想不起了。

我们最后总结一个问题,那就是Handler为什么能够执行更新UI的操作!现在我们就来分析一下这个过程,回想一下:
通常我们在另开的线程中执行耗时操作,当耗时操作执行完毕后,则调用sendMessage()最终让handler更新UI。
现在我们知道了,这时的Handler我们一定会是定义在主线程,即UI线程当中的。当我们在分线程中sendMessage的时候:
经过我们之前说到的一系列调用,最终会通过Handler来进行最终处理。而Handler本身是在主线程中运行的,自然也就能够操作UI。
所以说,如果说Handler能够让我们更新UI,不如说其本质是将操作切换到该Handler所在的线程来执行。
我们可以试着发送消息给在分线程中创建的Handler对象,然后在handleMessage仍然试图去访问UI。会发现结果当然是行不通的。

这里就说到我们之前说到的handler.post()这样的使用方式了,因为参数类型是Runnable,所以我们很容易认为是通过handler执行一个线程任务。
但实际情况是,假设Handler对象是在主线程中创建的。那么,通过post()方法,我们仍然可以去更新UI。这是为什么?
这就回到了我们之前说的,当handler去dispatchMessage后,如果判断msg.callback不等于null,就会通过msg.callback.run()来处理消息。
这个时候实际本质上就是在切换到主线程去执行了Runnable对象的实例方法run()而已。所以当然能够更新UI。

而我们可能会有这样一种需求,那就是想要在指定的时间之后去执行一个耗时的线程任务,这个时候可能会想到Handler的postDelayed方法。
我想说的使用误区就是,这个时候的Handler一定不要是创建在主线程当中的,因为这样耗时操作最终还是在主线程执行,自然也就会引发ANR。
如果我们一定想要通过Handler实现我们这样的需求,其实很简单,当然就是要把Handler创建在分线程当中,就像下面这样:

              new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Looper.prepare();;
                        final Handler handler = new Handler();
                        Looper.loop();
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    Thread.sleep(5000);
                                    handler.sendEmptyMessage(0);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }

                            }
                        });
                    }
                }).start();

Android闹钟设置的解决方案

Android设置闹钟并不像IOS那样这么简单,做过Android设置闹钟的开发者都知道里面的坑有多深。下面记录一下,我解决Android闹钟设置的解决方案。

主要问题

  1. API19开始AlarmManager的机制修改。
  2. 应用程序被Kill掉后,设置的闹钟不响。
  3. 6.0以上进入Doze模式会使JobScheduler停止工作。
  4. 手机设置重启后,闹钟失效问题。

API19以上AlarmManager机制的修改

API19之前AlarmManager提供了三个设置闹钟的方法,由于业务需求闹钟只需要一次性,所以采用set(int type,long startTime,PendingIntent pi);这个方法。
从API 19开始,AlarmManager的机制都是非准确传递,操作系统将会转换闹钟,来最小化唤醒和电池使用。

由于之前的程序,没有对API19以上的闹钟设置做处理,导致在4.4以上的手机设置闹钟无响应(应用程序没有被杀死的情况也没有闹钟)。

因些,设置闹钟需要根据API的版本进行分别处理设置。代码如下:

AlarmManager am = (AlarmManager) getActivity()
       .getSystemService(Context.ALARM_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    am.setExact(AlarmManager.RTC_WAKEUP, TimeUtils
        .stringToLong(recordTime, TimeUtils.NO_SECOND_FORMAT), sender);
}else {
    am.set(AlarmManager.RTC_WAKEUP, TimeUtils
        .stringToLong(recordTime, TimeUtils.NO_SECOND_FORMAT), sender);
}

这样,保证闹钟在应用程序没有被Kill掉的情况闹钟。

应用程序被Kill掉时的处理

应用程序被Kill掉后,设置的闹钟失效,这里利用守护进程以及灰色保活来保证后台闹钟服务不被Kill掉。当应用程序以及闹钟服务被Kill掉,守护进程以及灰色保活来重新启动闹钟服务,并且重新设置闹钟。
关于守护进程的处理,这里采用开源的守护进程库。Android-AppDaemon

在闹钟服务的onCreat加入Android-AppDaemon这个开源的守护进程。代码如下:

@Override
public void onCreate() {
    super.onCreate();
    Daemon.run(DaemonService.this, 
           DaemonService.class, Daemon.INTERVAL_ONE_MINUTE);
    startTimeTask();
    grayGuard();
}

为进一步保证闹钟服务的存活,同加上灰色保活(利用系统的漏洞启动前台Service)。
代码如下:

private void grayGuard() {
    if (Build.VERSION.SDK_INT < 18) {
        //API < 18 ,此方法能有效隐藏Notification上的图标
        startForeground(GRAY_SERVICE_ID, new Notification());
    } else {
        Intent innerIntent = new Intent(this, DaemonInnerService.class);
        startService(innerIntent);
        startForeground(GRAY_SERVICE_ID, new Notification());
    }

    //发送唤醒广播来促使挂掉的UI进程重新启动起来
    AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    Intent alarmIntent = new Intent();
    alarmIntent.setAction(WakeReceiver.GRAY_WAKE_ACTION);
    PendingIntent operation = PendingIntent.getBroadcast(this, 
        WAKE_REQUEST_CODE, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        alarmManager.setWindow(AlarmManager.RTC_WAKEUP, 
            System.currentTimeMillis(), ALARM_INTERVAL, operation);
    }else {
        alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, 
            System.currentTimeMillis(), ALARM_INTERVAL, operation);
    }
}

/**
 * 给 API >= 18 的平台上用的灰色保活手段
 */
public static class DaemonInnerService extends Service {

    @Override
    public void onCreate() {
        Log.i(LOG_TAG, "InnerService -> onCreate");
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(LOG_TAG, "InnerService -> onStartCommand");
        startForeground(GRAY_SERVICE_ID, new Notification());
        //stopForeground(true);
        stopSelf();
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        Log.i(LOG_TAG, "InnerService -> onDestroy");
        super.onDestroy();
    }
}

上面操作尽可能提高闹钟服务的存活。但是在5.0以上的手机,利用系统的自带的Clean功能的时候,还是会将闹钟服务彻底的干掉。为了解决5.0以上的问题,这里引入5.0以上的新特性 JobScheduler。

5.0以上的JobScheduler

关于5.0新增JobScheduler·API可以先阅读这篇文章。here
在这里利用5.0以上的JobScheduler创建一个定时的任务,定时检测闹钟服务是否存在,没在存在则重新启动闹钟服务。(这里我设置每一分钟检测一次闹钟服务)

在进入应用程序的时候检测当前系统是否是5.0以上,如果是则启动JobScheduler这个服务。代码如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
    JobInfo.Builder builder = new JobInfo.Builder(JOB_ID,
            new ComponentName(getPackageName(), JobSchedulerService.class.getName()));

    builder.setPeriodic(60 * 1000); //每隔60秒运行一次
    builder.setRequiresCharging(true);
    builder.setPersisted(true);  //设置设备重启后,是否重新执行任务
    builder.setRequiresDeviceIdle(true);

    if (mJobScheduler.schedule(builder.build()) <= 0) {
        //If something goes wrong
    }
}

其中的builder.setPersisted(true); 方法是设备重启后,是否重新执行任务,在这测过是可以重新启动任务的。

上面的操作进一步保证了闹钟服务被Kill掉后,重新启动服务。但是在6.0以上引入了Doze模式,当6.0以上的手机进入这个模式后,便会使JobScheduler停止工作。

6.0以上Doze模式的处理

为了让JobScheduler可以在6.0以上进入Doze模式工作,这里针对6.0以上的Doze模式做特殊的处理-忽略电池的优化

  1. 在Manifest.xml中加入权限。
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
  2. 在设置闹钟的时候,判断系统是否是6.0以上,如果是,则判断是否忽略电池的优化。判断是否忽略电池优化代码如下:
    @TargetApi(Build.VERSION_CODES.M)
    public static boolean isIgnoringBatteryOptimizations(Activity activity){
     String packageName = activity.getPackageName();
     PowerManager pm = (PowerManager) activity
             .getSystemService(Context.POWER_SERVICE);
     if (pm.isIgnoringBatteryOptimizations(packageName)) {
         return true;
     }else {
         return false;
     }
    }
  3. 如果没有忽略电池优化的时候,弹出提醒对话框,提示用户进行忽略电池优化操作。代码如下:
    /**
    * 针对N以上的Doze模式
    *
    * @param activity
    */
    public static void isIgnoreBatteryOption(Activity activity) {
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
         try {
             Intent intent = new Intent();
             String packageName = activity.getPackageName();
             PowerManager pm = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
             if (!pm.isIgnoringBatteryOptimizations(packageName)) {
    //               intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
                 intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
                 intent.setData(Uri.parse("package:" + packageName));
                 activity.startActivityForResult(intent, REQUEST_IGNORE_BATTERY_CODE);
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
    }

    在界面重写onActivityResult方法来捕获用户的选择。如,代码如下:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
     if (resultCode == RESULT_OK) {
         if (requestCode == BatteryUtils.REQUEST_IGNORE_BATTERY_CODE){
             //TODO something
         }
     }else if (resultCode == RESULT_CANCELED){
         if (requestCode == BatteryUtils.REQUEST_IGNORE_BATTERY_CODE){
             ToastUtils.show(getActivity(), "请开启忽略电池优化~");
         }
     }
    }

补充

当应用程序被Kill掉,但是闹钟的服务没有被Kill掉的,这时候又设置了闹钟。这就意味着设置的闹钟没有放到闹钟服务那里。所以这种情况,设置的闹钟会失效。为了解决这种情况,利用AIDL(闹钟服务在另一个进程的需要进程间通信)调用闹钟服务的重新设置闹钟方法重设闹钟。

在应用程序的onCreat()方法启动闹钟服务,然后再绑定闹钟服务。

private void initAlarmService() {
    startService(new Intent(this, DaemonService.class));//启动闹钟服务
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        //JobScheduler
        ...
    }

    //绑定闹钟服务
    Intent intent = new Intent(this, DaemonService.class);
    intent.setAction("android.intent.action.DaemonService");
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}

在onDestroy()方法,调用闹钟服务的重设闹钟方法。代码如下:

@Override
protected void onDestroy() {
    super.onDestroy();
    try {//判断是否有闹钟,没有则关闭闹钟服务
        String alarm = localPreferencesHelper.getString(LocalPreferencesHelper.ALARM_CLOCK);
        if (daemonService != -1 && mIRemoteService != null) {
//                android.os.Process.killProcess(daemonService);
            mIRemoteService.resetAlarm();
        }

        if (!alarm.equals("[]")) {
            if (daemonService != -1) {
                startService(new Intent(this, DaemonService.class));
            }
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                mJobScheduler.cancel(JOB_ID);
            }

        }
        unbindService(mConnection); //解除绑定服务。
    } catch (Exception e) {

    }
}

这里说明一下,当服务启动并且被绑定的情况下,unbindService是不会停止服务的。具体可以查看这篇文章。here

最后

以上并不代表所有的Android手机的闹钟都可以用,这只是尽最大的可能保证大部分的手机。
原文链接:http://www.jianshu.com/p/1f919c6eeff6#

安卓实战开发之JNI入门及高效的配置(android studio一键生成.h,so及方法签名)

前言

以前也讲过NDK开发,但是开始是抱着好玩的感觉去开始的,然后呢会helloWord就觉得大大的满足,现在静下来想这NDK开发到底是干什么呢?

NDK开发,其实是为了项目需要调用底层的一些C/C++的一些东西;另外就是为了效率更加高效些但是在java与C相互调用时平白又增大了开销(其实效率不见得有所提高),然后呢,基于安全性的考虑也是为了防止代码被反编译我们为了安全起见,使用C语言来编写这些重要的部分来增大系统的安全性,最后呢生成so库便于给人提供方便。

好了,我们来看一下qq的结构,我们就能理解任何有效的代码混淆对于会smail语法反编译你apk是分分钟的事,即使你加壳也不能幸免高手的攻击,当然你的apk没有什么机密和交易信息就没有人去做这事了。

分析qq的apk架构:

1.使用ClassyShark.jar来打开qq.apk

2.点开Archive我们来查看架构

从上图我们可以看出qq里面是一堆的so库是吗,所以呢so库可见比代码混淆安全系数高的多。

JNI与NDK的关系

  • NDK:

NDK是一系列工具的集合。它提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的。它集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。它可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。

  • JNI:

JavaNative Interface (JNI)标准是java平台的一部分,JNI是Java语言提供的Java和C/C++相互沟通的机制,Java可以通过JNI调用本地的C/C++代码,本地的C/C++的代码也可以调用java代码。JNI 是本地编程接口,Java和C/C++互相通过的接口。Java通过C/C++使用本地的代码的一个关键性原因在于C/C++代码的高效性。

现在明白了吧,NDK就是为我们生成了c/c++的动态链接库而已,jni呢只不过是java和c沟通而已,两者与Android没有半毛钱关系,只因为安卓是java程序开发然后jni又能与c沟通,所以使“Java+C”的开发方式终于转正。

Android是JVM架设在Linux之上的架构。所以无论如何,在Linux OS层面,都应该可以跑C/C++程序。

Android Native C就是使用C/C++程序直接跑到Linux OS层面上的程序。与其它平台类似,只需要交叉编译后。并得到Linux OS root权限,就可以直接跑起来了。

android studio 中简单的jni开发

Let’s Go!!!

准备工作不再需要什么cgwin来编译ndk(太特么操蛋了),现在只需要你下载一下NDK的库就ok了,然后你也可以去离线下载http://www.androiddevtools.cn最新版,这里吐槽一下android studio对NDK的支持还有待提高。

效果

看下今天的效果:(安卓jni获取 apk的包名及签名信息)
这里写图片描述

必须的步骤

1.配置你的ndk路径(local.properties)

ndk.dir=E:\Android\sdk\android-ndk-r11b-windows-x86_64\android-ndk-r11b

2.grale配置使用ndk(gradle.properties)

android.useDeprecatedNdk=true

3.在module下的build.gradle添加ndk以及jni生成目录

ndk{
moduleName “JNI_ANDROID”
abiFilters “armeabi”, “armeabi-v7a”, “x86” //输出指定三种abi体系结构下的so库,目前可有可无。
}
sourceSets.main{
jniLibs.srcDirs = [‘libs’]
}

准备工作做好了开始写代码:(jni实现获取应用的包名和签名信息)

步骤1:先写要实现本地方法的类,及加载库(JNI_ANDROID也就是ndk 里面配的moduleName)

package com.losileeya.getapkinfo;

/**
 * User: Losileeya (847457332@qq.com)
 * Date: 2016-07-16
 * Time: 11:09
 * 类描述:
 *
 * @version :
 */
public class JNIUtils {
    /**
     * 获取应用的签名
     * @param o
     * @return
     */
    public static native String getSignature(Object o);

    /**
     * 获取应用的包名
     * @param o
     * @return
     */
    public static native String getPackname(Object o);

    /**
     * 加载so库或jni库
     */
    static {
        System.loadLibrary("JNI_ANDROID");
    }
}

注意我们 的加载c方法都加了native关键字,然后要使用jni下的c/c++文件就必须使用System.loadLibrary()。

步骤2:使用javah命令生成.h(头文件)

javah -jni com.losileeya.getapkinfo.JNIUtils

执行完之后你可以在module下文件夹app\build\intermediates\classes\debug下看见生成的 .h头文件为:

com_losileeya_getapkinfo_JNIUtils.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_losileeya_getapkinfo_JNIUtils */

#ifndef _Included_com_losileeya_getapkinfo_JNIUtils
#define _Included_com_losileeya_getapkinfo_JNIUtils
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_losileeya_getapkinfo_JNIUtils_getPackname(JNIEnv *, jobject, jobject);
JNIEXPORT jstring JNICALL Java_com_losileeya_getapkinfo_JNIUtils_getSignature(JNIEnv *, jobject, jobject);
#ifdef __cplusplus
}
#endif
#endif

在工程的main目录下新建一个名字为jni的目录,然后将刚才的.h文件剪切过来,当然文件名字是可以修改的

步骤3:根据.h文件生成相应的c/cpp文件

//
// Created by Administrator on 2016/7/16.
//
#include <stdio.h>
#include <jni.h>
#include <stdlib.h>
#include "appinfo.h"
JNIEXPORT jstring JNICALL Java_com_losileeya_getapkinfo_JNIUtils_getPackname(JNIEnv *env, jobject clazz, jobject obj)
{
jclass native_class = env->GetObjectClass(obj);
jmethodID mId = env->GetMethodID(native_class, "getPackageName", "()Ljava/lang/String;");
jstring packName = static_cast<jstring>(env->CallObjectMethod(obj, mId));
return packName;
}

JNIEXPORT jstring JNICALL Java_com_losileeya_getapkinfo_JNIUtils_getSignature(JNIEnv *env, jobject clazz, jobject obj)
{
jclass native_class = env->GetObjectClass(obj);
jmethodID pm_id = env->GetMethodID(native_class, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jobject pm_obj = env->CallObjectMethod(obj, pm_id);
jclass pm_clazz = env->GetObjectClass(pm_obj);
// 得到 getPackageInfo 方法的 ID
jmethodID package_info_id = env->GetMethodID(pm_clazz, "getPackageInfo","(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jstring pkg_str = Java_com_losileeya_getapkinfo_JNIUtils_getPackname(env, clazz, obj);
// 获得应用包的信息
jobject pi_obj = env->CallObjectMethod(pm_obj, package_info_id, pkg_str, 64);
// 获得 PackageInfo 类
jclass pi_clazz = env->GetObjectClass(pi_obj);
// 获得签名数组属性的 ID
jfieldID signatures_fieldId = env->GetFieldID(pi_clazz, "signatures", "[Landroid/content/pm/Signature;");
jobject signatures_obj = env->GetObjectField(pi_obj, signatures_fieldId);
jobjectArray signaturesArray = (jobjectArray)signatures_obj;
jsize size = env->GetArrayLength(signaturesArray);
jobject signature_obj = env->GetObjectArrayElement(signaturesArray, 0);
jclass signature_clazz = env->GetObjectClass(signature_obj);
jmethodID string_id = env->GetMethodID(signature_clazz, "toCharsString", "()Ljava/lang/String;");
jstring str = static_cast<jstring>(env->CallObjectMethod(signature_obj, string_id));
char *c_msg = (char*)env->GetStringUTFChars(str,0);

return str;
}

注意:要使用前得先声明,方法名直接从h文件考过来就好了,studio目前还是很操蛋的,对于jni的支持还是不很好。

步骤4:给项目添加Android.mk和Application.mk

此步骤显然也是不必要的,如果你需要生成so库添加一下也好,为什么不呢考过去改一下就好了,如果你不写这2文件也是没有问题的,因为debug下也是有这些so库的。
好吧,勉强看一下这2货:

Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := JNI_ANDROID
LOCAL_SRC_FILES =: appinfo.cpp
include $(BUILD_SHARED_LIBRARY)

Application.mk

APP_MODULES := JNI_ANDROID
APP_ABI := all

android studio下External Tools的高级配置NDK一键javah,ndk生成so库

eclipse开发ndk的时候你可能就配置过javah,所以android studio也可以配置,是不是很兴奋:
Settings—>Tools—->External Tools就可以配置我们的终端命令了,别急一个一个来:

  • javah -jni 命令的配置(一键生成h文件)

我们先来看参数的配置:

1.Program:JDKPath\bin\javah.exe 这里配置的是javah.exe的路径(基本一致)

2.Parametes: -classpath . -jni -d ModuleFileDir/src/main/jni FileClass这里指的是定位在Module的jni文件你指定的文件执行jni指令

3.Working:ModuleFileDir\src\main\java

  • ndk-build(一键生成so库)

我们同样来看参数的配置:

1.Program:E:\Android\sdk\android-ndk-r11b-windows-x86_64\android-ndk-r11b\ndk-build.cmd 这里配置的是ndk下的ndk-build.cmd的路径(自己去找下)

2.Working:ModuleFileDir\src\main\

  • javap-s(此命令用于c掉java方法时方法的签名)

我们同样来看参数的配置:

1.Program:JDKPath\bin\javap.exe 这里配置的是javap.exe的路径(基本一致)

2.Parametes: -classpath ModuleFileDir/build/intermediates/classes/debug -s FileClass 这里指的是定位到build的debug目录下执行 javap -s class文件

3.Working:ModuleFileDir

这里介绍最常用的3个命令,对你的帮助应该还是很大的来看一下怎么使用:

  • javah -jni的使用:选中native文件—>右键—>External Tools—>javah -jni
    效果如下:


是不是自动生成了包名.类名的.h文件。

  • ndk-build的使用:选中jni文件—>右键—>External Tools—>ndk-build
    效果如下:


是不是一键生成了7种so库,你还想去debug目录下面去找吗

  • javap-s的使用:选中native文件—>右键—>External Tools—>javap-s
    效果如下:

看见了每个方法下的descriptor属性的值就是你所要的方法签名。

3种一键生成的命令讲完了,以后你用到了什么命令都可以这样设置,是不是很给力。

新实验版Gradle插件与AS下NDK开发

近期的 AS 与 Gradle 版本的快速更新对 NDK 开发又有了更加牛叉的体验,因为它完全支持使用 GDB 和 LLDB (不清楚这两是啥的请自行脑部Unix编程基础)来 GUI 化 debug 我们得 native 代码了(以前真的好蛋疼,命令行巴拉巴拉的,泪奔啊!)。
总之现在的 AS 和 Gradle 已经趋于实验完善 NDK 开发了,主要表现在如下方面:

  • AS 完全支持 GUI 模式的 GDB/LLDB 调试 native 代码(LLDB调试引擎需要gradle-experimental plugin的支持)。
  • AS 可以很好的直接编写 native 代码。
  • Java 层代码声明好以后 AS 可以自动帮我们生成 JNI 接口规范代码。
    *推出了几乎针对 NDK 的实验版 Gradle 插件。
    可以看见,现在 NDK 开发已经渐渐的变得越来越方便了,牛叉的一逼!
    因为是实验版本,所以我就试了下,不作为今后开发的主要方向,但是还是需要了解下。区别如下:1.情况1
//Project的build.gradle文件
buildscript {
    repositories {
       jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle-experimental:0.7.0-alpha1'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

2.情况2

//Module的build.gradle文件
apply plugin: 'com.android.model.application'

model {
    android {
        compileSdkVersion = 23
        buildToolsVersion = "23.0.2"

        defaultConfig.with {
            applicationId = "com.losileeya.getapkinfo"
            minSdkVersion.apiLevel = 8
            targetSdkVersion.apiLevel = 23
        }
    }

    /*
     * native build settings
     */
    android.ndk {
        moduleName = "JNI_ANDROID"
        /*
         * Other ndk flags configurable here are
         * cppFlags.add("-fno-rtti")
         * cppFlags.add("-fno-exceptions")
         * ldLibs.addAll(["android", "log"])
         * stl       = "system"
         */
    }

    android.productFlavors {
        // for detailed abiFilter descriptions, refer to "Supported ABIs" @
        // https://developer.android.com/ndk/guides/abis.html#sa
        create("arm") {
            ndk.abiFilters.add("armeabi")
        }
        create("arm7") {
            ndk.abiFilters.add("armeabi-v7a")
        }
        create("arm8") {
            ndk.abiFilters.add("arm64-v8a")
        }
        create("x86") {
            ndk.abiFilters.add("x86")
        }
        create("x86-64") {
            ndk.abiFilters.add("x86_64")
        }
        create("mips") {
            ndk.abiFilters.add("mips")
        }
        create("mips-64") {
            ndk.abiFilters.add("mips64")
        }
        // To include all cpu architectures, leaves abiFilters empty
        create("all")
    }

 

可以明显感觉到 Project 和 Module 的 build.gradle 文件编写闭包都有了变化。
入门就讲完了,你也可以删掉jni目录,把so库放入jniLibs下,效果还是一模一样的,很晚了,睡觉。

总结

初步使用ndk的技巧已经说完了,后续还会介绍jni中c调用java以及java调用c和相关一系列ndk开发中所要注意的。

注意事项

1.jni调用前记得申明,比如:#include stdio.h,#include jni.h,#include stdlib.h,方法被调用者写前面或者头文件里面

2.c中env调方法时(*env)->但是cpp中就得这样env->,原因是cpp中是一级指针,所以指针特别注意

demo 传送门:GetApkInfo.rar

 

转自:http://blog.csdn.net/u013278099/article/details/51927631

Android内存泄漏的八种可能

Java是垃圾回收语言的一种,其优点是开发者无需特意管理内存分配,降低了应用由于局部故障(segmentation fault)导致崩溃,同时防止未释放的内存把堆栈(heap)挤爆的可能,所以写出来的代码更为安全。

Java是垃圾回收语言的一种,其优点是开发者无需特意管理内存分配,降低了应用由于局部故障(segmentation fault)导致崩溃,同时防止未释放的内存把堆栈(heap)挤爆的可能,所以写出来的代码更为安全。

不幸的是,在Java中仍存在很多容易导致内存泄漏的逻辑可能(logical leak)。如果不小心,你的Android应用很容易浪费掉未释放的内存,最终导致内存用光的错误抛出(out-of-memory,OOM)。

一般内存泄漏(traditional memory leak)的原因是:当该对象的所有引用都已经释放了,对象仍未被释放。(译者注:Cursor忘记关闭等)

逻辑内存泄漏(logical memory leak)的原因是:当应用不再需要这个对象,当仍未释放该对象的所有引用。

如果持有对象的强引用,垃圾回收器是无法在内存中回收这个对象。

在Android开发中,最容易引发的内存泄漏问题的是Context。比如Activity的Context,就包含大量的内存引用,例如View Hierarchies和其他资源。一旦泄漏了Context,也意味泄漏它指向的所有对象。Android机器内存有限,太多的内存泄漏容易导致OOM。

检测逻辑内存泄漏需要主观判断,特别是对象的生命周期并不清晰。幸运的是,Activity有着明确的生命周期,很容易发现泄漏的原因。Activity.onDestroy()被视为Activity生命的结束,程序上来看,它应该被销毁了,或者Android系统需要回收这些内存(译者注:当内存不够时,Android会回收看不见的Activity)。

如果这个方法执行完,在堆栈中仍存在持有该Activity的强引用,垃圾回收器就无法把它标记成已回收的内存,而我们本来目的就是要回收它!

结果就是Activity存活在它的生命周期之外。

Activity是重量级对象,应该让Android系统来处理它。然而,逻辑内存泄漏总是在不经意间发生。(译者注:曾经试过一个Activity导致20M内存泄漏)。在Android中,导致潜在内存泄漏的陷阱不外乎两种:

全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用的怪物。

活在Activity生命周期之外的线程。没有清空对Activity的强引用。

检查一下你有没有遇到下列的情况。

Static Activities

在类中定义了静态Activity变量,把当前运行的Activity实例赋值于这个静态变量。

如果这个静态变量在Activity生命周期结束后没有清空,就导致内存泄漏。因为static变量是贯穿这个应用的生命周期的,所以被泄漏的Activity就会一直存在于应用的进程中,不会被垃圾回收器回收。

  1. static Activity activity;
  2.     void setStaticActivity() {
  3.       activity = this;
  4.     }
  5.     View saButton = findViewById(R.id.sa_button);
  6.     saButton.setOnClickListener(new View.OnClickListener() {
  7.       @Override public void onClick(View v) {
  8.         setStaticActivity();
  9.         nextActivity();
  10.       }
  11.     });

Memory Leak 1 – Static Activity

Static Views

类似的情况会发生在单例模式中,如果Activity经常被用到,那么在内存中保存一个实例是很实用的。正如之前所述,强制延长Activity的生命周期是相当危险而且不必要的,无论如何都不能这样做。

特殊情况:如果一个View初始化耗费大量资源,而且在一个Activity生命周期内保持不变,那可以把它变成static,加载到视图树上(View Hierachy),像这样,当Activity被销毁时,应当释放资源。(译者注:示例代码中并没有释放内存,把这个static view置null即可,但是还是不建议用这个static view的方法)

  1. static view;
  2.     void setStaticView() {
  3.       view = findViewById(R.id.sv_button);
  4.     }
  5.     View svButton = findViewById(R.id.sv_button);
  6.     svButton.setOnClickListener(new View.OnClickListener() {
  7.       @Override public void onClick(View v) {
  8.         setStaticView();
  9.         nextActivity();
  10.       }
  11.     });

Memory Leak 2 – Static View

Inner Classes

继续,假设Activity中有个内部类,这样做可以提高可读性和封装性。将如我们创建一个内部类,而且持有一个静态变量的引用,恭喜,内存泄漏就离你不远了(译者注:销毁的时候置空,嗯)。

  1. private static Object inner;
  2.        void createInnerClass() {
  3.         class InnerClass {
  4.         }
  5.         inner = new InnerClass();
  6.     }
  7.     View icButton = findViewById(R.id.ic_button);
  8.     icButton.setOnClickListener(new View.OnClickListener() {
  9.         @Override public void onClick(View v) {
  10.             createInnerClass();
  11.             nextActivity();
  12.         }
  13.     });

Memory Leak 3 – Inner Class

内部类的优势之一就是可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。

Anonymous Classes

相似地,匿名类也维护了外部类的引用。所以内存泄漏很容易发生,当你在Activity中定义了匿名的AsyncTsk

。当异步任务在后台执行耗时任务期间,Activity不幸被销毁了(译者注:用户退出,系统回收),这个被AsyncTask持有的Activity实例就不会被垃圾回收器回收,直到异步任务结束。

  1. void startAsyncTask() {
  2.         new AsyncTask<Void, Void, Void>() {
  3.             @Override protected Void doInBackground(Void… params) {
  4.                 while(true);
  5.             }
  6.         }.execute();
  7.     }
  8.     super.onCreate(savedInstanceState);
  9.     setContentView(R.layout.activity_main);
  10.     View aicButton = findViewById(R.id.at_button);
  11.     aicButton.setOnClickListener(new View.OnClickListener() {
  12.         @Override public void onClick(View v) {
  13.             startAsyncTask();
  14.             nextActivity();
  15.         }
  16.     });

Memory Leak 4 – AsyncTask

Handler

同样道理,定义匿名的Runnable,用匿名类Handler执行。Runnable内部类会持有外部类的隐式引用,被传递到Handler的消息队列MessageQueue中,在Message消息没有被处理之前,Activity实例不会被销毁了,于是导致内存泄漏。

  1. void createHandler() {
  2.         new Handler() {
  3.             @Override public void handleMessage(Message message) {
  4.                 super.handleMessage(message);
  5.             }
  6.         }.postDelayed(new Runnable() {
  7.             @Override public void run() {
  8.                 while(true);
  9.             }
  10.         }, Long.MAX_VALUE >> 1);
  11.     }
  12.     View hButton = findViewById(R.id.h_button);
  13.     hButton.setOnClickListener(new View.OnClickListener() {
  14.         @Override public void onClick(View v) {
  15.             createHandler();
  16.             nextActivity();
  17.         }
  18.     });

Memory Leak 5 – Handler

Threads

我们再次通过Thread和TimerTask来展现内存泄漏。

  1. void spawnThread() {
  2.         new Thread() {
  3.             @Override public void run() {
  4.                 while(true);
  5.             }
  6.         }.start();
  7.     }
  8.     View tButton = findViewById(R.id.t_button);
  9.     tButton.setOnClickListener(new View.OnClickListener() {
  10.       @Override public void onClick(View v) {
  11.           spawnThread();
  12.           nextActivity();
  13.       }
  14.     });

Memory Leak 6 – Thread

TimerTask

只要是匿名类的实例,不管是不是在工作线程,都会持有Activity的引用,导致内存泄漏。

  1. oid scheduleTimer() {
  2.         new Timer().schedule(new TimerTask() {
  3.             @Override
  4.             public void run() {
  5.                 while(true);
  6.             }
  7.         }, Long.MAX_VALUE >> 1);
  8.     }
  9.     View ttButton = findViewById(R.id.tt_button);
  10.     ttButton.setOnClickListener(new View.OnClickListener() {
  11.         @Override public void onClick(View v) {
  12.             scheduleTimer();
  13.             nextActivity();
  14.         }
  15.     });

Memory Leak 7 – TimerTask

Sensor Manager

最后,通过Context.getSystemService(int name)可以获取系统服务。这些服务工作在各自的进程中,帮助应用处理后台任务,处理硬件交互。如果需要使用这些服务,可以注册监听器,这会导致服务持有了Context的引用,如果在Activity销毁的时候没有注销这些监听器,会导致内存泄漏。

  1. void registerListener() {
  2.                SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
  3.                Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
  4.                sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
  5.         }
  6.         View smButton = findViewById(R.id.sm_button);
  7.         smButton.setOnClickListener(new View.OnClickListener() {
  8.             @Override public void onClick(View v) {
  9.                 registerListener();
  10.                 nextActivity();
  11.             }
  12.         });

Memory Leak 8 – Sensor Manager

总结

看过那么多会导致内存泄漏的例子,容易导致吃光手机的内存使垃圾回收处理更为频发,甚至最坏的情况会导致OOM。垃圾回收的操作是很昂贵的开销,会导致肉眼可见的卡顿。所以,实例化的时候注意持有的引用链,并经常进行内存泄漏检查。

原 文: Eight Ways Your Android App Can Leak Memory

一个神奇的控件——Android CoordinatorLayout与Behavior使用指南

CoordinatorLayout是support.design包中的控件,它可以说是Design库中最重要的控件。

本文通过模仿知乎介绍了自定义Behavior,通过模仿百度地图介绍了BottomSheetBehavior的使用。

1.CoordinatorLayout介绍

官方对CoordinatorLayout的描述是这样的:

CoordinatorLayout is a super-powered FrameLayout.

CoordinatorLayout is intended for two primary use cases:

  • As a top-level application decor or chrome layout
  • As a container for a specific interaction with one or more child views

官方对Behavior的描述是这样的:

Interaction behavior plugin for child views of CoordinatorLayout.

简单来说,CoordinatorLayout是用来协调其子view们之间动作的一个父view,而Behavior就是用来给CoordinatorLayout的子view们实现交互的。

CoordinatorLayout在我之前的文章中都有出镜:

没时间解释了,快使用Snackbar!——Android Snackbar花式使用指南中Snackbar显示时FloatingActionButton相应上移为其留出位置

Snackbar与FloatingActionButton的互动

看,这个工具栏能伸缩折叠——Android CollapsingToolbarLayout使用介绍中CollapsingToolbarLayout折叠或展开时,FloatingActionButton跟随运动并且大小相应变化:

AppBarLayout与FloatingActionButton的互动

看下FloatingActionButton的源码就能发现其中有一个Behavior方法继承自CoordinatorLayout.Behavior,并在其中实现了与Snackbar互动时的逻辑。

我本文使用的support:design版本是23.4.0

本文图片接口来自干货集中营http://gank.io/api

2.自定义Behavior模仿知乎

知乎的效果.gif

本文实现的效果.gif

先看下布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/behavior_demo_coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.AppBarLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_scrollFlags="scroll|enterAlways|snap"
        android:background="?attr/colorPrimary" />
</android.support.design.widget.AppBarLayout>

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/behavior_demo_swipe_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/behavior_demo_recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
</android.support.v4.widget.SwipeRefreshLayout>

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginRight="16dp"
    android:layout_marginBottom="72dp"
    android:src="@android:drawable/ic_dialog_email"
    app:layout_behavior="com.example.zcp.coordinatorlayoutdemo.behavior.MyFabBehavior"
    android:layout_gravity="bottom|right" />

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:layout_gravity="bottom"
    android:background="@color/colorPrimary"
    android:gravity="center"
    app:layout_behavior="com.example.zcp.coordinatorlayoutdemo.behavior.MyBottomBarBehavior">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:textColor="#ffffff"
        android:text="这是一个底栏"/>
</LinearLayout>

</android.support.design.widget.CoordinatorLayout>

SwipeRefreshLayout、FloatingActionButton和当做底栏的LinearLayout上有一个app:layout_behavior配置。

SwipeRefreshLayout配置的”@string/appbar_scrolling_view_behavior”是系统提供的,用来使滑动控件与AppBarLayout互动。

FloatingActionButton和底栏上配置的是我们接下来要自定义的Behavior。

先看FloatingActionButton的Behavior。

public class MyFabBehavior extends CoordinatorLayout.Behavior<View> {

private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();

private float viewY;//控件距离coordinatorLayout底部距离
private boolean isAnimate;//动画是否在进行

public MyFabBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

//在嵌套滑动开始前回调
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {

    if(child.getVisibility() == View.VISIBLE&&viewY==0){
        //获取控件距离父布局(coordinatorLayout)底部距离
        viewY=coordinatorLayout.getHeight()-child.getY();
    }

    return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//判断是否竖直滚动
}

//在嵌套滑动进行时,对象消费滚动距离前回调
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
    //dy大于0是向上滚动 小于0是向下滚动

    if (dy >=0&&!isAnimate&&child.getVisibility()==View.VISIBLE) {
        hide(child);
    } else if (dy <0&&!isAnimate&&child.getVisibility()==View.GONE) {
        show(child);
    }
}

//隐藏时的动画
private void hide(final View view) {
    ViewPropertyAnimator animator = view.animate().translationY(viewY).setInterpolator(INTERPOLATOR).setDuration(200);

    animator.setListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animator) {
            isAnimate=true;
        }

        @Override
        public void onAnimationEnd(Animator animator) {
            view.setVisibility(View.GONE);
            isAnimate=false;
        }

        @Override
        public void onAnimationCancel(Animator animator) {
            show(view);
        }

        @Override
        public void onAnimationRepeat(Animator animator) {
        }
    });
    animator.start();
}

//显示时的动画
private void show(final View view) {
    ViewPropertyAnimator animator = view.animate().translationY(0).setInterpolator(INTERPOLATOR).setDuration(200);
    animator.setListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animator) {
            view.setVisibility(View.VISIBLE);
            isAnimate=true;
        }

        @Override
        public void onAnimationEnd(Animator animator) {
            isAnimate=false;
        }

        @Override
        public void onAnimationCancel(Animator animator) {
            hide(view);
        }

        @Override
        public void onAnimationRepeat(Animator animator) {
        }
    });
    animator.start();
}
}

逻辑并不复杂,我们通过重写Behavior中关于嵌套滑动的两个回调完成了FloatingActionButton的隐藏和显示判断及操作。

单独出场的底栏也可以利用上面一样的方法来设置隐藏或显示,我的底栏是和AppBarLayout一起出场,所以我就让底栏从属于AppBarLayout活动。代码如下:

public class MyBottomBarBehavior extends CoordinatorLayout.Behavior<View> {

public MyBottomBarBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
}

//确定所提供的子视图是否有另一个特定的同级视图作为布局从属。
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
//这个方法是说明这个子控件是依赖AppBarLayout的
    return dependency instanceof AppBarLayout;
}

//用于响应从属布局的变化
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

    float translationY = Math.abs(dependency.getTop());//获取更随布局的顶部位置

    child.setTranslationY(translationY);
    return true;
}

}

代码量比上个还少。我们还可以通过重写onMeasureChild()来使控件响应从属控件的大小变化。

Behavior提供的很多,我这里用到的只是一部分,大家可以看看源码,根据具体需求去使用。

3.BottomSheetBehavior使用(仿百度地图)

BottomSheetBehavior是23.2版本后提供的控件,用于实现从底部滑出一个折叠布局的操作,就是谷歌地图中的效果。

别问为什么我不模仿谷歌地图,当然是因为中国特色国情→_→(雾)。

百度地图BottomSheet.gif

本文BottomSheet.gif

来看下布局

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bottom_sheet_demo_coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v4.widget.SwipeRefreshLayout
    android:id="@+id/bottom_sheet_demo_swipe_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/bottom_sheet_demo_recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
</android.support.v4.widget.SwipeRefreshLayout>

<RelativeLayout
    android:id="@+id/design_bottom_sheet_bar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/colorAccent"
    app:layout_anchor="@+id/design_bottom_sheet"
    app:layout_anchorGravity="top"
    android:layout_gravity="bottom"
    android:visibility="gone"
    >
    <ImageView
        android:layout_width="23dp"
        android:layout_height="23dp"
        android:layout_marginLeft="23dp"
        android:src="@mipmap/ic_arrow_back_white"
        android:layout_centerVertical="true"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点击收起BottomSheet"
        android:textColor="#ffffff"
        android:layout_centerInParent="true"/>
</RelativeLayout>

<RelativeLayout
    android:id="@+id/design_bottom_sheet"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minHeight="100dp"
    app:behavior_peekHeight="56dp"
    app:behavior_hideable="false"
    app:layout_behavior="@string/bottom_sheet_behavior"
    android:background="#ffffff"
    >
    <TextView
        android:id="@+id/bottom_sheet_tv"
        android:layout_width="wrap_content"
        android:layout_height="56dp"
        android:layout_centerHorizontal="true"
        android:gravity="center"
        android:text="这是一个BottomSheet"/>
    <ImageView
        android:id="@+id/bottom_sheet_iv"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:padding="10dp"
        android:minHeight="100dp"
        android:adjustViewBounds="true"
        android:scaleType="centerInside"
        android:layout_gravity="center"/>
</RelativeLayout>

</android.support.design.widget.CoordinatorLayout>

名为design_bottom_sheet的RelativeLayout是一个BottomSheet(这个到底叫什么,底纸?底板?底部折叠纸片? (⊙_⊙?)),因为它设置了

app:layout_behavior="@string/bottom_sheet_behavior"

app:behavior_hideable=”false”说明这个BottomSheet不可以被手动滑动隐藏,设置为true则可以滑到屏幕最底部隐藏。

app:behavior_peekHeight设置的是折叠状态时的高度。

名为design_bottom_sheet_bar的RelativeLayout是用来实现BottomSheet完全展开时的自定义顶部工具条,

 app:layout_anchor="@+id/design_bottom_sheet"
 app:layout_anchorGravity="top"
 android:layout_gravity="bottom"

上面这段设置是使design_bottom_sheet_bar的位置与design_bottom_sheet锚定,然后调整了下位置。

design_bottom_sheet_bar=(RelativeLayout) findViewById(R.id.design_bottom_sheet_bar);

design_bottom_sheet=(RelativeLayout) findViewById(R.id.design_bottom_sheet);
bottom_sheet_iv=(ImageView) findViewById(R.id.bottom_sheet_iv);
bottom_sheet_tv=(TextView) findViewById(R.id.bottom_sheet_tv);

BottomSheetBehavior behavior = BottomSheetBehavior.from(design_bottom_sheet);

这段是BottomSheet相关控件初始化,我省略了RecyclerView相关配置。

design_bottom_sheet设置的高度是充满父布局,我们需要其给design_bottom_sheet_bar留出位置,所以要修改一下design_bottom_sheet的高度,我把这部分操作放在了onWindowFocusChanged()里。

    public void onWindowFocusChanged(boolean hasFocus) {
    // TODO Auto-generated method stub
    super.onWindowFocusChanged(hasFocus);

    //修改SetBottomSheet的高度 留出顶部工具栏的位置
    if(!isSetBottomSheetHeight){
        CoordinatorLayout.LayoutParams linearParams =(CoordinatorLayout.LayoutParams) design_bottom_sheet.getLayoutParams();
        linearParams.height=coordinatorLayout.getHeight()-design_bottom_sheet_bar.getHeight();
        design_bottom_sheet.setLayoutParams(linearParams);
        isSetBottomSheetHeight=true;//设置标记 只执行一次
    }

}

然后我们设置design_bottom_sheet_bar点击后design_bottom_sheet变为折叠状态

design_bottom_sheet_bar.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
        }
    });

BottomSheetBehavior设置为折叠状态的话,BottomSheet显示出来的就是peekHeight设置的高度,展开状态显示的是完整的布局,隐藏状态就是滑到屏幕最底部隐藏。

最后为BottomSheetBehavior设置监听

behavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {

        //BottomSheet状态改变时的回调
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {

            if(newState!=BottomSheetBehavior.STATE_COLLAPSED&&bottom_sheet_tv.getVisibility()==View.VISIBLE){
                bottom_sheet_tv.setVisibility(View.GONE);
                bottom_sheet_iv.setVisibility(View.VISIBLE);                
            }else if(newState==BottomSheetBehavior.STATE_COLLAPSED&&bottom_sheet_tv.getVisibility()==View.GONE){
                bottom_sheet_tv.setVisibility(View.VISIBLE);
                bottom_sheet_iv.setVisibility(View.GONE);              
            }
        }

        //BottomSheet滑动时的回调
        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {

            if(bottomSheet.getTop()<2*design_bottom_sheet_bar.getHeight()){
                design_bottom_sheet_bar.setVisibility(View.VISIBLE);
                design_bottom_sheet_bar.setAlpha(slideOffset);
                design_bottom_sheet_bar.setTranslationY(bottomSheet.getTop()-2*design_bottom_sheet_bar.getHeight());
            }
            else{
                design_bottom_sheet_bar.setVisibility(View.INVISIBLE);
            }
        }
    });

源码 CoordinatorLayoutDemo

说实话,知乎中控件跟随用户滑动操作的隐藏和显示这种设计挺普遍的,但是有的时候感觉这个动画挺烦的,尤其是控件色彩明亮的时候,不自觉的就会吸引用户注意力。建议大家在设计交互的时候注意。

就是这些[]~( ̄▽ ̄)~*
原文链接:http://www.jianshu.com/p/488283f74e69

解决WordPress重定向次数过多的问题

出现的302重定向的解决方法:

解决方法一:

修改wp-include下的/template-loader.php文件,修改起来并不复杂,缺点是内核文件改动,以后升级务必注意。官方没有给出解决办法,于是只好采用这个办法。

找到wp-include文件夹下的template-loader.php文件,使用//注释掉第六行和第七行。

if ( defined(‘WP_USE_THEMES’) && WP_USE_THEMES )

do_action(‘template_redirect’);

修改为:

//if ( defined(‘WP_USE_THEMES’) && WP_USE_THEMES )

//do_action(‘template_redirect’);

解决方法二:

通过给do_redirect赋值解决wordpress循环重定向

找到wp-include文件夹下的canonical.php文件,使用//注释掉第37行。另起一行加入下面代码。其实也就是把变量$do_redirect的值修改为false。

将canonical.php文件,第37行

function redirect_canonical( $requested_url = null, $do_redirect = true ) {

修改为:

//function redirect_canonical( $requested_url = null, $do_redirect = true ) {

增加:

function redirect_canonical( $requested_url = null,$do_redirect = false) {

修改后截图:

解决方法三:

插件办法,下载Permalink Fix & Disable Canonical Redirects Pack安装上就ok。

Android Vector曲折的兼容之路

Android Vector曲折的兼容之路

两年前写书的时候,就在研究Android L提出的Vector,可研究下来发现,完全不具备兼容性,相信这也是它没有被广泛使用的一个原因,经过Google的不懈努力,现在Vector终于迎来了它的春天。

这里写图片描述

在文章后面,会给出本文的Demo和效果图,并开源在Github

Vector Drawable

Android 5.0发布的时候,Google提供了Vector的支持。Vector Drawable相对于普通的Drawable来说,有以下几个好处:

  • Vector图像可以自动进行适配,不需要通过分辨率来设置不同的图片
  • Vector图像可以大幅减少图像的体积,同样一张图,用Vector来实现,可能只有PNG的几十分之一
  • 使用简单,很多设计工具,都可以直接导出SVG图像,从而转换成Vector图像
  • 功能强大,不用写很多代码就可以实现非常复杂的动画
  • 成熟、稳定,前端已经非常广泛的进行使用了

Vector图像刚发布的时候,是只支持Android 5.0+的,对于Android pre-L的系统来说,并不能使用,所以,可以说那时候的Vector并没有什么卵用。不过自从AppCompat 23.2之后,Google对p-View的Android系统也进行了兼容,也就是说,Vector可以使用于Android 2.1以上的所有系统,只需要引用com.android.support:appcompat-v7:23.2.0以上的版本就可以了,这时候,Vector应该算是迎来了它的春天。

如何获得Vector图像

概念

首先,需要讲解两个概念——SVG和Vector。

SVG,即Scalable Vector Graphics 矢量图,这种图像格式在前端中已经使用的非常广泛了,详见WIKI:https://en.wikipedia.org/wiki/Scalable_Vector_Graphics

Vector,在Android中指的是Vector Drawable,也就是Android中的矢量图,详见:https://developer.android.com/reference/android/graphics/drawable/VectorDrawable.html

因此,可以说Vector就是Android中的SVG实现,因为Android中的Vector并不是支持全部的SVG语法,也没有必要,因为完整的SVG语法是非常复杂的,但已经支持的SVG语法已经够用了,特别是Path语法,几乎是Android中Vector的标配,详细可以参考:http://www.w3.org/TR/SVG/paths.html

Vector语法简介

Android以一种简化的方式对SVG进行了兼容,这种方式就是通过使用它的Path标签,通过Path标签,几乎可以实现SVG中的其它所有标签,虽然可能会复杂一点,但这些东西都是可以通过工具来完成的,所以,不用担心写起来会很复杂。

Path指令解析如下所示:

  1. 支持的指令:
    • M = moveto(M X,Y) :将画笔移动到指定的坐标位置
    • L = lineto(L X,Y) :画直线到指定的坐标位置
    • H = horizontal lineto(H X):画水平线到指定的X坐标位置
    • V = vertical lineto(V Y):画垂直线到指定的Y坐标位置
    • C = curveto(C X1,Y1,X2,Y2,ENDX,ENDY):三次贝赛曲线
    • S = smooth curveto(S X2,Y2,ENDX,ENDY)
    • Q = quadratic Belzier curve(Q X,Y,ENDX,ENDY):二次贝赛曲线
    • T = smooth quadratic Belzier curveto(T ENDX,ENDY):映射
    • A = elliptical Arc(A RX,RY,XROTATION,FLAG1,FLAG2,X,Y):弧线
    • Z = closepath():关闭路径
  2. 使用原则:
    • 坐标轴为以(0,0)为中心,X轴水平向右,Y轴水平向下
    • 所有指令大小写均可。大写绝对定位,参照全局坐标系;小写相对定位,参照父容器坐标系
    • 指令和数据间的空格可以省略
    • 同一指令出现多次可以只用一个

注意,’M’处理时,只是移动了画笔, 没有画任何东西。 它也可以在后面给出上同时绘制不连续线。

关于这些语法,开发者需要的并不是全部精通,而是能够看懂即可,其它的都可以交给工具来实现。

从PNG到SVG

  • 设计师

要从一般使用的PNG图像转换到SVG图像,对于设计师来说,并不是一件难事,因为大部分的设计工具(PS、Illustrator等等)都支持导出各种格式的图像,如PNG、JPG,当然,也包括SVG,因此,设计师可以完全按照原有的方式进行设计,只是最后导出的时候,选择SVG即可。

  • 程序员

不要求开发者都去学习使用这些设计工具,开发者可以利用一些工具,自己转换一些比较基础的图像,http://inloop.github.io/svg2android/ 就是这样一个非常牛逼的网站,可以在线将普通图像转换为Android Vector Drawable。如图所示:

这里写图片描述

或者,还可以使用SVG的编辑器来进行SVG图像的编写,例如http://editor.method.ac/

这里写图片描述

使用Android Studio

利用Android Studio的Vector Asset,可以非常方便的创建Vector图像,甚至可以直接通过本地的SVG图像来生成Vector图像,如图所示:

这里写图片描述

进去之后,就可以生成Vector图像,如图所示:

这里写图片描述

Google的兼容之路

只兼容L+

Vector是在Android L中提出来的新概念,所以在刚开始的时候是只兼容L+的。

Gradle Plugin 1.5的兼容

从Gradle Plugin 1.5开始,Google支持了一种兼容方式,即在Android L之上,使用Vector,而在L之下,则使用Gradle将Vector生成PNG图像。

Android gradle plugin 1.5发布以后,加入了一个跟VectorDrawable有关的新功能。Android build tools 提供了另外一种解决兼容性的方案,如果编译的版本是5.0之前的版本,那么build tools 会把VectorDrawable生成对应的png图片,这样在5.0以下的版本则使用的是生成的png图,而在5.0以上的版本中则使用VectorDrawable.在build.gradle添加generatedDensities配置,可以配置生成的png图片的密度。

AppCompat23.2的兼容

从AppCompat23.2开始,Google开始支持在低版本上使用Vector。

静态Vector图像

我们有很多方法能够得到这些Vector,那么如何使用它们呢,Android 5.0以上的使用就不讲了,不太具有普遍代表性,我们从pre-L版本的兼容开始做起。

pre-L版本兼容

VectorDrawableCompat依赖于AAPT的一些功能,它能保持最近矢量图使用的添加的属性ID,以便他们可以被pre-L版本之前的引用。

在Android 5.0之前使用Vector,需要aapt来对资源进行一些处理,这一过程可以在aapt的配置中进行设置,如果没有启用这样一个flag,那么在5.0以下的设备上运行就会发生android.content.res.Resources$NotFoundException。

首先,你需要在项目的build.gradle脚本中,增加对Vector兼容性的支持,代码如下所示:

使用Gradle Plugin 2.0以上:

android {

    defaultConfig {
        vectorDrawables.useSupportLibrary = true
    }
}

使用Gradle Plugin 2.0以下,Gradle Plugin 1.5以上:

android {
  defaultConfig {
    // Stops the Gradle plugin’s automatic rasterization of vectors
    generatedDensities = []
  }
  // Flag to tell aapt to keep the attribute ids around
  aaptOptions {
    additionalParameters "--no-version-vectors"
  }
}

像前面提到的,这种兼容方式实际上是先关闭AAPT对pre-L版本使用Vector的妥协,即在L版本以上,使用Vector,而在pre-L版本上,使用Gradle生成相应的PNG图片,generatedDensities这个数组,实际上就是要生成PNG的图片分辨率的数组,使用appcompat后就不需要这样了。

当然,最重要的还是添加appcompat的支持:

compile 'com.android.support:appcompat-v7:23.4.0'

同时,确保你使用的是AppCompatActivity而不是普通的Activity。

Vector图像

一个基本的Vector图像,实际上也是一个xml文件,如下所示:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="200dp"
        android:height="200dp"
        android:viewportHeight="500"
        android:viewportWidth="500">

    <path
        android:name="square"
        android:fillColor="#000000"
        android:pathData="M100,100 L400,100 L400,400 L100,400 z"/>

</vector>

显示如图所示:

这里写图片描述

这里需要解释下这里的几个标签:

  • android:width \ android:height:定义图片的宽高
  • android:viewportHeight \ android:viewportWidth:定义图像被划分的比例大小,例如例子中的500,即把200dp大小的图像划分成500份,后面Path标签中的坐标,就全部使用的是这里划分后的坐标系统。

这样做有一个非常好的作用,就是将图像大小与图像分离,后面可以随意修改图像大小,而不需要修改PathData中的坐标。

  • android:fillColor:PathData中的这些属性就不详细讲了,与Canvas绘图的属性基本类似。

在控件中使用

有了静态的Vector图像,就可以在控件中使用了。

可以发现,这里我们使用的都是普通的ImageView,好像并不是AppcomatImageView,这是因为使用了Appcomat后,系统会自动把ImageView转换为AppcomatImageView。

ImageView\ImageButton

对于ImageView这样的控件,要兼容Vector图像,只需要将之前的android:src属性,换成app:srcCompat即可,示例代码如下所示:

<ImageView
    android:id="@+id/iv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:srcCompat="@drawable/vector_image"/>

在代码中设置的话,代码如下所示:

ImageView iv = (ImageView) findViewById(R.id.iv);
iv.setImageResource(R.drawable.vector_image);

setBackgroundResource也是可以设置Vector的API

Button

Button并不能直接使用app:srcCompat来使用Vector图像,需要通过Selector来进行使用,首先,创建两个图像,用于Selector的两个状态,代码如下所示:

selector1.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportHeight="24.0"
        android:viewportWidth="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M14.59,8L12,10.59 9.41,8 8,9.41 10.59,12 8,14.59 9.41,16 12,13.41 14.59,16 16,14.59 13.41,12 16,9.41 14.59,8zM12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>

 

selector2.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportHeight="24.0"
        android:viewportWidth="24.0">
    <path
        android:fillColor="#FF000000"
        android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

 

selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/selector1" android:state_pressed="true"/>
    <item android:drawable="@drawable/selector2"/>
</selector>

非常简单,只是把普通的Selector中的图像换成了Vector图像而已,接下来,在Button中使用这个Selector即可:

<Button
    android:id="@+id/btn"
    android:layout_width="70dp"
    android:layout_height="70dp"
    android:background="@drawable/selector"/>

然后运行,如果你认为可以运行,那就是太天真了,都说了是兼容,怎么能没有坑呢,这里就是一个坑……

这个坑实际上是有历史渊源的,Google的一位开发者在博客中写到:

First up, this functionality was originally released in 23.2.0, but then we found some memory usage and Configuration updating issues so we it removed in 23.3.0. In 23.4.0 (technically a fix release) we’ve re-added the same functionality but behind a flag which you need to manually enable.

实际上,他们的这个改动,就影响了类似DrawableContainers(DrawableContainers which reference other drawables resources which contain only a vector resource)这样的类,它的一个典型,就是Selector(StateListDrawable也是)。这个开发者在文中提到的flag,就是下面的这段代码,放在Activity的前面就可以了:

static {
    AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}

开启这个flag后,你就可以正常使用Selector这样的DrawableContainers了。同时,你还开启了类似android:drawableLeft这样的compound drawable的使用权限,以及RadioButton的使用权限,以及ImageView’s src属性。

RadioButton

RadioButton的Button同样可以定义,代码如下所示:

<RadioButton
    android:layout_width="50dp"
    android:layout_height="50dp"
    android:button="@drawable/selector"/>

动态Vector基础

动态Vector才是Android Vector Drawable的精髓所在

动态的Vector需要通过animated-vector标签来进行实现,它就像一个粘合剂,将控件与Vector图像粘合在了一起,一个基础的animated-vector代码如下所示:

<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/XXXXX1">

    <target
        android:name="left"
        android:animation="@animator/XXXXX2"/>

</animated-vector>

实际上这里面只有两个重点是需要关注的,XXXXX1和XXXXX2。一个具体的示例如下所示:

<animated-vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_arrow">

    <target
        android:name="left"
        android:animation="@animator/anim_left"/>

    <target
        android:name="right"
        android:animation="@animator/anim_right"/>

</animated-vector>

这里表示目标图像是drawable/ic_arrow,对left、right分别使用了anim_left、anim_right动画。这里的name属性,就是在静态Vector图像中group或者path标签的name属性。

animated-vector标签在现在的Android Studio中实际上是会报错的,但这个并不影响编译和运行,属于Android Studio的Bug。

目标图像

XXXXX1是目标Vector图像,也就是静态的Vector图像,例如:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="120dp"
        android:height="120dp"
        android:viewportHeight="24.0"
        android:viewportWidth="24.0">

    <group android:name="left">
        <path
            android:fillColor="#FF000000"
            android:pathData="M9.01,14L2,14v2h7.01v3L13,15l-3.99,-4v3"/>
    </group>

    <group android:name="right">
        <path
            android:fillColor="#FF000000"
            android:pathData="M14.99,13v-3L22,10L22,8h-7.01L14.99,5L11,9l3.99,4"/>
    </group>

</vector>

可以发现,这里的Vector图像比之前我们看见的要多了一个group标签。group标签的作用有两个:

  • 对Path进行分组,由于我们后面需要针对Path进行动画,所以可以让具有同样动画效果的Path在同一个Group中
  • 拓展动画效果,单个的path标签是没有translateX和translateY属性的,因此无法使用属性动画来控制path translateY,而group标签是有的,所以我们需要先将相关的path标签元素包裹在一个个的group标签中.

动画效果

XXXXX2实际上就是模板要实现的动画,动画效果实际上就是基础的属性动画,例如:

anim_left.xml

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:interpolator="@android:interpolator/anticipate_overshoot"
    android:propertyName="translateX"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="0"
    android:valueTo="-10"
    android:valueType="floatType"/>

anim_right.xml

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:interpolator="@android:interpolator/anticipate_overshoot"
    android:propertyName="translateX"
    android:repeatCount="infinite"
    android:repeatMode="reverse"
    android:valueFrom="0"
    android:valueTo="10"
    android:valueType="floatType"/>

在代码中使用

ImageView imageView = (ImageView) findViewById(R.id.iv);
AnimatedVectorDrawableCompat animatedVectorDrawableCompat = AnimatedVectorDrawableCompat.create(
        this, R.drawable.square_anim
);
imageView.setImageDrawable(animatedVectorDrawableCompat);
((Animatable) imageView.getDrawable()).start();

动态Vector兼容性问题

向下兼容问题

一说到兼容,就不得不提到坑,几乎所有的为了兼容而做的改动,都会留下一些不可填满的坑,动态Vector动画也不例外,虽然Google已经对Vector图像进行了Android 2.1以上的兼容,但对于动态Vector动画,还是有很多限制的,例如:

  • Path Morphing,即路径变换动画,在Android pre-L版本下是无法使用的。
  • Path Interpolation,即路径插值器,在Android pre-L版本只能使用系统的插值器,不能自定义。
  • Path Animation,即路径动画,这个一般使用贝塞尔曲线来代替,所以没有太大影响。

向上兼容问题

除了在低版本上的兼容性问题,在L版本以上,也存在兼容性问题,即继承了AppCompatActivity的界面,如果直接设置ImageView的srcCompat,那么Path Morphing动画是无法生效的,因为默认的AppCompatActivity已经默认使用ImageViewCompat给转换了,但是AnimatedVectorDrawableCompat是不支持Path Morphing动画的,所以,在AppCompatActivity界面里面就无效了。

解决办法很简单,即使用代码来给ImageView添加动画:

ImageView imageView = (ImageView) view;
AnimatedVectorDrawable morphing = (AnimatedVectorDrawable) getDrawable(morphing);
imageView.setImageDrawable(morphing);
if (morphing != null) {
    morphing.start();
}

注意不要使用AnimatedVectorDrawableCompat即可。

抽取string兼容问题

开发者有时候为了代码简洁可能会把Vector图像中的pathData放到string.xml中,然后在Vector图像中引用string。

但这种方式如果通过生成png来兼容5.0以下机型的话,会报pathData错误,编译器不会去读取string.xml,只能把pathData写到Vector图像中,动画文件中也是一样,这也是为了兼容做出的牺牲吗,不得而知。

其它兼容问题

其它非常奇怪、诡异、不能理解的兼容性问题,只能通过版本文件夹的方式来进行兼容了,例如drawable-v21和drawable,分别创建两个文件名相同的资源在两个文件夹下,这样在21以上版本,会使用drawable-v21的资源,而其它会使用drawable下的资源。

动态Vector进阶

用好ObjectAnimator

所谓Vector动画进阶,实际上就是在利用ObjectAnimator的一些属性,特别是trimPathStart、trimPathEnd这两个针对Vector的属性(要注意pathData属性不兼容pre-L)。

这两个属性的官方文档如下所示:

android:trimPathStart
The fraction of the path to trim from the start, in the range from 0 to 1.
android:trimPathEnd
The fraction of the path to trim from the end, in the range from 0 to 1.
android:trimPathOffset
Shift trim region (allows showed region to include the start and end), in the range from 0 to 1.

其实很简单,就是一个图像的截取,设置一个比例即可,即当前绘制多少比例的图像,其余部分不绘制,Start和End分别就是从PathData的Start和End开始算,大家参考几个例子就能理解了。

理解Path Morph

Path Morph动画是Vector动画的一个高级使用,说到底,也就是两个PathData的转换,但是这种转换并不是随心所欲的,对于两个PathData,它们能进行Path Morph的前提是,它们具有相同个数的关键点,即两个路径的变换,只是关键点的坐标变化,掌握了这一个基本原理,实现Path Morph就非常容易了。

学习Vector

在Github上我开源了一个Vector的动画Demo库,地址如下所示:

https://github.com/xuyisheng/VectorDemo

这个Demo分为两部分,一部分是可以兼容Android pre-L版本和L+版本的Vector动画,另一部分(通过Actionbar的按钮切换)是只能兼容L+的Vector动画。

每个Vector动画,基本都包含四部分内容,即:

  • Vector:图像资源
  • Animated-vector:动画、图像粘合剂
  • ObjectAnimator:动画资源
  • 代码:启动动画

每个Vector动画通过这四个部分去进行分析,就非常清晰了。

这里展示下Demo的效果图:

这里写图片描述

参考

https://medium.com/@shemag8/animated-vector-drawable-e4d7743d372c#.3vkt12j20
https://github.com/jpuderer/AnimatedButton

NGiNX 转发到 Apache

序言

NGiNX 做前端反向代理处理静态请求,并发能力强,稳定,且占用内存少。 Apache 做后端 WEB 服务处理动态请求,高效,稳定,所以二者的搭配恰到好处。

  • CentOS 6.4 x86_64
  • nginx 1.4.1
  • Apache 2.2.15

配置 NGiNX 转发

# vi /etc/nginx/nginx.conf

添加以下内容;

# NGiNX 负载均衡
upstream backend {
    server 127.0.0.1:81 weight=1 fail_timeout=30s;
    server 127.0.0.1:82 weight=1 fail_timeout=30s;
    server 127.0.0.1:83 weight=1 fail_timeout=30s;
}

server {
    # 监听 80
    listen       80;
    server_name  test.aboutc.net;

    access_log  /var/log/nginx/aboutc-test.access.log  main;

    # 静态文件访问目录
    root   /var/www/html/test;
    index  index.html index.php;

    # 监听 ".php" 文件
    location ~ \.php$ {
        # 转发
        proxy_pass          http://backend;
        proxy_redirect      off;
        proxy_set_header    Host             $host;
        # 获取真实 IP
        proxy_set_header    X-Real-IP        $remote_addr;
        proxy_set_header    X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header    X-Client-Verify  SUCCESS;
        proxy_set_header    X-SSL-Subject    $ssl_client_s_dn;
        proxy_set_header    X-SSL-Issuer     $ssl_client_i_dn;
        proxy_read_timeout  65;
    }
}

首先 NGiNX 要监听 80 端口成为前端代理的身份, 其次要配置 upstream 通过 proxy_pass 转发到后端 Apache 的监听地址, 完成动态请求的处理。这里的 NGiNX 只需要转发 .php 文件即可,它自身可以 直接获取 js/css 等静态文件。

配置 Apache VirtualHost

# vi /etc/httpd/conf.d/vhost.conf

注释掉 NameVirtualHost:

#NameVirtualHost *:80

并添加以下内容

Listen 81
Listen 82
Listen 83

<VirtualHost *:81>
    DocumentRoot /var/www/html/test
    ServerName test.aboutc.net
    ErrorLog logs/test81-error_log
    CustomLog logs/test81-access_log common
</VirtualHost>
<VirtualHost *:82>
    DocumentRoot /var/www/html/test
    ServerName test.aboutc.net
    ErrorLog logs/test82-error_log
    CustomLog logs/test82-access_log common
</VirtualHost>
<VirtualHost *:83>
    DocumentRoot /var/www/html/test
    ServerName test.aboutc.net
    ErrorLog logs/test83-error_log
    CustomLog logs/test83-access_log common
</VirtualHost>

/etc/httpd/conf.d/vhost.conf 文件是 Apache 配置文件 /etc/httpd/conf/httpd.conf 通过 Include conf.d/*.conf 引入的。请根据实际环境配置相应文件。

重启 NGiNX 和 Apache 服务使之生效

重新加载 NGiNX 配置:

# service nginx reload

重启 Apache 服务:

# service httpd start
(13)Permission denied: make_sock: could not bind to address [::]:81
(13)Permission denied: make_sock: could not bind to address 0.0.0.0:81
no listening sockets available, shutting down
Unable to open logs
                                                           [FAILED]

此问题是由于开启 SELINUX 引起的,关闭 SELINUX:

# setenforce 0

此方法是临时关闭 SELINUX,如果想永久关闭 SELINUX,编辑 /etc/selinux/config, 将SELINUX=enforcing 改为 SELINUX=disabled,重启系统。

再次重启 Apache:

# service httpd restart
Stopping httpd:                                            [  OK  ]
Starting httpd:                                            [  OK  ]

测试是否转发成功

  1. 添加 test.aboutc.net 到 /etc/hosts,以便我们可以直接在本地访问测试:

    vi /etc/hosts

添加以下内容:

127.0.0.1    test.aboutc.net
  1. 编辑测试文件

    $ vi /var/www/html/test/index.php

添加以下内容:

<?php
phpinfo();
?>
  1. 打开浏览器,访问测试地址 http://test.aboutc.net/,如下图

NGiNX转发到Apache

转自:http://blog.aboutc.net/nginx/40/nginx-forwarded-to-apache

Android 线程的正确使用姿势

线程是程序员进阶的一道重要门槛。对于移动开发者来说,“将耗时的任务放到子线程去执行,以保证UI线程的流畅性”是线程编程的第一金科玉律,但这条铁则往往也是UI线程不怎么流畅的主因。我们在督促自己更多的使用线程的同时,还需要时刻提醒自己怎么避免线程失控。除了了解各类开线程的API之外,更需要理解线程本身到底是个什么样的存在,并行是否真的高效?系统是怎么样去调度线程的?开线程的方式那么多,什么样的姿势才正确?

多线程编程之所以复杂原因之一在于其并行的特性,人脑的工作方式更符合单线程串行的特点。一个接着一个的处理任务是大脑最舒服的状态,频繁的在任务之间切换会产生“头痛”这类系统异常。人脑的多任务和计算机的多任务性能差异太大导致我们在设计并行的业务逻辑之时,很容易犯错。

另一个复杂点在于线程所带来的副作用,这些副作用包括但不限于:多线程数据安全,死锁,内存消耗,对象的生命周期管理,UI的卡顿等。每一个新开的线程就像扔进湖面的石子,在你忽视的远处产生涟漪。

把抽象的东西具像化是我们认知世界的主要方式。线程作为操作系统世界的“公民”之一,是如何被调度获取到CPU和内存资源的,又怎么样去和其他“公民”互通有无进而实现效益最大化?把这些实体和行为具像到大脑,像操作系统一样开“上帝视角”,才能正确掌控线程这头强大的野兽。

进程优先级(Process Priority)

线程寄宿在进程当中,线程的生命周期直接被进程所影响,而进程的存活又和其优先级直接相关。在处理进程优先级的时候,大部分人靠直觉都能知道前台进程(Foreground Process)优先级要高于后台进程(Background Process)。但这种粗糙的划分无法满足操作系统高精度调度的需求。无论Android还是iOS,系统对于Foreground,Background进程有进一步的细化。

Foreground Process

Foreground一般意味着用户双眼可见,可见却不一定是active。在Android的世界里,一个Activity处于前台之时,如果能采集用户的input事件,就可以判定为active,如果中途弹出一个Dialog,Dialog变成新的active实体,直接面对用户的操作。被部分遮挡的activity尽管依然可见,但状态却变为inactive。不能正确的区分visible和active是很多初级程序员会犯的错误。

Background Process

后台进程同样有更细的划分。所谓的Background可以理解为不可见(invisible)。对于不可见的任务,Android也有重要性的区分。重要的后台任务定义为Service,如果一个进程包含Service(称为Service Process),那么在“重要性”上就会被系统区别对待,其优先级自然会高于不包含Service的进程(称为Background Process),最后还剩一类空进程(Empty Process)。Empty Process初看有些费解,一个Process如果什么都不做,还有什么存在的必要。其实Empty Process并不Empty,还存在不少的内存占用。

在iOS的世界里,Memory被分为Clean Memory和Dirty Memory,Clean Memory是App启动被加载到内存之后原始占用的那一部分内存,一般包括初始的stack, heap, text, data等segment,Dirty Memory是由于用户操作所改变的那部分内存,也就是App的状态值。系统在出现Low Memory Warning的时候会首先清掉Dirty Memory,对于用户来说,操作的进度就全部丢失了,即使再次点击App图标,也是一切从头开始。但由于Clean Memory没有被清除,避免了从磁盘重新读取app数据的io损耗,启动会变快。这也是为什么很多人会感觉手机重启后,app打开的速度都比较慢。

同理Android世界当中的Empty Process还保存有App相关的Clean Memory,这部分Memory对于提升App的启动速度大有帮助。显而易见Empty Process的优先级是最低的。

综上所述,我们可以把Android世界的Process按优先级分为如下几类:

111

 

进程的优先级从高到低依次分为五类,越往下,在内存紧张的时候越有可能被系统杀掉。简而言之,越是容易被用户感知到的进程,其优先级必定更高。

线程调度(Thread Scheduling)

Android系统基于精简过后的linux内核,其线程的调度受时间片轮转和优先级控制等诸多因素影响。不少初学者会认为某个线程分配到的time slice多少是按照其优先级与其它线程优先级对比所决定的,这并不完全正确。

Linux系统的调度器在分配time slice的时候,采用的CFS(completely fair scheduler)策略。这种策略不但会参考单个线程的优先级,还会追踪每个线程已经获取到的time slice数量,如果高优先级的线程已经执行了很长时间,但低优先级的线程一直在等待,后续系统会保证低优先级的线程也能获取更多的CPU时间。显然使用这种调度策略的话,优先级高的线程并不一定能在争取time slice上有绝对的优势,所以Android系统在线程调度上使用了cgroups的概念,cgroups能更好的凸显某些线程的重要性,使得优先级更高的线程明确的获取到更多的time slice。

Android将线程分为多个group,其中两类group尤其重要。一类是default group,UI线程属于这一类。另一类是background group,工作线程应该归属到这一类。background group当中所有的线程加起来总共也只能分配到5~10%的time slice,剩下的全部分配给default group,这样设计显然能保证UI线程绘制UI的流畅性。

222

 

有不少人吐槽Android系统之所以不如iOS流畅,是因为UI线程的优先级和普通工作线程一致导致的。这其实是个误会,Android的设计者实际上提供了background group的概念来降低工作线程的CPU资源消耗,只不过与iOS不同的是,Android开发者需要显式的将工作线程归于background group。

所以在我们决定新启一个线程执行任务的时候,首先要问自己这个任务在完成时间上是否重要到要和UI线程争夺CPU资源。如果不是,降低线程优先级将其归于background group,如果是,则需要进一步的profile看这个线程是否造成UI线程的卡顿。

虽说Android系统在任务调度上是以线程为基础单位,设置单个thread的优先级也可以改变其所属的control groups,从而影响CPU time slice的分配。但进程的属性变化也会影响到线程的调度,当一个App进入后台的时候,该App所属的整个进程都将进入background group,以确保处于foreground,用户可见的新进程能获取到尽可能多的CPU资源。用adb可以查看不同进程的当前调度策略。

当你的App重新被用户切换到前台的时候,进程当中所属的线程又会回归的原来的group。在这些用户频繁切换的过程当中,thread的优先级并不会发生变化,但系统在time slice的分配上却在不停的调整。

是否真的需要新线程?

开线程并不是提升App性能,解决UI卡顿的万金油。每一个新启的线程会消耗至少64KB的内存,系统在不同的线程之间switch context也会带来额外的开销。如果随意开启新线程,随着业务的膨胀,很容易在App运行的某个时间点发现几十个线程同时在运行。后果是原本想解决UI流畅性,却反而导致了偶现的不可控的卡顿。

移动端App新启线程一般都是为了保证UI的流畅性,增加App用户操作的响应度。但是否需要将任务放入工作线程需要先了解任务的瓶颈在哪,是i/o,gpu还是cpu?UI出现卡顿并不一定是UI线程出现了费时的计算,有可能是其它原因,比如layout层级太深。

尽量重用已有的工作线程(使用线程池)可以避免出现大量同时活跃的线程,比如对HTTP请求设置最大并发数。或者将任务放入某个串行的队列(HandlerThread)按顺序执行,工作线程任务队列适合处理大量耗时较短的任务,避免出现单个任务阻塞整个队列的情况。

用什么姿势开线程?

new Thread()

这是Android系统里开线程最简单的方式,也只能应用于最简单的场景,简单的好处却伴随不少的隐患。

这种方式仅仅是起动了一个新的线程,没有任务的概念,不能做状态的管理。start之后,run当中的代码就一定会执行到底,无法中途取消。

Runnable作为匿名内部类还持有了外部类的引用,在线程退出之前,该引用会一直存在,阻碍外部类对象被GC回收,在一段时间内造成内存泄漏。

没有线程切换的接口,要传递处理结果到UI线程的话,需要写额外的线程切换代码。

如果从UI线程启动,则该线程优先级默认为Default,归于default cgroup,会平等的和UI线程争夺CPU资源。这一点尤其需要注意,在对UI性能要求高的场景下要记得

虽说处于background group的线程总共只能争取到5~10%的CPU资源,但这对绝大部分的后台任务处理都绰绰有余了,1ms和10ms对用户来说,都是快到无法感知,所以我们一般都偏向于在background group当中执行工作线程任务。

AsyncTask

一个典型的AsyncTask实现如下:

和使用Thread()不同的是,多了几处API回调来严格规范工作线程与UI线程之间的交互。我们大部分的业务场景几乎都符合这种规范,比如去磁盘读取图片,缩放处理需要在工作线程执行,最后绘制到ImageView控件需要切换到UI线程。

AsyncTask的几处回调都给了我们机会去中断任务,在任务状态的管理上较之Thread()方式更为灵活。值得注意的是AsyncTask的cancel()方法并不会终止任务的执行,开发者需要自己去检查cancel的状态值来决定是否中止任务。

AsyncTask也有隐式的持有外部类对象引用的问题,需要特别注意防止出现意外的内存泄漏。

AsyncTask由于在不同的系统版本上串行与并行的执行行为不一致,被不少开发者所诟病,这确实是硬伤,绝大部分的多线程场景都需要明确任务是串行还是并行。

线程优先级为background,对UI线程的执行影响极小。

HandlerThread

在需要对多任务做更精细控制,线程切换更频繁的场景之下,Thread()和AsyncTask都会显得力不从心。HandlerThread却能胜任这些需求甚至更多。

HandlerThread将Handler,Thread,Looper,MessageQueue几个概念相结合。Handler是线程对外的接口,所有新的message或者runnable都通过handler post到工作线程。Looper在MessageQueue取到新的任务就切换到工作线程去执行。不同的post方法可以让我们对任务做精细的控制,什么时候执行,执行的顺序都可以控制。HandlerThread最大的优势在于引入MessageQueue概念,可以进行多任务队列管理。

HandlerThread背后只有一个线程,所以任务是串行执行的。串行相对于并行来说更安全,各任务之间不会存在多线程安全问题。

HandlerThread所产生的线程会一直存活,Looper会在该线程中持续的检查MessageQueue。这一点和Thread(),AsyncTask都不同,thread实例的重用可以避免线程相关的对象的频繁重建和销毁。

HandlerThread较之Thread(),AsyncTask需要写更多的代码,但在实用性,灵活度,安全性上都有更好的表现。

ThreadPoolExecutor

Thread(),AsyncTask适合处理单个任务的场景,HandlerThread适合串行处理多任务的场景。当需要并行的处理多任务之时,ThreadPoolExecutor是更好的选择。

线程池可以避免线程的频繁创建和销毁,显然性能更好,但线程池并发的特性往往也是疑难杂症的源头,是代码降级和失控的开始。多线程并行导致的bug往往是偶现的,不方便调试,一旦出现就会耗掉大量的开发精力。

ThreadPool较之HandlerThread在处理多任务上有更高的灵活性,但也带来了更大的复杂度和不确定性。

IntentService

不得不说Android在API设计上粒度很细,同一样工作可以通过各种不同的类来完成。IntentService又是另一种开工作线程的方式,从名字就可以看出这个工作线程会带有service的属性。和AsyncTask不同,没有和UI线程的交互,也不像HandlerThread的工作线程会一直存活。IntentService背后其实也有一个HandlerThread来串行的处理Message Queue,从IntentService的onCreate方法可以看出:

只不过在所有的Message处理完毕之后,工作线程会自动结束。所以可以把IntentService看做是Service和HandlerThread的结合体,适合需要在工作线程处理UI无关任务的场景。

结束语

Android开线程的方式虽然五花八门,但归根到底最后还是映射到linux下的pthread,业务的设计还是脱不了和线程相关的基础概念范畴:线程的执行顺序,调度策略,生命周期,串行还是并行,同步还是异步等等。摸清楚各类API下线程的行为特点,在设计具体业务的线程模型的时候自然轻车熟路了,线程模型的设计要有整个app视角的广度,切忌各业务模块各玩各的。