0 Android开发艺术探索-目录

2020/01/14 posted in  Android开发艺术探索

01 Activity的生命周期和启动模式

  1. 典型情况:在有用户参与的情况下,Activity所经过的生命周期的改变
  2. 异常情况:Activity被系统回收或者由于当前设备的Configuration发生改变从而导致Activity被销毁重建。

1.1 Activity的生命周期全面分析 / 1

1.1.1 典型情况下的生命周期分析 / 2

  • onStart和onResume,onPause和onStop的区别?
    • onStart和onStop是从Activity是否可见来回调的
    • onResume和onPause是从Activity是否位于前台这个角度来回调的。
  • 当前Activity为A,打开ActivityB,A的onPause会先于B的onResume执行。所以不能在onPause中执行重量级的操作,因为onPause方法执行完新的Activity的onResume才可以执行。

1.1.2 异常情况下的生命周期分析 / 8

情况1:资源相关的系统配置发生改变导致Activity被杀死并重新创建

  1. Activity异常状态终止时,系统会调用onSaveInstanceState来保存Activity的状态
  2. 当Activity被重新创建时,系统会调用onRestoreInstanceState,同时把被销毁时保存的Bundle对象传递给方法。
  3. 系统默认恢复的:文本框的输入、ListView的滚动位置
  4. 保存和恢复View的层次结构:委托上层保存数据 Activity > Window > ViewGroup(DecorView) 委托思想 上层委托下层,父容器委托子元素处理事情。

情况2:内存资源不足导致低优先级的Activity被杀死

  1. Activity按优先级从高到低:
    1. 前台Activity:正在和用户交互的Activity
    2. 可见但非前台Activity:有弹窗的Activity
    3. 后台Activity:已经被暂停的Activity
  2. 系统配置发生改变时不重建Activity的方法:
    1. android:configChange="orientation"

1.2 Activity的启动模式 / 16

1.2.1 Activity的LaunchMode / 16

四种启动模式

  1. standard:标准模式
  2. singtop:栈顶复用模式,位于栈顶时不会重复创建
  3. singleTask:栈内复用模式,实例以上Activity全部出栈,把实例调到栈顶。
  4. singleInstance:单实例模式,创建实例时会创建新的任务栈

TaskAffinity 任务相关性

  1. taskAffinity:标识一个Activity所需要的任务栈的名字
  2. 默认Activity的栈名为包名
  3. 主要和singTask或allowTaskReparenting属性配对使用
    1. taskAffinity+singleTask,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中
    2. taskAffinity+allowTaskReparenting,应用A启动应用B的某个Activity后,此Activity的allowTaskReparenting为true时,此Activity会直接从应用A的任务栈转移到B的任务栈中。

1.2.2 Activity的Flags / 27

Flags影响启动模式、运行状态等

  1. FLAG_ACTIVITY_NEW_TASK
  2. FLAG_ACTIVITY_SINGLE_TOP
  3. FLAG_ACTIVITY_CLEAR_TOP
  4. FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

1.3 IntentFilter的匹配规则 / 28

Activity的隐式调用需要Intent能够匹配目标组件的IntentFilter中的过滤信息,如果不匹配将无法启动目标Activity。

匹配规则:

  1. 一个intent-filter中可以有多个cation、category、data
  2. 一个Intent同时匹配action、category、data 才算完全匹配,只有完全匹配才能成功启动目标Activity。
  3. 一个Activity 中可以有多个intent-filter,只要能匹配任何一组intent-filter即可成功启动对应的Activity。

  • action的匹配规则
    • 要求Intent中的action存在,且必须和过滤规则中的其中一个action相同。
  • category的匹配规则
    • 要求Intent 中如果含有category,那么所有的category都必须和过滤规则中的其中一个category相同。
  • data的匹配规则
    • 如果过滤规则中定义了data, 那么Intent 中必须也要定义可匹配的data。
    • data由两部分组成,mineType和URI。
      • mimeType指媒体类型,比如image/jpeg、audio/mpeg4-generic和video/*等,可以表示图片、文本、视频等不同的媒体格式。
      • URI包含的数据就比较多了,URI结构:scheme://host:port/path|pathPrefix|pathPattern
      • Scheme、Host、Port、Path等
  • 隐式启动一个Activity时,可能出现错误,判断是否有Activity符合匹配规则:
    • PackageManager.resolveActivity或Intent.resolveActivity,返回最佳匹配的Activity信息
    • queryIntentActivities,返回所有成功的Activity信息
2019/02/26 posted in  Android开发艺术探索

02 IPC机制

2.1 Android IPC简介

  1. IPC Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
    • 进程:一个执行单元,PC和移动设备上指一个程序或应用
    • 线程:CPU调度的最小单元,一种有限的系统资源
  2. 为什么需要多进程?
    1. 一个应用因为某些原因需要采用多进程模式来实现。比如某些模块运行在单独进程中,或者为了更多的内存空间
    2. 向其他应用获取数据

2.2 Android中的多进程模式

2.2.1 开启多进程模式

指定android:process属性开启多进程模式:

  • ":":当前应用的私有进程,其他应用的组件不可以和它跑在一个进程中;
  • ".":全局进程,其他应用通过ShareUID方式可以和他跑在同一个进程中。

Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程,需要这两个应用有相同的ShareUID并且签名相同才可以。在这种情况下,它们可以互相访问对方的私有数据,比如data目录,组件信息等,不管他们是否跑在同一个进程中。如果在同一个进程中,还可以共享内存数据。

2.2.2 多进程模式的运行机制

Android为每一个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间。

使用多进程会导致的问题:

  1. 静态成员和单例模式完全失效
  2. 线程同步机制完全失效
  3. SharedPreferences的可靠性下降
  4. Application会创建多次

2.3 IPC基础概念介绍

序列化:将对象的状态信息转换为可以存储或传输的形式的过程。

2.3.1 Serializable接口

  • serialVersionUID
    • 辅助序列化和反序列化
    • serialVersionUID相同才能反序列化成功
  • 两种变量不参与序列化:
    • 静态成员属于类,不属于对象
    • transient关键字标记的成员变量
  • 使用ObjectOutputStreamObjectInputStream进行对象的序列化和反序列化。
  • 重写writeObject()readObject()方法可以重写序列化和反序列化过程。
  • 反序列化失败的几种情况:未指定serialVersionUID;成员变量的数量、类型结构等变化时;非常规性改版:类名变化。

2.3.2 Parcelable接口

Parcelable主页方法:

  • writeToParcel:序列化
  • CREATOR:read方法,反序列化
  • describeContents:内容描述

Serializable和Parcelable:

  • 两者都可以实现序列化并可用于intent间的数据传递
  • Serializable使用简单,但是开销很大,推荐使用在存储设备或者网络传输;
  • Parcelable效率很高,主要用在内存序列化上。

2.3.3 Binder

Binder的理解:

  • 直观来说,Binder 是Android中的一个类,它实现了IBinder接口。
  • 从IPC角度来说,Binder是Android中的一种跨进程通信方式:
    • 从Android Framework角度来说,Binder是ServiceManager连接各种Manager (ActivityManager、WindowManager, 等等)和相应ManagerService的桥梁;
    • 从Android应用层来说,Binder 是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder 对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。

Android开发中, Binder主要用在Service中,包括AIDL和Messenger。其中普通的Service的Binder不涉及进程间通讯;而Messenger的底层其实就是AIDL。

  1. 新建一个AIDL示例,SDK会自动为我们生产AIDL所对应的Binder类。创建Book.javaBook.aidlIBookManager.aidl,Build后生成IBookManager.java

  • Book.java

    public class Book implements Parcelable {
        public int bookId;
    public String bookName;
    //...
    }
  • Book.aidl

    package com.wz.testaidl;
    import com.wz.testaidl.Book;
    parcelable Book ;
  • IBookManager.aidl

    package com.wz.testaidl;
    import com.wz.testaidl.Book;
    interface IBookManager{
    List<Book> getBookList();
    void addBook(in Book book);
    }

AIDL自动生成的java文件方法说明:

  • DESCRIPTOR:Binder的唯一标识,一般用当前Binder的类名表示。
  • asInterface:将服务端的Binder对象转换成客户端所需要的AIDL接口类型的对象;
    • 客户端和服务端位于相同进程,那么此方法返回的就是服务端Stub对象本身;
    • 否则返回系统封装后的Stub.proxy对象。
  • asBinder:用于返回当前的Binder对象
  • onTransact:运行在服务端的Binder线程池中,当客户端发起跨进程通讯时,远程请求会通过系统底层封装交由此方法处理。
  • Proxy#[Method]:代理类中的接口方法。内部实现:
    1. 首先创建该方法所需要的输入型参数Parcel对象_data和输出型参数Parcel对象_reply;
    2. 然后把参数写入_data中(如果有参数的话);
    3. 接着调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;
    4. 然后服务端的onTransace方法会被调用直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;
    5. 最后返回_reply中的数据。

首先,当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此远程请求;其它,由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。

Binder死亡通知
Binder的两个重要方法linkToDeathunlinkToDeath

  • 通过linkToDeath可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,然后就可以重新发起连接请求。
  • 声明一个DeathRecipient对象,DeathRecipient是一个接口,其内部只有一个方法binderDied,实现这个方法后就可以在Binder死亡的时候收到通知了。

    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
        @override
    public void binderDied(){
    if(mBookManager==null){
    return;
    }
    mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
    mBookManager=null;
    //TODO:重新绑定远程Service
    }
    }
  • 在客户端绑定远程服务成功后,给binder设置死亡代理:

    mService = IMessageBoxManager.Stub.asInterface(binder);
    binder.linkToDeath(mDeathRecipient,0);
    
  • 另外,通过Binder的isBinderAlive方法,也可以判断Binder是否死亡。

  • binderDiedonServiceDisconnected区别:

    • binderDied在客户端的Binder线程池中被回调
    • onServiceDisconnected在UI线程中被回调

2.4 Android中的IPC方式

2.4.1 使用Bundle

我们知道,四大组件中的三大组件( Activity、Service、 Receiver) 都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable 接口,所以它可以方便地在不同的进程间传输。基于这一点,当我们在一个进程中启动了另一个进程的Activity、 Service 和Receiver,我们就可以在Bundle 中附加我们需要传输给远程进程的信息并通过Intent 发送出去。当然,我们传输的数据必须能够被序列化,比如基本类型、实现了Parcellable 接口的对象、实现了Serializable 接口的对象以及一些Android支持的特殊对象,具体内容可以看Bundle这个类,就可以看到所有它支持的类型。Bundle不支持的类型我们无法通过它在进程间传递数据。

除了直接传递数据这种典型的使用场景,它还有一种特殊的使用场景。比如A进程正在进行一个计算,计算完成后它要启动B进程的一个组件并把计算结果传递给B进程,可是遗憾的是这个计算结果不支持放入Bundle中,因此无法通过Intent来传输,这个时候如果我们用其他IPC方式就会略显复杂。可以考虑如下方式:我们通过Intent启动进程B的一个Service组件(比如IntentService), 让Service在后台进行计算,计算完毕后再启动B进程中真正要启动的目标组件,由于Service也运行在B进程中,所以目标组件就可以直接获取计算结果,这样一来就轻松解决了跨进程的问题。这种方式的核心思想在于将原本需要在A进程的计算任务转移到B进程的后台Service中去执行,这样就成功地避免了进程间通信问题,而且只用了很小的代价。

2.4.2 使用文件共享

通过文件共享这种方式来共享数据对文件格式是没有具体要求的,比如可以是文本文件,也可以是XML文件,只要读/写双方约定数据格式即可。通过文件共享的方式也是有局限性的,比如并发读/写的问题,如果并发读/写,那么我们读出的内容就有可能不是最新的,如果是并发写的话那就更严重了。因此我们要尽量避免并发写这种情况的发生或者考虑使用线程同步来限制多个线程的写操作。文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。

当然,SharedPreferences 是个特例,SharedPreferences 是Android中提供的轻量级存储方案,它通过键值对的方式来存储数据,在底层实现上它采用XML文件来存储键值对,每个应用的SharedPreferences文件都可以在当前包所在的data目录下查看到。一般来说, 它的目录位于/data/data/package name/shared_ prefs 目录下,其中package name表示的是当前应用的包名。从本质上来说,SharedPreferences 也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问,Sharedpreferences有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences

2.4.3 使用Messenger

  1. 构建服务端Service,运行在独立进程中:

    public class MessengerService extends Service {
        private static final String TAG = "MessengerService";
    @Override
    public IBinder onBind(Intent intent) {
    return mMessenger.getBinder();
    }
    private final Messenger mMessenger = new Messenger(new MessengerHandler());
    private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(@NonNull Message msg) {
    Log.e(TAG, "server receive msg: " + msg.what);
    final Messenger replyTo = msg.replyTo;
    final Message replyMsg = Message.obtain();
    replyMsg.what = 999;
    try {
    replyTo.send(replyMsg);
    } catch (RemoteException e) {
    e.printStackTrace();
    }
    }
    }
    }
    // AndroidManifext.xml
    <service android:name=".MessengerService"
    android:process=":remote"/>
  2. 客户端:

    1. 通过绑定服务端返回的binder创建Messenger对象,并通过这个Messenger对象向服务端发送消息。
    2. 服务端给客户端回复消息:使用Message的replyTo参数。
    public class MainActivity extends AppCompatActivity {
        private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final Intent intent = new Intent(this, MessengerService.class);
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
    private ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
    Log.e(TAG, "onServiceConnected: ");
    Messenger mServerMessenger = new Messenger(iBinder);
    final Message msg = Message.obtain();
    msg.what = 666;
    //赋值replyTo,服务端才可以回复消息
    msg.replyTo = mClientMessenger;
    try {
    mServerMessenger.send(msg);
    } catch (RemoteException e) {
    e.printStackTrace();
    }
    }
    @Override
    public void onServiceDisconnected(ComponentName componentName) {
    }
    };
    private Messenger mClientMessenger = new Messenger(new MessengerHandler());
    private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(@NonNull Message msg) {
    Log.e(TAG, "client receive msg: " + msg.what);
    }
    }
    @Override
    protected void onDestroy() {
    unbindService(serviceConnection);
    super.onDestroy();
    }
    }

    流程图如下:

总结:

  • Messenger 是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。
  • Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了,但是我们可以使用AIDL来实现跨进程的方法调用。

2.4.4 使用AIDL

使用AIDL来进行进程间通信的流程,分为服务端和客户端两个方面。

  1. 服务端:

    1. 创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明(见2.3.3)
    2. 创建一个Service用来监听客户端的连接请求,在Service中实现AIDL接口。
    public class BookMangerService extends Service {
        private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
    @Override
    public IBinder onBind(Intent intent) {
    return mBinder;
    }
    private IBinder mBinder = new IBookManager.Stub(){
    @Override
    public List<Book> getBookList() {
    return mBookList;
    }
    @Override
    public void addBook(Book book) {
    mBookList.add(book);
    }
    };
    }
    <service android:name=".BookMangerService"
        android:process=":remote"/>
    
  2. 客户端

    1. 首先需要绑定服务端的Service
    2. 绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
    public class MainActivity extends AppCompatActivity {
        private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final Intent intent = new Intent(this, BookMangerService.class);
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
    private ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
    final IBookManager bookManager = IBookManager.Stub.asInterface(iBinder);
    try {
    Log.e(TAG, "bookList.size(): " + bookManager.getBookList().size());
    bookManager.addBook(new Book());
    bookManager.addBook(new Book());
    Log.e(TAG, "bookList.size(): " + bookManager.getBookList().size());
    } catch (RemoteException e) {
    e.printStackTrace();
    }
    }
    @Override
    public void onServiceDisconnected(ComponentName componentName) {
    }
    };
    @Override
    protected void onDestroy() {
    unbindService(serviceConnection);
    super.onDestroy();
    }
    }

RemoteCallbackList

在解注册的过程中,服务端无法找到之前注册的那个listener,因为Binder会把客户端传递过来的对象重新转化并生成一个新的对象。

RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。Remote一CallbackList是一个泛型,支持管理任意的AIDL接口。内部有一个Map结构专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value 是Callback类型,如下所示。

public class RemoteCallbackList<E extends IInterface>{
    ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>() ;
}

其中Callback中封装了真正的远程listener。 当客户端注册listener 的时候,它会把这个listener的信息存入mCallbacks中,其中key和value分别通过下面的方式获得:

IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);

当客户端解注册的时候,只要遍历服务端所有的listener,找出那个和解注册listener具有相同Binder对象的服务端listener并把它删除就可以了。当客户端进程终止后,RemoteCallbackList能够自动移除客户端所注册的listener。RemoteCallbackList内部自动实现了线程同步的功能,所以使用它来注册和解注册时,不需要做额外的线程同步工作。

使用RemoteCallbackList,有一点需要注意。我们无法像操作List一样去操作它,尽管它的名字中也带个List,但是它并不是一个List。遍历RemoteCallbackList,必须要按照下面的方式进行,其中beginBroadcastfinishBroadcast必须配对使用,哪怕我们仅仅是想要获取RemoteCallbackList的元素个数。

final int N = mListenerList.beginBroadcast();
for(int i=0;i<N;i++){   
    IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
    if(l!=null){
        //TODO
    }
}
mListenerList.finishBroadcast();

如何在AIDL中使用权限验证功能?

  1. 第一种方法:可以在onBind中进行验证,验证不通过就直接返回null。然后在AndroidMenifest中声明所需的权限。

    public IBinder onBind(Intent intent){
        int check = checkCallingOrSelfPermission("xx.xx.xx");
    if(check==PackageManager.PERMISSION_DENIED){
    return null;
    }
    return mBinder;
    }
    <uses-permission android:name="xx.xx.xx"/>
    
  2. 第二种方法:可以在服务端的onTransact中进行权限验证,如果验证失败,就直接返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果。可以验证permission,也可以验证Uid和Pid。

    public boolean onTransact(int code,Parcel data,Parcel reply,int flags) throws RemoteException{
        int check = checkCallingOrSelfPermission("xx.xx.xx");
    if(check==PackageManager.PERMISSION_DENIED){
    return false;
    }
    String packageName = null;
    String[] packages = getPackageManager().getPackgesForUid(getCallingUid());
    if(packages!=null&&packages.length>0){
    packageName = packages[0];
    }
    if(!packageName.startWith("xx.xx")){
    return false;
    }
    return super.onTransact(code,data,reply,flags);
    }
  3. 还可以为Service指定android:permission属性等。

2.4.5 使用ContentProvider

2.4.6 使用Socket

2.5 Binder连接池

随着AIDL数量的增加,我们不能无限制地增加Service,Service 是四大组件之一一,本身就是一种系统资源。针对上述问题,我们需要减少Service的数量,将所有的AIDL放在同一个Service中去管理。

每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节我们要单独开来,然后向服务端提供自己的唯一标识和其对应的 Binder 对象;对于服务端来说,只需要一个 Service就可以了,服务端提供一个queryBinder 接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。由此可见,Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service中去执行,从而避免了重复创建Service的过程。

2.6 选用合适的IPC方式

2019/02/28 posted in  Android开发艺术探索

03 View的事件体系

3.1 View基础知识

3.1.1 什么是View

  • View是Android中所有控件的基类,是界面层的控件的一种抽象
  • ViewGroup,继承View,内部包含了许多控件
  • 通过View和ViewGroup构成View树的结构,类似于Web中的Dom树

3.1.2 View的位置参数

  • 坐标:X轴和Y轴,正方向为右和下
  • View的属性:
    • x/y:View左上角的位置
    • top/left/right/bottom:上下左右
    • translationX/translationY:平移量

3.1.3 MotionEvent和TouchSlop

1.MotionEvent
手指接触屏幕后产生的事件。

属性 说明
ACTION_DOWN 按下
ACTION_MOVE 滑动
ACTION_UP 离开
ACTION_CANCEL 取消
ACTION_OUTSIDE 超出边界
ACTION_POINTER_DOWN 多点按下
ACTION_POINTER_UP 多点离开

ACTION_CANCEL:从当前控件转移到外层控件时会触发

获得点击事件发生时的X Y坐标:

  • getX()/getY():相对控件的位置
  • getRawX()/getRawY():相对整个屏幕的位置

2.TouchSlop

系统能识别的滑动最小距离,和设备相关。
获取方法:

ViewConfiguration.get(getContext()).getScaledTouchSlop()

3.1.4 VelocityTracker、GestureDetector和Scroller

1.VelocityTracker

速度追踪

使用:
首先在view的onTouchEvent方法中追踪当前单击事件的速度:

VelocityTracker velocityTracker = VelocityTracker.obtain();//实例化一个VelocityTracker 对象
velocityTracker.addMovement(event);//添加追踪事件

接着在ACTION_UP事件中获取当前的速度:

velocityTracker .computeCurrentVelocity(1000);//指定时间
float xVelocity = velocityTracker .getXVelocity();//水平方向
float yVelocity = velocityTracker .getYVelocity();//垂直方向

最后,当不需要使用它的时候,释放、回收:

/*清除释放*/
velocityTracker.clear();
velocityTracker.recycle()

速度 = (终点位置-起点位置)/时间

2.GestureDetector

手势检测,辅助检测用户单击、滑动、长按、双击等行为

使用:

创建一个GestureDetecor对象并实现OnGestureListener接口:

GestureDetector mGestureDetector = new GestureDetector(this);//实例化一个GestureDetector对象
mGestureDetector.setIsLongpressEnabled(false);// 解决长按屏幕后无法拖动的现象

然后,在待监听view的onTouchEvent方法中添加如下实现:

//目标view的onTouchEvent方法中修改返回值
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

建议:

监听滑动,使用onTouchEvent
监听双击,使用GestureDetector

3.Scroller

使用Scroller的实现滑动的步骤:

  1. 初始化一个Scroller对象
  2. 重写View.computeScroll()方法
  3. 调用startScroll()开始滑动,invalidate()重绘
Scroller scroller = new Scroller(mContext); //实例化一个Scroller对象

@Override
public void computeScroll() {
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        ((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
         //通过不断的重绘不断的调用computeScroll方法
         invalidate();
    }  
}

public void smoothScrollTo(int destX,int destY){
      int scrollX=getScrollX();
      int delta=destX-scrollX;
      //1000秒内滑向destX
      mScroller.startScroll(scrollX,0,delta,0,2000);
      invalidate();
  }

3.2 View的滑动

3.2.1 使用scrollTo/scrollBy

  • scrollTo:基于所传参数的绝对滑动
  • scrollBy:基于当前位置的相对滑动,调用的也是scrollTo方法。

3.2.2 使用动画

1.视图动画:

//translate.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="300" android:duration="1000"/>
</set>

//Java代码
mCustomView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));

2.属性动画:

ObjectAnimator.ofFloat(mCustomView,"translationX",0,300).setDuration(1000).start();

3.2.3 改变布局参数

通过改变View的布局参数,实现View的滑动。

LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

3.2.4 各种滑动方式的对比

针对上面的分析做一下总结,如下所示:

  • scrollTo/scrollBy:操作简单,适合对 View 内容的滑动;
  • 动画:操作简单,主要适用于没有交互的View 和实现复杂的动画效果;
  • 改变布局参数:操作稍微复杂,适用于有交互的View。

3.3 弹性滑动

弹性滑动思想:将一次大的滑动分成若干次小的滑动
并在一个时间段內完成,弹性滑动的具体实现方式有很多,比如通过 Scroller、Handler#postDelayed以及Thread#sleep等。

3.3.1 使用Scroller

Scroller scroller = new Scroller(mContext); //实例化一个Scroller对象

@Override
public void computeScroll() {
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        ((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
         //通过不断的重绘不断的调用computeScroll方法
         invalidate();
    }  
}

public void smoothScrollTo(int destX,int destY){
      int scrollX=getScrollX();
      int delta=destX-scrollX;
      //1000秒内滑向destX
      mScroller.startScroll(scrollX,0,delta,0,2000);
      invalidate();
  }

分析:
Scroller.invalidate方法导致View重绘,View.draw方法被执行,然后在重写的View.computeScroll方法中,会判断computeScrollOffset,true则调用scrollTo/scrollBy滚动、invalidate/postInvalidate再次重绘,如此反复,直到computeScrollOffset为false。

3.3.2 通过动画

动画本身就是一种渐近的过程,因此通过它来实现的滑动天然就具有弹性效果,比如以下代码可以让一个 View 的内容在 100ms 内向左移动 100 像素。

ObjectAnimator.ofFloat (targetView, “translationX”, 0, 100).setDuration
(100).start () ;

我们还可以利用动画的特性来实现一些动画不能实现的效果,例如:

final int startX = 0;
final int deltaX = 100;

ValueAnimator animator = ValueAnimator.ofInt (0, 1).setDuration (1000);
animator.addUpdateListener (new AnimatorUpdateListener () {
    @Override
    public void onAnimationUpdate (ValueAnimator animator) {
        float fraction =  animator.getAnimatedFraction ();
        mButton1.scrollTo (startX + (int) (deltaX * fraction), 0);
    }
});
animator.start ();

利用onAnimationUpdate,我们就可以在动画的每一帧到来时获取动画完成的比例,然后再根据这个比例计算出当前View 所要滑动的距离。

3.3.3 使用延时策略

3.4 View的事件分发机制

3.4.1 点击事件的传递规则

所谓点击事件的事件分发,其实就是对 MotionEvent事件的分发过程,即当一个MotionEvent 产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。

事件分发的三个方法:

  • dispatchTouchEvent
    • 用来进行事件的分发。如果事件能够传递给当前 View,那么此方法一定会被调用,返回结果受当前View 的 onTouchEvent 和下级 View的dispatchTouchEvent 方法的影响,表示是否消耗当前事件
  • onInterceptTouchEvent
    • 在 dispatchTouchEvent 方法中调用,用来判断是否拦截某个事件,如果当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
  • onTouchEvent
    • 在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
//伪代码
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean result = false;
    if(onInterceptTouchEvent(ev)) { //是否拦截
        result = onTouchEvent(ev);  //拦截,处理事件
    } else {    //不拦截,分发事件
        result = child.dispatchTouchEvent(ev);
    }
    return result;
}

点击事件和监听优先级
onTouchListener > onTouchEvent > onClickListener

当一个点击事件产生后,它的传递过程遵循如下顺序: Activity-> Window-> View,即事件总是先传递给 Activity, Activity 再传递给 Window,最后后Window 再传递给顶级 View。顶级 View 接收到事件后,就会按照事件分发机制去分发事件。
考虑一种情况,如果一个View 的 onTouchEvent 返回false,那么它的父容器的 onTouchEvent 将会被调用,依此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 处理,即Activity的onTouchEvent 方法会被调用。

3.4.2 事件分发的源码解析

1.Activity对点击事件的分发过程

  1. Activity#dispatchTouchEvent

    public boolean dispatchTouchEvent (MotionEvent ev) {
        if (ev.getAction () == MotionEvent.ACTION DOWN) {
    onUserInteraction ();
    }
    if (getWindow ().superDispatchTouchEvent (ev)) {
    return true;
    }
    return onTouchEvent (ev) ;
    }
  2. Window#superDispatchTouchEvent

    Window 类可以控制顶级 View 的外观和行为策略,它的唯一实现位于 android.policy.PhoneWindow 。

    public abstract boolean superDispatchTouchEvent(MotionEvent event);
    
  3. PhoneWindow#superDispatchTouchEvent

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
    }
  4. DecorView#getDecorView

    顶层View,是一个FrameLayout。

2.顶级View对点击事件的分发过程

  • 如果顶级 ViewGroup 拦截事件即onInterceptTouchEvent 返回 true,则事件由 ViewGroup 处理,这时如果 ViewGroup 的mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent会被调用。也就是说,如果都提供的话,onTouch 会屏蔽掉 onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则 onClick 会被调用。
  • 如果顶级ViewGroup 不拦截事件,则事件会传递给它所在的点击事件链上的子 View,这时子 View 的 dispatchTouchEvent 会被调用。到此为止,事件已经从顶级 View 传递给了下一层 View,接下来的传递过程和顶级 View 是一致的,如此循环,完成整个事件的分发。

ViewGroup#dispatchTouchEvent

  1. 判断当前View是否拦截点击事件

    // Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
    || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action); // restore action in case it was changed
    } else {
    intercepted = false;
    }
    } else {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
    }
    • 判断拦截:
      1. MotionEvent.ACTION_DOWN
      2. mFirstTouchTarget != null
      3. 判断mGroupFlags,可通过子View的requestDisallowInterceptTouchEvent()设置
  2. ViewGroup遍历子所有View

    1. 判断子元素是否能接受这个点击事件。主要是通过两点:
      1. 子元素是否在播放动画
      2. 点击事件的着落点是否在子元素的区域内。
    2. dispatchTransformedTouchEvent()
      1. 实际就是调用子元素的dispatchTouchEvent()方法。
      2. 返回值为true,那么mFirstTouchTarget将会赋值并且跳出循环
      3. 返回值为false,将事件分发给下一个子View。
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--){
    ...
    //2. 通过dispatchTransformedTouchEvent()将事件传递给子View
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    // Child wants to receive touch within its bounds.
    mLastTouchDownTime = ev.getDownTime();
    if (preorderedList != null) {
    // childIndex points into presorted list, find original index
    for (int j = 0; j < childrenCount; j++) {
    if (children[childIndex] == mChildren[j]) {
    mLastTouchDownIndex = j;
    break;
    }
    }
    } else {
    mLastTouchDownIndex = childIndex;
    }
    mLastTouchDownX = ev.getX();
    mLastTouchDownY = ev.getY();
    //3.对mFirstTouchTarget赋值
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
    }
    ev.setTargetAccessibilityFocus(false);
    }
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
    final boolean handled;
    if (child == null) {
    handled = super.dispatchTouchEvent(transformedEvent);
    } else {
    handled = child.dispatchTouchEvent(transformedEvent);
    }
    return handled;
    }
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    //mFirstTouchTarget为链表结构
    mFirstTouchTarget = target;
    return target;
    }

    mFirstTouchTarget是否赋值,将会影响ViewGroup的拦截策略。如果mFirstTouchTarget为null,那么ViewGroup将会拦截下来同一序列的所有事件,自己处理并不再向子元素传递。那mFirstTouchTarget在什么情况下才为null呢?一般在两种情况下,

    • 要么是ViewGroup遍历了所有的子元素事件没有被处理;
    • 要么是子元素处理了ACTION_DOWN但是dispatchTouchEvent返回为false。
    if (mFirstTouchTarget == null) {
        // 此处的第三个参数为null,代表事件交给ViewGroup自己处理
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
    TouchTarget.ALL_POINTER_IDS);
    }

3.View对点击事件的处理过程

  1. 是否设置了onTouchListener()
  2. 是:调用onTouch()
  3. 否:调用onTouchEvent()

onTouch()的优先级要高于onTouchEvent()。

public boolean dispatchTouchEvent(MotionEvent event) {
    //...
    //首先判断是否设置了onTouchListener()
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    //调用onTouchEvent(event)
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    //...
    return result;
}

View的点击事件是在ACTION_UP事件中调用了performClick()方法处理,长按事件是在ACTION_DOWN事件中调用了checkForLongClick()方法处理。

public boolean onTouchEvent(MotionEvent event) {
    ...
    //如果View设有代理,将会执行TouchDelegate.onTouchEvent(event)
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    //只要View的CLICKABLE和LONG_CLICKABLE有一个返回true,他就会被消耗这个事件。
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                  ...
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            //点击事件
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    ...
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_DOWN:
                ...
                //长按事件
                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }
                ...
                break;
                ...
        }
        return true;
    }
    return false;
}

3.5 View的滑动冲突

3.5.1 常见的滑动冲突场景

  1. 外部滑动方向和内部滑动方向不一致;
  2. 外部滑动方向和内部滑动方向一致;
  3. 上面两种情况的嵌套。

3.5.2 滑动冲突的处理规则

  1. 场景1,当用户左右滑动时,让外部的View拦截点击事件;当用户上下滑动时,让内部View拦截点击事件。
  2. 场景2,根据业务不同进行判断,让外部或内部View拦截处理。
  3. 场景3,相对复杂,根据业务进行处理。

如何判断是左右滑动还是上下滑动?
根据水平或垂直方向的距离差夹角速度差进行判断。

3.5.3 滑动冲突的解决方式

1)外部拦截法

父容器如果需要此事件就拦截,重写父容器的onInterceptTouchEvent方法。

public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        intercepted = false;    //!!!
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        if (满足父容器的拦截要求) {
            intercepted = true;
        } else {
            intercepted = false;
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
    }
    default:
        break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}

2)内部拦截法

父容器不拦截任何事件,所有事件传递给子元素,如果子元素需要就直接消耗,否则就交给父容器处理。父容器需要的话,调用parent.requestDisallowInterceptTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        parent.requestDisallowInterceptTouchEvent(true);
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        if (父容器需要此类点击事件) {
            parent.requestDisallowInterceptTouchEvent(false);
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        break;
    }
    default:
        break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

重写父容器的onInterceptTouchEvent方法。

public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}
2019/12/11 posted in  Android开发艺术探索

04 View的工作原理

4.1 初识ViewRoot和DecorView

  1. ActivityThread#handleResumeActivity
    1. 当 Activity 对象被创建完毕后,会将 DecorView 通过 WindowManager 添加到 Window 中。
  2. WindowManagerGlobal#addView
    1. 创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
  3. ViewRootImpl#setView
    1. ViewRootImpl#requestLayout
    2. ViewRootImpl#scheduleTraversals
    3. ViewRootImpl.TraversalRunnable#performTraversals
      1. ViewRootImpl#performMeasure
      2. ViewRootImpl#performLayout
      3. ViewRootImpl#performDraw

4.2 理解MeasureSpec

4.2.1 MeasureSpec

  • 测量规格,决定View的大小。
  • 是一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)。
  • 三种模式:
    • UNSPECIFIED 父容器不对View有任何限制,要多大就给多大。常用于系统内部。
    • EXACTLY 父容器已经检测出View所需要的精确大小即SpecSize。对应LyaoutParams中的match_parent或具体数值。
    • AT_MOST 父容器为子视图指定一个最大尺寸SpecSize。对应LayoutParams中的wrap_content。
  • View的MeasureSpec由LayoutParams和父容器的LayoutParams共同决定。

4.2.2 MeasureSpec和LayoutParams的对应关系

4.3 View的工作流程

  1. 先将DecorView加载到Window中
  2. 然后开始View的绘制,调用ViewRootImpl的PerformTraversals方法
  3. performTraversals()中依次调用performMeasure()、performLayout()和performDraw()三个方法,分别完成顶级 View的绘制
  4. performMeasure() > measure() > onMeasure(),其中会实现子View的measure过程,layout和draw同理。

4.3.1 measure过程

测量View的宽高

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
  • View的Measure
    • measure() > onMeasure() > setMeasuredDimension() > getDefaultSize()
  • ViewGroup的Measure
    • measureChildren() > measureChild() > getChildMeasureSpec() > child.measure()

直接继承View的自定义View,需要重写onMeasure()并设置wrap_content时的自身大小,否则效果相当于macth_parent。

如何保证某个View已经测量完毕?

  1. Activity/View#onWindowFocusChanged
  2. view.post(runnable)
  3. ViewTreeObserver
  4. view.measure(int widthMeasureSpec , int heightMeasureSpec)

4.3.2 layout过程

确定元素的位置

  1. View的layout(),通过setFrame()来设定自己的四个顶点,确定自己的位置
  2. onLayout()确定子元素的位置,空方法,不同的ViewGroup实现不同。

4.3.3 draw过程

将View绘制到屏幕上

  1. 绘制背景(drawBackground)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

4.4 自定义View

4.4.1 自定义View的分类

  1. 继承View
    • 重写onDraw
    • 支持wrap_content、处理padding
  2. 继承ViewGroup
    • 处理自身和子元素的测量和布局
  3. 继承特定的View(如TextView)
  4. 继承ViewGroup(如LinearLayout)

4.4.2 自定义View须知

  1. 尽量不要在View中使用Handler,使用post
  2. 及时停止线程和动画
  3. 滑动嵌套时,处理滑动冲突
  4. 自定义属性
    1. values下创建自定义属性的xml
    2. View的构造方法中解析自定义属性并处理
    3. 在布局中使用自定义属性

4.4.3 自定义View示例

4.4.4 自定义View的思想

2019/12/11 posted in  Android开发艺术探索

05 理解RemoteViews

2020/01/14 posted in  Android开发艺术探索

06 Drawable

Drawable简介

可绘制对象资源是一般概念,是指可在屏幕上绘制的图形,以及可以使用 getDrawable(int) 等 API 检索或者应用到具有 android:drawableandroid:icon2 等属性的其他 XML 资源的图形。

详情见官方文档

Drawable分类

BitmapDrawable

表示一张图片

bitmap
    |- src="@drawable/res_id"
    |- antialias="[true | false]"
    |- dither="[true | false]"
    |- filter="[true | false]"
    |- tileMode="[disabled | clamp | repeat | mirror]"
    |- gravity="[top | bottom | left | right | center_vertical |
    |            fill_vertical | center_horizontal | fill_horizontal |
    |            center | fill | clip_vertical | clip_horizontal]"

NinePatchDrawable

表示一张.9格式的图片,可自动地根据所需的宽/高进行相应的缩放并保证不失真。

nine-patch
    |- src="@drawable/9_png_resid"
    |- dither="[true | false]"

ShapeDrawable

可表示纯色、有渐变效果的基础几何图形(矩形,圆形,线条等)。

<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="[rectangle | oval | line | ring]"
    <corners
        android:radius="integer"
        android:topLeftRaidus="integer"
        android:topRightRaidus="integer"
        android:bottomLeftRaidus="integer"
        android:bottomRightRaidus="integer" />
    <gradient
        android:angle="integer"
        android:centerX="integer"
        android:centerY="integer"
        android:centerColor="color"
        android:endColor="color"
        android:gradientRadius="integer"
        android:startColor="color"
        android:type="[linear | radial | sweep]"
        android:useLevel="[true | false]" />
    <padding
        android:left="integer"
        android:top="integer"
        android:right="integer"
        android:bottom="integer" />
    <size
        android:width="integer"
        android:height="integer" />
    <solid
        android:color="color" />
    <stroke
        android:width="integer"
        android:color="color"
        android:dashWidth="integer"
        android:dashGap="integer" />

LayerDrawable

表示一种层次化的Drawable集合,通过将不同的Drawable放置在不同的层上面从而达到一种叠加后的效果。

layer-list
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- id="@+id/xxx_id"
    |    |- top="dimension"
    |    |- left="dimension"
    |    |- right="dimension"
    |    |- bottom="dimension"

StateListDrawable

表示一个Drawable的集合,每个Drawable对应着View的一种状态。

selector
    |-constantSize="[true | false]"
    |-dither="[true | false]"
    |-variablePadding="[true | false]"
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- state_pressed="[true | false]"
    |    |- state_focused="[true | false]"
    |    |- state_selected="[true | false]"
    |    |- state_hovered="[true | false]"
    |    |- state_checked="[true | false]"
    |    |- state_checkable="[true | false]"
    |    |- state_enabled="[true | false]"
    |    |- state_activated="[true | false]"
    |    |- state_window_focused="[true | false]"

LevelListDrawable

表示一个Drawable集合,集合中的每个Drawable都有一个等级的概念。

level-list
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- maxLevel="integer"
    |    |- minlevel="integer"

TransitionDrawable

LayerDrawable的子类,实现两层 Drawable之间的淡入淡出效果。

transition
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- id="@+id/xxx_id"
    |    |- top="dimension"
    |    |- left="dimension"
    |    |- right="dimension"
    |    |- bottom="dimension"

InsetDrawable

表示把一个Drawable嵌入到另外一个Drawable的内部,并在四周留一些间距。

inset
    |- drawable="@drawable/drawable_id"
    |- visible="[true | false]"
    |- insetTop="dimension"
    |- insetLeft="dimension"
    |- insetRight="dimension"
    |- insetBottom="dimension"

ScaleDrawable

表示将Drawable缩放到一定比例。

scale
    |- drawable="@drawable/drawable_id"
    |- scaleGravity="[top | bottom | left | right |
        center_vertical | center_horizontal | center |
        fill_vertical | fill_horizontal | fill |
        clip_vertical | clip_horizontal]"
    |- scaleWidth="percentage"
    |- scaleHeight="percentage"

ClipDrawable

表示裁剪一个Drawable。

scale
    |- drawable="@drawable/drawable_id"
    |- gravity="[top | bottom | left | right |
        center_vertical | center_horizontal | center |
        fill_vertical | fill_horizontal | fill |
        clip_vertical | clip_horizontal]"
    |- clipOrientation="[vertical | horizontal]"
2019/03/06 posted in  Android开发艺术探索

07 Android动画深入分析

2019/03/06 posted in  Android开发艺术探索

08 理解Window和WindowManager

8.1 Window和WindowManager

Window表示的是一个窗口的概念,它的具体实现是PhoneWindow,创建一个Window很简单,只需要WindowManager去实现,WindowManager是外界访问Window的入口,Window的具体实现是在WindowManagerService中,他们两个的交互是一个IPC的过程,Android中的所有视图都是通过Window来实现的。

Flag参数

  • FLAG_NOT_FOCUSABLE
    • 表示窗口不需要获取焦点,也不需要接收各种事件,最终的事件会传递给下层的具体焦点的window
  • FLAG_NOT_TOUCH_MODAL
    • 在此模式下,系统会将当前window区域以外的单击事件传递给底层的Window,此前的Window区域以内的单机事件自己处理,
  • FLAG_SHOW_WHEN_LOCKED
    • 开启这个属性可以让window显示在锁屏上

Type参数表示window的类型,window有三种类型,分别是应用,子,系统,应用window对应一个Activity,子Window不能单独存在,需要依赖一个父Window。

Window是分层的,每个Window对应着z-ordered,层级大的会覆盖在层级小的Window上面,这和HTML中的z-index的概念是一致的,在这三类中,应用是层级范围是1-99,子window的层级是1000-1999,系统的层级是2000-2999。这些范围对应着type参数,如果想要window在最顶层,那么层级范围设置大一点就好了,很显然系统的值要大一些,系统的值很多,我们一般会选择TYPE_SYSTEM_OVERLAY和TYPE_SYSTEM_ERROR,还需要声明权限。

WindowManager所提供的功能很简单,常用的有三个方法,添加View,更新View,删除View,这三个方法定义在ViewManager中,而WindowManager继承自ViewManager。

    public interface ViewManager {
        public void addView(View view, ViewGroup.LayoutParams params);
        public void updateViewLayout(View view, ViewGroup.LayoutParams params);
        public void removeView(View view);
    }

我们常见的可以拖动的View,其实也很好实现,就是不断的更改他xy的位置:

    btn.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int rawX = (int) event.getRawX();
                int rawY = (int) event.getRawY();
                switch (event.getAction()) {
                    case MotionEvent.ACTION_MOVE:
                        layout.x = rawX;
                        layout.y = rawY;
                        wm.updateViewLayout(btn,layout);
                        break;
                }
                return false;
            }
        });

8.2 Window的内部机制

Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootlmpl,Window和View通过ViewRootImpl 来建立联系,因此Window并不是实际存在的,它是以View的形式存在。这点从WindowManager的定义也可以看出,它提供的三个接口方法addView、updateViewLayout以及removeView都是针对View的,这说明View才是Window存在的实体。在实际使用中无法直接访问Window, 对Window 的访问必须通过WindowManager。为了分析Window的内部机制,这里从Window的添加、删除以及更新说起。

8.2.1 Window的添加过程

WindowManagerImpl并没有直接去实现一个Window的三大操作,而是全部交给了WindowManagerGlobal来处理,WindowManagerGlobal是一个工厂的性质提供自己的实现。WindowManagerImpl这种工作模式就是典型的桥接模式,将所有的操作全部委托给WindowManagerGlobal去实现,WindowManagerGlobal的addView方法主要分如下几步:

  1. 检查参数是否合法,如果是子Window还需要调整一下参数
  2. 创建ViewRootImpl并将View添加到列表中
  3. 通过ViewRootImpl来更新界面并完成Window的添加

8.2.2 Window的删除过程

8.2.3 Window的更新过程

8.3 Window的创建过程

8.3.1 Activity的Window创建过程

8.3.2 Dialog的Window创建过程

8.3.3 Toast的Window创建过程

2019/03/06 posted in  Android开发艺术探索

09 四大组件的工作过程

9.1 四大组件的运行状态 / 316

1、Activity(展示型组件)

Activity的主要作用是展示一个界面并和用户交互,它扮演的是一种前台界面的角色。

  • 需要借助Intent启动。有显示Intent和隐式Intent。隐式Intent指向一个或多个目标Activity组件,当然也可能没有任何一个Activity组件可以处理这个隐式Intent。
  • 可以具有特定的启动模式,比如singleTop、singleTask等。
  • 通过Activity的finish方法来结束一个Activity组件的运行。

2、Service(计算型组件)

Service用于在后台执行一系列计算任务。

  • 启动状态:做后台计算,不需要和外界有直接的交互。
  • 绑定状态:这个时候Service内部同样可以进行后台计算,但是处于这种状态时外界可以很方便的和Service组件进行通信。
  • 尽管Service组件用于执行后台计算,但它本身是运行在主线程中的,因此耗时的后台计算仍然需要在单独的线程中去完成。
  • 灵活采用stopService和unBindService这两个方法才能完全停止一个Service组件。

3、BroadcastReceiver(消息型组件)

BroadcastReceiver用于在不同的组件乃至不同的应用之间传递消息。

  • 可以在AndroidManifest中静态注册
  • 动态注册,Context.registerReceiver()和 Context.unRegisterReceiver()
  • 通过Context的一系列send方法来发送广播,发送和接收过程的匹配是通过广播接收者的来描述的。
  • 不适合用来执行耗时操作。
  • BroadcastReceiver组件一般来说不需要停止,它也没有停止的概念。

4、ContentProvider(数据共享型组件)

ContentProvider用于向其他组件乃至其他应用共享数据。

  • 它的内部需要实现增删查改这四种操作,在它的内部维持着一份数据集合,这个数据集合既可以通过数据库来实现,也可以采用其他任何类型来实现,比如List和Map,ContentProvider对数据集合的具体实现并没有任何要求。
  • ContentProvider内部的insert、deleted、update和query方法需要处理好线程同步,因为这几个方法是在Binder线程池中被调用的。
  • ContentProvider无需手动停止。

9.2 Activity的工作过程 / 318

performLaunchActivity主要完成:

  1. 从ActivityClientRecord中获取待启动的Activity的组件信息
  2. 通过Instrumentation的newActivity方法使用类加载器创建Activity对象
  3. 通过LoadedApk的makeApplication方法来尝试创建新的Application对象
  4. 创建ContextImpl对象并通过Activity的attach方法来完成一些重要数据的初始化
  5. 调用Activity的onCreate()方法

9.3 Service的工作过程 / 336

9.3.1 Service的启动过程 / 336

handleCreateService主要完成

  1. 通过类加载器创建Service的实例
  2. 创建Application对象并调用onCreate()
  3. 创建ContextImpl并通过Service的attach方法建立联系
  4. 调用Service的onCreate方法并将Service对象存储到ActivityThread的一个列表中

9.3.2 Service的绑定过程 / 344

9.4 BroadcastReceiver的工作过程 / 352

9.4.1 广播的注册过程 / 353

9.4.2 广播的发送和接收过程 / 356

9.5 ContentProvider的工作过程 / 362

2019/03/06 posted in  Android开发艺术探索

10 Android的消息机制

10.1 Android的消息机制概述

Android的消息机制主要是指Handler的运行机制,从开发的角度来说,Handler 是Android消息机制的上层接口,通过它可以轻松地将一个任务切换到Handler所在的线程中去执行。Handler 的运行需要底层的MessageQueue和Looper的支撑。

  • MessageQueue 消息队列,内部存储了一组消息,以队列的形式对外提供插入和删除的工作,采用单链表的数据结构来存储消息列表。
  • Looper 消息循环。由于MessageQueue只是一个消息的存储单元,它不能去处理消息,而Looper就填补了这个功能,Looper 会以无限循环的形式去查找是否有新消息,如果有的话就处理消息,否则就一直等待着 。
  • ThreadLocal 并不是线程,它的作用是可以在每个线程中存储数据。我们知道,Handler 创建的时候会采用当前线程的Looper来构造消息循环系统,那么Handler内部如何获取到当前线程的Looper呢?这就要使用ThreadLocal了,ThreadLocal可以在不同的线程中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper。
  • 当然需要注意的是,线程是默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread, ActivityThread 被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。

10.2 Android的消息机制分析

子线程中无法访问UI

  • 如何解决子线程中无法访问UI
    • 答:使用Handler,将访问UI的工作切换到主线程。
  • 为什么子线程中无法访问UI
    • 答:这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态。那为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:首先加上锁机制会让UI访问的逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。鉴于这两个缺点,最简单且高效的方法就是采用单线程模型来处理UI操作,对于开发者来说也不是很麻烦,只是需要通过Handler切换一下UI访问的执行线程即可。

Handler工作原理

  1. 创建Handler和Looper。
    1. Handler创建时会采用当前线程的Looper来构建内部的消息循环系统,如果当前线程没有Looper,那么就会报错,需要手动创建Looper。
  2. 发送消息。
    1. 通过Handler的post方法将一个Runnable投递到Handler内部的Looper中去处理,
    2. 也可以通过Handler的send方法发送一个消息,这个消息同样会在Looper中去处理。其实post方法最终也是通过send方法来完成的,接下来主要来看一下send方法的工作过程。
    3. 当Handler的send方法被调用时,它会调用MessageQueue的enqueueMesssagge方法将这个消息放入消息队列中。
  3. 处理消息。
    1. Looper发现有新消息到来时,就会处理这个消息,最终消息中的Runnable或者Handler的handleMessage方法就会被调用。
    2. 注意Looper是运行在创建Handler所在的线程中的,这样一来Handler中的业务逻辑就被切换到创建Handler所在的线程中去执行了。

10.2.1 ThreadLocal的工作原理

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

在日常开发中用到ThreadLocal的地方较少,但是在某些特殊的场景下,通过ThreadLocal可以轻松地实现一些看起来很复杂的功能,这一点在Android的源码中也有所体现,比如Looper、 ActivityThread以及AMS中都用到了ThreadLocal。

ThreadLocal的使用场景:

  1. 当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于Handler来说,它需要获取当前线程的Looper, 很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取。
  2. ThreadLocal另一个使用场景是复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性,在这种情况下,我们又需要监听器能够贯穿整个线程的执行过程,这个时候可以怎么做呢?其实这时就可以采用ThreadLocal,采用ThreadLocal可以让监听器作为线程内的全局对象而存在,在线程内部只要通过get方法就可以获取到监听器。

10.2.2 消息队列的工作原理

消息队列在Android中指的是MessageQueue, MessageQueue主要包含两个操作:插入和读取。数据结构是单链表,单链表在插入和删除上比较有优势。

  • enqueueMessage 插入消息
  • next 读取并移除一条消息。

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

10.2.3 Looper的工作原理

Looper在Android的消息机制中扮演着消息循环的角色,具体来说就是它会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则就一直阻塞在那里。

Looper的工作原理

  1. Looper.prepare()创建Looper
    • Looper的构造方法中会创建一个MessageQueue
  2. Looper.loop()开启消息轮询
  3. msg.target.dispatchMessage(msg)消息的发送者Handler分发消息然后处理

Looper的其他方法:

  • prepareMainLooper方法,这个方法主要是给主线程也就是ActivityThread创建Looper使用的,其本质也是通过prepare方法来实现的。由于主线程的Looper比较特殊,所以Looper提供了一个getMainLooper方法,通过它可以在任何地方获取到主线程的Looper。
  • 退出Looper,Looper提供了quit和quitSafely来退出一个Looper,二者的区别是: quit 会直接退出Looper,而quitSafely 只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。Looper 退出后,通过Handler发送的消息会失败,这个时候Handler的send方法会返回false。在子线程中,如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终

10.2.4 Handler的工作原理

Handler的工作主要包含消息的发送和接收过程。消息的发送可以通过post 的一系列方法以及send的一 系列方法来实现, post的一系列方法最终 是通过send的一系列方法来实现的。发送一条消息的典型过程如下所示。

  1. 发送消息
    • Handler.sendMessageDelayed -> Handler.sendMessageAtTime() -> MessageQueue.enqueueMessage()
  2. 分发消息 (Handler#dispatchMessage)
    1. 使用Message的callback处理消息
    2. 创建Handler的实例handleCallback处理消息
    3. 调用Handler的handleMessage来处理消息

10.3 主线程的消息循环

主线程ActivityThread的main方法中:

  1. 创建主线程的Looper:Looper.prepareMainLooper();
  2. 开启主线程的消息循环:Looper.loop();

ActivityThread.H
负责ActivityThread中的消息队列进行,它内部定义了一组消息类型,主要包含了四大组件的启动和停止等过程,如下所示。

Activity和四大组件通信间通信:

  1. ActivityThread通过ApplicationThread和AMS发送请求
  2. AMS完成请求后回调ApplicationThread中的Binder方法,然后ApplicationThread会向H发送消息
  3. H收到消息后将ApplicationThread的逻辑切换到Activity中去执行
2019/03/06 posted in  Android开发艺术探索

11 Android的线程和线程池

  • AsyncTask封装了线程池和Handler,它主要是为了方便开发者在子线程中更新UI。
  • HandlerThread是一种具有消息循环的线程,在它的内部可以使用Handler。
  • IntentService是一个服务,系统对其进行了封装使其可以更方便地执行后台任务,IntentService 内部采用HandlerThread来执行任务,当任务执行完毕后IntentService 会自动退出。 它不容易被系统杀死从而可以尽量保证任务的执行。

11.1 主线程和子线程

  • 主线程:进程所拥有的线程,主要处理界面交互相关的逻辑。
  • 子线程:除主线程之外都是子线程,主要用于执行耗时操作。

11.2 Android中的线程形态

11.2.1 AsyncTask

轻量级的异步任务类,可以执行后台任务以及在子线程中进行UI操作。

主要方法:

  1. onPreExecute()
  2. doInBackground(Params…params):
  3. onProgressUpdate(Progress…values)
  4. onPostExecute(Result result)
  5. onCancelled()
  6. execute(Params...params)

11.2.2 AsyncTask的工作原理

  1. AsyncTask构造方法中:
    1. 实例化Handler,类型是InternalHandler,用于切换到主线程
    2. 实例化mWorker,类型是WorkerRunnable,封装了Params和Result;
    3. 实例化mFuture,类型是FutureTask,参数为mWorker。FutureTask是一个并发类,在这里它充当了Runnable的作用。
  2. 执行任务
    1. execute方法会调用executeOnExecutor(sDefaultExecutor, params)
      1. 参数sDefaultExecutor,类型为SerialExecutor,是一个串行的线程池,一个进程中所有的AsyncTask全部在这个串行的线程池中排队执行。
      2. SerialExecutor#execute。(1) ArrayDeque.offer(Runnable)。把FutureTask对象插入到任务队列mTasks中;(2) scheduleNext()。如果没有正在活动的任务,或者当一个任务执行完后,会调用scheduleNext方法执行下一个任务,直到所有的任务都被执行。scheduleNext中,调用THREAD_POOL_EXECUTOR.execute()真正地执行任务
    2. executeOnExecutor
      1. 调用onPreExecute()
      2. exec.execute(mFuture);线程池执行。
  3. 响应结果
    1. mWorker的call()中调用doInBackground(mParams)postResult()
    2. postResult中,Handler调用sendToTarget()发送消息。
    3. InternalHandler中,onProgressUpdatefinish
  4. 结束或取消onCancelledonPostExecute

    从Android 3.0 开始,默认情况下AsyncTask是串行执行的,可以调用executeOnExecutor方法并行执行。

11.2.3 HandlerThread

  • HandlerThread本质上是一个线程类,它继承了Thread;
  • HandlerThread有自己的内部Looper对象,可以进行looper循环;
  • 通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage方法中执行异步任务。
  • 创建HandlerThread后必须先调用HandlerThread.start()方法,Thread会先调用run方法,创建Looper对象。
  • 当不需要HandlerThread时,通过HandlerThread.quit()/quitSafely()方法来终止线程的执行

11.2.4 IntentService

可自动创建子线程来执行任务,且任务执行完毕后自动退出。

  • 在IntentService.onCreate()里创建一个Handle对象即HandlerThread,利用其内部的Looper会实例化一个ServiceHandler对象;
  • 任务请求的Intent会被封装到Message并通过ServiceHandler发送给Looper的MessageQueue,最终在HandlerThread中执行;
  • 在ServiceHandler.handleMessage()中会调用IntentService.onHandleIntent(),可在该方法中处理后台任务的逻辑。

11.3 Android中的线程池

线程池的优点:

  • 重用线程池中的线程,避免线程的创建和销毁带来的性能消耗;
  • 有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致阻塞现象;
  • 能够进行线程管理,提供定时/循环间隔执行等功能。

Android中的线程池的概念来源于Java中的Executor, Executor是一个接口,真正的线程池的实现为ThreadPoolExecutor。ThreadPoolExecutor 提供了一系列参数来配置线程池,通过不同的参数可以创建不同的线程池。

11.3.1 ThreadPoolExecutor

ThreadPoolExecutor构造方法:

public ThreadPoolExecutor(
    int corePoolSize,   //核心线程数
    int maximumPoolSize,//最大线程数
    long keepAliveTime,//非核心线程超时时间
    TimeUnit unit,//keepAliveTime参数的时间单位
    BlockingQueue<Runnable> workQueue,//任务队列
    ThreadFactory threadFactory,//线程工厂,可创建新线程
    RejectedExecutionHandler handler//饱和策略
)

ThreadPoolExecutor的默认工作策略:

  • 如果线程池中的线程数量未达到核心线程数,则会直接启动一个核心线程执行任务。
  • 若线程池中的线程数量已达到或者超过核心线程数量,则任务会被插入到任务列表等待执行。
  • 若任务无法插入到任务列表中,往往由于任务列表已满,此时如果:
    • 线程数量未达到线程池最大线程数,则会启动一个非核心线程执行任务;
    • 线程数量已达到线程池规定的最大值,则拒绝执行此任务

如果条件为否:核心线程 > 任务列表 > 非核心线程 > 拒接任务。

AsyncTask的THREAD_POOL_EXECUTOR线程池配置参数:

  • 核心线程数:CPU_COUNT - 1,最小为2最大为4;
  • 线程池的最大线程数:CPU_COUNT * 2+ 1;
  • 核心线程无超时机制,非核心线程在闲置时的超时时间为30秒;
  • 任务队列的容量为128。

11.3.2 线程池的分类

  • FixedThreadPool:
    • 含义:线程数量固定的线程池,所有线程都是核心线程,当线程空闲时不会被回收。
    • 特点:能快速响应外界请求。
  • CachedThreadPool:
    • 含义:线程数量不定的线程池(最大线程数为Integer.MAX_VALUE),只有非核心线程,空闲线程有超时机制,60s超时回收。
    • 特点:适合于执行大量的耗时较少的任务
  • ScheduledThreadPool:
    • 含义:核心线程数量固定,非核心线程数量不定。
    • 特点:定时任务和固定周期的任务。
  • SingleThreadExecutor:
    • 含义:只有一个核心线程,可确保所有的任务都在同一个线程中按顺序执行。
    • 特点:无需处理线程同步问题。
2019/03/06 posted in  Android开发艺术探索

12 Bitmap的加载和Cache

12.1 Bitmap的高效加载

BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四类方法最终是在Android的底层实现的,对应着BitmapFactory类的几个native方法。

如何高效地加载Bitmap 呢?其实核心思想也很简单,那就是采用BitmapFactory.Options来加载所需尺寸的图片。通过BitmapFactory.Options来缩放图片,主要是用到了它的inSampleSize参数,即采样率。

通过采样率即可有效地加载图片,那么到底如何获取采样率呢?获取采样率也很简单,遵循如下流程:

  1. 将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。
  2. 从BitmapFactory.Options 中取出图片的原始宽高信息,它们对应于outWidth 和outHeight参数。
  3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
  4. 将BitmapFactory.Options的inJustDecodeBounds 参数设为false, 然后重新加载图片。

这里说明一下inJustDecodeBounds参数,当此参数设为true时,BitmapFactory 只会解析图片的原始宽/高信息,并不会去真正地加载图片,所以这个操作是轻量级的。

12.2 Android中的缓存策略

缓存策略:内次缓存+存储缓存。当应用打算从网络上请求一张图片时,程序首先从内存中去获取,如果内存中没有那就从存储设备中去获取,如果存储设备中也没有,那就从网络上下载这张图片。

一般来说, 缓存策略主要包含缓存的添加、获取和删除这三类操作。删除缓存目前常用的一种缓存算法是LRU(Least Recently Used),LRU是近期最少使用算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存。

12.2.1 LruCache

LruCache它是一个泛型类,它内部采用一个LinkedHashMap,当强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。

  • 强引用:直接的对象引用
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。
  • 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

LruCache是线程安全的,因为用到了LinkedHashMap。

  • sizeOf 计算缓存对象的大小
  • LruCache.get(key) 获取一个缓存对象
  • LruCache.put(key, bitmap) 添加一个缓存对象
  • LruCache.remove删除一个指定的缓存对象。
  • entryRemoved 移除旧缓存时会调用
    • 可以进行一些资源回收工作

12.2.2 DiskLruCache

DiskLruCache用于实现存储设备缓存,即磁盘存储,它通过将缓存对象写入文件系统从而实现缓存的效果。

  1. DiskLruCache 的创建 open
  2. DiskLruCache 的缓存添加 edit commit abort
  3. DiskLruCache的缓存查找和删除 get remove delete

12.2.3 ImageLoader的实现

一般来说,一个优秀的ImageLoader应该具备如下功能:

  • 图片的同步加载
  • 图片的异步加载
  • 图片压缩
  • 内存缓存
  • 磁盘缓存
  • 网络拉取

实现步骤:

  1. 图片压缩功能的实现
    1. 根据宽高修改BitmapFactory.Options采样率
  2. 内存缓存和磁盘缓存的实现
    1. LruCache和DiskLruCache
  3. 同步加载和异步加载接口的设计
    1. 使用线程池和Handler

12.3 ImageLoader的使用

12.3.1 照片墙效果

12.3.2 优化列表的卡顿现象

  • 不要在getView中执行耗时操作。
  • 控制异步任务的执行频率。可以考虑在列表滑动的时候,停止加载图片,尽管这个过程是异步的,等列表停下来以后在加载图片仍然可以获得良好的用户体验。
  • 开启硬件加速可以解决莫名的卡顿问题,通过设置android:hardwareAccelerated = "true"即可为Activity开启硬件加速。
2019/03/06 posted in  Android开发艺术探索

13 综合技术

2020/01/14 posted in  Android开发艺术探索

14 JNI和NDK编程

2020/01/14 posted in  Android开发艺术探索

15 性能优化

本节介绍了一些有效的性能优化方法,主要内容包括布局优化、绘制优化、内存泄露优化、响应速度优化、ListView 优化、Bitmap 优化、线程优化以及一些性能优化建议,在介绍响应速度优化的同时还介绍了ANR日志的分析方法。

15.1.1 布局优化

布局优化的思想很简单,就是尽量减少布局文件的层级,布局中的层级少了,这就意味着Android绘制时的工作量少了,那么程序的性能自然就高了。

首先删除布局中无用的控件和层级,其次有选择地使用性能较低的ViewGroup。布局优化的另外一种手段是采用标签、 标签和ViewStub。

  • 标签主要用于布局重用;
  • 标签一般和配合使用,它可以降低减少布局的层级;
  • ViewStub则提供了按需加载的功能,当需要时才会将ViewStub中的布局加载到内存,这提高了程序的初始化效率。

15.1.2 绘制优化

绘制优化是指View的onDraw方法要避免执行大量的操作,这主要体现在两个方面。

  1. 首先,onDraw中不要创建新的局部对象,这是因为onDraw方法可能会被频繁调用,这样就会在一瞬间产生大量的临时对象,这不仅占用了过多的内存而且还会导致系统更加频繁gc,降低了程序的执行效率。
  2. 另外一方面,onDraw 方法中不要做耗时的任务,也不能执行成千上万次的循环操作,尽管每次循环都很轻量级,但是大量的循环仍然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。按照Google官方给出的性能优化典范中的标准,View 的绘制帧率保证60fps是最佳的,这就要求每帧的绘制时间不超过16ms ( 16ms= 1000/ 60),虽然程序很难保证16ms这个时间,但是尽量降低onDraw方法的复杂度总是切实有效的。

15.1.3 内存泄露优化

内存泄露的优化分为两个方面,一方面是在开发过程中避免写出有内存泄露的代码,另一方 面是通过一些分析工具比如来找出潜在的内存泄露继而解决。

  • 场景1:静态变量导致的内存泄露
  • 场景2:单例模式导致的内存泄露
  • 场景3:属性动画导致的内存泄露

15.1.4 响应速度优化和ANR日志分析

响应速度优化的核心思想是避免在主线程中做耗时操作,但是有时候的确有很多耗时操作,怎么办呢?
可以将这些耗时操作放在线程中去执行,即采用异步的方式执行耗时操作。响应速度过慢更多地体现在Activity 的启动速度上面,如果在主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至出现ANR。
Android 规定,Activity 如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR,而BroadcastReceiver 如果10秒钟之内还未执行完操作也会出现ANR。
在实际开发中,ANR是很难从代码上发现的,如果在开发过程中遇到了ANR,那么怎么定位问题呢?其实当一个进程发生ANR了以后,系统会在/data/anr目录下创建一个文件traces.txt,通过分析这个文件就能定位出ANR的原因。

15.1.5 ListView和Bitmap优化

ListView的优化(适用于GridView):

  1. 首先要采用ViewHolder并避免在getView中执行耗时操作;
  2. 其次要根据列表的滑动状态来控制任务的执行频率,比如当列表快速滑动时显然是不太适合开启大量的异步任务的;
  3. 最后可以尝试开启硬件加速来使Listview的滑动更加流畅。

Bitmap的优化,主要是通过BitmapFactory.Options来根据需要对图片进行采样,采样过程中主要用到了BitmapFactory.Options 的inSampleSize参数。

15.1.6 线程优化

线程优化的思想是采用线程池,避免程序中存在大量的Thread。线程池可以重用内部的线程,从而避免了线程的创建和销毁所带来的性能开销,同时线程池还能有效地控制线程池的最大并发数,避免大量的线程因互相抢占系统资源从而导致阻塞现象的发生。因此在实际开发中,我们要尽量采用线程池,而不是每次都要创建一个 Thread对象。

15.1.7 一些性能优化建议

  • 避免创建过多的对象;
  • 不要过多使用枚举,枚举占用的内存空间要比整型大;
  • 常量请使用static final来修饰;
  • 使用一 些Android 特有的数据结构,比如SparseArray和Pair等,它们都具有更好的性能;
  • 适当使用软引用和弱引用;
  • 采用内存缓存和磁盘缓存:
  • 尽量采用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄露。

15.2 内存泄露分析之MAT工具

15.3 提高程序的可维护性

2019/03/06 posted in  Android开发艺术探索