读书笔记(13)第13章 综合技术
最后更新于:2022-04-01 05:36:33
## 13.1 使用CrashHandler来获取应用的Crash信息
(1)应用发生Crash在所难免,但是如何采集crash信息以供后续开发处理这类问题呢?利用Thread类的`setDefaultUncaughtExceptionHandler`方法!`defaultUncaughtHandler`是Thread类的静态成员变量,所以如果我们将自定义的`UncaughtExceptionHandler`设置给Thread的话,那么当前进程内的所有线程都能使用这个UncaughtExceptionHandler来处理异常了。
~~~
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {
Thread.defaultUncaughtHandler = handler;
}
~~~
(2)作者实现了一个简易版本的UncaughtExceptionHandler类的子类`CrashHandler`,[源码传送门](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_13/CrashTest/src/com/ryg/crashtest/CrashHandler.java)
CrashHandler的使用方式就是在Application的`onCreate`方法中设置一下即可
~~~
//在这里为应用设置异常处理程序,然后我们的程序才能捕获未处理的异常
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
~~~
##13.2 使用multidex来解决方法数越界
(1)在Android中单个dex文件所能够包含的最大方法数是`65536`,这包含Android Framework、依赖的jar以及应用本身的代码中的所有方法。如果方法数超过了最大值,那么编译会报错`DexIndexOverflowException`。
有时方法数没有超过最大值,但是安装在低版本手机上时应用异常终止了,报错`Optimization failed`。这是因为应用在安装的时候,系统会通过`dexopt`程序来优化dex文件,在优化的过程中dexopt采用一个固定大小的缓冲区来存储应用中所有方法的信息,这个缓冲区就是`LinearAlloc`。LinearAlloc缓冲区在新版本的Android系统中大小是8MB或者16MB,但是在Android 2.2和2.3中却只有5MB,当待安装的应用的方法数比较多的时候,尽管它还没有达到最大方法数,但是它的存储空间仍然有可能超过5MB,这种情况下dexopt就会报错导致安装失败。
(2)如何解决方法数越界的问题呢? Google在2014年提出了简单方便的`multidex`的解决方案。
在Android 5.0之前使用multidex需要引入`android-support-multidex.jar`包,从Android 5.0开始,系统默认支持了multidex,它可以从apk中加载多个dex。Multidex方案主要针对AndroidStudio和Gradle编译环境。
使用Multidex的步骤:
1.在`build.gradle`文件中添加`multiDexEnabled true`
~~~
android {
...
defaultConfig {
...
multiDexEnabled true // [添加的配置] enable multidex support
}
...
}
~~~
2.添加对multidex的依赖
~~~
compile 'com.android.support:multidex:1.0.0'
~~~
3.在代码中添加对multidex的支持,这里有三种方案:
① 在AndroidManifest文件中指定Application为`MultiDexApplication`
~~~
<application android:name="android.support.multidex.MultiDexApplication"
...
</application>
~~~
② 让应用的Application继承自`MultiDexApplication`
③ 重写Application的`attachBaseContext`方法,这个方法要先于`onCreate`方法执行
~~~
public class TestApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this); //
}
}
~~~
采用上面的配置之后,如果应用的方法数没有越界,那么Gradle并不会生成多个dex文件;如果方法数越界后,Gradle就会在apk中打包2个或者多个dex文件,具体会打包多少个dex文件要看当前项目的代码规模。在有些情况下,可能需要指定主dex文件中所要包含的类,这个可以通过`--main-dex-list`选项来实现这个功能。
~~~
afterEvaluate {
println "afterEvaluate"
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
def listFile = project.rootDir.absolutePath + '/app/maindexlist.txt'
println "root dir:" + project.rootDir.absolutePath
println "dex task found: " + dx.name
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
dx.additionalParameters += '--main-dex-list=' + listFile
dx.additionalParameters += '--minimal-main-dex'
}
}
~~~
`--multi-dex`表明当方法数越界时生成多个dex文件,`--main-dex-list`指定了要在主dex中打包的类的列表,`--minimal-main-dex`表明只有`--main-dex-list`所指定的类才能打包到主dex中。multidex的jar包中的9个类必须要打包到主dex中,其次不能在Application中成员以及代码块中访问其他dex中的类,否个程序会因为无法加载对应的类而中止执行。
(3)Multidex方案可能带来的问题:
1.应用启动速度会降低,因为应用启动的时候会加载额外的dex文件,所以要避免生成较大的dex文件;
2.需要做大量的兼容性测试,因为Dalvik LinearAlloc的bug,可能导致使用multidex的应用无法在Android 4.0以前的手机上运行。
## 13.3 Android的动态加载技术
(1)动态加载技术又称插件化技术,将应用插件化可以减轻应用的内存和CPU占用,还可以在不发布新版本的情况下更新某些模块。不同的插件化方案各有特色,但是都需要解决三个基础性问题:资源访问,Activity生命周期管理和插件ClassLoader的管理。
(2)宿主和插件:宿主是指普通的apk,插件是经过处理的dex或者apk。在主流的插件化框架中多采用特殊处理的apk作为插件,处理方式往往和编译以及打包环节有关,另外很多插件化框架都需要用到代理Activity的概念,插件Activity的启动大多数是借助一个代理Activity来实现的。
(3)资源访问:宿主程序调起未安装的插件apk,插件中凡是R开头的资源都不能访问了,因为宿主程序中并没有插件的资源,通过R来访问插件的资源是行不通的。
Activity的资源访问是通过`ContextImpl`来完成的,它有两个方法`getAssets()`和`getResources()`方法是用来加载资源的。
具体实现方式是通过反射,调用`AssetManager`的`addAssetPath`方法添加插件的路径,然后将插件apk中的资源加载到`Resources`对象中即可。
(4)Activity生命周期管理:有两种常见的方式,反射方式和接口方式。反射方式就是通过反射去获取Activity的各个生命周期方法,然后在代理Activity中去调用插件Activity对应的生命周期方法即可。
反射方式代码繁琐,性能开销大。接口方式将Activity的生命周期方法提取出来作为一个接口,然后通过代理Activity去调用插件Activity的生命周期方法,这样就完成了插件Activity的生命周期管理。
(5)插件ClassLoader的管理:为了更好地对多插件进行支持,需要合理地去管理各个插件的`DexClassLoader`,这样同一个插件就可以采用同一个ClassLoader去加载类,从而避免了多个ClassLoader加载同一个类时所引起的类型转换错误。
其他详细信息看作者插件化框架[singwhatiwanna/dynamic-load-apk](https://github.com/singwhatiwanna/dynamic-load-apk)
## 13.4 反编译初步
1.主要介绍使用`dex2jar`和`jd-gui`反编译apk和使用`apktool`对apk进行二次打包,比较简单,略过不总结。
## 第14章 JNI和NDK编程
本章主要是介绍JNI和NDK编程入门知识,比较简答,略过不总结。
如果感兴趣NDK开发可以阅读我之前总结的[Android NDK和OpenCV整合开发系列文章](http://hujiaweibujidao.github.io/blog/2013/11/18/android-ndk-and-opencv-developement/)。
## 第15章 Android性能优化
(1)2015年Google关于Android性能优化典范的专题视频 [Youtube视频地址](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE)
(2)布局优化
1.删除布局中无用的组件和层级,有选择地使用性能较低的ViewGroup;
2.使用`<include>`、`<merge>`、`<viewstub>`等标签:`<include>`标签主要用于布局重用,`<merge>`标签一般和`<include>`配合使用,它可以减少布局中的层级;`<viewstub>`标签则提供了按需加载的功能,当需要的时候才会将ViewStub中的布局加载到内存,提供了程序的初始化效率。
3.`<include>`标签只支持`android:layout_`开头的属性,`android:id`属性例外。
4.`ViewStub`继承自View,它非常轻量级且宽高都为0,它本身不参与任何的布局和绘制过程。实际开发中,很多布局文件在正常情况下不会显示,例如网络异常时的界面,这个时候就没有必要在整个界面初始化的时候加载进行,通过ViewStub可以做到在需要的时候再加载。
如下面示例,`android:id`是ViewStub的id,而`android:inflatedId`是布局的根元素的id。
~~~
<ViewStub android:id="@+id/xxx"
android:inflatedId="@+id/yyy"
android:layout="@layout/zzz"
...
</ViewStub>
~~~
(3)绘制优化
1.在`onDraw`中不要创建新的布局对象,因为`onDraw`会被频繁调用;
2.`onDraw`方法中不要指定耗时任务,也不能执行成千上万次的循环操作。
(4)内存泄露优化
1.可能导致内存泄露的场景很多,例如静态变量、单例模式、属性动画、AsyncTask、Handler等等
(5)响应速度优化和ANR日志分析
1.ANR出现的情况:Activity如果`5s`内没有响应屏幕触摸事件或者键盘输入事件就会ANR,而BroadcastReceiver如果`10s`内没有执行完操作也会出现ANR。
2.当一个进程发生了ANR之后,系统会在`/data/anr`目录下创建一个文件`traces.txt`,通过分析这个文件就能定位ANR的原因。
(6)ListView和Bitmap优化
1.ListView优化:采用`ViewHolder`并避免在`getView`方法中执行耗时操作;根据列表的滑动状态来绘制任务的执行频率;可以尝试开启硬件加速来使ListView的滑动更加流畅。
2.Bitmap优化:根据需要对图片进行采样,详情看[Android开发艺术探索》读书笔记 (12) 第12章 Bitmap的加载和Cache](http://hujiaweibujidao.github.io/blog/2015/11/30/Art-of-Android-Development-Reading-Notes-12/)。
(7)线程优化
1.采用线程池,详情看[《Android开发艺术探索》读书笔记 (11) 第11章 Android的线程和线程池](http://hujiaweibujidao.github.io/blog/2015/12/03/Art-of-Android-Development-Reading-Notes-11/)。
(8)其他优化建议
1.不要过多使用枚举,枚举占用的内存空间要比整型大;
2.常量请使用`static final`来修饰;
3.使用一些Android特有的数据结构,比如`SparseArray`和`Pair`等,他们都具有更好的性能;
4.适当使用软引用和弱引用;
5.采用内存缓存和磁盘缓存;
6.尽量采用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄露。
(9)MAT是功能强大的内存分析工具,主要有`Histograms`和`Dominator Tree`等功能
读书笔记(12)第12章 Bitmap的加载和Cache
最后更新于:2022-04-01 05:36:31
## 12.1 Bitmap的高速加载
(1)Bitmap是如何加载的?
`BitmapFactory`类提供了四类方法:`decodeFile`、`decodeResource`、`decodeStream`和`decodeByteArray`从不同来源加载出一个Bitmap对象,最终的实现是在底层实现的。
如何高效加载Bitmap?
采用`BitmapFactory.Options`按照一定的采样率来加载所需尺寸的图片,因为imageview所需的图片大小往往小于图片的原始尺寸。
(2)BitmapFactory.Options的`inSampleSize`参数,即采样率
官方文档指出采样率的取值应该是2的指数,例如k,那么采样后的图片宽高均为原图片大小的 1/k。
如何获取采样率?
下面是常用的获取采样率的代码片段:
~~~
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
~~~
将`inJustDecodeBounds`设置为true的时候,BitmapFactory只会解析图片的原始宽高信息,并不会真正的加载图片,所以这个操作是轻量级的。需要注意的是,这个时候BitmapFactory获取的图片宽高信息和图片的位置以及程序运行的设备有关,这都会导致BitmapFactory获取到不同的结果。
## 12.2 Android中的缓存策略
(1)最常用的缓存算法是LRU,核心是当缓存满时,会优先淘汰那些近期最少使用的缓存对象,系统中采用LRU算法的缓存有两种:`LruCache`(内存缓存)和`DiskLruCache`(磁盘缓存)。
(2)LruCache是Android 3.1才有的,通过support-v4兼容包可以兼容到早期的Android版本。LruCache类是一个线程安全的泛型类,它内部采用一个`LinkedHashMap`以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。
(3)DiskLruCache磁盘缓存,它不属于Android sdk的一部分,[它的源码可以在这里下载](https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java)
DiskLruCache的创建、缓存查找和缓存添加操作
(4)ImageLoader的实现 [具体内容看源码](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_12/src/com/ryg/chapter_12/loader/ImageLoader.java)
功能:图片的同步/异步加载,图片压缩,内存缓存,磁盘缓存,网络拉取
## 12.3 ImageLoader的使用
避免发生列表item错位的解决方法:给显示图片的imageview添加`tag`属性,值为要加载的图片的目标url,显示的时候判断一下url是否匹配。
优化列表的卡顿现象
(1)不要在getView中执行耗时操作,不要在getView中直接加载图片,否则肯定会导致卡顿;
(2)控制异步任务的执行频率:在列表滑动的时候停止加载图片,等列表停下来以后再加载图片;
(3)使用硬件加速来解决莫名的卡顿问题,给Activity添加配置`android:hardwareAccelerated="true"`。
本章的精华就是作者写的[ImageLoader类](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_12/src/com/ryg/chapter_12/loader/ImageLoader.java),建议阅读源码感受下。
读书笔记(11)第11章 Android的线程和线程池
最后更新于:2022-04-01 05:36:28
##11.1 主线程和子线程
(1)在Java中默认情况下一个进程只有一个线程,也就是主线程,其他线程都是子线程,也叫工作线程。Android中的主线程主要处理和界面相关的事情,而子线程则往往用于执行耗时操作。线程的创建和销毁的开销较大,所以如果一个进程要频繁地创建和销毁线程的话,都会采用线程池的方式。
(2)在Android中除了Thread,还有`HandlerThread`、`AsyncTask`以及`IntentService`等也都扮演着线程的角色,只是它们具有不同的特性和使用场景。AsyncTask封装了线程池和Handler,它主要是为了方便开发者在子线程中更新UI。HandlerThread是一种具有消息循环的线程,在它的内部可以使用Handler。IntentService是一个服务,它内部采用HandlerThread来执行任务,当任务执行完毕后就会自动退出。因为它是服务的缘故,所以和后台线程相比,它比较不容易被系统杀死。
(3)从Android 3.0开始,系统要求网络访问必须在子线程中进行,否则网络访问将会失败并抛出`NetworkOnMainThreadException`这个异常,这样做是为了避免主线程由于被耗时操作所阻塞从而出现ANR现象。
(4)AsyncTask是一个抽象泛型类,它提供了`Params`、`Progress`、`Result`三个泛型参数,如果task确实不需要传递具体的参数,那么都可以设置为`Void`。下面是它的四个核心方法,其中`doInBackground`不是在主线程执行的。
`onPreExecute`、`doInBackground`、`onProgressUpdate`、`onPostResult`
## 11.2 Android中的线程形态
(1)`AsyncTask`的使用过程中的条件限制:
1.AsyncTask的类必须在主线程中加载,这个过程在Android 4.1及以上版本中已经被系统自动完成。
2.AsyncTask对象必须在主线程中创建,`execute`方法必须在UI线程中调用。
3.一个AsyncTask对象只能执行一次,即只能调用一次`execute`方法,否则会报运行时异常。
4.在Android 1.6之前,AsyncTask是串行执行任务的,Android 1.6的时候AsyncTask开始采用线程池并行处理任务,但是从Android 3.0开始,为了避免AsyncTask带来的并发错误,AsyncTask又采用一个线程来串行执行任务。尽管如此,在Android 3.0以及后续版本中,我们可以使用AsyncTask的`executeOnExecutor`方法来并行执行任务。但是这个方法是Android 3.0新添加的方法,并不能在低版本上使用。
(2)AsyncTask的原理
1.AsyncTask中有两个线程池:`SerialExecutor`和`THREAD_POOL_EXECUTOR`。前者是用于任务的排队,默认是串行的线程池;后者用于真正执行任务。AsyncTask中还有一个Handler,即`InternalHandler`,用于将执行环境从线程池切换到主线程。AsyncTask内部就是通过InternalHandler来发送任务执行的进度以及执行结束等消息。
2.AsyncTask排队执行过程:系统先把参数`Params`封装为`FutureTask`对象,它相当于Runnable;接着将FutureTask交给SerialExecutor的`execute`方法,它先把FutureTask插入到任务队列tasks中,如果这个时候没有正在活动的AsyncTask任务,那么就会执行下一个AsyncTask任务,同时当一个AsyncTask任务执行完毕之后,AsyncTask会继续执行其他任务直到所有任务都被执行为止。
(3)`HandlerThread`就是一种可以使用Handler的Thread,它的实现就是在run方法中通过`Looper.prepare()`来创建消息队列,并通过`Looper.loop()`来开启消息循环,这样在实际的使用中就允许在HandlerThread中创建Handler了,外界可以通过Handler的消息方式通知HandlerThread执行一个具体的任务。HandlerThread的run方法是一个无限循环,因此当明确不需要再使用HandlerThread的时候,可以通过它的`quit`或者`quitSafely`方法来终止线程的执行。HandlerThread的最主要的应用场景就是用在IntentService中。
(4)`IntentService`是一个继承自Service的抽象类,要使用它就要创建它的子类。IntentService适合执行一些高优先级的后台任务,这样不容易被系统杀死。IntentService的`onCreate`方法中会创建HandlerThread,并使用HandlerThread的Looper来构造一个Handler对象ServiceHandler,这样通过ServiceHandler对象发送的消息最终都会在HandlerThread中执行。IntentService会将Intent封装到Message中,通过ServiceHandler发送出去,在ServiceHandler的`handleMessage`方法中会调用IntentService的抽象方法`onHandleIntent`,所以IntentService的子类都要是实现这个方法。
##11.3 Android中的线程池
(1)使用线程池的好处:
1.重用线程,避免线程的创建和销毁带来的性能开销;
2.能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象;
3.能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。
(2)`Executor`只是一个接口,真正的线程池是`ThreadPoolExecutor`。ThreadPoolExecutor提供了一系列参数来配置线程池,通过不同的参数可以创建不同的线程池,Android的线程池都是通过`Executors`提供的工厂方法得到的。
(3)ThreadPoolExecutor的构造参数
1.`corePoolSize`:核心线程数,默认情况下,核心线程会在线程中一直存活;
2.`maximumPoolSize`:最大线程数,当活动线程数达到这个数值后,后续的任务将会被阻塞;
3.`keepAliveTime`:非核心线程闲置时的超时时长,超过这个时长,闲置的非核心线程就会被回收;
4.`unit`:用于指定keepAliveTime参数的时间单位,有`TimeUnit.MILLISECONDS`、`TimeUnit.SECONDS`、`TimeUnit.MINUTES`等;
5.`workQueue`:任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中;
6.`threadFactory`:线程工厂,为线程池提供创建新线程的功能。它是一个接口,它只有一个方法`Thread newThread(Runnable r)`;
7.`RejectedExecutionHandler`:当线程池无法执行新任务时,可能是由于任务队列已满或者是无法成功执行任务,这个时候就会调用这个Handler的`rejectedExecution`方法来通知调用者,默认情况下,`rejectedExecution`会直接抛出一个`rejectedExecutionException`。
(4)ThreadPoolExecutor执行任务的规则:
1.如果线程池中的线程数未达到核心线程的数量,那么会直接启动一个核心线程来执行任务;
2.如果线程池中的线程数量已经达到或者超过核心线程的数量,那么任务会被插入到任务队列中排队等待执行;
3.如果在步骤2中无法将任务插入到的任务队列中,可能是任务队列已满,这个时候如果线程数量没有达到规定的最大值,那么会立刻启动非核心线程来执行这个任务;
4.如果步骤3中线程数量已经达到线程池规定的最大值,那么就拒绝执行此任务,ThreadPoolExecutor会调用`RejectedExecutionHandler`的`rejectedExecution`方法来通知调用者。
(5)AsyncTask的THREAD_POOL_EXECUTOR线程池的配置:
1.`corePoolSize`=CPU核心数+1;
2.`maximumPoolSize`=2倍的CPU核心数+1;
3.核心线程无超时机制,非核心线程在闲置时间的超时时间为`1s`;
4.任务队列的容量为`128`。
(6)Android中常见的4类具有不同功能特性的线程池:
1.`FixedThreadPool`:线程数量固定的线程池,它只有核心线程;
2.`CachedThreadPool`:线程数量不固定的线程池,它只有非核心线程;
3.`ScheduledThreadPool`:核心线程数量固定,非核心线程数量没有限制的线程池,主要用于执行定时任务和具有固定周期的任务;
4.`SingleThreadPool`:只有一个核心线程的线程池,确保了所有的任务都在同一个线程中按顺序执行。
读书笔记(10)第10章 Android的消息机制
最后更新于:2022-04-01 05:36:26
## 10.1 Android消息机制概述
(1)Android的消息机制主要是指Handler的运行机制,其底层需要`MessageQueue`和`Looper`的支撑。MessageQueue是以单链表的数据结构存储消息列表但是以队列的形式对外提供插入和删除消息操作的消息队列。MessageQueue只是消息的存储单元,而Looper则是以无限循环的形式去查找是否有新消息,如果有的话就去处理消息,否则就一直等待着。
(2)Handler的主要作用是将一个任务切换到某个指定的线程中去执行。
为什么要提供这个功能呢?
Android规定UI操作只能在主线程中进行,`ViewRootImpl`的`checkThread`方法会验证当前线程是否可以进行UI操作。
为什么不允许子线程访问UI呢?
这是因为UI组件不是线程安全的,如果在多线程中并发访问可能会导致UI组件处于不可预期的状态。另外,如果对UI组件的访问进行加锁机制的话又会降低UI访问的效率,所以还是采用单线程模型来处理UI事件。
(3)Handler的创建会采用当前线程的Looper来构建内部的消息循环系统,如果当前线程中不存在Looper的话就会报错。Handler可以用`post`方法将一个Runnable投递到消息队列中,也可以用`send`方法发送一个消息投递到消息队列中,其实`post`最终还是调用了`send`方法。
## 10.2 Android的消息机制分析
(1)`ThreadLocal`的工作原理
1.ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,可以考虑使用ThreadLocal。 对于Handler来说,它需要获取当前线程的Looper,而Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以实现Looper在线程中的存取了。
2.ThreadLocal的原理:不同线程访问同一个ThreadLocal的`get`方法时,ThreadLocal内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找出对应的value值,不同线程中的数组是不同的,这就是为什么通过ThreadLocal可以在不同线程中维护一套数据的副本并且彼此互不干扰。
3.ThreadLocal是一个泛型类`public class ThreadLocal<T>`,下面是它的`set`方法
~~~
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
~~~
`Values`是Thread类内部专门用来存储线程的ThreadLocal数据的,它内部有一个数组`private Object[] table`,ThreadLocal的值就存在这个table数组中。如果values的值为null,那么就需要对其进行初始化然后再将ThreadLocal的值进行存储。
ThreadLocal数据的存储规则:ThreadLocal的值在table数组中的存储位置总是ThreadLocal的索引+1的位置。
(2)`MessageQueue`的工作原理
1.MessageQueue其实是通过单链表来维护消息列表的,它包含两个主要操作`enqueueMessage`和`next`,前者是插入消息,后者是取出一条消息并移除。
2.next方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞在这里。当有新消息到来时,next方法会返回这条消息并将它从链表中移除。
(3)`Looper`的工作原理
1.为一个线程创建Looper的方法,代码如下所示
~~~
new Thread("test"){
@Override
public void run() {
Looper.prepare();//创建looper
Handler handler = new Handler();//可以创建handler了
Looper.loop();//开始looper循环
}
}.start();
~~~
2.Looper的`prepareMainLooper`方法主要是给主线程也就是`ActivityThread`创建Looper使用的,本质也是调用了`prepare`方法。
3.Looper的`quit`和`quitSafely`方法的区别是:前者会直接退出Looper,后者只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。Looper退出之后,通过Handler发送的消息就会失败,这个时候Handler的send方法会返回false。
在子线程中,如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。
4.Looper的`loop`方法会调用`MessageQueue`的`next`方法来获取新消息,而next是一个阻塞操作,当没有消息时,next方法会一直阻塞着在那里,这也导致了loop方法一直阻塞在那里。如果MessageQueue的next方法返回了新消息,Looper就会处理这条消息:`msg.target.dispatchMessage(msg)`,其中的`msg.target`就是发送这条消息的Handler对象。
(4)Handler的工作原理
1.Handler就是处理消息的发送和接收之后的处理;
2.Handler处理消息的过程
~~~
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);//当message是runnable的情况,也就是Handler的post方法传递的参数,这种情况下直接执行runnable的run方法
} else {
if (mCallback != null) {//如果创建Handler的时候是给Handler设置了Callback接口的实现,那么此时调用该实现的handleMessage方法
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);//如果是派生Handler的子类,就要重写handleMessage方法,那么此时就是调用子类实现的handleMessage方法
}
}
private static void handleCallback(Message message) {
message.callback.run();
}
/**
* Subclasses must implement this to receive messages.
*/
public void handleMessage(Message msg) {
}
~~~
3.Handler还有一个特殊的构造方法,它可以通过特定的Looper来创建Handler。
~~~
public Handler(Looper looper){
this(looper, null, false);
}
~~~
4.Android的主线程就是`ActivityThread`,主线程的入口方法就是main,其中调用了`Looper.prepareMainLooper()`来创建主线程的Looper以及MessageQueue,并通过`Looper.loop()`方法来开启主线程的消息循环。主线程内有一个Handler,即`ActivityThread.H`,它定义了一组消息类型,主要包含了四大组件的启动和停止等过程,例如`LAUNCH_ACTIVITY`等。
`ActivityThread`通过`ApplicationThread`和`AMS`进行进程间通信,AMS以进程间通信的方法完成ActivityThread的请求后会回调ApplicationThread中的`Binder`方法,然后ApplicationThread会向`H`发送消息,`H`收到消息后会将ApplicationThread中的逻辑切换到ActivityThread中去执行,即切换到主线程中去执行,这个过程就是主线程的消息循环模型。
读书笔记(9)第9章 四大组件的工作过程
最后更新于:2022-04-01 05:36:24
## 9.1 四大组件的运行状态
(1)四大组件中只有`BroadcastReceiver`既可以在AndroidManifest文件中注册,也可以在代码中注册,其他三个组件都必须在AndroidManifest文件中注册;`ContentProvider`的调用不需要借助Intent,其他三个组件都需要借助Intent。
(2)Activity是一种展示型组件,用于向用户展示界面,可由显式或者隐式Intent来启动。
(3)Service是一种计算型组件,用于在后台执行计算任务。尽管service是用于后台执行计算的,但是它本身是运行在主线程中的,因此耗时的后台计算仍然需要在单独的线程中去完成。Service组件有两种状态:启动状态和绑定状态。当service处于绑定状态时,外界可以很方便的和service进行通信,而在启动状态中是不可与外界通信的。
(4)BroadcastReceiver是一种消息型组件,用于在不同的组件乃至不同的应用之间传递消息,它工作在系统内部。广播有两种注册方式:静态注册和动态注册。静态注册是在AndroidManifest中注册,在应用安装的时候会被系统解析,这种广播不需要应用启动就可以收到相应的广播。动态注册需要通过`Context.registerReceiver()`来注册,这种广播需要应用启动才能注册并接收广播。BroadcastReceiver组件一般来说不需要停止,它也没有停止的概念。
(5)ContentProvider是一种数据共享型组件,用于向其他组件乃至其他应用共享数据。ContentProvider中的`insert`、`delete`、`update`、`query`方法需要处理好线程同步,因为这几个方法是在Binder线程池中被调用的,另外ContentProvider组件也不需要手动停止。
[下面对四大组件的工作过程的总结需要感谢[`amurocrash`童鞋的读书笔记](http://blog.csdn.net/amurocrash/article/details/48858353)以及他细心制作的UML图,帮助我从原书复杂的方法调用中跳出来看到整体的大致流程]
## 9.2 Activity的工作过程
(1)Activity启动的大致流程
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-10_56692ec8304e7.png)
(2)`ApplicationThread`是`ActivityThread`的一个内部类,它继承自`ApplicationThreadNative`,而`ApplicationThreadNative`继承自`Binder`并实现了`IApplicationThread`接口,`ApplicationThreadNative`的作用其实就和系统为AIDL文件生成的类是一样的。
(3)`ActivityManagerService`(AMS)继承自`ActivityManagerNative`,而`ActivityManagerNative`继承自`Binder`并实现了`IActivityManager`这个Binder接口,因此AMS也是一个Binder。
(4)一个应用只有一个Application对象,它的创建也是通过`Instrumentation`来完成的,这个过程和Activity对象的创建过程一样,都是通过类加载器来实现的。
(5)`ContextImpl`是Context的具体实现,ContextImpl是通过Activity的`attach`方法来和Activity建立关联的,在`attach`方法中Activity还会完成Window的创建并建立自己和Window的关联,*这样当window接收到外部输入事件后就可以将事件传递给Activity*。 [这里可能有误,应该是Activity将事件传递给window]
## 9.3 Service的工作过程
(1)Service有两种状态:启动状态和绑定状态,两种状态是可以共存的。
启动过程:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-10_56692ec85a3ba.png)
绑定过程:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-10_56692ec889cf4.png)
## 9.4 BroadcastReceiver的工作过程
(1)BroadcastReceiver的工作过程包括广播注册过程、广播发送和接收过程。
注册过程:静态注册的时候是由`PackageManagerService`来完成整个注册过程,下面是动态注册的过程
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-10_56692ec8a71f6.png)
发送和接收过程:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-10_56692ec8a71f6.png)
(2)广播的发送有几种类型:普通广播、有序广播和粘性广播,有序广播和粘性广播与普通广播相比具有不同的特性,但是发送和接收过程是类似的。
(3)一个应用处于停止状态分为两种情况:一是应用安装后未运行;二是应用被手动或者其他应用强停了。从Android 3.1开始,处于停止状态的应用无法接受到开机广播。
##9.5 ContentProvider的工作过程
(1)当ContentProvider所在的进程启动的时候,它会同时被启动并被发布到AMS中,这个时候它的onCreate要先去Application的onCreate执行。
(2)ContentProvider的启动过程:
1.当一个应用启动时,入口方法是`ActivityThread`的`main`方法,其中创建ActivityThread的实例并创建主线程的消息队列;
2.`ActivityThread`的`attach`方法中会远程调用`ActivityManagerService`的`attachApplication`,并将`ApplicationThread`提供给AMS,ApplicationThread主要用于ActivityThread和AMS之间的通信;
3.`ActivityManagerService`的`attachApplication`会调用`ApplicationThread`的`bindApplication`方法,这个方法会通过`H`切换到ActivityThread中去执行,即调用`handleBindApplication`方法;
4.`handleBindApplication`方法会创建Application对象并加载ContentProvider,注意是先加载ContentProvider,然后调用Application的`onCreate`方法。
(3)ContentProvider的`android:multiprocess`属性决定它是否是单实例,默认值是false,也就是默认是单实例。当设置为true时,每个调用者的进程中都存在一个ContentProvider对象。
(4)当调用ContentProvider的`insert`、`delete`、`update`、`query`方法中的任何一个时,如果ContentProvider所在的进程没有启动的话,那么就会触发ContentProvider的创建,并伴随着ContentProvider所在进程的启动。下图是ContentProvider的query操作的大致过程:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-10_56692ec8cd215.png)
详细的过程分析建议阅读原书,简直精彩!
其他学习资料
1.[Android开发艺术探索读书笔记(三)](http://blog.csdn.net/amurocrash/article/details/48858353)
读书笔记(8)第8章 理解Window和WindowManager
最后更新于:2022-04-01 05:36:21
## 8.1 Window和WindowManager
(1)`Window`是抽象类,具体实现是`PhoneWindow`,通过`WindowManager`就可以创建Window。WindowManager是外界访问Window的入口,但是Window的具体实现是在`WindowManagerService`中,WindowManager和WindowManagerService的交互是一个IPC过程。所有的视图例如Activity、Dialog、Toast都是附加在Window上的。
(2)通过WindowManager添加View的过程:将一个Button添加到屏幕坐标为(100,300)的位置上
~~~
mFloatingButton = new Button(this);
mFloatingButton.setText("test button");
mLayoutParams = new WindowManager.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0,
PixelFormat.TRANSPARENT);//0,0 分别是type和flags参数,在后面分别配置了
mLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
| LayoutParams.FLAG_NOT_FOCUSABLE
| LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.type = LayoutParams.TYPE_SYSTEM_ERROR;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mFloatingButton.setOnTouchListener(this);
mWindowManager.addView(mFloatingButton, mLayoutParams);
~~~
flags参数解析:
`FLAG_NOT_FOCUSABLE`:表示window不需要获取焦点,也不需要接收各种输入事件。此标记会同时启用`FLAG_NOT_TOUCH_MODAL`,最终事件会直接传递给下层的具有焦点的window;
`FLAG_NOT_TOUCH_MODAL`:在此模式下,系统会将window区域外的单击事件传递给底层的window,当前window区域内的单击事件则自己处理,一般都需要开启这个标记;
`FLAG_SHOW_WHEN_LOCKED`:开启此模式可以让Window显示在锁屏的界面上。 [奇怪的是我删除这个标记还是在锁屏看到了添加的组件orz]
type参数表示window的类型,window共有三种类型:应用window,子window和系统window。应用window对应着一个Activity,子window不能独立存在,需要附属在特定的父window之上,比如Dialog就是子window。系统window是需要声明权限才能创建的window,比如Toast和系统状态栏这些都是系统window,需要声明的权限是`<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />`。
(3)window是分层的,每个window都对应着`z-ordered`,层级大的会覆盖在层级小的上面,应用window的层级范围是`1~99`,子window的层级范围是`1000~1999`,系统window的层级范围是`2000~2999`。
[注意,应用window的层级范围并不是`1~999`哟]
(4)WindowManager继承自`ViewManager`,常用的只有三个方法:`addView`、`updateView`和`removeView`。
## 8.2 Window的内部机制
(1)Window是一个抽象的概念,不是实际存在的,它也是以View的形式存在。在实际使用中无法直接访问Window,只能通过WindowManager才能访问Window。每个Window都对应着一个View和一个`ViewRootImpl`,Window和View通过ViewRootImpl来建立联系。
(2)Window的添加、删除和更新过程都是IPC过程,以Window的添加为例,WindowManager的实现类对于`addView`、`updateView`和`removeView`方法都是委托给`WindowManagerGlobal`类,该类保存了很多数据列表,例如所有window对应的view集合`mViews`、所有window对应的ViewRootImpl的集合`mRoots`等,之后添加操作交给了ViewRootImpl来处理,接着会通过`WindowSession`来完成Window的添加过程,这个过程是一个IPC调用,因为最终是通过`WindowManagerService`来完成window的添加的。
## 8.3 Window的创建过程
(1)Activity的window创建过程
1.Activity的启动过程很复杂,最终会由`ActivityThread`中的`performLaunchActivity`来完成整个启动过程,在这个方法内部会通过类加载器创建Activity的实例对象,并调用它的`attach`方法为其关联运行过程中所依赖的一系列上下文环境变量;
2.Activity实现了Window的`Callback`接口,当window接收到外界的状态变化时就会回调Activity的方法,例如`onAttachedToWindow`、`onDetachedFromWindow`、`dispatchTouchEvent`等;
3.Activity的Window是由`PolicyManager`来创建的,它的真正实现是`Policy`类,它会新建一个`PhoneWindow`对象,Activity的`setContentView`的实现是由`PhoneWindow`来实现的;
4.Activity的顶级View是`DecorView`,它本质上是一个`FrameLayout`。如果没有DecorView,那么PhoneWindow会先创建一个DecorView,然后加载具体的布局文件并将view添加到DecorView的`mContentParent`中,最后就是回调Activity的`onContentChanged`通知Activity视图已经发生了变化;
5.还有一个步骤是让WindowManager能够识别DecorView,在`ActivityThread`调用`handleResumeActivity`方法时,首先会调用Activity的onResume方法,然后会调用`makeVisible`方法,这个方法中DecorView真正地完成了添加和显示过程。
~~~
ViewManager vm = getWindowManager();
vm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
~~~
(2)Dialog的Window创建过程
1.过程与Activity的Window创建过程类似,普通的Dialog的有一个特别之处,即它必须采用Activity的Context,如果采用Application的Context会报错。原因是Application没有`应用token`,应用token一般是Activity拥有的。[service貌似也有token?]
(3)Toast的Window创建过程
1.Toast属于系统Window,它内部的视图由两种方式指定:一种是系统默认的演示;另一种是通过`setView`方法来指定一个自定义的View。
2.Toast具有定时取消功能,所以系统采用了`Handler`。Toast的显示和隐藏是IPC过程,都需要`NotificationManagerService`来实现。在Toast和NMS进行IPC过程时,NMS会跨进程回调Toast中的`TN`类中的方法,TN类是一个Binder类,运行在Binder线程池中,所以需要通过Handler将其切换到当前发送Toast请求所在的线程,所以Toast无法在没有Looper的线程中弹出。
3.对于非系统应用来说,`mToastQueue`最多能同时存在`50`个`ToastRecord`,这样做是为了防止`DOS`(Denial of Service,拒绝服务)。因为如果某个应用弹出太多的Toast会导致其他应用没有机会弹出Toast。
其他学习资料
1.[Android应用开发之(WindowManager类使用)](http://blog.csdn.net/wang_shaner/article/details/8596380)
读书笔记(7)第7章 Android动画深入分析
最后更新于:2022-04-01 05:36:19
##7.1 View动画
(1)android动画分为view动画、帧动画和属性动画,属性动画是API 11(Android 3.0)的新特性,帧动画一般也认为是view动画。
(2)`AnimationSet`的属性`android:shareInterpolator`表示集合中的动画是否共享同一个插值器,如果集合不指定插值器,那么子动画需要单独指定所需的插值器或者使用默认值。
(3)自定义动画需要继承`Animation`抽象类,并重新它的`initialize`和`applyTransformation`方法,在initialize方法中做一些初始化工作,在applyTransformation方法中进行相应的矩阵变换,很多时候需要采用`Camera`类来简化矩阵变换的过程。
(4)帧动画使用比较简单,但是容易引起OOM,所以在使用的时候应尽量避免使用过多尺寸较大的图片。
##7.2 view动画的特殊使用场景
(1)布局动画(`LayoutAnimation`)属性分析
~~~
<layoutAnimation
xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="0.5"
android:animationOrder="reverse"
android:animation="@anim/anim_item"/>
~~~
`android:delay`:表示子元素开始动画的时间延迟,比如子元素入场动画的时间周期是300ms,那么0.5表示每个子元素都需要延迟150ms才能播放入场动画。
给ViewGroup指定LayoutAnimation的两种方式
~~~
//xml
android:layoutAnimation="xxx"
//java
Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);
~~~
(2)Activity切换效果
在startActivity方法后或者finish方法之后调用`overridePendingTransition(int inAnim, int outAnim)`方法设置进入或者退出的动画效果。
还有其他方式可以给Activity添加切换动画效果,但是往往有兼容性限制,参见[《Android群英传》第七章Android动画机制与使用技巧](http://hujiaweibujidao.github.io/blog/2015/11/27/Android-Heros-Reading-Notes-3/)。
## 7.3 属性动画
(1)属性动画可以对任意对象的属性进行动画而不仅仅是view,动画默认的时间间隔是`300ms`,默认帧率是`10ms/帧`。
(2)属性动画几乎是无所不能,但是它是从API 11才有的,所以存在兼容性问题,可以考虑使用开源动画库[nineoldandroids](http://nineoldandroids.com/)。它的功能和系统原生的`android.animations.*`中的类的功能完全一致,使用方法也是完全一样,只要我们用nineoldandroids编写动画,那么就能运行在所有的android系统上。
(3)属性`android:repeatMode`表示动画的重复模式,`repeat`表示连续重复播放,`reverse`表示逆向重复播放,也就是第一次播放完后第二次倒着播放动画,第三次还是重头开始播放动画,第四次再倒着播放,以此类推。
(4)插值器和估值器:属性动画实现非匀速动画的重要手段
时间插值器(`TimeInterpolator`)的作用是根据时间流逝的百分比计算出当前属性值改变的百分比,系统内置的插值器有线性插值器(`LinearInterpolator`)、加速减速插值器(`AccelerateDecelerateInterpolator`)和减速插值器(`DecelerateInterpolator`)。
类型估值器(`TypeEvaluator`)的作用是根据当前属性改变的百分比计算出改变后的属性值,系统内置的估值器有`IntEvaluator`、`FloatEvaluator`和`ArgbEvaluator`。
(5)动画监听器
`AnimatorListener`:监听动画的开始、结束、取消以及重复播放;
`AnimatorUpdateListener`:监听动画的整个过程,动画每播放一帧的时候`onAnimationUpdate`方法就会被回调一次。
(6)对任意属性做动画的方法:封装原始对象或者`ValueAnimator`
(7)属性动画的工作原理:属性动画需要运行在有Looper的线程中,反射调用get/set方法
## 7.4 使用动画的注意事项
(1)OOM:尽量避免使用帧动画,使用的话应尽量避免使用过多尺寸较大的图片;
(2)内存泄露:属性动画中的无限循环动画需要在Activity退出的时候及时停止,否则将导致Activity无法释放而造成内存泄露。view动画不存在这个问题;
(3)兼容性问题:某些动画在3.0以下系统上有兼容性问题;
(4)view动画的问题:view动画是对view的影像做动画,并不是真正的改变view的状态,因此有时候动画完成之后view无法隐藏,即`setVisibility(View.GONE)`失效了,此时需要调用`view.clearAnimation()`清除view动画才行。
(5)不要使用px;
(6)动画元素的交互:在android3.0以前的系统上,view动画和属性动画,新位置均无法触发点击事件,同时,老位置仍然可以触发单击事件。从3.0开始,属性动画的单击事件触发位置为移动后的位置,view动画仍然在原位置;
(7)硬件加速:使用动画的过程中,建议开启硬件加速,这样会提高动画的流畅性。
其他学习资料
1.[Android样式的开发:View Animation篇](http://keeganlee.me/post/android/20151003)
2.[Android样式的开发:Property Animation篇](http://keeganlee.me/post/android/20151026)
读书笔记(6)第6章 Android的Drawable
最后更新于:2022-04-01 05:36:17
## 6.1 Drawable简介
(1)Android的`Drawable`表示的是一种可以在`Canvas`上进行绘制的概念,它的种类很多,最常见的就是图片和颜色了。它有两个重要的优点:一是比自定义view要简单;二是非图片类型的drawable占用空间小,利于减小apk大小。
(2)Drawable是抽象类,是所有Drawable对象的基类。
(3)Drawable的内部宽/高可以通过`getIntrinsicWidth`和`getIntrinsicHeight`方法获取,但是并不是所有Drawable都有内部宽/高。图片Drawable的内部宽高就是图片的宽高,但是颜色Drawable就没有宽高的概念,它一般是作为view的背景,所以会去适应view的大小,这两个方法都是返回-1。
## 6.2 Drawable分类
(1)BitmapDrawable和NinePatchDrawable
~~~
<?xml version="1.0" encoding="utf-8"?>
<bitmap / nine-patch
xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@[package:]drawable/drawable_resource"
android:antialias=["true" | "false"]
android:dither=["true" | "false"]
android:filter=["true" | "false"]
android:gravity=["top" | "bottom" | "left" | "right" | "center_vertical" |
"fill_vertical" | "center_horizontal" | "fill_horizontal" |
"center" | "fill" | "clip_vertical" | "clip_horizontal"]
android:tileMode=["disabled" | "clamp" | "repeat" | "mirror"] />
~~~
属性分析:
`android:antialias`:是否开启图片抗锯齿功能。开启后会让图片变得平滑,同时也会一定程度上降低图片的清晰度,建议开启;
`android:dither`:是否开启抖动效果。当图片的像素配置和手机屏幕像素配置不一致时,开启这个选项可以让高质量的图片在低质量的屏幕上还能保持较好的显示效果,建议开启。
`android:filter`:是否开启过滤效果。当图片尺寸被拉伸或压缩时,开启过滤效果可以保持较好的显示效果,建议开启;
`android:gravity`:当图片小于容器的尺寸时,设置此选项可以对图片进行定位。
`android:tileMode`:平铺模式,有四种选项`["disabled" | "clamp" | "repeat" | "mirror"]`。当开启平铺模式后,gravity属性会被忽略。repeat是指水平和竖直方向上的平铺效果;mirror是指在水平和竖直方向上的镜面投影效果;clamp是指图片四周的像素会扩展到周围区域,这个比较特别。
(2)ShapeDrawable
~~~
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape=["rectangle" | "oval" | "line" | "ring"] >
<corners //当shape为rectangle时使用
android:radius="integer" //半径值会被后面的单个半径属性覆盖,默认为1dp
android:topLeftRadius="integer"
android:topRightRadius="integer"
android:bottomLeftRadius="integer"
android:bottomRightRadius="integer" />
<gradient //渐变
android:angle="integer"
android:centerX="integer"
android:centerY="integer"
android:centerColor="integer"
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 //指定大小,一般用在imageview配合scaleType属性使用
android:width="integer"
android:height="integer" />
<solid //填充颜色
android:color="color" />
<stroke //边框
android:width="integer"
android:color="color"
android:dashWidth="integer"
android:dashGap="integer" />
</shape>
~~~
`android:shape`:默认的shape是矩形,`line`和`ring`这两种形状需要通过`<stroke>`来制定线的宽度和颜色,否则看不到效果。
`gradient`:`solid`表示纯色填充,而`gradient`表示渐变效果。`andoid:angle`指渐变的角度,默认为0,其值必须是45的倍数,0表示从左到右,90表示从下到上,其他类推。
`padding`:这个表示的是包含它的view的空白,四个属性分别表示四个方向上的padding值。
`size`:ShapeDrawable默认情况下是没有宽高的概念的,但是可以如果指定了size,那么这个时候shape就有了所谓的固有宽高,但是作为view的背景时,shape还是会被拉伸或者缩小为view的大小。
(3)LayerDrawble
对应标签`<layer-list>`,表示层次化的Drawable集合,实现一种叠加后的效果。
属性`android:top/left/right/bottom`表示drawable相对于view的上下左右的偏移量,单位为像素。
(4)StateListDrawable
对应标签`<selector>`,也是表示Drawable集合,每个drawable对应着view的一种状态。
一般来说,默认的item都应该放在selector的最后一条并且不附带任何的状态。
(5)LevelListDrawable
对应标签`<level-list>`,同样是Drawable集合,每个drawable还有一个`level`值,根据不同的level,LevelListDrawable会切换不同的Drawable,level值范围从0到100000。
(6)TransitionDrawable
对应标签`<transition>`,它用于是吸纳两个Drawable之间的淡入淡出效果。
~~~
<transition xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:drawable="@drawable/shape_drawable_gradient_linear"/>
<item android:drawable="@drawable/shape_drawable_gradient_radius"/>
</transition>
TransitionDrawable drawable = (TransitionDrawable) v.getBackground();
drawable.startTransition(5000);
~~~
(7)InsetDrawable
对应标签`<inset>`,它可以将其他drawable内嵌到自己当中,并可以在四周留出一定的间距。当一个view希望自己的背景比自己的实际区域小的时候,可以采用InsetDrawable来实现。
~~~
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetBottom="15dp"
android:insetLeft="15dp"
android:insetRight="15dp"
android:insetTop="15dp" >
<shape android:shape="rectangle" >
<solid android:color="#ff0000" />
</shape>
</inset>
~~~
(8)ScaleDrawable
对应标签`<scale>`,它可以根据自己的level将指定的Drawable缩放到一定比例。如果level越大,那么内部的drawable看起来就越大。
(9)ClipDrawable
对应标签`<clip>`,它可以根据自己当前的level来裁剪另一个drawable,裁剪方向由`android:clipOrientation`和`andoid:gravity`属性来共同控制。level越大,表示裁剪的区域越小。
~~~
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:clipOrientation="vertical"
android:drawable="@drawable/image1"
android:gravity="bottom" />
~~~
## 6.3 自定义Drawable
(1)Drawable的工作核心就是`draw`方法,所以自定义drawable就是重写`draw`方法,当然还有`setAlpha`、`setColorFilter`和`getOpacity`这几个方法。当自定义Drawable有固有大小的时候最好重写`getIntrinsicWidth`和`getIntrinsicHeight`方法。
(2)Drawable的内部大小不等于Drawable的实际区域大小,Drawable的实际区域大小可以通过它的`getBounds`方法来得到,一般来说它和view的尺寸相同。
其他学习资料:
1.[Android样式的开发:shape篇](http://keeganlee.me/post/android/20150830)
2.[Android样式的开发:drawable篇](http://keeganlee.me/post/android/20150916)
3.[Android样式的开发:selector篇](http://keeganlee.me/post/android/20150905)
4.[Android样式的开发:layer-list篇](http://keeganlee.me/post/android/20150909)
读书笔记(5)第5章 理解RemoteViews
最后更新于:2022-04-01 05:36:15
## 5.1 `RemoteViews`的应用
(1)RemoteViews表示的是一个view结构,它可以在其他进程中显示。由于它在其他进程中显示,为了能够更新它的界面,RemoteViews提供了一组基础的操作用于跨进程更新它的界面。
(2)RemoteViews主要用于通知栏通知和桌面小部件的开发,通知栏通知是通过`NotificationManager`的`notify`方法来实现的;桌面小部件是通过`AppWidgetProvider`来实现的,它本质上是一个广播(BroadcastReceiver)。这两者的界面都是运行在`SystemServer`进程中。
(3)RemoteViews在Notification中的应用示例
~~~
Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_1.class);
intent.putExtra("sid", "" + sId);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
remoteViews.setTextViewText(R.id.msg, "chapter_5: " + sId);//设置textview的显示文本
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);//给图片添加点击事件
notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(sId, notification);
~~~
(4)RemoteViews在桌面小部件中的应用
1.定义小部件界面;
2.定义小部件配置信息:其中`updatePeriodMillis`定义小工具的自动更新周期,单位为ms。
~~~
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget"
android:minHeight="84dp"
android:minWidth="84dp"
android:updatePeriodMillis="86400000" >
</appwidget-provider>
~~~
3.定义小部件的实现类:书中的示例实现了一个显示一张图片的小部件,每次点击小部件的时候图片就会旋转一周;
~~~
public class MyAppWidgetProvider extends AppWidgetProvider {
public static final String TAG = "MyAppWidgetProvider";
public static final String CLICK_ACTION = "com.ryg.chapter_5.action.CLICK";
public MyAppWidgetProvider() {
super();
}
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
Log.i(TAG, "onReceive : action = " + intent.getAction());
// 这里判断是自己的action,做自己的事情,比如小工具被点击了要干啥,这里是做一个动画效果
if (intent.getAction().equals(CLICK_ACTION)) {
Toast.makeText(context, "clicked it", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
Bitmap srcbBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon1);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
for (int i = 0; i < 37; i++) {
float degree = (i * 10) % 360;
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
remoteViews.setImageViewBitmap(R.id.imageView1, rotateBitmap(context, srcbBitmap, degree));
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgetManager.updateAppWidget(new ComponentName(context, MyAppWidgetProvider.class),remoteViews);
SystemClock.sleep(30);
}
}
}).start();
}
}
/**
* 每次窗口小部件被点击更新都调用一次该方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate");
final int counter = appWidgetIds.length;
Log.i(TAG, "counter = " + counter);
for (int i = 0; i < counter; i++) {
int appWidgetId = appWidgetIds[i];
onWidgetUpdate(context, appWidgetManager, appWidgetId);
}
}
/**
* 窗口小部件更新
*/
private void onWidgetUpdate(Context context, AppWidgetManager appWidgeManger, int appWidgetId) {
Log.i(TAG, "appWidgetId = " + appWidgetId);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
// "窗口小部件"点击事件发送的Intent广播
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgeManger.updateAppWidget(appWidgetId, remoteViews);
}
private Bitmap rotateBitmap(Context context, Bitmap srcbBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
return Bitmap.createBitmap(srcbBitmap, 0, 0, srcbBitmap.getWidth(), srcbBitmap.getHeight(), matrix, true);
}
}
~~~
4.在AndroidManifest.xml文件中声明小部件
下面的示例中包含了两个action,第一个action用于识别小部件的单击行为,而第二个action是作为小部件必须存在的action `android.appwidget.action.APPWIDGET_UPDATE`,如果不加那么就无法显示小部件。
~~~
<receiver android:name=".MyAppWidgetProvider" >
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info" >
</meta-data>
<intent-filter>
<action android:name="com.ryg.chapter_5.action.CLICK" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
~~~
(5)AppWidgetProvider会自动根据广播的action通过`onReceive`方法来自动分发广播,也就是调用下面不同的方法:
`onEnable`:当小部件第一次添加到桌面时调用,小部件可以添加多次但是只在第一次添加的时候调用;
`onUpdate`:小部件被添加时或者每次小部件更新时都会调用一次该方法,每个周期小部件都会自动更新一次;
`onDeleted`:每删除一次小部件就调用一次;
`onDisabled`:当最后一个该类型的小部件被删除时调用该方法;
`onReceive`:这是广播内置的方法,用于分发具体的事件给其他方法,所以该方法一般要调用`super.onReceive(context, intent);` 如果自定义了其他action的广播,就可以在调用了父类方法之后进行判断,如上面代码所示。
(6)`PendingIntent`表示一种处于Pending状态的Intent,pending表示的是即将发生的意思,它是在将来的某个不确定的时刻放生,而Intent是立刻发生。
(7)PendingIntent支持三种待定意图:启动Activity(getActivity),启动Service(getService),发送广播(getBroadcast)。
`PendingIntent.getActivity(Context context, in requestCode, Intent intent, int flags)`
获得一个PendingIntent,当待定意图发生时,效果相当于Context.startActivity(intent)。
第二个参数`requestCode`是PendingIntent发送方的请求码,多数情况下设为0即可,另外requestCode会影响到flags的效果。
PendingIntent的匹配规则:如果两个PendingIntent内部的Intent相同,并且requestCode也相同,那么这两个PendingIntent就是相同的。
Intent的匹配规则:如果两个Intent的ComponentName和intent-filter都相同,那么这两个Intent就是相同的,Extras不参与Intent的匹配过程。
第四个参数flags常见的类型有:`FLAG_ONE_SHOT`、`FLAG_NO_CREATE`、`FLAG_CANCEL_CURRENT`、`FLAG_UPDATE_CURRENT`。
`FLAG_ONE_SHOT`:当前描述的PendingIntent只能被调用一次,然后它就会被自动cancel。如果后续还有相同的PendingIntent,那么它们的send方法就会调用失败。对于通知栏消息来说,如果采用这个flag,那么同类的通知只能使用一次,后续的通知单击后将无法打开。
`FLAG_NO_CREATE`:当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity、getService和getBroadcast方法会直接返回null,即获取PendingIntent失败。这个标志位使用很少。
`FLAG_CANCEL_CURRENT`:当前描述的PendingIntent如果已经存在,那么它们都会被cancel,然后系统会创建一个新的PendingIntent。
对于通知栏消息来说,那些被cancel的通知单击后将无法打开。
`FLAG_UPDATE_CURRENT`:当前描述的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换成最新的。
(8)分析`NotificationManager.nofify(id, notification)` [未测试,看着有点晕]
1.如果参数id是常量,那么多次调用notify只能弹出一个通知,后续的通知会把前面的通知完全替代掉;
2.如果参数id每次都不同,那么当PendingIntent不匹配的时候,不管采用何种标志位,这些通知之间不会相互干扰;
3.如果参数id每次都不同,且PendingIntent匹配的时候,那就要看标志位:
如果标志位是FLAG_ONE_SHOT,那么后续的通知中的PendingIntent会和第一条通知保持完全一致,包括其中的Extras,单击任何一条通知后,剩下的通知均无法再打开,当所有的通知都被清除后,会再次重复这个过程;
如果标志位是FLAG_CANCEL_CURRENT,那么只有最新的通知可以打开,之前弹出的所有通知都无法打开;
如果标志位是FLAG_UPDATE_CURRENT,那么之前弹出的通知中的PendingIntent会被更新,最终它们和最新的一条通知保持完全一致,包括其中的Extras,并且这些通知都是可以打开的。
## 5.2 RemoteViews的内部机制
(1)RemoteViews的构造方法 `public RemoteViews(String packageName, int layoutId)`,第一个参数是当前应用的包名,第二个参数是待加载的布局文件。
(2)RemoteViews只支持部分布局和View组件,下面列举的组件的子类是不支持的
布局:`FrameLayout、LinearLayout、RelativeLayout、GridLayout`
组件:`Button、ImageButton、ImageView、TextView、ListView、GridView、ViewStub`等
(3)RemoteViews提供了一系列的set方法完成view的设置,这是通过反射完成的调用的。
例如方法`setInt(int viewId, String methodName, int value)`就是反射调用view对象的名称为methodName的方法,传入参数value,同样的还有`setBoolean`、`setLong`等。
方法`setOnClickPendingIntent(int viewId, PendingIntent pi)`用来为view添加单击事件,事件类型只能为PendingIntent。
(4)通知和小部件分别由`NotificationManager`和`AppWidgetManager`管理,而它们通过Binder分别和SystemServer进程中的`NotificationManagerService`和`AppWidgetManagerService`进行通信。所以,布局文件实际上是两个Service加载的,运行在SystemServer进程中。
(5)RemoteViews实现了`Parcelable`接口,它会通过Binder传递到SystemServer进程,系统会根据RemoteViews中的包名信息获取到应用中的资源,从而完成布局文件的加载。
(6)系统将view操作封装成`Action`对象,Action同样实现了Parcelable接口,通过Binder传递到SystemServer进程。远程进程通过RemoteViews的`apply`方法来进行view的更新操作,RemoteViews的apply方法内部则会去遍历所有的action对象并调用它们的apply方法来进行view的更新操作。
这样做的好处是不需要定义大量的Binder接口,其次批量执行RemoteViews中的更新操作提高了程序性能。
(7)RemoteViews的`apply`和`reapply`方法的区别:`apply`方法会加载布局并更新界面,而`reapply`方法则只会更新界面。
(8)`setOnClickPendingIntent`、`setPendingIntentTemplate`和`setOnClickFillIntent`的区别
`setOnClickPendingIntent`用于给普通的view添加点击事件,但是不能给集合(ListView和StackView)中的view设置点击事件,因为开销太大了。如果需要给ListView和StackView中的item添加点击事件,需要结合`setPendingIntentTemplate`和`setOnClickFillIntent`一起使用。[并没有尝试(⊙o⊙)]
## 5.3 RemoteViews的意义
RemoteViews的最大的意义是实现了跨进程的UI更新,这节作者实现了一个模拟通知栏效果的应用来演示跨进程的UI更新,[源码传送门](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_5/src/com/ryg/chapter_5/MainActivity.java)。
读书笔记(4)第4章 View的工作原理
最后更新于:2022-04-01 05:36:12
##4.1 初始ViewRoot和DecorView
(1)`ViewRoot`对应`ViewRootImpl`类,它是连接`WindowManager`和`DecorView`的纽带,View的三大流程均通过ViewRoot来完成。
(2)`ActivityThread`中,Activity创建完成后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并建立两者的关联。
(3)View的绘制流程从ViewRoot的`performTraversals`方法开始,经过`measure`、`layout`和`draw`三大流程。
(4)`performMeasure`方法中会调用`measure`方法,在`measure`方法中又会调用`onMeasure`方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素了,这样就完成了一次measure过程,layout和draw的过程类似。 (书中175页画出详细的图示)
(5)measure过程决定了view的宽高,在几乎所有的情况下这个宽高都等同于view最终的宽高。layout过程决定了view的四个顶点的坐标和view实际的宽高,通过`getWidth`和`getHeight`方法可以得到最终的宽高。draw过程决定了view的显示。
(6)DecorView其实是一个FrameLayout,其中包含了一个竖直方向的LinearLayout,上面是标题栏,下面是内容栏(id为`android.R.id.content`)。
## 4.2 理解MeasureSpec
(1)`MeasureSpec`和`LayoutParams`的对应关系
在view测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。
MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定view的MeasureSpec,从而进一步确定view的宽高。对于DecorView,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams来决定;对于普通view,它的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。
(2)普通view的MeasureSpec的创建规则 (书中182页列出详细的表格)
当view采用固定宽高时,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式,并且大小是LayoutParams中的大小。
当view的宽高是`match_parent`时,如果父容器的模式是精确模式,那么view也是精确模式,并且大小是父容器的剩余空间;如果父容器是最大模式,那么view也是最大模式,并且大小是不会超过父容器的剩余空间。
当view的宽高是`wrap_content`时,不管父容器的模式是精确模式还是最大模式,view的模式总是最大模式,并且大小不超过父容器的剩余空间。
## 4.3 view的工作流程
(1)view的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了`onCreate`、`onStart`、`onResume`时某个view已经测量完毕了。如果view还没有测量完毕,那么获得的宽高就都是0。下面是四种解决该问题的方法:
1.`Activity/View # onWindowFocusChanged`方法
`onWindowFocusChanged`方法表示view已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没问题的。这个方法会被调用多次,当Activity继续执行或者暂停执行的时候,这个方法都会被调用。
2.`view.post(runnable)`
通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view也已经初始化好了。
3.`ViewTreeObserver`
使用`ViewTreeObserver`的众多回调方法可以完成这个功能,比如使用`onGlobalLayoutListener`接口,当view树的状态发生改变或者view树内部的view的可见性发生改变时,`onGlobalLayout`方法将被回调。伴随着view树的状态改变,这个方法也会被多次调用。
4.`view.measure(int widthMeasureSpec, int heightMeasureSpec)`
通过手动对view进行measure来得到view的宽高,这个要根据view的LayoutParams来处理:
`match_parent`:无法measure出具体的宽高;
`wrap_content`:如下measure,设置最大值
~~~
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
~~~
精确值:例如100px
~~~
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
~~~
(2)在view的默认实现中,view的测量宽高和最终宽高是相等的,只不过测量宽高形成于measure过程,而最终宽高形成于layout过程。
(3)draw过程大概有下面几步:
1.绘制背景:`background.draw(canvas)`;
2.绘制自己:`onDraw()`;
3.绘制children:`dispatchDraw`;
4.绘制装饰:`onDrawScrollBars`。
## 4.4 自定义view
(1)继承view重写onDraw方法需要自己支持`wrap_content`,并且`padding`也要自己处理。继承特定的View例如TextView不需要考虑。
(2)尽量不要在View中使用Handler,因为view内部本身已经提供了`post`系列的方法,完全可以替代Handler的作用。
(3)view中如果有线程或者动画,需要在`onDetachedFromWindow`方法中及时停止。
(4)处理好view的滑动冲突情况。
接下来是原书中的自定义view的示例,推荐阅读[源码](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_4/src/com/ryg/chapter_4/ui/CircleView.java)。
读书笔记(3)第3章 View的事件体系
最后更新于:2022-04-01 05:36:10
##3.1 View基本知识
(1)view的层次结构:`ViewGroup`也是View;
(2)view的位置参数:`top、left、right、bottom`,分别对应View的左上角和右下角相对于父容器的横纵坐标值。
从Android 3.0开始,view增加了`x、y、translationX、translationY`四个参数,这几个参数也是相对于父容器的坐标。x和y是左上角的坐标,而translationX和translationY是view左上角相对于父容器的偏移量,默认值都是0。
x = left + translationX
y = top + translationY
(3)`MotionEvent`是指手指接触屏幕后所产生的一系列事件,主要有`ACTION_UP`、`ACTION_DOWN`、`ACTION_MOVE`等。正常情况下,一次手指触屏会触发一系列点击事件,主要有下面两种典型情况:
1.点击屏幕后离开,事件序列是`ACTION_DOWN` -> `ACTION_UP`;
2.点击屏幕后滑动一会再离开,事件序列是`ACTION_DOWN` -> `ACTION_MOVE` -> `ACTION_MOVE` -> … -> `ACTION_UP`;
通过MotionEvent可以得到点击事件发生的x和y坐标,其中`getX`和`getY`是相对于当前view左上角的x和y坐标,`getRawX`和`getRawY`是相对于手机屏幕左上角的x和y坐标。
(4)`TouchSlope`是系统所能识别出的可以被认为是滑动的最小距离,获取方式是`ViewConfiguration.get(getContext().getScaledTouchSlope())`。
(5)`VelocityTracker`用于追踪手指在滑动过程中的速度,包括水平和垂直方向上的速度。
速度计算公式: `速度 = (终点位置 - 起点位置) / 时间段`
速度可能为负值,例如当手指从屏幕右边往左边滑动的时候。此外,速度是单位时间内移动的像素数,单位时间不一定是1秒钟,可以使用方法`computeCurrentVelocity(xxx)`指定单位时间是多少,单位是ms。例如通过`computeCurrentVelocity(1000)`来获取速度,手指在1s中滑动了100个像素,那么速度是100,即`100`(像素/1000ms)。如果`computeCurrentVelocity(100)`来获取速度,在100ms内手指只是滑动了10个像素,那么速度是10,即`10`(像素/100ms)。
VelocityTracker的使用方式:
~~~
//初始化
VelocityTracker mVelocityTracker = VelocityTracker.obtain();
//在onTouchEvent方法中
mVelocityTracker.addMovement(event);
//获取速度
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//重置和回收
mVelocityTracker.clear(); //一般在MotionEvent.ACTION_UP的时候调用
mVelocityTracker.recycle(); //一般在onDetachedFromWindow中调用
~~~
(6)`GestureDetector`用于辅助检测用户的单击、滑动、长按、双击等行为。GestureDetector的使用比较简单,主要也是辅助检测常见的触屏事件。作者建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector。
(7)`Scroller`分析:详细内容可以参见[《Android群英传》读书笔记 (2) 第五章 Scroll分析](http://hujiaweibujidao.github.io/blog/2015/11/26/Android-Heros-Reading-Notes-2/)
## 3.2 View的滑动
(1)常见的实现view的滑动的方式有三种:
第一种是通过view本身提供的scrollTo和scrollBy方法:操作简单,适合对view内容的滑动;
第二种是通过动画给view施加平移效果来实现滑动:操作简单,适用于没有交互的view和实现复杂的动画效果;
第三种是通过改变view的LayoutParams使得view重新布局从而实现滑动:操作稍微复杂,适用于有交互的view。
以上三种方法的详情可以参考阅读[《Android群英传》读书笔记 (2)](http://hujiaweibujidao.github.io/blog/2015/11/26/Android-Heros-Reading-Notes-2/)中的内容,此处不再细述。
(2)scrollTo和scrollBy方法只能改变view内容的位置而不能改变view在布局中的位置。 scrollBy是基于当前位置的相对滑动,而scrollTo是基于所传参数的绝对滑动。通过View的`getScrollX`和`getScrollY`方法可以得到滑动的距离。
(3)使用动画来移动view主要是操作view的translationX和translationY属性,既可以使用传统的view动画,也可以使用属性动画,使用后者需要考虑兼容性问题,如果要兼容Android 3.0以下版本系统的话推荐使用[nineoldandroids](http://nineoldandroids.com/)。
使用动画还存在一个交互问题:在android3.0以前的系统上,view动画和属性动画,新位置均无法触发点击事件,同时,老位置仍然可以触发单击事件。从3.0开始,属性动画的单击事件触发位置为移动后的位置,view动画仍然在原位置。
(4)动画兼容库nineoldandroids中的`ViewHelper`类提供了很多的get/set方法来为属性动画服务,例如`setTranslationX`和`setTranslationY`方法,这些方法是没有版本要求的。
## 3.3 弹性滑动
(1)Scroller的工作原理:Scroller本身并不能实现view的滑动,它需要配合view的computeScroll方法才能完成弹性滑动的效果,它不断地让view重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出view的当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成view的滑动。就这样,view的每一次重绘都会导致view进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,这就是Scroller的工作原理。
(2)使用延时策略来实现弹性滑动,它的核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用Handler的`sendEmptyMessageDelayed(xxx)`或view的`postDelayed`方法,也可以使用线程的sleep方法。
## 3.4 view的事件分发机制
(1)事件分发过程的三个重要方法
`public boolean dispatchTouchEvent(MotionEvent ev)`
用来进行事件的分发。如果事件能够传递给当前view,那么此方法一定会被调用,返回结果受当前view的onTouchEvent和下级view的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
`public boolean onInterceptTouchEvent(MotionEvent event)`
在`dispatchTouchEvent`方法内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列当中,此方法不会再被调用,返回结果表示是否拦截当前事件。
若返回值为True事件会传递到自己的onTouchEvent();
若返回值为False传递到子view的dispatchTouchEvent()。
`public boolean onTouchEvent(MotionEvent event)`
在`dispatchTouchEvent`方法内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接收到事件。
若返回值为True,事件由自己处理,后续事件序列让其处理;
若返回值为False,自己不消耗事件,向上返回让其他的父容器的onTouchEvent接受处理。
三个方法的关系可以用下面的伪代码表示:
~~~
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
~~~
(2)OnTouchListener的优先级比onTouchEvent要高
如果给一个view设置了OnTouchListener,那么OnTouchListener中的`onTouch`方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,那么当前view的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。
在onTouchEvent方法中,如果当前view设置了OnClickListener,那么它的onClick方法会被调用,所以OnClickListener的优先级最低。
(3)当一个点击事件发生之后,传递过程遵循如下顺序:Activity -> Window -> View。
如果一个view的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理(调用Activity的onTouchEvent方法)。
(4)正常情况下,一个事件序列只能被一个view拦截并消耗,因为一旦某个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且该元素的onInterceptTouchEvent方法不会再被调用了。
(5)某个view一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列的其他事件都不会再交给它来处理,并且事件将重新交给它的父容器去处理(调用父容器的onTouchEvent方法);如果它消耗ACTION_DOWN事件,但是不消耗其他类型事件,那么这个点击事件会消失,父容器的onTouchEvent方法不会被调用,当前view依然可以收到后续的事件,但是这些事件最后都会传递给Activity处理。
(6)ViewGroup默认不拦截任何事件,因为它的`onInterceptTouchEvent`方法默认返回false。view没有`onInterceptTouchEvent`方法,一旦有点击事件传递给它,那么它的`onTouchEvent`方法就会被调用。
(7)View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(`clickable`和`longClickable`都为false)。view的`longClickable`默认是false的,`clickable`则不一定,Button默认是true,而TextView默认是false。
(8)View的`enable`属性不影响onTouchEvent的默认返回值。哪怕一个view是`disable`状态,只要它的clickable或者longClickable有一个是true,那么它的onTouchEvent就会返回true。
(9)事件传递过程总是先传递给父元素,然后再由父元素分发给子view,通过`requestDisallowInterceptTouchEvent`方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外,即当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。
ViewGroup的dispatchTouchEvent方法中有一个标志位`FLAG_DISALLOW_INTERCEPT`,这个标志位就是通过子view调用`requestDisallowInterceptTouchEvent`方法来设置的,一旦设置为true,那么ViewGroup不会拦截该事件。
(10)以上结论均可以在书中的源码解析部分得到解释。Window的实现类为`PhoneWindow`,获取Activity的contentView的方法
~~~
((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);
~~~
## 3.5 view的滑动冲突
(1)常见的滑动冲突的场景:
1.外部滑动方向和内部滑动方向不一致,例如viewpager中包含listview;
2.外部滑动方向和内部滑动方向一致,例如viewpager的单页中存在可以滑动的bannerview;
3.上面两种情况的嵌套,例如viewpager的单个页面中包含了bannerview和listview。
(2)滑动冲突处理规则
可以根据滑动距离和水平方向形成的夹角;或者根绝水平和竖直方向滑动的距离差;或者两个方向上的速度差等
(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: {
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
~~~
2.内部拦截法:父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合`requestDisallowInterceptTouchEvent`方法才能正常工作。
~~~
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {]
getParent().requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (当前view需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
~~~
书中对这两种拦截法写了两个例子,感兴趣阅读源码看下,[外部拦截法使用示例链接](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_3/src/com/ryg/chapter_3/ui/HorizontalScrollViewEx.java)和[内部拦截法使用示例链接](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_3/src/com/ryg/chapter_3/ui/ListViewEx.java)。
读书笔记(2)第2章 IPC机制
最后更新于:2022-04-01 05:36:08
##2.1 Android IPC简介
(1)任何一个操作系统都需要有相应的IPC机制,Linux上可以通过命名通道、共享内存、信号量等来进行进程间通信。Android系统不仅可以使用了Binder机制来实现IPC,还可以使用Socket实现任意两个终端之间的通信。
## 2.2 Android中的多进程模式
(1)通过给四大组件指定`android:process`属性就可以开启多进程模式,默认进程的进程名是包名packageName,进程名以`:`开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以`:`开头的进程属于全局进程,其他应用通过`ShareUID`方法可以和它跑在同一个进程中。
~~~
android:process=":xyz" //进程名是 packageName:xyz
android:process="aaa.bbb.ccc" //进程名是 aaa.bbb.ccc
~~~
(2)Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。 在这种情况下,它们可以相互访问对方的私有数据,比如data目录、组件信息等,不管它们是否跑在同一个进程中。如果它们跑在同一个进程中,还可以共享内存数据,它们看起来就像是一个应用的两个部分。
(3)android系统会为每个进程分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,所以不同的虚拟机中访问同一个类的对象会产生多个副本。
(4)使用多线程容易造成以下几个问题:
1.静态成员和单例模式完全失效;
2.线程同步机制完全失效:无论锁对象还是锁全局对象都无法保证线程同步;
3.`SharedPreferences`的可靠性下降:SharedPreferences不支持并发读写;
4.Application会多次创建:当一个组件跑在一个新的进程的时候,系统要在创建新的进程的同时分配独立的虚拟机,应用会重新启动一次,也就会创建新的Application。运行在同一个进程中的组件是属于同一个虚拟机和同一个Application。
同一个应用的不同组件,如果它们运行在不同进程中,那么和它们分别属于两个应用没有本质区别。
## 2.3 IPC基础概念介绍
(1)`Serializable`接口是Java中为对象提供标准的序列化和反序列化操作的接口,而`Parcelable`接口是Android提供的序列化方式的接口。
(2)`serialVersionUId`是一串long型数字,主要是用来辅助序列化和反序列化的,原则上序列化后的数据中的serialVersionUId只有和当前类的serialVersionUId相同才能够正常地被反序列化。
serialVersionUId的详细工作机制:序列化的时候系统会把当前类的serialVersionUId写入序列化的文件中,当反序列化的时候系统会去检测文件中的serialVersionUId,看它是否和当前类的serialVersionUId一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化;否则说明版本不一致无法正常反序列化。一般来说,我们应该手动指定serialVersionUId的值。
1.静态成员变量属于类不属于对象,所以不参与序列化过程;
2.声明为`transient`的成员变量不参与序列化过程。
(3)`Parcelable`接口内部包装了可序列化的数据,可以在Binder中自由传输,`Parcelable`主要用在内存序列化上,可以直接序列化的有Intent、Bundle、Bitmap以及List和Map等等,下面是一个实现了`Parcelable`接口的示例
~~~
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book() {
}
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
//“内容描述”,如果含有文件描述符返回1,否则返回0,几乎所有情况下都是返回0
public int describeContents() {
return 0;
}
//实现序列化操作,flags标识只有0和1,1表示标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0
public void writeToParcel(Parcel out, int flags) {
out.writeInt(bookId);
out.writeString(bookName);
}
//实现反序列化操作
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
//从序列化后的对象中创建原始对象
public Book createFromParcel(Parcel in) {
return new Book(in);
}
public Book[] newArray(int size) {//创建指定长度的原始对象数组
return new Book[size];
}
};
private Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}
}
~~~
(4)`Binder`是Android中的一个类,它实现了`IBinder`接口。从IPC角度看,Binder是Android中一种跨进程通信的方式;Binder还可以理解为虚拟的物理设备,它的设备驱动是`/dev/binder`;从Framework层角度看,Binder是ServiceManager连接各种`Manager`和相应的`ManagerService`的桥梁;从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当`bindService`的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
在Android开发中,Binder主要用在Service中,包括AIDL和Messenger,其中普通Service中的Binder不涉及进程间通信,较为简单;而Messenger的底层其实是AIDL,正是Binder的核心工作机制。
(5)aidl工具根据aidl文件自动生成的java接口的解析:首先,它声明了几个接口方法,同时还声明了几个整型的id用于标识这些方法,id用于标识在`transact`过程中客户端所请求的到底是哪个方法;接着,它声明了一个内部类`Stub`,这个Stub就是一个`Binder`类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的`transact`过程,而当两者位于不同进程时,方法调用需要走transact过程,这个逻辑由Stub内部的代理类`Proxy`来完成。
所以,这个接口的核心就是它的内部类Stub和Stub内部的代理类Proxy。 下面分析其中的方法:
1.`asInterface(android.os.IBinder obj)`:用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端是在同一个进程中,那么这个方法返回的是服务端的`Stub`对象本身,否则返回的是系统封装的`Stub.Proxy`对象。
2.`asBinder`:返回当前Binder对象。
3.`onTransact`:这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。
这个方法的原型是`public Boolean onTransact(int code, Parcelable data, Parcelable reply, int flags)`
服务端通过`code`可以知道客户端请求的目标方法,接着从`data`中取出所需的参数,然后执行目标方法,执行完毕之后,将结果写入到`reply`中。如果此方法返回false,说明客户端的请求失败,利用这个特性可以做权限验证(即验证是否有权限调用该服务)。
4.`Proxy#[Method]`:代理类中的接口方法,这些方法运行在客户端,当客户端远程调用此方法时,它的内部实现是:首先创建该方法所需要的参数,然后把方法的参数信息写入到`_data`中,接着调用`transact`方法来发起RPC请求,同时当前线程挂起;然后服务端的`onTransact`方法会被调用,直到RPC过程返回后,当前线程继续执行,并从`_reply`中取出RPC过程的返回结果,最后返回`_reply`中的数据。
如果搞清楚了自动生成的接口文件的结构和作用之后,其实是可以不用通过AIDL而直接实现Binder的,[主席写的示例代码](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_2/src/com/ryg/chapter_2/manualbinder/BookManagerImpl.java)
(6)Binder的两个重要方法`linkToDeath`和`unlinkToDeath`
Binder运行在服务端,如果由于某种原因服务端异常终止了的话会导致客户端的远程调用失败,所以Binder提供了两个配对的方法`linkToDeath`和`unlinkToDeath`,通过`linkToDeath`方法可以给Binder设置一个死亡代理,当Binder死亡的时候客户端就会收到通知,然后就可以重新发起连接请求从而恢复连接了。
如何给Binder设置死亡代理呢?
1.声明一个`DeathRecipient`对象,`DeathRecipient`是一个接口,其内部只有一个方法`bindeDied`,实现这个方法就可以在Binder死亡的时候收到通知了。
~~~
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (mRemoteBookManager == null) return;
mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mRemoteBookManager = null;
// TODO:这里重新绑定远程Service
}
};
~~~
2.在客户端绑定远程服务成功之后,给binder设置死亡代理
~~~
mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);
~~~
## 2.4 Android中的IPC方式
(1)使用Bundle:Bundle实现了Parcelable接口,Activity、Service和Receiver都支持在Intent中传递Bundle数据。
(2)使用文件共享:这种方式简单,适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写的问题。
`SharedPreferences`是一个特例,虽然它也是文件的一种,但是由于系统对它的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读写就变得不可靠,当面对高并发读写访问的时候,有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。
(3)使用Messenger:`Messenger`是一种轻量级的IPC方案,它的底层实现就是AIDL。Messenger是以串行的方式处理请求的,即服务端只能一个个处理,不存在并发执行的情形,详细的示例见原书。
(4)使用AIDL
大致流程:首先建一个Service和一个AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub类中的抽象方法,在Service的onBind方法中返回这个类的对象,然后客户端就可以绑定服务端Service,建立连接后就可以访问远程服务端的方法了。
1.AIDL支持的数据类型:基本数据类型、`String`和`CharSequence`、`ArrayList`、`HashMap`、`Parcelable`以及`AIDL`;
2.某些类即使和AIDL文件在同一个包中也要显式import进来;
3.AIDL中除了基本数据类,其他类型的参数都要标上方向:`in`、`out`或者`inout`;
4.AIDL接口中支持方法,不支持声明静态变量;
5.为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另一个应用的时候,可以直接把整个包复制到客户端工程中。
6.`RemoteCallbackList`是系统专门提供的用于删除跨进程Listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自`IInterface`接口。
(5)使用ContentProvider
1.ContentProvider主要以表格的形式来组织数据,并且可以包含多个表;
2.ContentProvider还支持文件数据,比如图片、视频等,系统提供的`MediaStore`就是文件类型的ContentProvider;
3.ContentProvider对底层的数据存储方式没有任何要求,可以是SQLite、文件,甚至是内存中的一个对象都行;
4.要观察ContentProvider中的数据变化情况,可以通过`ContentResolver`的`registerContentObserver`方法来注册观察者;
(6)使用Socket
Socket是网络通信中“套接字”的概念,分为流式套接字和用户数据包套接字两种,分别对应网络的传输控制层的TCP和UDP协议。
## 2.5 Binder连接池
(1)当项目规模很大的时候,创建很多个Service是不对的做法,因为service是系统资源,太多的service会使得应用看起来很重,所以最好是将所有的AIDL放在同一个Service中去管理。整个工作机制是:每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节我们要单独开来,然后向服务端提供自己的唯一标识和其对应的Binder对象;对于服务端来说,只需要一个Service,服务端提供一个`queryBinder`接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。
Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service去执行,从而避免了重复创建Service的过程。
(2)作者实现的Binder连接池[`BinderPool`的实现源码](https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_2/src/com/ryg/chapter_2/binderpool/BinderPool.java),建议在AIDL开发工作中引入BinderPool机制。
## 2.6 选用合适的IPC方式
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-10_56692ec1c55ed.png)
读书笔记(1)第1章 Activity的生命周期和启动模式
最后更新于:2022-04-01 05:36:05
## 1.1 Activity生命周期全面分析
### 1.1.1 典型情况下生命周期分析
(1)一般情况下,当当前Activity从不可见重新变为可见状态时,`onRestart`方法就会被调用。
(2)当用户打开新的Activity或者切换到桌面的时候,回调如下:`onPause` -> `onStop`,但是如果新Activity采用了透明主题,那么`onStop`方法不会被回调。当用户再次回到原来的Activity时,回调如下:`onRestart` -> `onStart` -> `onResume`。
(3)`onStart`和`onStop`对应,它们是从Activity是否可见这个角度来回调的;`onPause`和`onResume`方法对应,它们是从Activity是否位于前台这个角度来回调的。
(4)从Activity A进入到Activity B,回调顺序是`onPause(A) -> onCreate(B) -> onStart(B) -> onResume(B) -> onStop(A)`,所以不能在onPause方法中做重量级的操作。
### 1.1.2 异常情况下生命周期分析
(1)`onSaveInstanceState`方法只会出现在Activity被异常终止的情况下,它的调用时机是在`onStop`之前,它和`onPause`方法没有既定的时序关系,可能在它之前,也可能在它之后。当Activity被重新创建的时候,`onRestoreInstanceState`会被回调,它的调用时机是`onStart`之后。
系统只会在Activity即将被销毁并且有机会重新显示的情况下才会去调用onSaveInstanceState方法。
当Activity在异常情况下需要重新创建时,系统会默认为我们保存当前Activity的视图结构,并且在Activity重启后为我们恢复这些数据,比如文本框中用户输入的数据、listview滚动的位置等,这些view相关的状态系统都会默认为我们恢复。具体针对某一个view系统能为我们恢复哪些数据可以查看view的源码中的onSaveInstanceState和onRestoreInstanceState方法。
(2)Activity按优先级的分类
前台Activity;可见但非前台Activity;后台Activity
(3)`android:configChanges="xxx"`属性,常用的主要有下面三个选项:
`local`:设备的本地位置发生了变化,一般指切换了系统语言;
`keyboardHidden`:键盘的可访问性发生了变化,比如用户调出了键盘;
`orientation`:屏幕方向发生了变化,比如旋转了手机屏幕。
配置了`android:configChanges="xxx"`属性之后,Activity就不会在对应变化发生时重新创建,而是调用Activity的`onConfigurationChanged`方法。
##1.2 Activity的启动模式
### 1.2.1 启动模式
(1)当任务栈中没有任何Activity的时候,系统就会回收这个任务栈。
(2)从非Activity类型的Context(例如ApplicationContext、Service等)中以`standard`模式启动新的Activity是不行的,因为这类context并没有任务栈,所以需要为待启动Activity指定`FLAG_ACTIVITY_NEW_TASK`标志位。
(3)任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity位于暂停状态,用户可以通过切换将后台任务栈再次调到前台。
(4)参数`TaskAffinity`用来指定Activity所需要的任务栈,意为任务相关性。默认情况下,所有Activity所需的任务栈的名字为应用的包名。TaskAffinity属性主要和`singleTask`启动模式或者`allowTaskReparenting`属性配对使用,在其他情况下没有意义。
当TaskAffinity和singleTask启动模式配对使用的时候,它是具有该模式的Activity的目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中;
当TaskAffinity和allowTaskReparenting结合的时候,当一个应用A启动了应用B的某个ActivityC后,如果ActivityC的allowTaskReparenting属性设置为true的话,那么当应用B被启动后,系统会发现Activity C所需的任务栈存在了,就将ActivityC从A的任务栈中转移到B的任务栈中。
(5)singleTask模式的具体分析:当一个具有singleTask启动模式的Activity请求启动之后,系统首先会寻找是否存在A想要的任务栈,如果不存在,就重新创建一个任务栈,然后创建Activity的实例把它放到栈中;如果存在Activity所需的任务栈,这时候要看栈中是否有Activity实例存在,如果有,那么系统就会把该Activity实例调到栈顶,并调用它的onNewIntent方法(它之上的Activity会被迫出栈,所以singleTask模式具有FLAG_ACTIVITY_CLEAR_TOP效果);如果Activity实例不存在,那么就创建Activity实例并把它压入栈中。
(6)设置启动模式既可以使用xml属性`android:launchMode`,也可以使用代码`intent.addFlags()`。区别在于限定范围不同,前者无法直接为Activity设置FLAG_ACTIVITY_CLEAR_TOP标识,而后者无法为Activity指定singleInstance模式。
### 1.2.2 Activity的Flags
`FLAG_ACTIVITY_NEW_TASK`,`FLAG_ACTIVITY_SINGLE_TOP`,`FLAG_ACTIVITY_CLEAR_TOP`
`FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS`:具有这个标记的Activity不会出现在历史Activity列表中,当某些情况下我们不希望用户通过历史列表回到我们的Activity的时候这个标记比较有用,它等同于属性设置`android:excludeFromRecents="true"`。
## 1.3 IntentFilter的匹配规则
(1)IntentFilter中的过滤信息有action、category、data,为了匹配过滤列表,需要同时匹配过滤列表中的action、category、data信息,否则匹配失败。一个过滤列表中的action、category、data可以有多个,所有的action、category、data分别构成不同类别,同一类别的信息共同约束当前类别的匹配过程。只有一个Intent同时匹配action类别、category类别和data类别才算完全匹配,只有完全匹配才能成功启动目标Activity。此外,一个Activity中可以有多个intent-filter,一个Intent只要能匹配任何一组intenf-filter即可成功启动对应的Activity。
~~~
<intent-filter>
<action android:name="com.ryg.charpter_1.c" />
<action android:name="com.ryg.charpter_1.d" />
<category android:name="com.ryg.category.c" />
<category android:name="com.ryg.category.d" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
~~~
(2)action匹配规则
只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功,action匹配区分大小写。
(3)category匹配规则
Intent中如果有category那么所有的category都必须和过滤规则中的其中一个category相同,如果没有category的话那么就是默认的category,即`android.intent.category.DEFAULT`,所以为了Activity能够接收隐式调用,配置多个category的时候必须加上默认的category。
(4)data匹配规则
data的结构很复杂,语法大致如下:
~~~
<data android:scheme="string"
android:host="string"
android:port="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:mimeType="string" />
~~~
主要由`mimeType`和`URI`组成,其中mimeType代表媒体类型,而URI的结构也复杂,大致如下:
`<scheme>://<host>:<port>/[<path>]|[<pathPrefix>]|[pathPattern]`
例如`content://com.example.project:200/folder/subfolder/etc`
`scheme、host、port`分别表示URI的模式、主机名和端口号,其中如果scheme或者host未指定那么URI就无效。
`path、pathPattern、pathPrefix`都是表示路径信息,其中path表示完整的路径信息,pathPrefix表示路径的前缀信息;pathPattern表示完整的路径,但是它里面包含了通配符(*)。
data匹配规则:Intent中必须含有data数据,并且data数据能够完全匹配过滤规则中的某一个data。
URI有默认的scheme!
如果过滤规则中的mimeType指定为`image/*`或者`text/*`等这种类型的话,那么即使过滤规则中没有指定URI,URI有默认的scheme是content和file!如果过滤规则中指定了scheme的话那就不是默认的scheme了。
~~~
//URI有默认值
<intent-filter>
<data android:mimeType="image/*"/>
...
</intent-filter>
//URI默认值被覆盖
<intent-filter>
<data android:mimeType="image/*" android:scheme="http" .../>
...
</intent-filter>
~~~
如果要为Intent指定完整的data,必须要调用`setDataAndType`方法!
不能先调用setData然后调用setType,因为这两个方法会彼此清除对方的值。
~~~
intent.setDataAndType(Uri.parse("file://abc"), "image/png");
~~~
data的下面两种写法作用是一样的:
~~~
<intent-filter>
<data android:scheme="file" android:host="www.github.com"/>
</intent-filter>
<intent-filter>
<data android:scheme="file"/>
<data android:host="www.github.com"/>
</intent-filter>
~~~
如何判断是否有Activity能够匹配我们的隐式Intent?
(1)`PackageManager`的`resolveActivity`方法或者Intent的`resolveActivity`方法:如果找不到就会返回null
(2)PackageManager的`queryIntentActivities`方法:它返回所有成功匹配的Activity信息
针对Service和BroadcastReceiver等组件,PackageManager同样提供了类似的方法去获取成功匹配的组件信息,例如`queryIntentServices`、`queryBroadcastReceivers`等方法
有一类action和category比较重要,它们在一起用来标明这是一个入口Activity,并且会出现在系统的应用列表中。
~~~
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
~~~
前言
最后更新于:2022-04-01 05:36:03
> 原文出处:http://hujiaweibujidao.github.io/blog/2015/12/05/Art-of-Android-Development-Reading-Notes/
啥也不说了,[@主席](http://weibo.com/uc83018062)的《Android开发艺术探索》真是业界良心之作,不得不看!感谢主席,膜拜主席!主席主席,我要跟你生猴子!(>^ω^<)
读书笔记中若有任何问题请留言告知,谢谢关注,谢谢阅读。