日期: 2021 年 10 月 8 日

Android进阶笔记:AIDL内部实现详解

Android进阶笔记:AIDL内部实现详解 (二)

接着上一篇分析的aidl的流程解析。知道了aidl主要就是利用Ibinder来实现跨进程通信的。既然是通过对Binder各种方法的封装,那也可以不使用aidl自己通过Binder来实现跨进程通讯。那么这篇博客就主要就写一下通过上篇(Android进阶笔记:AIDL详解(一))总结的知识来自己实现跨进程通讯从而更加透彻的了解aidl的核心逻辑。

首先上一篇博客(Android进阶笔记:AIDL详解(一))中总结出一个结论————“onTransact方法是提供给server端用的,transact方法(内部类proxy封装了transact方法)和asInterface方法是给client端用的。”因此很清楚,只要我们在Server端实现跨进程需要调用的方法(类似aidl的接口实现)和onTransact方法,而服务端只要通过获得的IBinder对象来调用transact方法就可以代替aidl来实现跨进程通讯了。既然思路已经整理清楚了,那就一步一步来实现它。

Server端

首先Server端是要通过Service的onBind方法来给Client端一个Binder对象,那就先从这个Binder对象入手。那就先来创建了一个MyBinder类,代码如下:

MyBinder.java

public class MyBinder extends Binder {
    //标记方法的
    private static final int METHOD_ADD_CODE = 1001;
    //标识binder对象的
    private static final String DESCRIPTION = "not use aidl";

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        if (code == METHOD_ADD_CODE) {
            //验证一下binder
            data.enforceInterface(DESCRIPTION);
            //从parcel对象中读取参数
            int arg0 = data.readInt();
            int arg1 = data.readInt();
            //写入结果
            reply.writeInt(add(arg0, arg1));
            return true;
        }
        return super.onTransact(code, data, reply, flags);
    }

    private int add(int arg0, int arg1) {
        return arg0 + arg1;
    }
}

代码非常简单,只是重新写了一下onTransact方法。其实一共只有4步:

  1. 根据code的值来判断client端具体想要调用哪个方法;
  2. 读取parcel对象(data)中传入的参数;
  3. 调用自己本地的方法(add)并将参数传入;
  4. 把结果写入parcel对象(reply)中;

接着只要把这个自己定义的MyBinder类的实例通过Service.onBInder方法返回给Client端就可以了。

MyService.java

public class MyService extends Service {
    private MyBinder myBinder;

    public MyService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //创建实例
        myBinder = new MyBinder();
    }

    @Override
    public IBinder onBind(Intent intent) {
        //返回自定义的binder对象
        return myBinder;
    }
}

Client端

client端的代码无非就是把之前写在aidl中的proxy内部类的方法拿出来了。具体看代码:

WithoutAidlActivity.java

public class WithoutAidlActivity extends AppCompatActivity {

    private ServiceConnection serviceConnection;
    private IBinder binder;

    //以下两个参数要和server端保持一致
    //标记方法的(告知server端调用哪个方法)
    private static final int METHOD_ADD_CODE = 1001;
    //标识binder对象的
    private static final String DESCRIPTION = "not use aidl";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_without_aidl);
        serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Log.d("onServiceConnected", "onServiceConnected: connected success!");
                binder = service;
                //这里就代替aidl中的proxy来直接调用transact方法
                //先准备参数
                Parcel data = Parcel.obtain();
                Parcel reply = Parcel.obtain();
                data.writeInterfaceToken(DESCRIPTION);
                data.writeInt(123);
                data.writeInt(456);
                try {
                    //调用transact方法
                    binder.transact(METHOD_ADD_CODE, data, reply, 0);
                    //获得结果
                    int result = reply.readInt();
                    Log.d("onServiceConnected", "result = " + result);
                } catch (RemoteException e) {
                    e.printStackTrace();
                } finally {
                    data.recycle();
                    reply.recycle();
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                binder = null;
            }
        };

        bindService(new Intent("com.coder_f.aidlserver.MyService"), serviceConnection, BIND_AUTO_CREATE);

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(serviceConnection);
    }
}

首先连接成功后在serviceConnection.onServiceConnected方法中获得了IBinder实例,然后总共做了3个事情:

  1. 创建两个parcel对象分别存放参数(data)和返回值(reply)
  2. 调用transact方法,传入data,reply,和你要调用的方法code。*后的flag传入0表示有返回值(1表示没有又返回值)
  3. 从reply中获得结果

完成以上工作就可以不通过aidl实现跨进程通讯了。但是还是要说一下,这里我们server端调用的只是一个简单的add方法不耗时的,而transact方法则是在onServiceConnected方法中被调用的其实是在主线程中执行的。如果add方法换成一个耗时方法,那么主线程(UI线程)是会卡死的,调用transact方法时当前线程会被挂起知道结果被返回(有兴趣可以去试试,只要在add方法里面加一个Thread.sleep就可以了)。所以*好的办法就是起一个线程来调用transact方法。

Android AIDL实例解析

Android AIDL实例解析

AIDL这项技术在我们的开发中一般来说并不是很常用,虽然自己也使用新浪微博的SSO登录,其原理就是使用AIDL,但是自己一直没有动手完整的写过AIDL的例子,所以就有了这篇简单的文章。
AIDL(AndRoid接口描述语言)是一种借口描述语言; 编译器可以通过aidl文件生成一段代码,通过预先定义的接口达到两个进程内部通信进程的目的. 如果需要在一个Activity中, 访问另一个Service中的某个对象, 需要先将对象转化成 AIDL可识别的参数(可能是多个参数), 然后使用AIDL来传递这些参数, 在消息的接收端, 使用这些参数组装成自己需要的对象.
说白了,AIDL就是定义一个接口,客户端(调用端)通过bindService来与远程服务端简历一个连接,在该连接建立时会将返回一个IBinder对象,该对象是服务端Binder的BinderProxy,在建立连接时,客户端通过asInterface函数将该BinderProxy对象包装成本地的Proxy,并将远程服务端的BinderProxy对象赋值给Proxy类的mRemote字段,就是通过mRemote执行远程方法调用。需要对Binder机制有更深的理解,请转到老罗的系列文字吧,Android系统进程间通信Binder机制在应用程序框架层的Java接口源代码分析。下面我们看一个AIDL实例。

AIDL接口声明
在src目录下创建一个com.example.advanceandroid.aidl包,然后在该包下创建一个ILogin.aidl文件,注意是创建文件而不是类或者接口类型。在ILogin.aidl中声明接口,实例如下 :

package com.example.advanceandroid.aidl;

interface ILogin {
String login();
}

注意看,接口和方法声明都不用public,方法加入public会提示错误。编写完后如果eclipse开启了自动编译则会在gen/com.example.advanceandroid.aidl下生成一个ILogin.java类,内容大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.example.advanceandroid.aidl;
public interface ILogin extends android.os.IInterface
{
    /** Local-side IPC implementation stub class. */
    public static abstract class Stub extends android.os.Binder implements
            com.example.advanceandroid.aidl.ILogin
    {
        private static final java.lang.String DESCRIPTOR = "com.example.advanceandroid.aidl.ILogin";
        /** Construct the stub at attach it to the interface. */
        public Stub()
        {
            this.attachInterface(this, DESCRIPTOR);
        }
        /**
         * Cast an IBinder object into an com.example.advanceandroid.aidl.ILogin
         * interface, generating a proxy if needed.
         */
        public static com.example.advanceandroid.aidl.ILogin asInterface(android.os.IBinder obj)
        {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.advanceandroid.aidl.ILogin))) {
                return ((com.example.advanceandroid.aidl.ILogin) iin);
            }
            return new com.example.advanceandroid.aidl.ILogin.Stub.Proxy(obj);
        }
        @Override
        public android.os.IBinder asBinder()
        {
            return this;
        }
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply,
                int flags) throws android.os.RemoteException
        {
            switch (code)
            {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_login: {                            // 1、登录请求,执行的是this.login();
                    data.enforceInterface(DESCRIPTOR);
                    java.lang.String _result = this.login();
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }
        private static class Proxy implements com.example.advanceandroid.aidl.ILogin
        {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote)
            {
                mRemote = remote;
            }
            @Override
            public android.os.IBinder asBinder()
            {
                return mRemote;
            }
            public java.lang.String getInterfaceDescriptor()
            {
                return DESCRIPTOR;
            }
            @Override
            public java.lang.String login() throws android.os.RemoteException                // 2、Proxy中的login,通过Binder机制实现IPC
            {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.lang.String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_login, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }
        static final int TRANSACTION_login = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    }
    public java.lang.String login() throws android.os.RemoteException;
}

可以看到,该类中自动生成了ILogin接口,该接口中又一个login()函数。但*重要的是里面生成了一个Stub类,该类集成子Binder类,并且实现了ILogin接口。Stub里面*重要的就是asInterface()这个函数,在这个函数中会判断obj参数的类型,如果是该obj是本地的接口类似,则认为不是IPC,会将该obj转换成ILogin类型;否则会通过自动生成的另一个内部类Proxy来包装obj,将其赋值给Proxy中的mRemote属性。Proxy类也实现了ILogin接口,在login()函数中,Proxy将通过Binder机制向服务端传递请求和数据,如上面代码中的注释2。这是客户端的工作算是完成了。

服务端AIDL接口
服务端也需要在相同的包下创建同名的aidl文件,我们直接将客户端的com.example.advanceandroid.aidl包下的ILogin.aidl拷贝到服务端即可,如果用到了自定义的类型,那么该自定义类型也需要在客户端、服务端都有。拷贝完aidl后,在服务端程序中也会在gen中生成对应的ILogin.java文件,内容同客户端一样。这里的重点我们要看onTransact函数,即上述代码中的注释1处,可以看到,在case TRANSACTION_login处执行了this.login()函数,意思是当接收到客户端的TRANSACTION_login请求时,执行this.login()函数,通过客户端的分析我们知道,当我们调用login()时实际上就是通过mRemote向服务端提交了一个TRANSACTION_login请求,因此就两端通过Binder机制就对接上了,我们可以简单的理解为C/S模式。

服务端还没有完,*重要的一步时建立一个Service,内容大致如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* AIDL服务端接口,LoginStubImpl实现了ILogin接口.
*
* @author mrsimple
*/
public class LoginService extends Service {
    /**
     *
     */
    IBinder mBinder = new LoginStubImpl();
    /**
     * @author mrsimple
     */
    class LoginStubImpl extends Stub {
        @Override
        public String login() throws RemoteException {
            return "这是从 " + this.getClass().getName() + " 返回的字符串";
        }
    }
    /*
     * 返回Binder实例,即实现了ILogin接口的Stub的子类,这里为LoginStubImpl
     * [url=home.php?mod=space&uid=133757]@see[/url] android.app.Service#onBind(android.content.Intent)
     */
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

该Service我们这里命名为LoginService,继承自Service,然后建一个名为LoginServiceImpl的内部类,该类继承自自动生成的Stub,然后实现login()方法。在LoginService中声明一个IBinder字段mBinder :

IBinder mBinder = new LoginStubImpl();

并且在LoginService的onBind函数中将mBinder对象返回。即在客户端建立与服务端的连接时,会调用onBind方法将mBinder对象返回,在客户端的ServiceConnection类的onServiceConnected函数中得到的对象IBinder就是经过BinderProxy包装的LoginService中的mBinder对象。因此在服务端中的onTransact中调用的this.login()函数实际上就是调用的LoginStubImpl中的login()函数。

在服务端程序的AndroidManifest.xml中注册LoginService,如下 :

1
2
3
4
5
6
<!-- aidl server service -->
    <service android:name="com.example.advanceandroid.aidl.LoginService">
        <intent-filter>
            
        </action></intent-filter>
    </service>

客户端建立连接

在Activity中加入如下代码 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
ServiceConnection mLoginConnection = new ServiceConnection() {
       @Override
       public void onServiceDisconnected(ComponentName name) {
           Log.d("", "### aidl disconnected.");
       }
       @Override
       public void onServiceConnected(ComponentName name, IBinder service) {
           Log.d("", "### aidl onServiceConnected.     service : " + service.getClass().getName());
           ILogin login = Stub.asInterface(service);
           Log.d("", "### after asInterface : " + login.getClass().getName());
           try {
               Log.d("", "### login : " + login.login());
               // Toast.makeText(MainActivity.this, "onServiceConnected : " +
               // login.login(),
               // Toast.LENGTH_SHORT).show();
           } catch (RemoteException e) {
               e.printStackTrace();
           }
       }
   };
   @Override
   protected void onResume() {
       super.onResume();
       // 服务端的action
       Intent aidlIntent = new Intent("com.example.advanceandroid.aidl.LoginService");
       bindService(aidlIntent, mLoginConnection, Context.BIND_AUTO_CREATE);
   }
   @Override
   protected void onStop() {
       super.onStop();
       // unbind
       unbindService(mLoginConnection);
   }

运行

先运行服务端程序,然后在启动客户端程序,可以看到客户端输出如下Log:

1
2
3
09-02 10:40:54.662: D/(9589): ### aidl onServiceConnected.     service : android.os.BinderProxy
09-02 10:40:54.662: D/(9589): ### after asInterface : com.example.advanceandroid.aidl.ILogin$Stub$Proxy
09-02 10:40:54.662: D/(9589): ### login : 这是从 com.example.advanceandroid.aidl.LoginService$LoginStubImpl 返回的字符串

可以看淡onServiceConnected(ComponentName name, IBinder service)中的service对象是BinderProxy类型,经过asInterface转换后被包装成了Proxy类型,但是调用的时候,执行的是服务端LoginStubImpl中的login()函数。因此,LoginStubImpl实例mBinder被服务端包装成BinderProxy类型,再经过客户端的Proxy进行包装,通过Binder机制进行数据传输,实现IPC。

Android ANR优化(二)

Android ANR优化 2

在实际情况中,当Android项目的用户量特别大时候,一些细小的问题也会被放大,ANR问题就是一个典型的例子。
一些ANR问题只会发生在用户实际使用的情景,当系统资源比较紧张等一些特殊情况下才会遇到,而这些ANR问题有很大一部分是因为我们的代码不合理导致,这就需要我们定位问题,修复问题,并且在以后的代码设计中尽量避免这些不合理。
*近工作中集中分析了项目的大量的用户自动上报的ANR问题日志,虽然网上ANR相关的文章已经很多了,在这里还是做一个总结。

提纲

一. 什么情况下会出现ANR
二. ANR机制的原理
三. 如何分析ANR问题
四. 如何避免ANR问题

一.什么情况下会出现ANR问题:

ANR(Application Not responding)。Android中,主线程(UI线程)如果在规定时内没有处理完相应工作,就会出现ANR。
具体来说,ANR会在以下几种情况中出现:

  1. 输入事件(按键和触摸事件)5s内没被处理: Input event dispatching timed out
  2. BroadcastReceiver的事件(onRecieve方法)在规定时间内没处理完(前台广播为10s,后台广播为60s):Timeout of broadcast BroadcastRecord
    07-27 19:18:47.448 1707 1766 W BroadcastQueue: Receiver during timeout: ResolveInfo{ccd831e com.example.qintong.myapplication/.MyBroadCastReciever m=0x108000}
    07-27 19:18:47.502 3513 3728 I WtEventController: ANR com.example.qintong.myapplication 7573
  3. service 前台20s后台200s未完成启动 Timeout executing service
  4. ContentProvider的publish在10s内没进行完:timeout publishing content providers
    在android文档(https://developer.android.com/training/articles/perf-anr.html)中,只写了*种和第二种情况,而根据源码和实际的实验,我们能发现service的启动和provider的publish同样会造成anr问题。
    这里需要注意的是,在后三种情况,以BroadcastReviever为例,在onRecieve()方法执行10秒内没发生*种ANR(也就是在这个过程中没有输入事件或输入事件还没到5s)才会发生Receiver timeout,否则将先发生事件无相应ANR,所以onRecieve()是有可能执行不到10s就发生ANR的,所以不要在onRecieve()方法里面干活,service的onCreate()和ContentProvider的onCreate()也一样,他们都是主线程的,不要在这些方法里干活,这个会在本文*后再细说。

二.ANR机制的实现原理:

文章:http://gityuan.com/2016/07/02/android-anr/从源码角度详细的分析了ANR机制实现的原理。对于上一章讲到的1-4中情况,分别找到了其源码中是如何实现的,对于每一种大概原理如下:1.在进行相关操作调用hander.sendMessageAtTime()发送一个ANR的消息,延时时间为ANR发生的时间(如前台Service是当前时间20s之后)。2.进行相关的操作3.操作结束后向remove掉该条message。如果相关的操作在规定时间没有执行完成,该条message将被handler取出并执行,就发生了ANR。
下面以BroadcastReceiver为例详细介绍:
BroadcastQueue.processNextBroadcast()

     final void processNextBroadcast(boolean fromMsg) {
        ...
        synchronized (mService) {
            ...
            do {
                if (mOrderedBroadcasts.size() == 0) {
                    ...
                    if (mService.mProcessesReady && r.dispatchTime > 0) {
                        long now = SystemClock.uptimeMillis();
                        if ((numReceivers > 0) &&
                                (now > r.dispatchTime + (2 * mTimeoutPeriod * numReceivers))) {
                            //1.发送延时消息
                            broadcastTimeoutLocked(false); // forcibly finish this broadcast
                            forceReceive = true;
                            r.state = BroadcastRecord.IDLE;
                        }
                    }

                    if (r.state != BroadcastRecord.IDLE) {
                        if (DEBUG_BROADCAST) Slog.d(TAG,
                                "processNextBroadcast("
                                        + mQueueName + ") called when not idle (state="
                                        + r.state + ")");
                        return;
                    }

                    if (r.receivers == null || r.nextReceiver >= numReceivers
                            || r.resultAbort || forceReceive) {
                        // No more receivers for this broadcast!  Send the final
                        // result if requested...
                        if (r.resultTo != null) {
                            try {
                                //2. 处理广播消息
                                performReceiveLocked(r.callerApp, r.resultTo,
                                        new Intent(r.intent), r.resultCode,
                                        r.resultData, r.resultExtras, false, false, r.userId);
                                // Set this to null so that the reference
                                // (local and remote) isn't kept in the mBroadcastHistory.
                                r.resultTo = null;
                            } catch (RemoteException e) {
                                ...
                            }
                        }
                        //3.取消延时消息
                        cancelBroadcastTimeoutLocked();
                        ...
                    }
                } while (r == null) ;
                ...
            }
        }
    }

1.发送延时消息:broadcastTimeoutLocked(false):

    final void broadcastTimeoutLocked(boolean fromMsg) {
    ...
        long now = SystemClock.uptimeMillis();
        if (fromMsg) {
            if (mService.mDidDexOpt) {
                // Delay timeouts until dexopt finishes.
                mService.mDidDexOpt = false;
                long timeoutTime = SystemClock.uptimeMillis() + mTimeoutPeriod;
                setBroadcastTimeoutLocked(timeoutTime);
                return;
            }
            if (!mService.mProcessesReady) {
                return;
            }

            long timeoutTime = r.receiverTime + mTimeoutPeriod;
            if (timeoutTime > now) {
                setBroadcastTimeoutLocked(timeoutTime);
                return;
            }
        }

他调用了setBroadcastTimeoutLocked(long timeoutTime):

    final void setBroadcastTimeoutLocked(long timeoutTime) {
        if (! mPendingBroadcastTimeoutMessage) {
            Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
            mHandler.sendMessageAtTime(msg, timeoutTime);
            mPendingBroadcastTimeoutMessage = true;
        }
    }

传入setBroadcastTimeoutLocked(long timeoutTime)的时间xxx + mTimeoutPeriod,mTimeoutPeriod就是onRecieve()可以执行的时间,在BroadcastQueue初始化时候被赋值,前台队列为10s后台队列为60s:
ActivityManagerService.java:

    public ActivityManagerService(Context systemContext) {
        ...
        static final int BROADCAST_FG_TIMEOUT = 10 * 1000;
        static final int BROADCAST_BG_TIMEOUT = 60 * 1000;
        ...
        mFgBroadcastQueue = new BroadcastQueue(this, mHandler,
                "foreground", BROADCAST_FG_TIMEOUT, false);
        mBgBroadcastQueue = new BroadcastQueue(this, mHandler,
                "background", BROADCAST_BG_TIMEOUT, true);
        ...
    }
  1. performReceiveLocked()为广播的实际处理,就不展开了
  2. cancelBroadcastTimeoutLocked() :
    该方法的主要工作是当service启动完成,则移除服务超时消息SERVICE_TIMEOUT_MSG。
    final void cancelBroadcastTimeoutLocked() {
        if (mPendingBroadcastTimeoutMessage) {
            mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this);
            mPendingBroadcastTimeoutMessage = false;
        }
    }

三.如何分析ANR问题:

从前文可以明确,ANR问题是由于主线程的任务在规定时间内没处理完任务,而造成这种情况的原因大致会有一下几点:

  1. 主线程在做一些耗时的工作
  2. 主线程被其他线程锁
  3. cpu被其他进程占用,该进程没被分配到足够的cpu资源。

判断一个ANR属于哪种情况便是分析ANR问题的关键。那么拿到一个anr的日志,应该如何分析呢?
在发生ANR的时候,系统会收集ANR相关的信息提供给开发者:首先在Log中有ANR相关的信息,其次会收集ANR时的CPU使用情况,还会收集trace信息,也就是当时各个线程的执行情况。trace文件保存到了/data/anr/traces.txt中,此外,ANR前后该进程打印出的log也有一定价值。一般来说可以按一下思路来分析:

  1. 从log中找到ANR反生的信息:可以从log中搜索“ANR in”或“am_anr”,会找到ANR发生的log,该行会包含了ANR的时间、进程、是何种ANR等信息,如果是BroadcastReceiver的ANR可以怀疑BroadCastReceiver.onRecieve()的问题,如果的Service或Provider就怀疑是否其onCreate()的问题。
  2. 在该条log之后会有CPU usage的信息,表明了CPU在ANR前后的用量(log会表明截取ANR的时间),从各种CPU Usage信息中大概可以分析如下几点:
    (1). 如果某些进程的CPU占用百分比较高,几乎占用了所有CPU资源,而发生ANR的进程CPU占用为0%或非常低,则认为CPU资源被占用,进程没有被分配足够的资源,从而发生了ANR。这种情况多数可以认为是系统状态的问题,并不是由本应用造成的。
    (2). 如果发生ANR的进程CPU占用较高,如到了80%或90%以上,则可以怀疑应用内一些代码不合理消耗掉了CPU资源,如出现了死循环或者后台有许多线程执行任务等等原因,这就要结合trace和ANR前后的log进一步分析了。
    (3). 如果CPU总用量不高,该进程和其他进程的占用过高,这有一定概率是由于某些主线程的操作就是耗时过长,或者是由于主进程被锁造成的。
  3. 除了上述的情况(1)以外,分析CPU usage之后,确定问题需要我们进一步分析trace文件。trace文件记录了发生ANR前后该进程的各个线程的stack。对我们分析ANR问题*有价值的就是其中主线程的stack,一般主线程的trace可能有如下几种情况:
    (1). 主线程是running或者native而对应的栈对应了我们应用中的函数,则很有可能就是执行该函数时候发生了超时。
    (2). 主线程被block:非常明显的线程被锁,这时候可以看是被哪个线程锁了,可以考虑优化代码。如果是死锁问题,就更需要及时解决了。
    (3). 由于抓trace的时刻很有可能耗时操作已经执行完了(ANR -> 耗时操作执行完毕 ->系统抓trace),这时候的trace就没有什么用了,主线程的stack就是这样的:
"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 obj=0x757855c8 self=0xb4d76500
  | sysTid=3276 nice=0 cgrp=default sched=0/0 handle=0xb6ff5b34
  | state=S schedstat=( 50540218363 186568972172 209049 ) utm=3290 stm=1764 core=3 HZ=100
  | stack=0xbe307000-0xbe309000 stackSize=8MB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/3276/stack)
  native: #00 pc 0004099c  /system/lib/libc.so (__epoll_pwait+20)
  native: #01 pc 00019f63  /system/lib/libc.so (epoll_pwait+26)
  native: #02 pc 00019f71  /system/lib/libc.so (epoll_wait+6)
  native: #03 pc 00012ce7  /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+102)
  native: #04 pc 00012f63  /system/lib/libutils.so (_ZN7android6Looper8pollOnceEiPiS1_PPv+130)
  native: #05 pc 00086abd  /system/lib/libandroid_runtime.so (_ZN7android18NativeMessageQueue8pollOnceEP7_JNIEnvP8_jobjecti+22)
  native: #06 pc 0000055d  /data/dalvik-cache/arm/system@[email protected] (Java_android_os_MessageQueue_nativePollOnce__JI+96)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:323)
  at android.os.Looper.loop(Looper.java:138)
  at android.app.ActivityThread.main(ActivityThread.java:5528)
  at java.lang.reflect.Method.invoke!(Native method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:740)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:630)

当然这种情况很有可能是由于该进程的其他线程消耗掉了CPU资源,这就需要分析其他线程的trace以及ANR前后该进程自己输出的log了。

四.如何降低ANR的概率:

有一些操作是很危险的,非常容易发生ANR,在写代码时候一定要避免:

  1. 主线程读取数据:在Android中主线程去读取数据是非常不好的,Android是不允许主线程从网络读数据的,但系统允许主线程从数据库或者其他地方获取数据,但这种操作ANR风险很大,也会造成掉帧等,影响用户体验。
    (1). 避免在主线程query provider,首先这会比较耗时,另外这个操作provider那一方的进程如果挂掉了或者正在启动,我们应用的query就会很长时间不会返回,我们应该在其他线程中执行数据库query、provider的query等获取数据的操作。
    (2). sharePreference的调用:针对sharePreference的优化点有很多,文章http://weishu.me/2016/10/13/sharedpreference-advices/ 详细介绍了几点sharepreference使用时候的注意事项。首先sharePreference的commit()方法是同步的,apply()方法一般是异步执行的。所以在主线程不要用其commit(),用apply()替换。其次sharePreference的写是全量写而非增量写,所以尽量都修改完同一apply,避免改一点apply一次(apply()方法在Activity stop的时候主线程会等待写入完成,提交多次就很容易卡)。并且存储文本也不宜过大,这样会很慢。另外,如果写入的是json或者xml,由于需要加和删转义符号,速度会比较慢。
  2. 不要在broadcastReciever的onRecieve()方法中干活,这一点很容易被忽略,尤其应用在后台的时候。为避免这种情况,一种解决方案是直接开的异步线程执行,但此时应用可能在后台,系统优先级较低,进程很容易被系统杀死,所以可以选择开个IntentService去执行相应操作,即使是后台Service也会提高进程优先级,降低被杀可能性。
  3. 各个组件的生命周期函数都不应该有太耗时的操作,即使对于后台Service或者ContentProvider来讲,应用在后台运行时候其onCreate()时候不会有用户输入引起事件无响应ANR,但其执行时间过长也会引起Service的ANR和ContentProvider的ANR。
  4. 尽量避免主线程的被锁的情况,在一些同步的操作主线程有可能被锁,需要等待其他线程释放相应锁才能继续执行,这样会有一定的ANR风险,对于这种情况有时也可以用异步线程来执行相应的逻辑。另外, 我们要避免死锁的发生(主线程被死锁基本就等于要发生ANR了)。

Android ANR优化(一)

1, 你碰到ANR了吗

在App使用过程中, 你可能遇到过这样的情况:

%title插图%num
ANR

恭喜你, 这就是传说中的ANR.

1.1 何为ANR

ANR全名Application Not Responding, 也就是”应用无响应”. 当操作在一段时间内系统无法处理时, 系统层面会弹出上图那样的ANR对话框.

1.2 为什么会产生ANR

在Android里, App的响应能力是由Activity Manager和Window Manager系统服务来监控的. 通常在如下两种情况下会弹出ANR对话框:

  • 5s内无法响应用户输入事件(例如键盘输入, 触摸屏幕等).
  • BroadcastReceiver在10s内无法结束.

造成以上两种情况的首要原因就是在主线程(UI线程)里面做了太多的阻塞耗时操作, 例如文件读写, 数据库读写, 网络查询等等.

1.3 如何避免ANR

知道了ANR产生的原因, 那么想要避免ANR, 也就很简单了, 就一条规则:

不要在主线程(UI线程)里面做繁重的操作.

这里面实际上涉及到两个问题:

  1. 哪些地方是运行在主线程的?
  2. 不在主线程做, 在哪儿做?

稍后解答.

2, ANR分析

2.1 获取ANR产生的trace文件

ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 可以通过adb命令将其导出到本地:

$adb pull data/anr/traces.txt .

2.2 分析traces.txt

2.2.1 普通阻塞导致的ANR

获取到的tracs.txt文件一般如下:

如下以GithubApp代码为例, 强行sleep thread产生的一个ANR.

----- pid 2976 at 2016-09-08 23:02:47 -----
Cmd line: com.anly.githubapp  // *新的ANR发生的进程(包名)

...

DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
  | sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
  | state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
  | stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
  | held mutexes=
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:1031)
  - locked <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:985) // 主线程中sleep过长时间, 阻塞导致无响应.
  at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
  - locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
  at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)  // 产生ANR的那个函数调用
  - locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
  at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
  at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起点
  at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
  at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
  at android.view.View.performClick(View.java:4780)
  at android.view.View$PerformClick.run(View.java:19866)
  at android.os.Handler.handleCallback(Handler.java:739)
  at android.os.Handler.dispatchMessage(Handler.java:95)
  at android.os.Looper.loop(Looper.java:135)
  at android.app.ActivityThread.main(ActivityThread.java:5254)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

拿到trace信息, 一切好说.
如上trace信息中的添加的中文注释已基本说明了trace文件该怎么分析:

  1. 文件*上的即为*新产生的ANR的trace信息.
  2. 前面两行表明ANR发生的进程pid, 时间, 以及进程名字(包名).
  3. 寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源.

以上的ANR trace是属于相对简单, 还有可能你并没有在主线程中做过于耗时的操作, 然而还是ANR了. 这就有可能是如下两种情况了:

2.2.2 CPU满负荷

这个时候你看到的trace信息可能会包含这样的信息:

Process:com.anly.githubapp
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...

100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait

*后一句表明了:

  1. 当是CPU占用100%, 满负荷了.
  2. 其中*大数是被iowait即I/O操作占用了.

此时分析方法调用栈, 一般来说会发现是方法中有频繁的文件读写或是数据库读写操作放在主线程来做了.

2.2.3 内存原因

其实内存原因有可能会导致ANR, 例如如果由于内存泄露, App可使用内存所剩无几, 我们点击按钮启动一个大图片作为背景的activity, 就可能会产生ANR, 这时trace信息可能是这样的:

// 以下trace信息来自网络, 用来做个示例
Cmdline: android.process.acore

DALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)

...

MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732

可以看到free的内存已所剩无几.

当然这种情况可能更多的是会产生OOM的异常…

2.2 ANR的处理

针对三种不同的情况, 一般的处理情况如下

  1. 主线程阻塞的
    开辟单独的子线程来处理耗时阻塞事务.
  2. CPU满负荷, I/O阻塞的
    I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行.
  3. 内存不够用的
    增大VM内存, 使用largeHeap属性, 排查内存泄露(这个在内存优化那篇细说吧)等.

3, 深入一点

没有人愿意在出问题之后去解决问题.
高手和新手的区别是, 高手知道怎么在一开始就避免问题的发生. 那么针对ANR这个问题, 我们需要做哪些层次的工作来避免其发生呢?

3.1 哪些地方是执行在主线程的

  1. Activity的所有生命周期回调都是执行在主线程的.
  2. Service默认是执行在主线程的.
  3. BroadcastReceiver的onReceive回调是执行在主线程的.
  4. 没有使用子线程的looper的Handler的handleMessage, post(Runnable)是执行在主线程的.
  5. AsyncTask的回调中除了doInBackground, 其他都是执行在主线程的.
  6. View的post(Runnable)是执行在主线程的.

3.2 使用子线程的方式有哪些

上面我们几乎一直在说, 避免ANR的方法就是在子线程中执行耗时阻塞操作. 那么在Android中有哪些方式可以让我们实现这一点呢.

3.2.1 启Thread方式

这个其实也是Java实现多线程的方式. 有两种实现方法, 继承Thread 或 实现Runnable接口:

继承Thread

class PrimeThread extends Thread {
    long minPrime;
    PrimeThread(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
         . . .
    }
}

PrimeThread p = new PrimeThread(143);
p.start();

实现Runnable接口

class PrimeRun implements Runnable {
    long minPrime;
    PrimeRun(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
         . . .
    }
}

PrimeRun p = new PrimeRun(143);
new Thread(p).start();

3.2.2 使用AsyncTask

这个是Android特有的方式, AsyncTask顾名思义, 就是异步任务的意思.

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
    // Do the long-running work in here
    // 执行在子线程
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            // Escape early if cancel() is called
            if (isCancelled()) break;
        }
        return totalSize;
    }

    // This is called each time you call publishProgress()
    // 执行在主线程
    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }

    // This is called when doInBackground() is finished
    // 执行在主线程
    protected void onPostExecute(Long result) {
        showNotification("Downloaded " + result + " bytes");
    }
}

// 启动方式
new DownloadFilesTask().execute(url1, url2, url3);

3.2.3 HandlerThread

Android中结合Handler和Thread的一种方式. 前面有云, 默认情况下Handler的handleMessage是执行在主线程的, 但是如果我给这个Handler传入了子线程的looper, handleMessage就会执行在这个子线程中的. HandlerThread正是这样的一个结合体:

// 启动一个名为new_thread的子线程
HandlerThread thread = new HandlerThread("new_thread");
thread.start();

// 取new_thread赋值给ServiceHandler
private ServiceHandler mServiceHandler;
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);

private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper) {
      super(looper);
    }
    
    @Override
    public void handleMessage(Message msg) {
      // 此时handleMessage是运行在new_thread这个子线程中了.
    }
}

3.2.4 IntentService

Service是运行在主线程的, 然而IntentService是运行在子线程的.
实际上IntentService就是实现了一个HandlerThread + ServiceHandler的模式.

以上HandlerThread的使用代码示例也就来自于IntentService源码.

3.2.5 Loader

Android 3.0引入的数据加载器, 可以在Activity/Fragment中使用. 支持异步加载数据, 并可监控数据源在数据发生变化时传递新结果. 常用的有CursorLoader, 用来加载数据库数据.

// Prepare the loader.  Either re-connect with an existing one,
// or start a new one.
// 使用LoaderManager来初始化Loader
getLoaderManager().initLoader(0, null, this);

//如果 ID 指定的加载器已存在,则将重复使用上次创建的加载器。
//如果 ID 指定的加载器不存在,则 initLoader() 将触发 LoaderManager.LoaderCallbacks 方法 //onCreateLoader()。在此方法中,您可以实现代码以实例化并返回新加载器

// 创建一个Loader
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    // This is called when a new Loader needs to be created.  This
    // sample only has one Loader, so we don't care about the ID.
    // First, pick the base URI to use depending on whether we are
    // currently filtering.
    Uri baseUri;
    if (mCurFilter != null) {
        baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                  Uri.encode(mCurFilter));
    } else {
        baseUri = Contacts.CONTENT_URI;
    }

    // Now create and return a CursorLoader that will take care of
    // creating a Cursor for the data being displayed.
    String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
            + Contacts.HAS_PHONE_NUMBER + "=1) AND ("
            + Contacts.DISPLAY_NAME + " != '' ))";
    return new CursorLoader(getActivity(), baseUri,
            CONTACTS_SUMMARY_PROJECTION, select, null,
            Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}

// 加载完成
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    // Swap the new cursor in.  (The framework will take care of closing the
    // old cursor once we return.)
    mAdapter.swapCursor(data);
}

具体请参看官网Loader介绍.

3.2.6 特别注意

使用Thread和HandlerThread时, 为了使效果更好, 建议设置Thread的优先级偏低一点:

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

因为如果没有做任何优先级设置的话, 你创建的Thread默认和UI Thread是具有同样的优先级的, 你懂的. 同样的优先级的Thread, CPU调度上还是可能会阻塞掉你的UI Thread, 导致ANR的.

结语

对于ANR问题, 个人认为还是预防为主, 认清代码中的阻塞点, 善用线程. 同时形成良好的编程习惯, 要有MainThread和Worker Thread的概念的…(实际上人的工作状态也是这样的~~哈哈)

AppCompatActivity

AppCompatActivity

刚开始看HelloWorld的目录结构
然后就发现Android Studio中的是

import android support.v7.app.AppcompatActivity;

public class MainActivity extends AppCompatActivity{
......
}

而不是继承自Activity

在光标指向AppCompatActivity,Ctrl+o即可查看本类中所有的方法
Alt+F7可以查看方法的引用
F4可以查看类的继承关系(Ctrl+H)


Android Support Library(安卓兼容包)是为了构件一个可以跑在不同版本Android平台的软件。

重构AppCompat
在新的AppCompat中,加入主题色,Toolbar等功能。在新版本中推荐使用AppCompatActivity代替ActionBarActivity。

android:theme
新版本AppCompat允许了Toolbar使用android:theme代替app:theme,兼容API11+

 <android.support.v7.widget.Toolbar
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/main_drawer_toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:layout_below="@+id/main_statusBar"
            android:background="?attr/colorPrimary"
            android:translationZ="4dp"
            android:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"/>

AppCompatDialog对话框

终于加入MD对话框和新主题theme Theme.AppCompat.Dialog使用

  AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setTitle("Dialog");
    builder.setMessage("少数派客户端");
    builder.setPositiveButton("OK", null);
    builder.setNegativeButton("Cancel", null);
    builder.show();

在API22之前我们使用标题栏基本都是在 ActionBarActivity的Activity 中处理的,而API22之后,谷歌遗弃了 ActionBarActivity,推荐我们也可以说是强制我们使用AppCompatActivity。
然而 ActionBarActivity 和AppCompatActivity 的使用大同小异。

详解如下:

1.初探AppCompatActivity
按照androidStudio默认顺序创建项目,默认Activity继承自AppCompatActivity.代码如下:

public class MainActivity extends AppCompatActivity { 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); 
}
}

运行。下面我们来一步一步扩展其标题栏,让它的内容更加丰富多彩。

  1. AppCompatActivity 与Toolbar结合

其实我们并不是要使用AppCompatActivity自带的标题栏,那样扩展会很麻烦,在14年的Android5.0的时候就用Toolbar替代了ActionBar,而ActionBarActivity又被AppCompatActivity替代,那么使用方法就是引入Toolbar设置到Activity中。

a. 首先我们必须在配置文件中,写入Toolbar,代码如下:

<android.support.v7.widget.Toolbar 
android:id="@+id/activity_main_toolbar"     
android:layout_height="wrap_content" 
android:layout_width="match_parent" 
android:minHeight="?attr/actionBarSize" 
android:background="?attr/colorPrimary"> 

</android.support.v7.widget.Toolbar>

解释(1)android:minHeight = “?attr/actionBarSize”:设置标题栏*小高度为ActionBar的高度。
解释(2)android:background = “?attr/colorPrimary”:该主题下的主色。也就是默认的灰色

我们知道在AndroidManifest.xml清单文件下application中设置了android:theme = “@style/AppTheme”而查看APPTheme看到如下样式

<resources> 
<!-- Base application theme. --> 
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> 
<!-- Customize your theme here. --> 
</style>
</resources>

从名字我们就可以看出来,默认的标题栏为黑色。我们使用了Toolbar就必须修改样式文件,将原来的标题栏去掉,修改后的样式文件如下:

<resources> 
<!-- Base application theme. --> 
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> 
<!-- Customize your theme here. --> 
</style>
</resources>

如下这个时候运行,那么你将看到界面也是没有标题栏的,因为你并没有设置到AppCompatActivity中去。

b. 将Toolbar显示到界面

我们创建一个方法名为initActionBar(),首先当然是获取Toolbar控件,然后将Toolbar设置到Activity中去。代码如下:

public void initActionBar() { 
Toolbar toolbar = (Toolbar) findViewById(R.id.activity_main_toolbar); 
setSupportActionBar(toolbar);
}

现在我们运行,我们将得到如下界面:

依然不是你期待的效果,下面我们将介绍AppCompatActivity的细节。

  1. AppCompatActivity详解

a. 设置title
我们可以看到,标题栏始终显示我们的项目名,这样显然给用户看是不理想的。那么怎么设置标题栏的字符串。我们可以在setSupportActionBar(toolbar)之前加入如下代码:

setTitle("liyuanjinglyi");

此setTitle(“liyuanjinglyj”)是AppCompatActivity的方法,并不是Toolbar的方法,那么我们运行一下程序得到如下界面:

那么我们将刚才的AppCompatActivity的setTitle换成Toolbar的setTitle看看效果会怎么样,其实结果如上图一样,没有任何变化。

可能你希望title显示在中间,很遗憾,toolbar与AppCompatActivity并没有提供相关的方法,不过可以在toolbar中加入一个TextView,配置其属性,让其显示在*中间。

b. 设置回退按钮

假如我们这个界面并不是主界面,而是一个子界面,这个时候我有一个需求,需要回退到上一个界面,那么怎么设置左边的图标并实现其方法。其实很简单,在setSupportActionBar(toolbar);后面加入如下代码:

toolbar.setNavigationIcon(R.drawable.back);
toolbar.setNavigationOnClickListener(new View.OnClickListener() { 

@Override 
public void onClick(View v) { 
finish(); 
}
});

为什么在setSupportActionBar(toolbar);后面假如而不在前面,你可以加入到前面试试,虽然可以显示回退的图片,但是点击并没有调用该按钮的点击事件,而在setSupportActionBar(toolbar);后面设置点击才有反应。一定要记住,不然回退是不会起作用的。

c. 设置logo

设置logo在标题栏并不少见,比如网易APP首页,其实代码很简单,也仅仅需要一条代码即可,在setSupportActionBar(toolbar)前加入如下代码:

toolbar.setLogo(R.id.app_logo);

d.设置副标题
在setSupportActionBar(toolbar)前加入如下代码:

toolbar.setSubtitile("liyuanjing");

运行界面后,如下:

e. 设置菜单

我们知道onCreateOptionMenu是上下文菜单,同理可以直接在该方法中设置菜单,代码如下:

@Override
public boolean onCreateOptionsMenu(Menu menu) { 
getMenuInflater().inflate(R.menu.menu_main, menu); 
return true;
}

此方法为Activity方法。因为你在上面已经将Toolbar设置到了Activity中,所以默认也会显示到标题栏中,如果你觉得这样不习惯,那么你可以添加一条语句,不过这条语句添加与否都没关系。

toolbar.setOnCreateContextMenuListener(this);
  1. Toolbar的Style

标题下的Toolbar是蓝色,且有很好的Style,那么这是怎么设置呢?虽然我们这里重点是剖析Activity但是遇到有关Activity标题栏样式的还是要提到。

我们粗略的讲解上面的标题栏用到了哪些Style:

a. <item name = “colorPrimary”>#4876FF<item>:如*步直接在Toolbar控件下面设置的属性一样,就是Toolbar的颜色

b. <item name=”colorPrimaryDark”>#3A5FCD</item>:状态栏颜色也就是标题栏上面的那个颜色,而我用的是小米1S测试的,其固件并没有很好的实现其功能。

c. <item name = “android:windowBackground”>@android:color/white</item>:窗口背景色,也就是Activity标题栏下面所有地方的颜色。

完整代码如下:

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> 
<!-- toolbar(actionbar)颜色 --> 
<item name="colorPrimary">#4876FF</item> 
<!-- 状态栏颜色 --> 
<item name="colorPrimaryDark">#3A5FCD</item> 
<!-- 窗口的背景颜色 --> 
<item name="android:windowBackground">@android:color/white</item>     
</style>
  1. Toolbar菜单样式

我们来看看如果直接使用上面的点击后边的菜单会有什么效果:

需要完成的任务是将该菜单的样式和其他App一样,在标题栏下面弹出。

首先要说明一下,toolbar菜单默认样式的父类为Widget.AppCompat.Light.PopupMenu.Overflow,那么要更改toobar中菜单的弹出的样式,就必须继承这个父类的样式。

其代码如下:

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> 
<!-- toolbar(actionbar)颜色 --> 
<item name="colorPrimary">#4876FF</item> 
<!-- 状态栏颜色 --> 
<item name="colorPrimaryDark">#3A5FCD</item> 
<!-- 窗口的背景颜色 --> 
<item name="android:windowBackground">@android:color/white</item>     
<item name="actionOverflowMenuStyle">@style/LYJMenuStyle</item>    
</style>

<style name="LYJMenuStyle" parent="@style/Widget.AppCompat.Light.PopupMenu.Overflow">     
<item name="overlapAnchor">false</item>
</style>

设置这一个属性那么其弹出的样式就与其他APP一样不会覆盖标题栏。

还有其他属性这里简要说明一下:

①<item name=”android:popupBackground”>?attr/colorPrimary</item>:弹出菜单背景色为标题栏的背景色

②<item name=”android:dropDownVerticalOffset”>0dip</item>:弹出菜单与标题栏的垂直间距

将这两条加入LYJMenuStyle中就可以了实现如下图所示的弹出菜单:

*后介绍一下菜单里面重要的几个属性:

app:showAsAction有三个值:
always:总是显示在界面上never:不显示在界面上,只让出现在右边的三个点中ifRoom:如果有位置才显示,不然就出现在右边的三个点中

可以用 | 同时使用两个上面的值。

系统也为菜单提供了默认的分享菜单与查询菜单,代码如下:

app:actionViewClass=”android.support.v7.widget.SearchView”
app:actionProviderClass=”android.support.v7.widget.ShareActionProvider”

从名字想必不需要过多解释,*后一个就是显示的优先级,也就是显示的顺序,谁在*前面,谁在后面:

android:orderInCategory数值越小,显示靠前,且优先级*大。


Android Tips:Hello AppCompatActivity,Goodbye ActionBarActivity

Beautiful Dialogs

Previously, to open a dialog may have used the following code:

var builder = new AlertDialog.Builder (this);

builder.SetTitle (“Hello Dialog”)
.SetMessage (“Is this material design?”)
.SetPositiveButton (“Yes”, delegate { Console.WriteLine(“Yes”); })
.SetNegativeButton (“No”, delegate { Console.WriteLine(“No”); });

builder.Create().Show ();


继承AppCompatActivity的Activity隐藏标题栏

继承AppCompatActivity的Activity隐藏标题栏

继承了AppCompatActivity的Activity无法通过调用requestWindowFeature(Window.FEATURE_NO_TITLE)来隐藏标题栏。

public class MainActivity extends AppCompatActivity {   
      @Override    
      protected void onCreate(Bundle savedInstanceState) {        
            super.onCreate(savedInstanceState);        
            requestWindowFeature(Window.FEATURE_NO_TITLE); //无效   
            setContentView(R.layout.activity_main);   
      }
}

既然这条路走不通,我就在AndroidManifest.xml将主题改为@android:style/Theme.NoTitleBar

<application    
   android:icon="@mipmap/ic_launcher"    
   android:label="@string/app_name"    
   android:theme="@android:style/Theme.NoTitleBar"//运行出错
>

在Android Monitor中看到错误报告:You need to use a Theme.AppCompat theme (or descendant) with this activity.(你必须使用Theme.AppCompat主题或者其子类主题)

解决方法:

在AndroidManifest.xml中将主题更改为Theme.AppCompat.Light.NoActionBar、Theme.AppCompat.NoActionBar或Theme.AppCompat.DayNight.NoActionBar中的一个,即可隐藏标题栏

<application    
   android:icon="@mipmap/ic_launcher"    
   android:label="@string/app_name"    
   android:theme="@style/Theme.AppCompat.Light.NoActionBar"//成功隐藏标题栏
>

 

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速