(1)第一次启动:onCreate->onStart->onResume。
(2)当用户打开一个新的activity,或者切换到桌面时:onPause->onStop。
存在一种特殊情况,新的activity使用的是透明主题时:当前activity不会回调onStop
(3)当用户再次回到原Activity时:onRestart->onStart->onResume。
(4)当用户按Back会退时:onPause->onStop->onDestroy。
(5)对于生命周期来说,onCreate和onDestroy是配对的,且只可能被调用一次,onStart和onStop是配对的,onResume和onPause是配对的,onStart是是否可见,onResume是是否在前台。
(6)当另起一个activity时,执行顺序是:MainAActivity:onPause->MainBActivity:onCreate->MainBActivity:onStart->MainBActivity:onResume->MainAActivity:onStop。
onPause不能执行太耗时的操作,所以资源回收和释放应该放在onDestroy中执行。
(1)横竖屏切换等改变系统配置
异常情况下终止,系统会在onStop之前调用onSaveInstanceState来保存当前activity的状态,当activity被重新创建后,系统会调用onRestoreInstanceState,并且把activity销毁时onSaveInstanceState方法所保存的Bundle对象作为参数同时传递给onRestoreInstanceState和onCreate方法,onRestoreInstanceState调用在onCreate之后。
(2)资源内存不足导致低优先级的activity被杀死
activity优先级从高到低:前台Activity->可见但非前台Activity->后台Activity
如果组件中没有四大组件在运行,则很容易被系统杀死,这种组件不建议放在后台执行,如要放在后台执行,建议放入service中从而保证进程有一定的优先级,不容易被系统杀死。
(3)我们可以在Activity的configChanges属性中指定某个选项,比如屏幕方向发生改变(android:configChanges=“orientation”),如果不指定选项,当配置发生改变之后,就会导致activity重建,常用的有locale(设备的本地位置发生改变,一般指切换了系统语音),orientation(屏幕方向发生改变),keyboardHidden(键盘的可访问性发生改变,比如用户调出了键盘)。
(1)standard:标准模式,每次启动一个activity都会重新创建一个实例
(2)singleTop:栈顶复用模式,如果新Activity已经在栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法会被回调,通过此参数可以取出当前请求的信息。
(3)singleTask:栈内复用模式。这是一种单实例模式,只要activity在一个栈中存在,那么多次启动也不会重新创建activity。举例1:目前任务栈S1中的情况为ABC,要创建的是D且需要任务栈S2,由于实例和栈都不存在,所以会创建任务栈S2,再创建D进入S2。举例2:假设D所需的是任务栈S1,由于S1已经存在,则直接创建D进入S1。举例3:假如S1中为ADBC,此时由于存在D,则不会重新创建D,而是把D上面的activity全都移出栈,此时S1内情况为AD。
(4)singleInstance:单实例模式,加强的singleTask模式,创建的activity只能单独在一个任务栈中。
可以通过activity的launchMode属性来设置启动模式
或者通过在Intent中设置标志位来为Activity指定启动模式
Intent intent=new Intent();
intent.setClass(MainActivity.class,SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
两种设置方式,优先级第二种高于第一种,当两种同时存在是,以第二种方式为准。第一种方式无法直接为Activity添加FLAGACTIVITYCLEAR_TOP标识,第二种方式无法指定singleInstance模式。
FLAG_ACTIVITY_NEW_TASK
为activity指定“singleTask”启动模式
FLAG_ACTIVITY_SINGLE_TOP
为activity指定“singleTop”启动模式
FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的activity,当它启动时,在同一个任务栈,位于它上面的activity都要出栈,一般与FLAG_ACTIVITY_NEW_TASK配合使用
IPC即Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
线程是CPU调度的最小单元,是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上是指一个程序或者应用。进程与线程是包含与被包含的关系。一个进程可以包含多个线程。最简单的情况下一个进程只有一个线程,即主线程( 例如Android的UI线程) 。
任何操作系统都需要有相应的IPC机制。如Windows上的剪贴板、管道和邮槽;Linux上命名管道、共享内容、信号量等。Android中最有特色的进程间通信方式就是binder,另外还支持socket。contentProvider是Android底层实现的进程间通信。
在Android中,IPC的使用场景大概有以下:
有些模块由于特殊原因需要运行在单独的进程中。
通过多进程来获取多份内存空间。
当前应用需要向其他应用获取数据。
在android中使用多进程只有一种方法,那就是给四大组件在AndroidMenifest中指定android:process属性。
使用 adb shell ps 或 adb shell ps|grep 包名 查看当前所存在的进程信息。
两种进程命名方式的区别
所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。
一般来说,使用多进程会造成如下几方面的问题
主要介绍 Serializable、 Parcelable 、 Binder 。Serializable和Parcelable接口可以完成对象的序列化过程,我们通过Intent和Binder传输数据时就需要Parcelabel和Serializable。还有的时候我们需要对象持久化到存储设备上或者通过网络传输到其他客户端,也需要Serializable完成对象持久化。
Serializable 是Java提供的一个序列化接口( 空接口) ,为对象提供标准的序列化和反序列化操作。只需要一个类去实现 Serializable 接口并声明一个 serialVersionUID 即可实现序列化。
private static final long serialVersionUID = 8711368828010083044L
比如User类是一个实现了Serializable接口的类,他可以被序列化和反序列化
public class User implements Serializable{
private static final long serialVersionUID = 8711368828010083044L
public int userId;
public String userName;
......
}
ObjectOutputStream和ObjectInputStream实现序列化和反序列化
//序列化过程
User user=new User(0,"jack");
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
//反序列化过程
ObjectInputStream in=new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser=(User)in,readObject;
in.close();
虽然serialVersionUID即使没有设置也可以序列化,但没有这个可能会导致反序列化失败。如果不手动指定 serialVersionUID 的值,反序列化时如果当前类有所改变( 比如增删了某些成员变量) ,那么系统就会重新计算当前类的hash值并更新 serialVersionUID 。这个时候当前类的 serialVersionUID 就和序列化数据中的serialVersionUID 不一致,导致反序列化失败,程序就出现crash。
静态成员变量属于类不属于对象,不参与序列化过程,其次 transient 关键字标记的成员变量也不参与序列化过程。
通过重写writeObject和readObject方法可以改变系统默认的序列化过程。
Parcelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并通过Intent和Binder传递。下面举个典型用法
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public User(int userId,String userName,boolean isMale){
this.userId=userId;
this.userName=userName;
this.isMale=isMale;
}
private User(Parcel in) {
userId=in.readInt();
userName=in.readString();
isMale=in.readInt()==1;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(userId);
out.writeString(userName);
out.writeInt(isMale?1:0);
}
public static final Parcelable.Creator CREATOR=new Parcelable.Creator(){
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}
序列化功能通过writeToParcel方法来完成,反序列化功能由CREATOR来完成。内容描述功能由describeContents方法来完成,几乎所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1。需要注意的是,如果User(Pracel in)中存在另一个可序列化对象,它的反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。
Binder是Android中的一个类,实现了 IBinder 接口。从IPC角度说,Binder是Andoird的一种跨进程通讯方式,Binder还可以理解为一种虚拟物理设备,它的设备驱动是/dev/binder。从Android Framework角度来说,Binder是 ServiceManager 连接各种Manager( ActivityManager· 、 WindowManager )和相应 ManagerService 的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService时,服务端返回一个包含服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务器端提供的服务或者数据( 包括普通服务和基于AIDL的服务)。
Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。
图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。
主要有以下方式:
四大组件中的三大组件( Activity、Service、Receiver) 都支持在Intent中传递 Bundle 数据。
Bundle实现了Parcelable接口,因此可以方便的在不同进程间传输。当我们在一个进程中启动了另一个进程的Activity、Service、Receiver,可以再Bundle中附加我们需要传输给远程进程的消息并通过Intent发送出去。被传输的数据必须能够被序列化。
两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获得数据。通过代码举个例子
//对象写入文件
private void persistToFile(){
new Thread(new Runnable() {
@Override
public void run() {
User user=new User(1,"hello word",false);
File dir=new File(Environment.getExternalStorageDirectory()+"/user.txt");
if (!dir.exists()){
dir.mkdirs();
}
File cachedFile=new File(getCacheDir().getPath());
ObjectOutputStream objectOutputStream=null;
try {
objectOutputStream=new ObjectOutputStream(new FileOutputStream(cachedFile));
objectOutputStream.writeObject(user);
Log.d(TAG,"persist user:"+user);
} catch (IOException e) {
e.printStackTrace();
}finally {
objectOutputStream.close();
}
}
}).start();
}
//从文件中读取对象
private void recoverFromFile(){
new Thread(new Runnable() {
@Override
public void run() {
User user=null;
File cachedFile=new File(getCacheDir().getPath());
if (cachedFile.exists()){
ObjectInputStream objectInputStream=null;
try {
objectInputStream=new ObjectInputStream(new FileInputStream(cachedFile));
user=(User) objectInputStream.readObject();
Log.d(TAG,"recover user:"+user);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
objectInputStream.close();
}
}
}
}).start();
}
注意一点,只会从文件中恢复之前存储的对象的内容,之所以说内容,是因为反序列化得到的对象的内容虽然和序列化之前的内容一样,但本质上是两个对象。
Messenger可以在不同进程间传递Message对象。是一种轻量级的IPC方案,底层实现是AIDL。它对AIDL进行了封装,使得我们可以更简便的进行IPC。
Messenger的工作原理如图所示
具体使用时,分为服务端和客户端:
总而言之,就是客户端和服务端 拿到对方的Messenger来发送 Message 。只不过客户端通过bindService 而服务端通过 message.replyTo 来获得对方的Messenger。
Messenger中有一个 Hanlder 以串行的方式处理队列中的消息。不存在并发执行,因此我们不用考虑线程同步的问题。
如果有大量的并发请求,使用Messenger就不太适合,同时如果需要跨进程调用服务端的方法,Messenger就无法做到了。这时我们可以使用AIDL。
流程如下:
自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。
如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
package com.ryg.chapter_2.aidl;
parcelable Book;
AIDL接口中的参数除了基本类型以外都必须表明方向in/out。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。
void addBook(in Book book);
AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用 CopyOnWriteArrayList 来进行自动线程同步。类似的还有 ConcurrentHashMap 。
因为客户端的listener和服务端的listener不是同一个对象,所以 RecmoteCallbackList 是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口,因为所有AIDL接口都继承自 IInterface 接口。
public class RemoteCallbackList
它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是 IBinder 类型,value是 Callback 类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。
客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。
权限验证
默认情况下,我们的远程服务任何人都可以连接,我们必须加入权限验证功能,权限验证失败则无法调用服务中的方法。通常有两种验证方法:
在onBind中验证,验证不通过返回null
验证方式比如permission验证,在AndroidManifest声明:
Android自定义权限和使用权限
public IBinder onBind(Intent intent){
int check = checkCallingOrSelefPermission(“com.ryq.chapter2.permission.ACCESSBOOK_SERVICE”);
if(check == PackageManager.PERMISSION_DENIED){
return null;
}
return mBinder;
}
这种方法也适用于Messager。
在onTransact中验证,验证不通过返回false
可以permission验证,还可以采用Uid和Pid验证。
ContentProvider是四大组件之一,天生就是用来进程间通信。和Messenger一样,其底层实现是用Binder。
系统预置了许多ContentProvider,比如通讯录、日程表等。要RPC访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。
创建自定义的ContentProvider,只需继承ContentProvider类并实现 onCreate 、 query 、 update、 insert 、 getType 六个抽象方法即可。getType用来返回一个Uri请求所对应的MIME类型,剩下四个方法对应于CRUD操作。这六个方法都运行在ContentProvider进程中,除了 onCreate 由系统回调并运行在主线程里,其他五个方法都由外界调用并运行在Binder线程池中。
ContentProvider是通过Uri来区分外界要访问的数据集合,例如外界访问ContentProvider中的表,我们需要为它们定义单独的Uri和UriCode。根据UriCode,我们就知道要访问哪个表了。
==query、update、insert、delete四大方法存在多线程并发访问,因此方法内部要做好线程同步。==若采用SQLite并且只有一个SQLiteDatabase,SQLiteDatabase内部已经做了同步处理。若是多个SQLiteDatabase或是采用List作为底层数据集,就必须做线程同步。
Socket也称为“套接字”,分为流式套接字和用户数据报套接字两种,分别对应于TCP和UDP协议。Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。我们以一个跨进程的聊天程序来演示。
在远程Service建立一个TCP服务,然后在主界面中连接TCP服务。服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到 Socket 对象就可以向服务端发送消息或者接受服务端发送的消息。
本例的客户端和服务端源代码
除了采用TCP套接字,也可以用UDP套接字。实际上socket不仅能实现进程间的通信,还可以实现设备间的通信(只要设备之间的IP地址互相可见)。
使用socket需要声明权限
前面提到AIDL的流程是:首先创建一个service和AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法,客户端在Service的onBind方法中拿到这个类的对象,然后绑定这个service,建立连接后就可以通过这个Stub对象进行RPC。
那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。
Binder连接池的工作原理如图
本章介绍View的事件分发和滑动冲突问题的解决方案。
介绍 View的位置参数、MotionEvent和TouchSlop对象、VelocityTracker、GestureDetector和Scroller对象。
View是Android中所有控件的基类,View的本身可以是单个空间,也可以是多个控件组成的一组控件,即ViewGroup,ViewGroup继承自View,其内部可以有子View,这样就形成了View树的结构。
View的位置主要由它的四个顶点来决定,分别对应View的四个属性,top,left,right,bottom,top为左上角的纵坐标,left为左上角的横坐标,right为右下角的横坐标,bottom为右下角的纵坐标。需要注意的是,这些坐标都是View相对于它的父容器来讲的
同时,我们可以得到View的大小:
width = right - left
height = bottom - top
而这四个参数可以由以下方式获取:
Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();
Android3.0后,View增加了x、y、translationX和translationY这几个参数。其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于容器的偏移量。他们之间的换算关系如下:
x = left + translationX;
y = top + translationY;
top,left表示原始左上角坐标,而x,y表示变化后的左上角坐标。在View没有平移时,x=left,y=top。
View平移的过程中,top和left不会改变,改变的是x、y、translationX和translationY。
ViewConfiguration.get(getContext()).getScaledTouchSlop()
VelocityTracker velocityTracker=VelocityTracker.obtain();
velocityTracker.addMovement(event);
计算速度,获得水平速度和竖直速度
velocityTracker.computeCurrentVelocity(1000);
int xVelocity= (int) velocityTracker.getXVelocity();
int yVelocity= (int) velocityTracker.getYVelocity();
注意,获取速度之前必须先计算速度,即调用computeCurrentVelocity方法,这里指的速度是指一段时间内手指滑过的像素数,1000指的是1000毫秒,得到的是1000毫秒内滑过的像素数。速度可正可负:速度 = ( 终点位置 - 起点位置) / 时间段
最后,当不需要使用的时候,需要调用clear()方法重置并回收内存:
velocityTracker.clear();
velocityTracker.recycle();
//手势检测
GestureDetector mGestureDetector=new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
接着,接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加如下实现
boolean consume=mGestureDetector.onTouchEvent(event);
return consume;
实现OnGestureListener和OnDoubleTapListener接口中的方法
其中常用的方法有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap( 双击)。建议:如果只是监听滑动相关的,可以自己在onTouchEvent中实现,如果要监听双击这种行为,那么就使用GestureDetector。
Scroller scroller = new Scroller(mContext);
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX,0,delta,0,1000);
invalidata();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX,mScroller.getCurrY());
postInvalidate();
}
scrollBy实际上是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo则实现了基于所传参数的绝对滑动
这边介绍一下mScrollX 和mScrollY,这两个属性都可以通过getScrollX和getScrollY得到,滑动过程中,mScrollX的值总是等于View的左边缘和view内容左边缘在水平方向上的距离,mScrollY的值总是等于View的上边缘和view内容上边缘在竖直方向上的距离。scrollTo和scrollBy改变的 是View内容的位置而不能改变View在布局中的位置。mScrollX 和mScrollY单位是像素,当View左边缘在View内容左边缘右边时,mScrollX为正值,否则为负值。当View上边缘在View内容上边缘下边时,mScrollY为正值,否则为负值。换句话说,如果从左往右滑动,mScrollX 为负值,反之为正值,从上往下滑动时,mScrollY为负值,反之为正值。简单图示如下
使用动画来移动View,主要操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果采用属性动画的话,为了能够兼容3.0以下的版本,需要采用开源动画库nineoldandroids
使用属性动画代码如下((View在100ms内向右移动100像素))
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
直接通过代码,改变控件的一些布局参数,例如marginLeft,width等
简单使用如下:
ViewGroup.MarginLayoutParams params= (ViewGroup.MarginLayoutParams) textView.getLayoutParams();
params.width+=100;
params.leftMargin+=100;
textView.requestLayout();
//或者textView.setLayoutParams(params);
scrollTo/scrollBy:操作简单,适合对View内容的滑动;
动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
改变布局参数:操作稍微复杂,适用于有交互的View。
使用Scroller实现弹性滑动的典型使用方法如下:
Scroller scroller = new Scroller(mContext);
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int dextY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
//1000ms内滑向destX,效果就是缓慢滑动
mScroller.startSscroll(scrollX,0,deltaX,0,1000);
invalidate();
}
@override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
从上面代码可以知道,我们首先会构造一个Scroller对象,并调用他的startScroll方法,该方法并没有让view实现滑动,只是把参数保存下来,我们来看看startScroll方法的实现就知道了:
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAminationTimeMills();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float)mDuration;
}
可以知道,startScroll方法的几个参数的含义,startX和startY表示滑动的起点,dx和dy表示的是滑动的距离,而duration表示的是滑动时间,注意,这里的滑动指的是View内容的滑动,在startScroll方法被调用后,马上调用invalidate方法,这是滑动的开始,invalidate方法会导致View的重绘,在View的draw方法中调用computeScroll方法,computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法进行第二次重绘,一直循环,直到computeScrollOffset()方法返回值为false才结束整个滑动过程。 我们可以看看computeScrollOffset方法是如何获得当前的scrollX和scrollY的:
public boolean computeScrollOffset(){
...
int timePassed = (int)(AnimationUtils.currentAnimationTimeMills() - mStartTime);
if(timePassed < mDuration){
switch(mMode){
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDuratio
nReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(y * mDeltaY);
break;
...
}
}
return true;
}
到这里我们就基本明白了,computeScroll向Scroller获取当前的scrollX和scrollY其实是通过计算时间流逝的百分比来获得的,每一次重绘距滑动起始时间会有一个时间间距,通过这个时间间距Scroller就可以得到View当前的滑动位置,然后就可以通过scrollTo方法来完成View的滑动了。
动画本身就是一种渐近的过程,因此通过动画来实现的滑动本身就具有弹性。实现也很简单:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()
;
//当然,我们也可以利用动画来模仿Scroller实现View弹性滑动的过程:
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();
上面的动画本质上是没有作用于任何对象上的,他只是在1000ms内完成了整个动画过程,利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,根据比例计算出View所滑动的距离。采用这种方法也可以实现其他动画效果,我们可以在onAnimationUpdate方法中加入自定义操作。
延时策略的核心思想是通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Hander和View的postDelayed方法,也可以使用线程的sleep方法。 下面以Handler为例:
private static final int MESSAGESCROLLTO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
public void handleMessage(Message msg){
switch(msg.what){
case MESSAGESCROLLTO:
mCount ++ ;
if (mCount <= FRAME_COUNT){
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX,0);
mHandelr.sendEmptyMessageDelayed(MESSAGESCROLLTO , DELAYED_TIME);
}
break;
default : break;
}
}
}
点击事件是MotionEvent。首先我们先看看下面一段伪代码,通过它我们可以理解到点击事件的传递规则:
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEnvet(ev);
}
return consume;
}
上面代码主要涉及到以下三个方法:
public boolean dispatchTouchEvent(MotionEvent ev);
这个方法用来进行事件的分发。如果事件传递给当前view,则调用此方法。返回结果表示是否消耗此事件,受onTouchEvent和下级View的dispatchTouchEvent方法影响。
public boolean onInterceptTouchEvent(MotionEvent ev);
这个方法用来判断是否拦截事件。在dispatchTouchEvent方法中调用。返回结果表示是否拦截。
public boolean onTouchEvent(MotionEvent ev);
这个方法用来处理点击事件。在dispatchTouchEvent方法中调用,返回结果表示是否消耗事件。如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
点击事件的传递规则:对于一个根ViewGroup,点击事件产生后,首先会传递给他,这时候就会调用他的dispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要拦截事件,接下来事件就会交给ViewGroup处理,调用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值为false,表示ViewGroup不拦截该事件,这时事件就传递给他的子View,接下来子View的dispatchTouchEvent方法,如此反复直到事件被最终处理。
当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用,同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。
由此可见处理事件时的优先级关系: onTouchListener > onTouchEvent >onClickListener
如果view的onTouchEvent返回的是false,则返回给上一级的onTouchEvent处理,如果再次返回false,则继续往上级传递。
关于事件传递的机制,这里给出一些结论:
对于场景一,处理的规则是:当用户左右( 上下) 滑动时,需要让外部的View拦截点击事件,当用户上下( 左右) 滑动的时候,需要让内部的View拦截点击事件。根据滑动的方向判断谁来拦截事件。
对于场景二,由于滑动方向一致,这时候只能在业务上找到突破点,根据业务需求,规定什么时候让外部View拦截事件,什么时候由内部View拦截事件。
场景三的情况相对比较复杂,同样根据需求在业务上找到突破点。
外部拦截法
所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。下面是伪代码:
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 = flase;
}
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default :
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
针对不同冲突,只需修改父容器需要当前事件的条件即可。其他不需修改也不能修改。
ACTION_DOWN:必须返回false。因为如果返回true,后续事件都会被拦截,无法传递给子View。
ACTION_MOVE:根据需要决定是否拦截
ACTIONUP:必须返回false。如果拦截,那么子View无法接受up事件,无法完成click操作。而如果是父容器需要该事件,那么在ACTIONMOVE时已经进行了拦截,根据上一节的结论3,ACTION_UP不会经过onInterceptTouchEvent方法,直接交给父容器处理。
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);
}
除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
因此,父元素要做以下修改:
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
优化滑动体验:
mScroller.abortAnimation();
由于内部拦截法实现起来会比外部拦截法复杂,所以一般采用外部拦截法来解决常见的滑动冲突。
ViewRoot的实现是 ViewRootImpl 类,是连接WindowManager和DecorView的纽带,View的三大流程( mearsure、layout、draw) 均是通过ViewRoot来完成。当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建 ViewRootImpl 对象,并将ViewRootImpl 对象和DecorView建立连接,源码如下:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);
Measure完成后, 可以通过getMeasuredWidth 、getMeasureHeight 方法来获取View测量后的宽/高。特殊情况下,测量的宽高不等于最终的宽高,详见后面。
Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四个定点坐标。
DecorView作为顶级View,其实是一个 FrameLayout ,它包含一个竖直方向的 LinearLayout ,这个 LinearLayout 分为标题栏和内容栏两个部分。
在Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。这个内容栏的id是 R.android.id.content ,通过
ViewGroup content = findViewById(R.android.id.content);
可以得到这个contentView。View层的事件都是先经过DecorView,然后才传递到子View。
MeasureSpec决定了一个View的尺寸规格。但是父容器会影响View的MeasureSpec的创建过程。系统将View的 LayoutParams 根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的宽高。
MeasureSpec代表一个32位int值,高2位代表SpecMode( 测量模式) ,低30位代表SpecSize( 在某个测量模式下的规格大小) 。
SpecMode有三种:
UNSPECIFIED :父容器不对View进行任何限制,要多大给多大,一般用于系统内部
EXACTLY:父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的 match_parent 和具体数值这两种模式
ATMOST :对应View的默认大小,不同View实现不同,View的大小不能大于父容器的SpecSize,对应 LayoutParams 中的 wrapcontent
对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定。而View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。
View的measure过程由ViewGroup传递而来,参考ViewGroup的 measureChildWithMargins 方法,通过调用子元素的 getChildMeasureSpec 方法来得到子元素的MeasureSpec,再调用子元素的 measure 方法。
View的工作l流程主要指measure,layout,draw,即测量,布局和绘制三大流程。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 在 MeasureSpec.AT_MOST 模式下,给定一个默认值mWidth,mHeight。默认宽高灵活指定
//参考TextView、ImageView的处理方式
//其他情况下沿用系统测量规则即可
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWith, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
measureChildren方法的流程:
通过LinearLayout的onMeasure方法里来分析ViewGroup的measure过程:
View的measure过程是三大流程中最复杂的一个,measure完成以后,通过 getMeasuredWidth/Height 方法就可以正确获取到View的测量后宽/高。在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,所以在onMeasure中拿到的宽/高很可能不是准确的。
如果我们想要在Activity启动的时候就获取一个View的宽高,怎么操作呢?
因为View的measure过程和Activity的生命周期并不是同步执行,无法保证在Activity的 onCreate、onStart、onResume 时某个View就已经测量完毕。所以有以下四种方式来获取View的宽高:
match_parent:
无法measure出具体的宽高,因为不知道父容器的剩余空间,无法测量出View的大小
具体的数值( dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
// View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
layout的作用是ViewGroup用来确定子View的位置,当ViewGroup的位置被确定后,它会在onLayout中遍历所有的子View并调用其layout方法,在 layout 方法中, onLayout 方法又会被调用。
View的 layout 方法确定本身的位置,源码流程如下:
以LinearLayout的 onLayout 方法为例:
View的测量宽高和最终宽高的区别:
在View的默认实现中,View的测量宽高和最终宽高相等,只不过测量宽高形成于measure过程,最终宽高形成于layout过程。但重写view的layout方法可以使他们不相等。
View的绘制过程遵循如下几步:
ViewGroup会默认启用 setWillNotDraw 为ture,导致系统不会去执行 onDraw ,所以自定义ViewGroup需要通过onDraw来绘制内容时,必须显式的关闭 WILLNOTDRAW 这个优化标记位,即调用 setWillNotDraw(false);
继承View 重写onDraw方法
通过 onDraw 方法来实现一些不规则的效果,这种效果不方便通过布局的组合方式来达到。这种方式需要自己支持 wrap_content ,并且padding也要去进行处理。
继承ViewGroup派生特殊的layout
实现自定义的布局方式,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子View的测量和布局过程。
继承特定的View子类( 如TextView、Button)
扩展某种已有的控件的功能,比较简单,不需要自己去管理 wrap_content 和padding。
继承特定的ViewGroup子类( 如LinearLayout)
比较常见,实现几种view组合一起的效果。与方法二的差别是方法二更接近底层实现。
略
掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等
面对新的自定义View时,对其分类并选择合适的实现思路。
什么是远程view呢?它和远程service一样,RemoteViews可以在其他进程中显示。我们可以跨进程更新它的界面。在Android中,主要有两种场景:通知栏和桌面小部件。
通知栏主要是通过NotificationManager的notify方法实现。桌面小部件是通过APPWidgetProvider来实现。APPWidgetProvider本质是一个广播。RemoteViews运行在系统的SystemServer进程。
我们用到自定义通知,首先要提供一个布局文件,然后通过RemoteViews来加载,可以自定义通知的样式。更新view时,通过RemoteViews提供的一系列方法。如果给一个控件加点击事件,要使用PendingIntent。
AppWidgetProvider是实现桌面小部件的类,本质是一个BroadcastReceiver。开发步骤如下:
定义小部件界面 代码
定义小部件配置信息 代码
定义小部件实现类,继承AppWidgetProvider 代码
上面的例子实现了一个简单地桌面小部件,在小部件上显示一张图片,点击后会旋转一周。
在AndroidManifest.mxl中声明小部件
receiver android:name=“.MyAppWidgetProvider” >
第一个action用于识别小部件的单击,第二个action作为小部件的标识必须存在。
AppWidgetProvider除了onUpdate方法,还有一系列方法。这些方法会自动被onReceive方法调用。当广播到来以后,AppWidgetProvider会自动根据广播的action通过onReceive方法分发广播。
onEnable:该小部件第一次添加到桌面时调用,添加多次只在第一次调用
onUpdate:小部件被添加或者每次小部件更新时调用,更新时机由updatePeriodMillis指定,每个周期小部件都会自动更新一次。
onDeleted:每删除一次桌面小部件都会调用一次
onDisabled:最后一个该类型的桌面小部件被删除时调用
onReceive:内置方法,用于分发具体事件给以上方法
PendingIntent表示一种处于待定的状态的intent。典型场景是RemoteViews添加单击事件。通过send和cancel方法来发送和取消待定intent。
PendingIntent支持三种待定意图:
其中requestCode多数情况下设为0即可,requestCode会影响flags的效果。
PendingIntent的匹配规则:
如果两个PendingIntent,它们内部的Intent相同且requestCode也相同,那这两个PendingIntent就是相同的。
Intent的匹配规则:
如果两个intent的ComponentName和intent-filter相同,那么这两个intent相同。Extras不参与匹配过程。
flags参数的含义
FLAGONESHOT
当前的PendingIntent只能被使用一次,然后就会被自动cancel,如果后续还有相同的PendingIntent,它们的send方法会调用失败。对于通知栏来说,同类的通知只能使用一次,后续的通知将无法打开。
FLAGNOCREATE
当前的PendingIntent不会主动创建,如果当前PendingIntent之前不存在(匹配的PendingIntent),那么获取PendingIntent失败。这个flag很少使用。
FLAGCANCELCURRENT
当前的PendingIntent如果存在(匹配的PendingIntent),那么它们都会被cancel,然后系统创建一个新的PendingIntent。对于通知栏来说,那些被cancel的消息单击后将无法打开。
FLAGUPDATECURRENT
当前PendingIntent如果已经存在(匹配的PendingIntent),那么它们都会被更新。即intent中的extras会被替换成最新的。
举例:
在manager.notify(id,notification)中,如果id是常量,那么多次调用notify只能弹出一个通知,后续的通知会把前面的通知完全替代。而如果每次id都不同,那么会弹出多个通知。
如果id每次都不同且PendingIntent不匹配,那么flags不会对通知之间造成干扰。
如果id不同且PendingIntent匹配:
通知栏和小组件分别由NotificationManager(NM)和AppWidgetManager(AWM)管理,而NM和AWM通过Binder分别和SystemService进程中的NotificationManagerService以及AppWidgetService中加载的,而它们运行在系统的SystemService中,这就和我们进程构成了跨进程通讯。
首先RemoteViews会通过Binder传递到SystemService进程,因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews的包名等信息拿到该应用的资源;然后通过LayoutInflater去加载RemoteViews中的布局文件。接着系统会对View进行一系列界面更新任务,这些任务就是之前我们通过set来提交的。set方法对View的更新并不会立即执行,会记录下来,等到RemoteViews被加载以后才会执行。
为了提高效率,系统没有直接通过Binder去支持所有的View和View操作。而是提供一个Action概念,Action同样实现Parcelable接口。系统首先将View操作封装到Action对象并将这些对象跨进程传输到SystemService进程,接着SystemService进程执行Action对象的具体操作。远程进程通过RemoteViews的apply方法来进行View的更新操作,RemoteViews的apply方法会去遍历所有的Action对象并调用他们的apply方法。这样避免了定义大量的Binder接口,也避免了大量IPC操作。
apply和reApply的区别在于:apply会加载布局并更新界面,而reApply则只会更新界面。RemoteViews在初始化界面时会调用apply方法,后续更新界面调用reApply方法。
关于单击事件,RemoteViews中只支持发起PendingIntent,不支持onClickListener那种模式。setOnClickPendingIntent用于给普通的View设置单击事件,不能给集合(ListView/StackView)中的View设置单击事件(开销大,系统禁止了这种方式)。如果要给ListView/StackView中的item设置单击事件,必须将setPendingIntentTemplate和setOnClickFillInIntent组合使用才可以。
当一个应用需要更新另一个应用的某个界面,我们可以选择用AIDL来实现,但如果更新比较频繁,效率会有问题,同时AIDL接口就可能变得很复杂。如果采用RemoteViews就没有这个问题,但RemoteViews仅支持一些常用的View,如果界面的View都是RemoteViews所支持的,那么就可以考虑采用RemoteViews。
Drawable有很多种,都表示图像的概念,但不全是图片,通过颜色也可以构造出各式各样的图像效果。实际开发中,Drawable常被用来作为View的背景使用。Drawable一般是通过XML来定义的,Drawable是所有Drawable对象的基类。
Drawable的内部宽、高这个参数比较重要,通过getIntrinsicWidth/getIntrinsicHeight这两个方法获取。但并不是所有Drawable都有宽高;图片Drawable的内部宽/高就是图片的宽/高,但是颜色形成的Drawable并没有宽/高的概念。
表示的就是一张图片,可以直接引用原始图片即可,也可以通过XML描述它,从而设置更多效果。
属性分析
android:src
图片资源id
android:antialias
是否开启图片抗锯齿功能。开启后会让图片变得平滑,同时也会一定程度上降低图片的清晰度,建议开启;
android:dither
是否开启抖动效果。当图片的像素配置和手机屏幕像素配置不一致时,开启这个选项可以让高质量的图片在低质量的屏幕上还能保持较好的显示效果,建议开启。
android:filter
是否开启过滤效果。当图片尺寸被拉伸或压缩时,开启过滤效果可以保持较好的显示效果,建议开启;
android:tileMode
平铺模式,有四种选项[“disabled” | “clamp” | “repeat” | “mirror”]。当开启平铺模式后,gravity属性会被忽略。repeat是指水平和竖直方向上的平铺效果;mirror是指在水平和竖直方向上的镜面投影效果;clamp是指图片四周的像素会扩展到周围区域,这个比较特别。
NinePatchDrawable
表示一张.9格式的图片,它和BitmapDrawable都表示一张图片。用XML描述的方式也和BitmapDrawable一样。在bitmap标签中也可以使用.9图。
可以理解为通过颜色来构造的图形,可以是纯色或渐变的图形。
属性分析
表示shape的四个角的角度(圆角程度)。只适用于矩形shape。其中android:radius是同时为4个角设置相同的角度,优先级较低,会被topLeftRadius这种具体指定角度的属性所覆盖。
gradient >
与标签相互排斥的,其中solid表示纯色填充,而gradient表示渐变效果;gradient有如下几个属性:
android:angle——渐变的角度,默认为0,其值必须是45的倍数,0表示从左往右,90表示从下到上。
solid
表示纯色填充,通过android:color即可指定shape中填充的颜色。
对应标签。它表示Drawable集合,每个Drawable对应View的一种状态,这样系统就会根据View的状态来选择合适的Drawable。主要用于设置可点击View的背景。
属性分析
android:constantSize
StateListDrawable的固有大小是否随着其状态的变化而变化,因为不同的Drawable有不同的固有大小。true表示固有大小保持不变,这时它的固有大小是内部所有Drawable的固有大小的最大值。默认值为false。
android:dither
是否开启抖动效果,默认true
android:variablePadding
StateListDrawable的padding是否随着状态变化而变化。true表示变化,false表示padding是内部所有Drawable的padding的最大值。默认为false。
默认的item一般放在最后并且不添加任何状态,这样当系统在之前的item无法选择的时候,就会匹配默认的item,因为item的默认状态不附带任何状态,所以它可以适配任何状态。
对应标签。同样表示Drawable集合,集合中的每个Drawable都会有一个等级的概念,根据等级不同来切换对于的Drawable。当它作为View的背景时,可以通过Drawable的setLevel方法来设置不同的等级从而切换具体的Drawable。level的值从0-10000,默认为0。
对应标签。用来实现两个Drawable之间淡入淡出的效果。
TransitionDrawable drawable = (TransitionDrawable) imageView.getBackground();
drawable.startTransition(1000);
startTransition和reverseTransition方法实现淡入淡出的效果以及它的逆过程。
对应于标签。它可以将其他Drawable内嵌到自己当中,并可以在四周留下一定的间距。当一个View希望自己的背景比自己的实际区域小的时候,可以采用InsetDrawable来实现。通过LayerDrawable也可以实现。
其中,inset中的shape距离view边界为10dp。
ScaleDrawable对应于xml文件中的标签,可以根据自己的level将指定的drawable缩放到一定比例。
其中,android:scaleGravity属性相当于gravity属性。android:scaleHeight/scaleWidth 表示Drawable的缩放比例。
缩放公式: w -= (int) (w * (10000 - level) * mState.mScaleWidth / 10000)
可见,level越大,Drawable看起来越大;scaleHeight/scaleWidth越大,Drawable看起来越小。注意的是,level设置为0时,Drawable不可见。level不应超过10000。
ClipDrawabe对应于标签,他可以根据自己当前的等级(level)来裁剪一个Drawable,裁剪方向可以通过Android:clipOrientation和android:gravity两个属性共同控制。
clipOrientation表示裁剪方向。gravity需要和clipOrientation一起才能发挥作用。如下所示:
使用步骤
ImageView imageClip = (ImageView) findViewById(R.id.image_clip);
ClipDrawable drawable = (ClipDrawable) imageClip.getDrawable();
drawable.setLevel(5000);
level=0的时候,表示完全裁剪,level=10000的时候表示完全不裁剪,level=5000的时候表示裁剪了一半。即等级越大,裁剪的区域越小。
在第5章中,我们分析了View的工作原理,系统会调用Drawable的draw方法绘制view的背景。所以我们可以通过重写Drawable的draw方法来自定义Drawable。但是,通常我们没必要自定义Drawable,因为自定义Drawable无法在XML中使用。只有在特殊情况下可以使用自定义Drawable。
Android动画分为三种:
介绍View动画和自定义View动画
View动画一些特殊的使用场景
对属性动画全面性的介绍
使用动画的一些注意事项
View动画的作用对象是View,支持四种动画效果:
要使用View动画,首先要创建动画的XML文件,这个文件的路径为:res/anim/filename.xml
View动画的描述文件有固定的语法
//透明度动画,对应 AlphaAnimation 类,可以改变 View 的透明度
//旋转动画,对应着 RotateAnimation ,它可以使 View 具有旋转的动画效果
//平移动画,对应 TranslateAnimation 类,可以使 View 完成垂直或者水平方向的移动效果。
//缩放动画,对应 ScaleAnimation 类,可以使 View 具有放大和缩小的动画效果。
1。 标签表示动画集合,对应AnimationSet类,可以包含一个或若干个动画,内部还可以嵌套其他动画集合。
android:interpolator 表示动画集合所采用的插值器,插值器影响动画速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。
android:shareInterpolator 表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或默认值。
AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(1500);
view.startAnimation(alphaAnimation);
通过 setAnimationListener 给 View 动画添加过程监听
public static interface AnimationListener {
void onAnimationStart(Animation animation);
void onAnimationEnd(Animation animation);
void onAnimationRepeat(Animation animation);
}
除了系统提供的四种动画外,我们可以根据需求自定义动画,自定义一个新的动画只需要继承 Animation 这个抽象类,然后重写它的 inatialize 和 applyTransformation 这两个方法,在 initialize 方法中做一些初始化工作,在 Transformation 方法中进行矩阵变换即可,很多时候才有 Camera 来简化矩阵的变换过程,其实自定义动画的主要过程就是矩阵变换的过程,矩阵变换是数学上的概念,需要掌握该方面知识方能轻松实现自定义动画,例子可以参考 Android 的 APIDemos 中的一个自定义动画 Rotate3dAnimation ,这是一个可以围绕 Y 轴旋转并同时沿着 Z 轴平移从而实现类似一种 3D 效果的动画。
package com.jandar.webview;
import android.graphics.Camera;
import android.graphics.Matrix;
import android.view.animation.Animation;
import android.view.animation.Transformation;
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
public Rotate3dAnimation(float fromDegrees,float toDegrees,float centerX,float centerY,float depthZ,boolean reverse) {
mFromDegrees=fromDegrees;
mToDegrees=toDegrees;
mCenterX=centerX;
mCenterY=centerY;
mDepthZ=depthZ;
mReverse=reverse;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera=new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final float fromDegrees=mFromDegrees;
float degrees=fromDegrees+((mToDegrees-fromDegrees)*interpolatedTime);
final float centerX=mCenterX;
final float centerY=mCenterY;
final Camera camera=mCamera;
final Matrix matrix=t.getMatrix();
camera.save();
if (mReverse){
camera.translate(0.0f,0.0f,mDepthZ*interpolatedTime);
}else {
camera.translate(0.0f,0.0f,mDepthZ*(1.0f-interpolatedTime));
}
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX,-centerY);
matrix.preTranslate(centerX,centerY);
}
}
帧动画是顺序播放一组预先定义好的图片,使用简单,但容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。系统提供了另一个类 AnimationDrawble 来使用帧动画,使用的时候,需要通过 XML 定义一个 AnimationDrawble ,如下:
//\res\drawable\frameanimationlist.xml
帧动画的使用比较简单,但比较容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。
View 动画除了可以实现的四种基本的动画效果外,还可以在一些特殊的场景下使用,比如在 ViewGroup 中可以控制子元素的出场效果,在 Activity 中可以实现不同 Activity 之间的切换效果。
作用于ViewGroup,为ViewGroup指定一个动画,当它的子元素出场时都会具有这种动画效果,一般用在ListView上。
//res/anim/layout_animation.xml
android:delay
表示子元素开始动画的延时时间,取值为子元素入场动画时间 duration 的倍数,比如子元素入场动画时间周期为 300ms ,那么 0.5 表示每个子元素都需要延迟 150ms 才能播放入场动画,即第一个子元素延迟 150ms 开始播放入场动画,第二个子元素延迟 300ms 开始播放入场动画,依次类推进行。
android:animationOrder
表示子元素动画的开场顺序,normal(正序)、reverse(倒序)、random(随机)。
android:layoutAnimation="@anim/layout_animation"
为 ViewGroup 指定属性
通过 LayoutAnimationController 来实现
//用于控制子 view 动画效果
LayoutAnimationController layoutAnimationController= new LayoutAnimationController(AnimationUtils.loadAnimation(this,R.anim.zoom_in));
layoutAnimationController.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(layoutAnimationController);
listView.startLayoutAnimation();
我们可以自定义Activity的切换效果,主要通过overridePendingTransition(int enterAnim , int exitAnim) 方法。该方法必须要在 startActivity(intent) 和 finish() 方法之后调用才会有效。
//启动新的Activity带动画
Intent intent=new Intent(MainActivity.this,Main2Activity.class);
startActivity(intent);
overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);
//退出Activity本身带动画
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);
}
Fragment 也可以添加切换动画,通过 FragmentTransation 中的 setCustomAnimations() 方法来实现切换动画,这个动画需要的是 View 动画,不能使用属性动画,因为属性动画也是 API11 才引入的,不兼容。
属性动画是 API 11 引入的新特性,属性动画可以对任何对象做动画,甚至还可以没有对象。可以在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。
与View动画相比,属性动画几乎无所不能,只要对象有这个属性,它都能实现动画效果。
API11以下可以通过 nineoldandroids 库来兼容以前版本。
属性动画有ValueAnimator、ObjectAnimator和AnimatorSet等概念。其中ObjectAnimator继承自ValueAnimator,AnimatorSet是动画集合。
举例:
private void translateViewByObjectAnimator(View targetView){
//TranslationY 目标 View 要改变的属性
//ivShow.getHeight() 要移动的距离
ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(targetView,“TranslationY”,ivShow.getHeight());
objectAnimator.start();
}
private void changeViewBackGroundColor(View targetView){
ValueAnimator valueAnimator=ObjectAnimator.ofInt(targetView,“backgroundColor”, Color.RED,Color.BLUE);
valueAnimator.setDuration(3000);
//设置估值器,该处插入颜色估值器
valueAnimator.setEvaluator(new ArgbEvaluator());
//无限循环
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
//反转模式
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.start();
}
private void startAnimationSet(View targetView){
AnimatorSet animatorSet=new AnimatorSet();
animatorSet.playTogether(ObjectAnimator.ofFloat(targetView,“rotationX”,0,360),
//旋转
ObjectAnimator.ofFloat(targetView,“rotationY”,0,360),
ObjectAnimator.ofFloat(targetView,“rotation”,0,-90),
//平移
ObjectAnimator.ofFloat(targetView,“translationX”,0,90),
ObjectAnimator.ofFloat(targetView,“translationY”,0,90),
//缩放
ObjectAnimator.ofFloat(targetView,“scaleX”,1,1.5f),
ObjectAnimator.ofFloat(targetView,“scaleY”,1,1.5f),
//透明度
ObjectAnimator.ofFloat(targetView,“alpha”,1,0.25f,1));
animatorSet.setDuration(3000).start();
}
也可以通过在xml中定义在 res/animator/ 目录下。具体如下:
\res\animator\value_animator.xml
使用动画
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(context , R.animator.ani
m);
set.setTarget(view);
set.start();
实际开发中建议使用代码实现属性动画。很多时候一个属性的起始值是无法提前确定的。
对于<objectAnimator>标签的各个属性的含义,做一下简单说明
属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorUpdateListener 和 AnimatorListener 。
public static interface AnimatorListener {
void onAnimationStart(Animator animation); //动画开始
void onAnimationEnd(Animator animation); //动画结束
void onAnimationCancel(Animator animation); //动画取消
void onAnimationRepeat(Animator animation); //动画重复播放
}
为了方便开发,系统提供了AnimatorListenerAdapter类,它是AnimatorListener的适配器类,可以有选择的实现以上4个方法。
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
AnimatorUpdateListener会监听整个动画的过程,动画由许多帧组成的,每播放一帧,onAnimationUpdate就会调用一次。利用这个特性,我们可以做一些特殊的事情。
属性动画原理:属性动画要求动画作用的对象提供 get 方法和 set 方法,属性动画根据外界传递该属性的初始值和最终值以动画的效果去多次调用 set 方法,每次传递给 set 方法的值都不一样,确切的来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对 object 对象属性 abc 做动画,如果想要动画生效,要同时满足两个条件:
/**
* 将 Button 沿着 X 轴方向放大
* @param button
*/
private void performAnimationByWrapper(View button){
ViewWrapper viewWrapper=new ViewWrapper(button);
ObjectAnimator.ofInt(viewWrapper,“width”,800)
.setDuration(5000)
.start();
}
private class ViewWrapper {
private View targetView;
public ViewWrapper(View targetView) {
this.targetView = targetView;
}
public int getWidth() {
//注意调用此函数能得到 View 的宽度的前提是, View 的宽度是精准测量模式,即不可以是 wrap_content
//否则得不到正确的测量值
return targetView.getLayoutParams().width;
}
public void setWidth(int width) {
//重写设置目标 view 的布局参数,使其改变大小
targetView.getLayoutParams().width = width;
//view 大小改变需要调用重新布局
targetView.requestLayout();
}
}
//new 一个整型估值器,用于下面比例值计算使用(可以自己去计算,这里直接使用系统的)
private IntEvaluator intEvaluator = new IntEvaluator();
private void performAnimatorByValue(final View targetView, final int start, final int end) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获取当前动画进度值
int currentValue = (int) animation.getAnimatedValue();
//获取当前进度占整个动画比例
int fraction = (int) animation.getAnimatedFraction();
//直接通过估值器根据当前比例计算当前 View 的宽度,然后设置给 View
targetView.getLayoutParams().width = intEvaluator.evaluate(fraction, start, end);
targetView.requestLayout();
}
});
valueAnimator.setDuration(5000)
.start();
}
属性动画需要运行在有Looper的线程中,系统通过反射调用被作用对象get/set方法。
Window是一个抽象类,具体实现是 PhoneWindow 。不管是 Activity 、 Dialog 、 Toast 它们的视图都是附加在Window上的,因此Window实际上是View的直接管理者。WindowManager 是外界访问Window的入口,通过WindowManager可以创建Window,而Window的具体实现位于 WindowManagerService 中,WindowManager和WindowManagerService的交互是一个IPC过程。
下面代码演示了通过WindowManager添加Window的过程:
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mFloatingButton = new Button(this);
mFloatingButton.setText("click me");
mLayoutParams = new WindowManager.LayoutParams(
LayoutParams.WRAPCONTENT, LayoutParams.WRAPCONTENT, 0, 0,
PixelFormat.TRANSPARENT);
mLayoutParams.flags = LayoutParams.FLAGNOTTOUCH_MODAL
| LayoutParams.FLAGNOTFOCUSABLE
| LayoutParams.FLAGSHOWWHEN_LOCKED;
mLayoutParams.type = LayoutParams.TYPESYSTEMERROR;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mFloatingButton.setOnTouchListener(this);
mWindowManager.addView(mFloatingButton, mLayoutParams);
上述代码将一个button添加到屏幕坐标为(100,300)的位置上。WindowManager的flags和type这两个属性比较重要。
Flags代表Window的属性,控制Window的显示特性
开启此模式Window将显示在锁屏界面上。
type参数表示Window的类型
Window是分层的,每个Window对应一个z-ordered,层级大的会覆盖在层级小的上面,和HTM的z-index概念一样。在三类Window中,应用Window的层级范围是199,子Window的层级范围是10001999,系统Window的层级范围是2000~2999,这些值对应WindowManager.LayoutParams的type参数。一般系统Window选用 TYPESYSTEMOVERLAY 或者 TYPESYSTEMERROR ( 同时需要权限 <uses-permission android:name=“android.permission.SYSTEMALERTWINDOW” /> ) 。
WindowManager提供的功能很简单,常用的只有三个方法:
这个三个方法定义在 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);
}
如何拖动window?
给view设置onTouchListener:mFloatingButton.setOnTouchListener(this)。在onTouch方法中更新view的位置,这个位置根据手指的位置设定。
Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。因此Window并不是实际存在的,它是以View的形式存在的。所以WindowManager的三个方法都是针对View的,说明View才是Window存在的实体。在实际使用中无法直接访问Window,必须通过WindowManager来访问Window。
Window的添加过程需要通过WindowManager的addView()来实现, 而WindowManager是一个接口, 它的真正实现是WindowManagerImpl类。
WindowManagerImpl并没有直接实现Window的三大操作, 而是全部交给了WindowManagerGlobal来处理. WindowManagerGlobal以工厂的形式向外提供自己的实例. 而WindowManagerImpl这种工作模式就典型的桥接模式, 将所有的操作全部委托给WindowManagerGlobal来实现.
WindowManagerService内部会为每一个应用保留一个单独的Session.
Window 的删除过程和添加过程一样, 都是先通过WindowManagerImpl后, 在进一步通过WindowManagerGlobal的removeView()来实现的.
方法内首先通过findViewLocked来查找待删除的View的索引, 这个过程就是建立数组遍历, 然后调用removeViewLocked来做进一步的删除.
这里通过ViewRootImpl的die()完成来完成删除操作. die()方法只是发送了请求删除的消息后就立刻返回了, 这个时候View并没有完成删除操作, 所以最后会将其添加到mDyingViews中, mDyingViews表示待删除的View的列表.
die方法中只是做了简单的判断, 如果是异步删除那么就发送一个MSG_DIE的消息, ViewRootImpl中的Handler会处理此消息并调用doDie(); 如果是同步删除, 那么就不发送消息直接调用doDie()方法.
在doDie()方法中会调用dispatchDetachedFromWindow()方法, 真正删除View的逻辑在这个方法内部实现. 其中主要做了四件事:
WindowManagerGlobal#updateViewLayout()方法做的比较简单, 它需要更新View的LayoutParams并替换掉老的LayoutParams, 接着在更新ViewRootImpl中的LayoutParams. 这一步主要是通过setLayoutParams()方法实现.
在ViewRootImpl中会通过scheduleTraversals()来对View重新布局, 包括测量,布局,重绘. 除了View本身的重绘以外, ViewRootImpl还会通过WindowSession来更新Window的视图, 这个过程最后由WMS的relayoutWindow()实现同样是一个IPC过程.
由之前的分析可以知道,View是Android中视图的呈现方式,但是View不能单独存在,必须附着在Window这个抽象的概念上面,因此有视图的地方就有Window。这些视图包括:Activity、Dialog、Toast、PopUpWindow、菜单等等。
Activity的大体启动流程: 最终会由ActivityThread中的PerformLaunchActivity()来完成整个启动过程, 这个方法内部会通过类加载器创建Activity的实例对象, 并调用其attach()方法为其关联运行过程中所依赖的一系列上下文环境变量。
在attach()方法里, 系统会创建Activity所属的Window对象并为其设置回调接口, Window对象的创建是通过PolicyManager#makeNewWindow()方法实现. 由于Activity实现了Window的CallBack接口, 因此当Window接收到外界的状态改变的时候就会回调Activity方法. 比如说我们熟悉的onAttachedToWindow(), onDetachedFromWindow(), dispatchTouchEvent()等等。
Activity将具体实现交给了Window处理, 而Window的具体实现就是PhoneWindow, 所以只需要看PhoneWindow的相关逻辑。分为以下几步
这个时候DecorView已经被创建并初始化完毕, Activity的布局文件也已经添加成功到DecorView的mContentParent中. 但是这个时候DecorView还没有被WindowManager正式添加到Window中. 虽然早在Activity的attach方法中window就已经被创建了, 但是这个时候由于DecorView并没有被WindowManager识别, 所以这个时候的Window无法提供具体功能, 因为他还无法接收外界的输入信息.
在ActivityThread#handleResumeActivity()方法中, 首先会调用Activity#onResume(), 接着会调用Activity#makeVisible(), 正是在makeVisible方法中, DecorView真正的完成了添加和显示这两个过程。
Dialog的Window的创建过程和Activity类似, 有如下几步
普通的Dialog有一个特殊之处, 那就是必须采用Activity的Content, 如果采用Application的Content, 那么就会报错. 报的错是没有应用token所导致的, 而应用token一般只有Activity才拥有.
还有一种方法. 系统Window比较特殊, 他可以不需要token, 因此只需要指定对话框的Window为系统类型就可以正常弹出对话框.
//JAVA 给Dialog的Window改变为系统级的Window
dialog.getWindow().setType(WindowManager.LayoutParams.TYPESYSTEMERROR);
//XML 声明权限
oast和Dialog不同, 它的工作过程就稍显复杂. 首先Toast也是基于Window来实现的. 但是由于Toast具有定时取消的功能, 所以系统采用了Handler. 在Toast的内部有两类IPC过程, 第一类是Toast访问NotificationManagerService()后面简称NMS. 第二类是NotificationManagerService回调Toast里的TN接口.
Toast属于系统Window, 它内部的视图有两种方式指定, 一种是系统默认的样式, 另一种是通过setView方法来指定一个自定义View. 不管如何, 他们都对应Toast的一个View类型的内部成员mNextView. Toast内部提供了cancel和show两个方法. 分别用于显示和隐藏Toast. 他们内部是一个IPC过程.
显示和隐藏Toast都是需要通过NMS来实现的. 由于NMS运行在系统的进程中, 所以只能通过远程调用的方式来显示和隐藏Toast. 而TN这个类是一个Binder类. 在Toast和NMS进行IPC的过程中, 当NMS处理Toast的显示或隐藏请求时会跨进程回调TN的方法. 这个时候由于TN运行在Binder线程池中, 所以需要通过Handler将其切换到当前主线程. 所以由其可知, Toast无法在没有Looper的线程中弹出, 因为Handler需要使用Looper才能完成切换线程的功能.
对于非系统应用来说, 最多能同时存在对Toast封装的ToastRecord上限为50个. 这样做是为了防止DOS(Denial of Service). 如果不这样, 当通过大量循环去连续的弹出Toast, 这将会导致其他应用没有机会弹出Toast, 那么对于其他应用的Toast请求, 系统的行为就是拒绝服务, 这就是拒绝服务攻击的含义.
在ToastRecord被添加到mToastQueue()中后, NMS就会通过showNextToastLocked()方法来显示当前的Toast.
Toast的显示是由ToastRecord的callback来完成的. 这个callback实际上就是Toast中的TN对象的远程Binder. 通过callback来访问TN中的方法是需要跨进程的. 最终被调用的TN中的方法会运行在发起Toast请求的应用的Binder线程池.
Toast的隐藏也会通过ToastRecord的callback完成的.同样是一次IPC过程. 方式和Toast显示类似.
以上基本说明Toast的显示和影响过程实际上是通过Toast中的TN这个类来实现的. 他有两个方法show(), hide(). 分别对应着Toast的显示和隐藏. 由于这两个方法是被NMS以跨进程的方式调用的, 因此他们运行在Binder线程池中. 为了将执行环境切换到Toast请求所在线程中, 在他们内部使用了handler。
TN的handleShow中会将Toast的视图添加到Window中.
TN的handleHide中会将Toast的视图从Window中移除.
以上三节的总结
显示和隐藏Toast都通过NotificationManagerService( NMS) 来实现,而NMS运行在系统进程中,所以只能通过IPC来进行显示/隐藏Toast。而TN是一个Binder类,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示/隐藏请求时会跨进程回调TN中的方法,这时由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程( 即发起Toast请求所在的线程) ,然后通过WindowManager的 addView/removewView 方法真正完成显示和隐藏Toast。
Android的四大组件除了BroadcastReceiver以外,都需要在AndroidManifest文件注册,BroadcastReceiver可以通过代码注册。调用方式上,除了ContentProvider以外的三种组件都需要借助intent。
Activity
是一种展示型组件,用于向用户直接地展示一个界面,并且可以接收用户的输入信息从而进行交互,扮演的是一个前台界面的角色。Activity的启动由intent触发,有隐式和显式两种方式。一个Activity可以有特定的启动模式,finish方法结束Activity运行。
Service
是一种计算型组件,在后台执行一系列计算任务。它本身还是运行在主线程中的,所以耗时的逻辑仍需要单独的线程去完成。Activity只有一种状态:启动状态。而service有两种:启动状态和绑定状态。当service处于绑定状态时,外界可以很方便的和service进行通信,而在启动状态中是不可与外界通信的。Service可以停止, 需要灵活采用stopService和unBindService
BroadcastReceiver
是一种消息型组件,用于在不同的组件乃至不同的应用之间传递消
息。
静态注册
在清单文件中进行注册广播, 这种广播在应用安装时会被系统解析, 此种形式的广播不需要应用启动就可以接收到相应的广播.
动态注册
需要通过Context.registerReceiver()来实现, 并在不需要的时候通过Context.unRegisterReceiver()来解除广播. 此种形态的广播要应用启动才能注册和接收广播. 在实际开发中通过Context的一系列的send方法来发送广播, 被发送的广播会被系统发送给感兴趣的广播接收者, 发送和接收的过程的匹配是通过广播接收者的来描述的.可以实现低耦合的观察者模式, 观察者和被观察者之间可以没有任何耦合. 但广播不适合来做耗时操作.
两种状态是可以共存的
和service的启动过程类似的:
继承Runnable接口, run() 方法的实现也是简单调用了ServiceDispatcher的 doConnected 方法。
由于ServiceDispatcher内部保存了客户端的ServiceConntion对象,可以很方便地调用ServiceConntion对象的 onServiceConnected 方法。
客户端的onServiceConnected方法执行后,Service的绑定过程也就完成了。
根据步骤8、9、10service绑定后通过ServiceDispatcher通知客户端的过程可以说明ServiceDispatcher起着连接ServiceConnection和InnerConnection的作用。 至于Service的停止和解除绑定的过程,系统流程都是类似的。
广播的发送有几种:普通广播、有序广播和粘性广播,他们的发送/接收流程是类似的,因此
只分析普通广播的实现。
Args: 实现类Runnable
mActivityThread: 是一个Handler, 就是ActivityThread中的mH. mH就是ActivityThread$H. 这个内部类H以前说过.
实现Runnable接口的Args中BroadcastReceiver#onReceive()方法被执行了, 也就是说应用已经接收到了广播, 同时onReceive()方法是在广播接收者的主线程中被调用的.
android 3.1开始就增添了两个标记为. 分别是FLAGINCLUDESTOPPEDPACKAGES, FLAGEXCLUDESTOPPEDPACKAGES. 用来控制广播是否要对处于停止的应用起作用.
FLAGINCLUDESTOPPED_PACKAGES: 包含停止应用, 广播会发送给已停止的应用.
FLAG_EXCLUDESTOPPEDPACKAGES: 不包含已停止应用, 广播不会发送给已停止的应用
在android 3.1开始, 系统就为所有广播默认添加了FLAG_EXCLUDESTOPPEDPACKAGES标识。 当这两个标记共存的时候以FLAGINCLUDESTOPPED_PACKAGES(非默认项为主).
应用处于停止分为两种
应用安装后未运行
被手动或者其他应用强停
开机广播同样受到了这个标志位的影响. 从Android 3.1开始处于停止状态的应用同样无法接受到开机广播, 而在android 3.1之前处于停止的状态也是可以接收到开机广播的.
ContentProvider是一种内容共享型组件, 它通过Binder向其他组件乃至其他应用提供数据. 当ContentProvider所在的进程启动时, ContentProvider会同时启动并发布到AMS中. 要注意:这个时候ContentProvider的onCreate()方法是先于Application的onCreate()执行的,这一点在四大组件是少有的现象.
ContentProvider的android:multiprocess属性决定它是否是单实例,默认值是false,也就是默认是单实例。当设置为true时,每个调用者的进程中都存在一个ContentProvider对象。
当调用ContentProvider的insert、delete、update、query方法中的任何一个时,如果ContentProvider所在的进程没有启动的话,那么就会触发ContentProvider的创建,并伴随着ContentProvider所在进程的启动。
以query调用为例
经过了上述的四个步骤, ContentProvider已经启动成功, 并且其所在的进程的Application也已经成功, 这意味着ContentProvider所在的进程已经完成了整个的启动过程, 然后其他应用就可以通过AMS来访问这个ContentProvider了.
当拿到了ContentProvider以后, 就可以通过它所提供的接口方法来访问它. 这里要注意: 这里的ContentProvider并不是原始的ContentProvider. 而是ContentProvider的Binder类型对象IContentProvider, 而IContentProvider的具体实现是ContentProviderNative和ContentProvider.Transport. 后者继承了前者.
如果还用query方法来解释流程: 那么最开始其他应用通过AMS获取到ContentProvider的Binder对象就是IContentProvider. 而IContentProvider的实际实现者是ContentProvider.Transport. 因此实际上外部应用调用的时候本质上会以进程间通信的方式调用ContentProvider.Transport的query()方法。
Android的消息机制主要是指Handler的运行机制。从开发的角度来说,Handler是Android消息机制的上层接口。Handler的运行需要底层的 MessageQueue 和 Looper 的支撑。
MessageQueue是一个消息队列,内部存储了一组消息,以队列的形式对外提供插入和删除的工作,内部采用单链表的数据结构来存储消息列表。
Lopper会以无限循环的形式去查找是否有新消息,如果有就处理消息,否则就一直等待着。
ThreadLocal并不是线程,它的作用是在每个线程中存储数据。Handler通过ThreadLocal可以获取每个线程中的Looper。
线程是默认没有Looper的,使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread,被创建时就会初始化Looper。
Handler的主要作用是将某个任务切换到Handler所在的线程中去执行。为什么Android要提供这个功能呢?这是因为Android规定访问UI只能通过主线程,如果子线程访问UI,程序可能会导致ANR。那我们耗时操作在子线程执行完毕后,我们需要将一些更新UI的操作切换到主线程当中去。所以系统就提供了Handler。
系统为什么不允许在子线程中去访问UI呢? 因为Android的UI控件不是线程安全的,多线程并发访问可能会导致UI控件处于不可预期的状态,为什么不加锁?因为加锁机制会让UI访问逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。所以Android采用了高效的单线程模型来处理UI操作。
Handler创建时会采用当前线程的Looper来构建内部的消息循环系统,如果当前线程没有Looper就会报错。Handler可以通过post方法发送一个Runnable到消息队列中,也可以通过send方法发送一个消息到消息队列中,其实post方法最终也是通过send方法来完成。
MessageQueue的enqueueMessage方法最终将这个消息放到消息队列中,当Looper发现有新消息到来时,处理这个消息,最终消息中的Runnable或者Handler的handleMessage方法就会被调用,注意Looper是运行Handler所在的线程中的,这样一来业务逻辑就切换到了Handler所在的线程中去执行了。
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定线程中存储数据,数据存储后,只有在指定线程中可以获取到存储的数据,对于其他线程来说无法获得数据。
在某些特殊的场景下,ThreadLocal可以轻松实现一些很复杂的功能。Looper、ActivityThread以及AMS都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
对于Handler来说,它需要获取当前线程的Looper,而Looper的作用于就是线程并且不同的线程具有不同的Looper,通过ThreadLocal可以轻松实现线程中的存取。
ThreadLocal的另一个使用场景是可以让监听器作为线程内的全局对象而存在,在线程内部只要通过get方法就可以获取到监听器。如果不采用ThreadLocal,只能采用函数参数调用和静态变量的方式。而第一种方式在调用栈很深时很糟糕,第二种方式不具有扩展性,比如同时多个线程执行。
虽然在不同线程访问同一个ThreadLocal对象,但是获得的值却是不同的。不同线程访问同一个ThreadLoacl的get方法,ThreadLocal的get方法会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal的索引去查找对应的Value值。
ThreadLocal的set方法:
public void set(T value) {
Thread currentThread = Thread.currentThread();
//通过values方法获取当前线程中的ThreadLoacl数据——localValues
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
ThreadLocal的get方法:
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);//找到localValues对象
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {//找到ThreadLocal的reference对象在table数组中的位置
return (T) table[index + 1];//reference字段所标识的对象的下一个位置就是ThreadLocal的值
}
} else {
values = initializeValues(currentThread);
}
return (T) values.getAfterMiss(this);
}
从ThreadLocal的set/get方法可以看出,它们所操作的对象都是当前线程的localValues对象的table数组,因此在不同线程中访问同一个ThreadLocal的set/get方法,它们ThreadLocal的读/写操作仅限于各自线程的内部。理解ThreadLocal的实现方式有助于理解Looper的工作原理。
消息队列指的是MessageQueue,主要包含两个操作:插入和读取。读取操作本身会伴随着删除操作。
MessageQueue内部通过一个单链表的数据结构来维护消息列表,这种数据结构在插入和删除上的性能比较有优势。
插入和读取对应的方法分别是:enqueueMessage和next方法。
enqueueMessage()的源码实现主要操作就是单链表的插入操作
next()的源码实现也是从单链表中取出一个元素的操作,next()方法是一个无线循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞在这里。当有新消息到来时,next()方法会返回这条消息并将其从单链表中移除。
Looper在Android的消息机制中扮演着消息循环的角色,具体来说就是它会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立即处理,否则就一直阻塞在那里。
通过Looper.prepare()方法即可为当前线程创建一个Looper,再通过Looper.loop()开启消息循环。prepareMainLooper()方法主要给主线程也就是ActivityThread创建Looper使用的,本质也是通过prepare方法实现的。
Looper提供quit和quitSafely来退出一个Looper,区别在于quit会直接退出Looper,而quitSafely会把消息队列中已有的消息处理完毕后才安全地退出。 Looper退出后,这时候通过Handler发送的消息会失败,Handler的send方法会返回false。
在子线程中,如果手动为其创建了Looper,在所有事情做完后,应该调用Looper的quit方法来终止消息循环,否则这个子线程就会一直处于等待状态;而如果退出了Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。
loop()方法会调用MessageQueue的next()方法来获取新消息,而next是是一个阻塞操作,但没有信息时,next方法会一直阻塞在那里,这也导致loop方法一直阻塞在那里。如果MessageQueue的next方法返回了新消息,Looper就会处理这条消息:msg.target.dispatchMessage(msg),这里的msg.target是发送这条消息的Handler对象,这样Handler发送的消息最终又交给Handler来处理了。
Handler的工作主要包含消息的发送和接收过程。通过post的一系列方法和send的一系列方法来实现。
Handler发送过程仅仅是向消息队列中插入了一条消息。MessageQueue的next方法就会返回这条消息给Looper,Looper拿到这条消息就开始处理,最终消息会交给Handler的dispatchMessage()来处理,这时Handler就进入了处理消息的阶段。
Android的主线程就是ActivityThread,主线程的入口方法为main(String[] args),在main方法中系统会通过Looper.prepareMainLooper()来创建主线程的Looper以及MessageQueue,并通过Looper.loop()来开启主线程的消息循环。
ActivityThread通过ApplicationThread和AMS进行进程间通信,AMS以进程间通信的方式完成ActivityThread的请求后会回调ApplicationThread中的Binder方法,然后ApplicationThread会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到ActivityTread中去执行,即切换到主线程中去执行。四大组件的启动过程基本上都是这个流程。
Looper.loop(),这里是一个死循环,如果主线程的Looper终止,则应用程序会抛出异常。那么问题来了,既然主线程卡在这里了,(1)那Activity为什么还能启动;(2)点击一个按钮仍然可以响应?
问题1:startActivity的时候,会向AMS(ActivityManagerService)发一个跨进程请求(AMS运行在系统进程中),之后AMS启动对应的Activity;AMS也需要调用App中Activity的生命周期方法(不同进程不可直接调用),AMS会发送跨进程请求,然后由App的ActivityThread中的ApplicationThread会来处理,ApplicationThread会通过主线程线程的Handler将执行逻辑切换到主线程。重点来了,主线程的Handler把消息添加到了MessageQueue,Looper.loop会拿到该消息,并在主线程中执行。这就解释了为什么主线程的Looper是个死循环,而Activity还能启动,因为四大组件的生命周期都是以消息的形式通过UI线程的Handler发送,由UI线程的Looper执行的。
问题2:和问题1原理一样,点击一个按钮最终都是由系统发消息来进行的,都经过了Looper.loop()处理。
在Android系统,线程主要分为主线程和子线程,主线程处理和界面相关的事情,而子线程一般用于执行耗时操作。AsyncTask底层是线程池;IntentService/HandlerThread底层是线程;
在Android中,线程的形态有很多种:
操作系统中,线程是操作系统调度的最小单元,同时线程又是一种受限的系统资源,其创建和销毁都会有相应的开销。同时当系统存在大量线程时,系统会通过时间片轮转的方式调度每个线程,因此线程不可能做到绝对的并发,除非线程数量小于等于CPU的核心数。
频繁创建销毁线程不明智,使用线程池是正确的做法。线程池会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。
主线程主要处理界面交互逻辑,由于用户随时会和界面交互,所以主线程在任何时候都需要有较高响应速度,则不能执行耗时的任务;
android3.0开始,网络访问将会失败并抛出NetworkOnMainThreadException这个异常,这样做是为了避免主线程由于被耗时操作所阻塞从而现ANR现象
AsyncTask是一种轻量级的异步任务类, 他可以在线程池中执行后台任务, 然后把执行的进度和最终的结果传递给主线程并在主线程更新UI. 从实现上来说. AsyncTask封装了Thread和Handler, 通过AsyncTask可以更加方便地执行后台任务,但是AsyncTask并不适合进行特别耗时的后台任务,对于特别耗时的任务来说, 建议使用线程池。
AsyncTask就是一个抽象的泛型类. 这三个泛型的意义
如果不需要传递具体的参数, 那么这三个泛型参数可以用Void来代替.
四个方法 :
onPreExecute()
在主线程执行, 在异步任务执行之前, 此方法会被调用, 一般可以用于做一些准备工作
doInBackground()
在线程池中执行, 此方法用于执行异步任务, 参数params表示异步任务的输入参数. 在此方法中可以通过publishProgress()方法来更新任务的进度, publishProgress()方法会调用onProgressUpdate()方法. 另外此方法需要返回计算结果给onPostExecute()
onProgressUpdate()
在主线程执行,在异步任务执行之后, 此方法会被调用, 其中result参数是后台任务的返回值, 即doInBackground()的返回值.
onPostExecute()
在主线程执行, 在异步任务执行之后, 此方法会被调用, 其中result参数是后台任务的返回值, 即doInBackground的返回值.
除了上述的四种方法,还有onCancelled(), 它同样在主线程执行, 当异步任务被取消时, onCancelled()方法会被调用, 这个时候onPostExecute()则不会被调用.
AsyncTask在使用过程中有一些条件限制
AsyncTask中有两个线程池(SerialExecutor和THREADPOOLEXECUTOR)和一个Handler(InternalHandler), 其中线程池SerialExecutor用于任务的排列, 而线程池THREADPOOLEXECUTOR用于真正的执行任务, 而InternalHandler用于将执行环境从线程切换到主线程, 其本质仍然是线程的调用过程.
AsyncTask的排队过程:首先系统会把AsyncTask#Params参数封装成FutureTask对象, FutureTask是一个并发类, 在这里充当了Runnable的作用. 接着这个FutureTask会交给SerialExecutor#execute()方法去处理. 这个方法首先会把FutureTask对象插入到任务队列mTasks中, 如果这个时候没有正在活动AsyncTask任务, 那么就会调用SerialExecutor#scheduleNext()方法来执行下一个AsyncTask任务. 同时当一个AsyncTask任务执行完后, AsyncTask会继续执行其他任务直到所有的任务都执行完毕为止, 从这一点可以看出, 在默认情况下, AsyncTask是串行执行的
HandlerThread继承了Thread, 它是一种可以使用Handler的Thread, 它的实现也很简单, 就是run方法中通过Looper.prepare()来创建消息队列, 并通过Looper.loop()来开启消息循环, 这样在实际的使用中就允许在HandlerThread中创建Handler.
从HandlerThread的实现来看, 它和普通的Thread有显著的不同之处. 普通的Thread主要用于在run方法中执行一个耗时任务; 而HandlerThread在内部创建了消息队列, 外界需要通过Handler的消息方式来通知HandlerThread执行一个具体的任务. HandlerThread是一个很有用的类, 在Android中一个具体使用场景就是IntentService.
由于HandlerThread#run()是一个无线循环方法, 因此当明确不需要再使用HandlerThread时, 最好通过quit()或者quitSafely()方法来终止线程的执行.
优点:
Android中的线程池的概念来源于Java中的Executor, Executor是一个接口, 真正的线程池的实现为ThreadPoolExecutor.Android的线程池 大部分都是通 过Executor提供的工厂方法创建的。 ThreadPoolExecutor提供了一系列参数来配制线程池, 通过不同的参数可以创建不同的线程池. 而从功能的特性来分的话可以分成四类.
ThreadPoolExecutor是线程池的真正实现, 它的构造方法提供了一系列参数来配置线程池, 这些参数将会直接影响到线程池的功能特性.
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize: 线程池的核心线程数, 默认情况下, 核心线程会在线程池中一直存活, 即使都处于闲置状态. 如果将ThreadPoolExecutor#allowCoreThreadTimeOut属性设置为true, 那么闲置的核心线程在等待新任务到来时会有超时的策略, 这个时间间隔由keepAliveTime属性来决定. 当等待时间超过了keepAliveTime设定的值那么核心线程将会终止.
maximumPoolSize: 线程池所能容纳的最大线程数, 当活动线程数达到这个数值之后, 后续的任务将会被阻塞.
keepAliveTime: 非核心线程闲置的超时时长, 超过这个时长, 非核心线程就会被回收. allowCoreThreadTimeOut这个属性为true的时候, 这个属性同样会作用于核心线程.
unit: 用于指定keepAliveTime参数的时间单位, 这是一个枚举, 常用的有TimeUtil.MILLISECONDS(毫秒), TimeUtil.SECONDS(秒)以及TimeUtil.MINUTES(分)
workQueue: 线程池中的任务队列, 通过线程池的execute方法提交的Runnable对象会存储在这个参数中.
threadFactory: 线程工厂, 为线程池提供创建新线程的功能. ThreadFactory是一个接口.
ThreadPoolExecutor执行任务大致遵循如下规则:
AsyncTask的THREADPOOLEXECUTOR线程池配置:
FixedThreadPool
通过Executor#newFixedThreadPool()方法来创建. 它是一种线程数量固定的线程池, 当线程处于空闲状态时, 它们并不会被回收, 除非线程池关闭了. 当所有的线程都处于活动状态时, 新任务都会处于等待状态, 直到有线程空闲出来. 由于FixedThreadPool只有核心线程并且这些核心线程不会被回收, 这意味着它能够更加快速地响应外界的请求.
CachedThreadPool
通过Executor#newCachedThreadPool()方法来创建. 它是一种线程数量不定的线程池, 它只有非核心线程, 并且其最大值线程数为Integer.MAX_VALUE. 这就可以认为这个最大线程数为任意大了. 当线程池中的线程都处于活动的时候, 线程池会创建新的线程来处理新任务, 否则就会利用空闲的线程来处理新任务. 线程池中的空闲线程都有超时机制, 这个超时时长为60S, 超过这个时间那么空闲线程就会被回收.
和FixedThreadPool不同的是, CachedThreadPool的任务队列其实相当于一个空集合, 这将导致任何任务都会立即被执行, 因为在这种场景下SynchronousQueue是无法插入任务的. SynchronousQueue是一个非常特殊的队列, 在很多情况下可以把它简单理解为一个无法存储元素的队列. 在实际使用中很少使用.这类线程比较适合执行大量的耗时较少的任务
ScheduledThreadPool
通过Executor#newScheduledThreadPool()方法来创建. 它的核心线程数量是固定的, 而非核心线程数是没有限制的, 并且当非核心线程闲置时会立刻被回收掉. 这类线程池用于执行定时任务和具有固定周期的重复任务
SingleThreadExecutor
通过Executor#newSingleThreadPool()方法来创建. 这类线程池内部只有一个核心线程, 它确保所有的任务都在同一个线程中按顺序执行. 这类线程池意义在于统一所有的外界任务到一个线程中, 这使得在这些任务之间不需要处理线程同步的问题
主要介绍:
常见的图片格式.
那么如何加载一个图片?首先BitmapFactory类提供了四种方法: decodeFile(), decodeResource(), decodeStream(), decodeByteArray(). 分别用于从文件系统, 资源文件, 输入流以及字节数组加载出一个Bitmap对象. 其中decodeFile和decodeResource又间接调用了decodeStream()方法, 这四类方法最终是在Android的底层实现的, 对应着BitmapFactory类的几个native方法.
高效加载的Bitmap的核心思想:采用BitmapFactory.Options来加载所需尺寸的图片. 比如说一个ImageView控件的大小为300300. 而图片的大小为800800. 这个时候如果直接加载那么就比较浪费资源, 需要更多的内存空间来加载图片, 这不是很必要的. 这里我们就可以先把图片按一定的采样率来缩小图片在进行加载. 不仅降低了内存占用,还在一定程度上避免了OOM异常. 也提高了加载bitmap时的性能.
而通过Options参数来缩放图片: 主要是用到了inSampleSize参数, 即采样率.
如果是inSampleSize=1那么和原图大小一样,
如果是inSampleSize=2那么宽高都为原图1/2, 而像素为原图的1/4, 占用的内存大小也为原图的1/4
如果是inSampleSize=3那么宽高都为原图1/3, 而像素为原图的1/9, 占用的内存大小也为原图的1/9
以此类推……
要知道Android中加载图片具体在内存中的占有的大小是根据图片的像素决定的, 而与图片的实际占用空间大小没有关系.而且如果要加载mipmap下的图片, 还会根据不同的分辨率下的文件夹进行不同的放大缩小.
列举现在有一张图片像素为:10241024, 如果采用ARGB8888(四个颜色通道每个占有一个字节,相当于1点像素占用4个字节的空间)的格式来存储.(这里不考虑不同的资源文件下情况分析) 那么图片的占有大小就是102410244那现在这张图片在内存中占用4MB.
如果针对刚才的图片进行inSampleSize=2, 那么最后占用内存大小为512512*4, 也就是1MB
采样率的数值必须是大于1的整数是才会有缩放效果, 并且采样率同时作用于宽/高, 这将导致缩放后的图片以这个采样率的2次方递减, 即内存占用缩放大小为1/(inSampleSize的二次方). 如果小于1那么相当于=1的时候. 在官方文档中指出, inSampleSize的取值应该总是为2的指数, 比如1,2,4,8,16,32…如果外界传递inSampleSize不为2的指数, 那么系统会向下取整并选择一个最接近的2的指数来代替. 比如如果inSampleSize=3,那么系统会选择2来代替. 但是这条规则并不作用于所有的android版本, 所以可以当成一个开发建议
整理一下开发中代码流程:
通过这些步骤就可以整理出以下的工具加载图片类调用decodeFixedSizeForResource()即可.
public class MyBitmapLoadUtil {
/**
* 对一个Resources的资源文件进行指定长宽来加载进内存, 并把这个bitmap对象返回
*
* @param res 资源文件对象
* @param resId 要操作的图片id
* @param reqWidth 最终想要得到bitmap的宽度
* @param reqHeight 最终想要得到bitmap的高度
* @return 返回采样之后的bitmap对象
*/
public static Bitmap decodeFixedSizeForResource(Resources res, int resId, int reqWidth, int reqHeight){
// 首先先指定加载的模式 为只是获取资源文件的大小
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//Calculate Size 计算要设置的采样率 并把值设置到option上
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 关闭只加载属性模式, 并重新加载的时候传入自定义的options对象
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* 一个计算工具类的方法, 传入图片的属性对象和 想要实现的目标大小. 通过计算得到采样值
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//Raw height and width of image
//原始图片的宽高属性
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
// 如果想要实现的宽高比原始图片的宽高小那么就可以计算出采样率, 否则不需要改变采样率
if (reqWidth < height || reqHeight < width){
int halfWidth = width/2;
int halfHeight = height/2;
// 判断原始长宽的一半是否比目标大小小, 如果小那么增大采样率2倍, 直到出现修改后原始值会比目标值大的时候
while((halfHeight/inSampleSize) >= reqHeight && (halfWidth/inSampleSize) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
当程序第一次从网络上加载图片后,将其缓存在存储设备中,下次使用这张图片的时候就不用再从网络从获取了。很多时候为了提高应用的用户体验,往往还会把图片在内存中再缓存一份,因为从内存中加载图片比存储设备中快。一般情况会把图片存一份到内存中,一份到存储设备中,如果内存中没找到就去存储设备中找,还没有找到就从网络上下载。
缓存策略包含缓存的添加、获取和删除操作。不管是内存还是存储设备,缓存大小都是有限制的。如何删除旧的缓存并添加新的缓存,就对应缓存算法。
目前常用的一种缓存算法是LRU(Least Recently Used), 最近最少使用算法. 核心思想: 当缓存存满时, 会优先淘汰那些近期最少使用的缓存对象. 采用LRU算法的缓存有两种: LruCache和DiskLruCache,LruCache用于实现内存缓存, DiskLruCache则充当了存储设备缓存, 当组合使用后就可以实现一个类似ImageLoader这样的类库.
LruCache是Android 3.1所提供的一个缓存类, 通过support-v4兼容包可以兼容到早期的Android版本
LruCache是一个泛型类, 它内部采用了一个LinkedHashMap以强引用的方式存储外界的缓存对象, 其提供了get和put方法来完成缓存的获取和添加的操作. 当缓存满了时, LruCache会移除较早使用的缓存对象, 然后在添加新的缓存对象. 普及一下各种引用的区别:
强引用: 直接的对象引用
软引用: 当一个对象只有软引用存在时, 系统内存不足时此对象会被gc回收
弱引用: 当一个对象只有弱引用存在时, 对象会随下一次gc时被回收
LruCache是线程安全的。
LruCache 典型初始化过程:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
这里只需要提供缓存的总容量大小(一般为进程可用内存的1/8)并重写 sizeOf 方法即可.sizeOf方法作用是计算缓存对象的大小。这里大小的单位需要和总容量的单位(这里是kb)一致,因此除以1024。一些特殊情况下,需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用entryRemoved方法,因此可以在entryRemoved中完成一些资源回收工作(如果需要的话)。
还有获取和添加方法,都比较简单:
mMemoryCache.get(key)
mMemoryCache.put(key,bitmap)
通过remove方法可以删除一个指定的对象。
从Android 3.1开始,LruCache称为Android源码的一部分。
DiskLruCache用于实现磁盘缓存,DiskLruCache得到了Android官方文档推荐,但它不属于Android SDK的一部分
DiskLruCache的创建
DiskLruCache并不能通过构造方法来创建, 他提供了open()方法用于创建自身, 如下所示
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
//初始化DiskLruCache,包括一些参数的设置
public void initDiskLruCache
{
//配置固定参数
// 缓存空间大小
private static final long DISKCACHESIZE = 1024 * 1024 * 50;
//下载图片时的缓存大小
private static final long IOBUFFERSIZE = 1024 * 8;
// 缓存空间索引,用于Editor和Snapshot,设置成0表示Entry下面的第一个文件
private static final int DISKCACHEINDEX = 0;
//设置缓存目录
File diskLruCache = getDiskCacheDir(mContext, "bitmap");
if(!diskLruCache.exists())
diskLruCache.mkdirs();
//创建DiskLruCache对象,当然是在空间足够的情况下
if(getUsableSpace(diskLruCache) > DISKCACHESIZE)
{
try
{
mDiskLruCache = DiskLruCache.open(diskLruCache,
getAppVersion(mContext), 1, DISKCACHESIZE);
mIsDiskLruCache = true;
}catch(IOException e)
{
e.printStackTrace();
}
}
}
//上面的初始化过程总共用了3个方法
//设置缓存目录
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
// 获取可用的存储大小
@TargetApi(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDKINT >= VERSIONCODES.GINGERBREAD)
return path.getUsableSpace();
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
//获取应用版本号,注意不同的版本号会清空缓存
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
DiskLruCache的缓存添加
DiskLruCache的缓存添加的操作是通过Editor完成的, Editor表示一个缓存对象的编辑对象.
如果还是缓存图片为例子, 每一张图片都通过图片的url为key, 这里由于url可能会有特殊字符所以采用url的md5值作为key. 根据这个key就可以通过edit()来获取Editor对象, 如果这个缓存对象正在被编辑, 那么edit()就会返回null. 即DiskLruCache不允许同时编辑一个缓存对象.
当用.edit(key)获得了Editor对象之后. 通过editor.newOutputStream(0)就可以得到一个文件输出流. 由于之前open()方法设置了一个节点只能有一个数据. 所以在获得输出流的时候传入常量0即可.
有了文件输出流, 可以当网络下载图片时, 图片就可以通过这个文件输出流写入到文件系统上.最后,要通过Editor中commit()来提交写操作, 如果下载中发生异常, 那么使用Editor中abort()来回退整个操作.
DiskLruCache的缓存查找
和缓存的添加过程类似, 缓存查找过程也需要将url转换成key, 然后通过DiskLruCache#get()方法可以得到一个Snapshot对象, 接着在通过Snapshot对象即可得到缓存的文件输入流, 有了文件输入流, 自然就可以得到Bitmap对象. 为了避免加载图片出现OOM所以采用压缩的方式. 在前面对BitmapFactory.Options的使用说明了. 但是这中方法对FileInputStream的缩放存在问题. 原因是FileInputStream是一种有序的文件流, 而两次decodeStream调用会影响文件的位置属性, 这样在第二次decodeStream的时候得到的会是null. 针对这一个问题, 可以通过文件流来得到它所对应的文件描述符, 然后通过BitmapFactory.decodeFileDescription()来加载一张缩放后的图片.
/**
* 磁盘缓存的读取
* @param url
* @param reqWidth
* @param reqHeight
* @return
*/
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException
{
if(Looper.myLooper() == Looper.getMainLooper())
Log.w(TAG, "it's not recommented load bitmap from UI Thread");
if(mDiskLruCache == null)
return null;
Bitmap bitmap = null;
String key = hashKeyForDisk(url);
Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot != null)
{
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISKCACHEINDEX);
FileDescriptor fd = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
if(bitmap != null)
addBitmapToMemoryCache(key, bitmap);
}
return bitmap;
}
一个好的ImageLoader应该具备以下几点:
图片的压缩
网络拉取
内存缓存
磁盘缓存
图片的同步加载
图片的异步加载
实现照片墙效果,如果图片都需要是正方形;这样做很快,自定义一个ImageView,重写onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,widthMeasureSpec);//将原来的参数heightMeasureSpec换成widthMeasureSpec
}
本章主要讲解:
如何检测崩溃并了解详细的crash信息? 首先需实现一个uncaughtExceptionHandler对象,在它的uncaughtException方法中获取异常信息并将其存储到SD卡或者上传到服务器中,然后调用Thread的setDefaultUncaughtExceptionHandler为当前进程的所有线程设置异常处理器。
CrashHandler源码
在Application初始化的时候为线程设置CrashHandler,这样之后,Crash就会通过我们自己的异常处理器来处理异常了。
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
}
}
Android中单个dex文件所能包含的最大方法数为65536, 这包含了FrameWork, 依赖的jar包以及应用本身的代码中的所有方法. 会爆出:
com.android.dex.DexIndexOverflowException: method ID not in[0, 0xffff] :65536
可能在一些低版本的手机, 即使没有超过方法数的上限却还是出现错误
E/dalvikvm: Optimization failed
E/installd: dexopt failed on '/data/dalvik-cache/.....'
这个现象, 首先dexpot是一个程序, 应用在安装时, 系统会通过dexopt来优化dex文件, 在优化过程中dexopt采用一个固定大小的缓冲区来存储应用中所有方法消息, 这个缓冲区就是linearAlloc. LinearAlloc缓冲区在新版本的Android系统中大小为8MB或者16MB. 在Android 2.2和2.3中却只有5MB. 这是如果方法过多, 即使方法数没有超过65535也有可能会因为存储空间失败而无法安装.
解决方案
插件化: 是一套重量级的技术方案, 通过将一个dex拆分成两个或者多个dex,可以在一定程度上解决方法数的越界问题. 但是还有兼容性问题需要考虑, 所以需要权衡是否需要使用这个方案.
multidex: 这是Google在2014年提出的解决方案.在Android5.0之前需要引入Google提供的android-support-multidex.jar;从5.0开始系统默认支持了multidex,它可以从apk文件中加载多个dex文件。
使用步骤:
public class BaseApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
采用上面的配置项后,如果这个应用方法数没有越界,那么Gradle是不会生成多个dex文件的,当方法数越界后,Gradle就会在apk中打包2个或多个dex文件。当需要指定主dex文件中所包含的类,这时候就需要通过–multi-dex-list来选项来实现这个功能。
//在对应工程目录下的build.gradle文件,加入
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'
}
}
maindexlist.txt
com/ryg/multidextest/TestApplication.class
com/ryg/multidextest/MainActivity.class
// multidex 这9个类必须在主Dex中
android/support/multidex/MultiDex.class
android/support/multidex/MultiDexApplication.class
android/support/multidex/MultiDexExtractor.class
android/support/multidex/MultiDexExtractor$1.class
android/support/multidex/MultiDex$V4.class
android/support/multidex/MultiDex$V14.class
android/support/multidex/MultiDex$V19.class
android/support/multidex/ZipUtil.class
android/support/multidex/ZipUtil$CentralDirectory.class
需要注意multidex的jar中的9个类必须要打包到主dex中,因为Application的attachBaseContext方法中需要用到MultiDex.install(this)需要用到MultiDex。
Multidex的缺点:
动态加载也叫插件化. 当项目越来越大的时候, 可以通过插件化来减轻应用的内存和CPU占用. 还可以实现热插拔, 即可以在不发布新版本的情况下更新某些模块.
学习一下作者的插件化开源框架:dynamic-load-apk
各种插件化方案都需要解决3个基础性问题
宿主和插件的概念:宿主是指普通的apk, 而插件一般指经过处理的dex或者apk. 在主流的插件化框架中多采用经过处理的apk来作为插件, 处理方式往往和编译以及打包环节有关, 另外很多插件化框架都需要用到代理Activity的概念, 插件Activity的启动大多数是借助一个代理Activity来实现.
资源访问
插件中凡是以R开头的资源文件都不能访问。
Activity的工作主要是通过ContextImpl完成的,Activity中有一个mBase的成员变量,它的类型就是ContextImpl。Context有两个获取资源的抽象方法getAsssets()和getResources();只要实现这两个方法就可以解决资源问题。
protected void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
从loadResources()的实现看出,加载资源的方法是反射,通过调用AssetManager的addAssetPath方法,我们可以将一个apk中的资源加载到Resources对象中。传递的路径可以是zip或资源目录,因此直接将apk的路径传给它,资源就加载到AssetManager了。然后再通过AssetManager创建一个新的Resources对象,通过这个对象就可以访问插件apk中的资源了。
接着在代理Activity中实现getAssets()和getResources()。关于代理Activity参考作者的插件化开源框架。
@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}
@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
Activity的生命周期管理
为什么会有这个问题,其实很好理解,apk被宿主程序调起以后,apk中的activity其实就是一个普通的对象,不具有activity的性质,因为系统启动activity是要做很多初始化工作的,而我们在应用层通过反射去启动activity是很难完成系统所做的初始化工作的,所以activity的大部分特性都无法使用包括activity的生命周期管理,这就需要我们自己去管理。
ClassLoader的管理
为了避免多个ClassLoader加载了同一个类所引发的类型转换错误。将不同插件的ClassLoader存储在一个HashMap中。
Java JNI本意为Java Native Interface(java本地接口), 是为方便java调用C或者C++等本地代码所封装的一层接口. 由于Java的跨平台性导致本地交互能力的不好, 一些和操作系统相关的特性Java无法完成, 于是Java提供了JNI专门用于和本地代码交互.
NDK是android所提供的一个工具合集, 通过NDK可以在Android中更加方便地通过JNI来访问本地代码. NDK还提供了交叉编译工具, 开发人员只需要简单的修改mk文件就可以生成特定的CPU平台的动态库. 好处如下:
在Java中声明natvie方法
创建一个类
生命了两个native方法:get和set(String)。这是需要在JNI实现的方法。JniTest头部有一个加载动态库的过程, 加载so库名称填入的虽然是jni-test, 但是so库全名称应该是libjni-test.so,这是加载so库的规范。
编辑Java源文件得到class文件, 然后通过javah命令导出JNI头文件
在包的的根路径, 进行命令操作
javac com/szysky/note/androiddevseek_14/JNITest.java
javah com.szysky.note.androiddevseek_14.JNITest
执行之后会在, 操作的路径下生成一个comszyskynoteandroiddevseek14_JNITest.h头文件, 这个就是第二步生成的东西.
#ifdef cplusplus
extern "C" {
#endif
而这个宏定义是必须的, 作用是指定extern”C”内部的函数采用C语言的命名风格来编译. 如果设定那么当JNI采用C++来实现时, 由于C/C++编译过程对函数的命名风格不同, 这将导致JNI在链接时无法根据函数名找到具体的函数, 那么JNI调用肯定会失效.
用C/C++实现natvie方法
JNI方法是指的Java中声明的native方法, 这里可以选择c++和c来实现. 过程都是类似的. 只有少量的区别, 这里两种都实现一下.
在工程的主目录创建一个子目录, 名称任意, 然后将之前通过javah命令生成的.h头文件复制到创建的目录下, 接着创建test.cpp和test.c两个文件,实现如下:
test.app
#include “comszyskynoteandroiddevseek14_JNITest.h”
#include
JNIEXPORT jstring JNICALL Java_comszyskynoteandroiddevseek114_JNITest_get(JNIEnv env, jobject thiz){
printf(“执行在c++文件中 get方法\n”);
return env->NewStringUTF(“Hello from JNI .”);
}
JNIEXPORT void JNICALL Java_comszyskynoteandroiddevseek114_JNITest_get(JNIEnv env, jobject thiz, jstring string){
printf(“执行在c++文件中 set方法\n”);
char str = (char) env->GetStringUTFChars(string, NULL);
printf(“\n, str”);
env->ReleaseStringUTFChars(string, str);
}
test.c
#include “comszyskynoteandroiddevseek14_JNITest.h”
#include
JNIEXPORT jstring JNICALL Java_comszyskynoteandroiddevseek114_JNITest_get(JNIEnv *env, jobject thiz){
printf(“执行在c文件中 get方法\n”);
return (env)->NewStringUTF(“Hello from JNI .”);
JNIEXPORT void JNICALL Java_comszyskynoteandroiddevseek114_JNITest_get(JNIEnv env, jobject thiz, jstring string){
printf(“执行在c文件中 set方法\n”);
char str = (char) (*env)->GetStringUTFChars(env, string, NULL);
printf(“%s\n, str”);
(*env)->ReleaseStringUTFChars(env, string, str);
}}
其实C\C++在实现上很相似, 但是对于env的操作方式有所不同.
C++: env->ReleaseStringUTFChars(string, str);
C: (*env)->ReleaseStringUTFChars(env, string, str);
编译so库并在java中调用
so库的编译这里采用gcc. 命令cd到放置刚才生成c/c++的目录下.
使用如下命令:
gcc -shared -I /user/lib/jvm/java-7-openjdk-amd64/include -fPIC test.cpp -o libjni-test.so
gcc -shared -I /user/lib/jvm/java-7-openjdk-amd64/include -fPIC test.c -o libjni-test.so
/user/lib/jvm/java-7-openjdk-amd64是本地jdk的安装路径,libjni-test.so是生产的so库的名字。Java中通过:System.loadLibrary(“jni-test”)加载,其中lib和.so不需要指出。
切换到主目录,通过Java指令执行Java程序:java -Djava.library.path=jni com.ryg.JniTest。其中-Djava.library.path=jni指明了so库的路径。
下载并配置NDK
下载好NDK开发包,并且配置好NDK的全局变量。
创建一个Android项目,并声明所需的native方法
public static native String getStringFromC();
实现Android项目中所声明的native方法
# Copyright (C) 2009 The Android Open Source Project
# #
Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# #
http://www.apache.org/licenses/LICENSE-2.0
# #
Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# L
OCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
## 对应Java部分 System.loadLibrary(String libName) 的libname
LOCAL_MODULE := hello
## 对应c/c++的实现文件名
LOCALSRCFILES := hello.c
include $(BUILDSHAREDLIBRARY)
编写Application.mk,来指定需生成的平台对应的动态库,这里是全平台支持,也可以特殊指定。目前常见的架构平台有armeabi、x86和mips。其中移动设备主要是armeabi,因此大部分apk中只包含armeabi的so库。
APP_ABI := all
切换到jni目录的父目录,然后通过ndk-build命令编译产生so库
ndk-build 命令会默认指定jni目录为本地源码的目录
将编译好的so库放到Android项目中的 app/src/main/jniLbis 目录下,或者通过如下app的gradle设置新的存放so库的目录:
android{
……
sourceSets.main{
jniLibs.srcDir 'src/main/jni_libs'
}
}
还可以通过 defaultConfig 区域添加NDK选项
android{
……
defaultConfig{
……
ndk{
moduleName "jni-test"
}
}
}
还可以在 productFlavors 设置动态打包不同平台CPU对应的so库进apk( 缩小APK体积)
gradle
android{
……
productFlavors{
arm{
ndk{
adiFilter "armeabi"
}
}
x86{
ndk{
adiFilter "x86"
}
}
}
} `
在Android中调用
public class MainActivity extends Activity {
public static native String getStringFromC();
static{//在静态代码块中调用所需要的so文件,参数对应.so文件所对应的LOCAL_MODULE;
System.loadLibrary("hello");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//在需要的地方调用native方法
Toast.makeText(getApplicationContext(), get(), Toast.LENGTH_LONG).show();
}
}
JNI的数据类型包含两种: 基本类型和引用类型.
基本类型主要有jboolean, jchar, jint等, 和Java中的数据类型对应如下:
|JNI类型 |Java类型 |描述
|–|–|–|–
|jboolean| boolean |无符号8位整型
|jbyte |byte |无符号8位整型
|jchar| char |无符号16位整型
|jshort |short |有符号16位整型
|jint |int |32位整型
|jlong |long |64位整型
|jfloat |float |32位浮点型
|jdouble |double |64位浮点型
|void |void| 无类型
JNI中的引用类型主要有类, 对象和数组. 他们和Java中的引用类型的对应关系如下:
|JNI类型 |Java类型| 描述|
|–|–|–|
|jobject| Object| Object类型
|jclass |Class |Class类型
|jstring |String |String类型
|jobjeckArray| Object[] |对象数组
|jbooleanArray |boolean[] |boolean数组
|jbyteArray |byte[] |byte数组
|jcharArray |char[] |char数组
|jshortArray |short[] |short数组
|jintArray |int[] |int数组
|jlongArray |long[] |long数组
|jfloatArray| float[] |float数组
|jdoubleArray |double[] |double数组
|jthrowable |Throwable |Throwable
JNI的类型签名标识了一个特定的Java类型, 这个类型既可以是类也可以是方法, 也可以是数据类型.
类的签名比较简单, 它采用L+包名+类型+;的形式, 只需要将其中的.替换为/即可. 例如java.lang.String, 它的签名为Ljava/lang/String;, 末尾的;也是一部分.
基本数据类型的签名采用一系列大写字母来表示, 如下:
|Java类型| 签名| Java类型| 签名| Java类型| 签名|
|–|–|–|–|–|–|
|boolean |Z| byte |B |char |C
|short |S |int |I |long |J
|float| F| double |D |void |V
基本数据类型的签名基本都是单词的首字母, 但是boolean除外因为B已经被byte占用, 而long的表示也被Java类签名占用. 所以不同.
而对象和数组, 对象的签名就是对象所属的类签名, 数组的签名[+类型签名例如byte数组. 首先类型为byte,所以签名为B然后因为是数组那么最终形成的签名就是[B.例如如下各种对应:
char[] [C
float[] [F
double[] [D
long[] [J
String[] [Ljava/lang/String;
Object[] [Ljava/lang/Object;
如果是多维数组那么就根据数组的维度多少来决定[的多少, 例如int[][]那么就是[[I
方法的签名为(参数类型签名)+返回值类型签名。
方法boolean fun(int a, double b, int[] c). 参数类型的签名是连在一起, 那么按照方法的签名规则就是(ID[I)Z
方法:void fun(int a, String s, int[] c), 那么签名就是(ILjava/lang/String;[I)V
方法:int fun(), 对应签名()I
方法:int fun(float f), 对应签名(F)I
JNI调用java方法的流程是先通过类名找到类, 然后在根据方法名找到方法的id, 最后就可以调用这个方法了. 如果是调用Java的非静态方法, 那么需要构造出类的对象后才可以调用它。
演示一下调用静态的方法
static{
System.loadLibrary(“jni-test”);
}
/**
* 定义一个静态方法 , 提供给JNI调用
*/
public static void methodCalledByJni(String fromJni){
Log.e(“susu”, “我是从JNI被调用的消息, JNI返回的值是:”+fromJni );
}
// 定义调用本地方法, 好让本地方法回调java中的方法
public native void callJNIConvertJavaMethod();
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.btn_jni2java:
// 调用JNI的方法
callJNIConvertJavaMethod();
break;
}
}
// 定义调用java中的方法的函数
void callJavaMethod( JNIEnv *env, jobject thiz){
// 先找到要调用的类
jclass clazz = env -> FindClass(“com/szysky/note/androiddevseek_14/MainActivity”);
if (clazz == NULL){
printf(“找不到要调用方法的所属类”);
return;
}
// 获取java方法id
// 参数二是调用的方法名, 参数三是方法的签名
jmethodID id = env -> GetStaticMethodID(clazz, “methodCalledByJni”, “(Ljava/lang/String;)V”);
if (id == NULL){
printf(“找不到要调用方法”);
return;
}
jstring msg = env->NewStringUTF(“我是在c中生成的字符串”);
// 开始调用java中的静态方法
env -> CallStaticVoidMethod(clazz, id, msg);
}
void Java_comszyskynoteandroiddevseek114_MainActivity_callJNIConvertJavaMethod(JNIEnv *env, jobject thiz){
printf(“调用c代码成功, 马上回调java中的代码”);
callJavaMethod(env, thiz);
}
稍微说明一下, 程序首先根据类名com/szysky/note/androiddevseek_14/MainActivity找到类, 然后在根据方法名methodCalledByJni找到方法, 并传入方法对应签名(Ljava/lang/String;), 最后通过JNIEnv对象的CallStaticVoidMethod()方法来完成最终调用。
最后只要在Java_comszyskynoteandroiddevseek114_MainActivity_callJNIConvertJavaMethod方法中调用callJavaMethod方法即可.
流程就是–> 按钮触发了点击的onClikc –> 然后Java中会调用JNI的callJNIConvertJavaMethod() –> JNI的callJNIConvertJavaMethod()方法内部会调用具体实现回调Java中的方法callJavaMethod() –> 方法最终通过CallStaticVoidMethod()调用了Java中的methodCalledByJni()来接收一个参数并打印一个log。
结果图:
生成so库的文件保存在git中的app/src/main/backup目录下一个两个版本代码, 第一个就是第二小节中的NDK开发代码, 第二个就是第四小节的代码就是目前的. 而so库是最新的, 包含了所有的JNI代码生成的库文件。
JNI调用Java的过程和Java中方法的定义有很大关联, 针对不同类型的java方法, JNIEnv提供了不同的接口去调用, 更为细节的部分要去开发中或者去网站去了解更多.
Android设备作为一种移动设备,不管是内存还是CPU的性能都受到了一定的限制,也意味着Android程序不可能无限制的使用内存和CPU资源,过多的使用内存容易导致OOM,过多的使用CPU资源容易导致手机变得卡顿甚至无响应(ANR)。这也对开发人员提出了更高的要求。 本章主要介绍一些有效的性能优化方法。主要包括布局优化、绘制优化、内存泄漏优化、响应速度优化、ListView优化、Bitmap优化、线程优化等;同时还介绍了ANR日志的分析方法。
Google官方的Android性能优化典范专题短视频课程是学习Android性能优化极佳的课程,目前已更新到第五季
布局优化的思想就是尽量减少布局文件的层级,这样绘制界面时工作量就少了,那么程序的性能自然就高了。
删除无用的控件和层级
有选择的使用性能较低的ViewGroup,如果布局中既可以使用Linearlayout也可以使用RelativeLayout,那就是用LinearLayout,因为RelativeLayout功能比较复杂,它的布局过程需要花费更多的CPU时间。
有时候通过LinearLayou无法实现产品效果,需要通过嵌套来完成,这种情况还是推荐使用RelativeLayout,因为ViewGroup的嵌套相当于增加了布局的层级,同样降低程序性能。
采用标签、标签和ViewStub
include标签
标签用于布局重用,可以将一个指定的布局文件加载到当前布局文件中。只支持android:layout开头的属性,当然android:id这个属性是个特例;如果指定了android:layout这种属性,那么要求android:layoutwidth和android:layout_height必须存在,否则android:layout属性无法生效。如果指定了id属性,同时被包含的布局文件的根元素也指定了id属性,会以指定的这个id属性为准。
merge标签
标签一般和标签一起使用从而减少布局的层级。如果当前布局是一个竖直方向的LinearLayout,这个时候被包含的布局文件也采用竖直的LinearLayout,那么显然被包含的布局文件中的这个LinearLayout是多余的,通过标签就可以去掉多余的那一层LinearLayout。
ViewStub
ViewStub意义在于按需加载所需的布局文件,因为实际开发中,有很多布局文件在正常情况下是不会现实的,比如网络异常的界面,这个时候就没必要在整个界面初始化的时候将其加载进来,在需要使用的时候再加载会更好。在需要加载ViewStub布局时:
((ViewStub)findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
//或者
View importPanel = ((ViewStub)findViewById(R.id.stub_import)).inflate();
当ViewStub通过setVisibility或者inflate方法加载后,ViewStub就会被它内部的布局替换掉,ViewStub也就不再是整个布局结构的一部分了。
View的onDraw方法要避免执行大量的操作;
onDraw中不要创建大量的局部对象,因为onDraw方法会被频繁调用,这样就会在一瞬间产生大量的临时对象,不仅会占用过多内存还会导致系统频繁GC,降低程序执行效率。
onDraw也不要做耗时的任务,也不能执行成千上万的循环操作,尽管每次循环都很轻量级,但大量循环依然十分抢占CPU的时间片,这会造成View的绘制过程不流畅。根据Google官方给出的标准,View绘制保持在60fps是最佳的,这也就要求每帧的绘制时间不超过16ms(1000/60);所以要尽量降低onDraw方法的复杂度。
内存泄露是最容易犯的错误之一,内存泄露优化主要分两个方面;一方面是开发过程中避免写出有内存泄露的代码,另一方面是通过一些分析工具如LeakCanary或MAT来找出潜在的内存泄露继而解决。
静态变量导致的内存泄露
比如Activity内,一静态Conext引用了当前Activity,所以当前Activity无法释放。或者一静态变量,内部持有了当前Activity,Activity在需要释放的时候依然无法释放。
单例模式导致的内存泄露
比如单例模式持有了Activity,而且也没用解注册的操作。因为单例模式的生命周期和Application保存一致,生命周期比Activity要长,这样一来就导致Activity对象无法及时被释放。
属性动画导致的内存泄露
属性动画中有一类无限循环的动画,如果在Activity播放了此类动画并且没有在onDestroy中去停止动画,那么动画会一直播放下去,并且这个时候Activity的View会被动画持有,而View又持有了Activity,最终导致Activity无法释放。解决办法是在Activity的onDrstroy中调用animator.cancel()来停止动画。
响应速度优化的核心思想就是避免在主线程中去做耗时操作,将耗时操作放在其他线程当中去执行。Activity如果5秒无法响应屏幕触摸事件或者键盘输入事件就会触发ANR,而BroadcastReceiver如果10秒还未执行完操作也会出现ANR。
当一个进程发生ANR以后系统会在/data/anr的目录下创建一个文件traces.txt,通过分析该文件就能定位出ANR的原因。
通过一个例子来了解如何去分析文件, 首先在onCreate()添加如下代码, 让主线程等待一个锁,然后点击返回5秒后会出现ANR。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 以下代码是为了模拟一个ANR的场景来分析日志
new Thread(new Runnable() {
@Override
public void run() {
testANR();
}
}).start();
SystemClock.sleep(10);
initView();
}
/**
* 以下两个方法用来模拟出一个稍微不好发现的ANR
*/
private synchronized void testANR(){
SystemClock.sleep(3000 * 1000);
}
private synchronized void initView(){}
这样会出现ANR, 然后导出/data/anr/straces.txt文件. 因为内容比较多只贴出关键部分
DALVIK THREADS (15):
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 obj=0x73db0970 self=0xf4306800
| sysTid=19949 nice=0 cgrp=apps sched=0/0 handle=0xf778d160
| state=S schedstat=( 151056979 25055334 199 ) utm=5 stm=9 core=1 HZ=100
| stack=0xff5b2000-0xff5b4000 stackSize=8MB
| held mutexes=
at com.szysky.note.androiddevseek_15.MainActivity.initView(MainActivity.java:0)
- waiting to lock <0x2fbcb3de> (a com.szysky.note.androiddevseek_15.MainActivity)
- held by thread 15
at com.szysky.note.androiddevseek_15.MainActivity.onCreate(MainActivity.java:42)
这段可以看出最后指明了ANR发生的位置在ManiActivity的42行. 并且通过上面看出initView方法正在等待一个锁<0x2fbcb3de>锁的类型是一个MainActivity对象. 并且这个锁已经被线程id为15(tid=15)的线程持有了. 接下来找一下线程15
"Thread-404" prio=5 tid=15 Sleeping
| group="main" sCount=1 dsCount=0 obj=0x12c00f80 self=0xeb95bc00
| sysTid=19985 nice=0 cgrp=apps sched=0/0 handle=0xef34be80
| state=S schedstat=( 391248 0 1 ) utm=0 stm=0 core=2 HZ=100
| stack=0xe2bfe000-0xe2c00000 stackSize=1036KB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x2e3896a7> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1031)
- locked <0x2e3896a7> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:985)
at android.os.SystemClock.sleep(SystemClock.java:120)
at com.szysky.note.androiddevseek_15.MainActivity.testANR(MainActivity.java:50)
- locked <0x2fbcb3de> (a com.szysky.note.androiddevseek_15.MainActivity)
tid = 15 就是相关信息如上, 首行已经标出线程的状态为Sleeping, 原因在50行, 就是SystemClock.sleep(3000 * 1000);这句话. 也就是testANR(). 而最后一行也表明了持有的locked<0x2fbcb3de>就是主线程在等待的那个锁对象.
ListView/GridView优化:
主要思想就是采用线程池, 避免程序中存在大量的Thread. 线程池可以重用内部的线程, 避免了线程创建和销毁的性能开销. 同时线程池还能有效的控制线程的最大并发数, 避免了大量线程因互相抢占系统资源从而导致阻塞现象的发生.详细参考第11章的内容。
MAT全程Eclipse Memory Analyzer, 是一个内存泄漏分析工具. 下载后解压即可. 下载地址http://www.eclipse.org/mat/downloads.php. 这里仅简单说一下. 这个我没有手动去实践, 就当个记录, 因为现在Android Studio可以直接分析hprof文件.
可以手动写一个会造成内存泄漏的代码, 然后打开DDMS, 然后选中要分析的进程, 然后单击Dump HPROF file这个按钮. 等一小段会生成一个文件. 这个文件不能被MAT直接识别. 需要使用Android SDK中的工具进行格式转换一下.这个工具在platform-conv文件夹下
hprof-conv 要转换的文件名 输出的文件名文件名的签名有包名.
然后打开MAT通过菜单打开转换后的这个文件. 这里常用的就有两个
Histogram: 可以直观的看出内存中不同类型的buffer的数量和占用内存大小
Dominator Tree: 把内存中的对象按照从大到小的顺序进行排序, 并且可以分析对象之间的引用关系, 内存泄漏分析就是通过这个完成的.
分析内存泄漏的时候需要分析Dominator Tree里面的内存信息, 一般会不直接显示出来, 可以按照从大到小的顺序去排查一遍. 如果发生了了泄漏, 那么在泄漏对象处右键单击Path To GC Roots->exclude wake/soft references. 可以看到最终是什么对象导致的无法释放. 刚才的操作之所以排除软引用和弱引用是因为,大部分情况下这两种类型都可以被gc回收掉,所以基本也就不会造成内存泄漏.
同样这里也可以使用搜索功能, 假如我们手动模拟了内存泄漏, 泄漏的对象就是Activity那么我们back退出重进循环几次, 会发现其实很多个Activit对象.
提高可读性
代码的层级性
不要把一段业务逻辑放在一个方法或者一个类中全部实现,要把它分成几个子逻辑,然后每个子逻辑做自己的事情,这样即显得代码层级分明,这样利于提高程序的可扩展性。
程序的扩展性
由于很多时候在开发过程中无法保证已经做好的需求不在后面的版本发生更改, 因此在写程序的时候要时刻考虑到扩展的问题, 考虑如果这个逻辑以后发生了改变那么哪些需要修改, 以及怎样在以后修改的时候降低工作量, 而面向扩展编程可以让程序具有很好的扩展性.
恰当的使用设计模式可以提高代码的可维护性和可扩展性,Android程序容易遇到性能瓶颈,要控制设计的度,不能太牵强,避免过度设计。作者推荐查看《 大话设计模式》 和《 Android源码设计模式解析和实战》 这两本书来学习设计模式。