2.IPC机制
介绍Android中的序列化机制和Binder,Bundle、文件共享、AIDL、Messager、ContentProvider、socket,还有Binder连接池的概念。
2.1 IPC简介
IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。按照操作系统中的描述,线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。
IPC不是Android中所独有的,任何一个操作系统都需要有相应的IPC机制,比如Windows上可以通过剪贴板、管道和邮槽等来进行进程间通信;Linux上可以通过命名管道、共享内容、信号量等来进行进程间通信。
在Android中最有特色的进程间通信方式就是Binder了,通过Binder可以轻松地实现进程间通信。除了Binder,Android还支持Socket,通过Socket也可以实现任意两个终端之间的通信,当然同一个设备上的两个进程通过Socket通信自然也是可以的
2.2 Android中的多进程模式
给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest中指定android:process属性,就可以完成多进程。
<activity
android:name="com.ryg.chapter_2.SecondActivity"
android:configChanges="screenLayout"
android:label="@string/app_name"
android:process=":remote" />
<activity
android:name="com.ryg.chapter_2.ThirdActivity"
android:configChanges="screenLayout"
android:label="@string/app_name"
android:process="com.ryg.chapter_2.remote" />
两个方面:
1、“:”的含义是指要在当前的进程名前面附加上当前的包名,这是一种简写的方法,对于SecondActivity来说,它完整的进程名为com.ryg.chapter_2:remote,
2、以“:”
开头的进程属于当前应用的私有进程
,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程
,其他应用通过ShareUID方式可以和它跑在同一个进程中。
Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据
。这里要说明的是,两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同
才可以。
一般来说,使用多进程会造成如下几方面的问题:
(1)静态成员和单例模式完全失效。
(2)线程同步机制完全失效。
(3)SharedPreferences的可靠性下降。
(4)Application会多次创建。
2.3 IPC基础概念介绍
2.3.1 Serializable接口
Serializable是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。
serialVersionUID的作用
serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被反序列化。serialVersionUID的详细工作机制是:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),当反序列化的时候系统会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化;
2.3.2 Parcelable接口
Serializable是Java中的序列化接口,其使用起来简单但是开销很大
,序列化和反序列化过程需要大量I/O操作
。而Parcelable是Android中的序列化方式,因此更适合用在Android平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高,这是Android推荐的序列化方式,因此我们要首选Parcelable。Parcelable主要用在内存序列化上
,通过Parcelable将对象序列化到存储设备
中或者将对象序列化后通过网络传输也都是可以的,但是这个过程会稍显复杂,因此在这两种情况下建议大家使用Serializable。以上就是Parcelable和Serializable和
区别
2.3.3 Binder
Binder是Android中的一个类,它继承了IBinder接口。
从IPC角度来说,Binder是Android中的一种跨进程通信方式
,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;
从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager,等等)和相应ManagerService的桥梁
;
从Android应用层来说,Binder是客户端和服务端进行通信的媒介
,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
Book.java是一个表示图书信息的类,它实现了Parcelable接口。
Book.aidl是Book类在AIDL中的声明。
IBook.aidl是我们定义的一个接口,里面有两个方法:getBookList和addBook,
注意:不同的后缀名,放入的文件夹不一样。
public class Book implements Parcelable {
public int bookId;
public String bookName;
...
}
// Book.aidl
package com.example.aidldemo;
parcelable Book;
// IBook.aidl
package com.example.aidldemo;
import com.example.aidldemo.Book;
interface IBook {
List<Book> getBookList();
void addBook(in Book book);
}
然后编译生成一个IBook.java 接口类。路径为build/generated/aidl_source_output_dir/debug/compileDebugAidl
/out/com/example/aidldemo
DESCRIPTOR
Binder的唯一标识,一般用当前Binder的类名表示,比如本例中的“com.ryg.chapter_2.aidl.IBookManager”。
asInterface(android.os.IBinder obj)
用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象
,这种转换过程是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象
。
asBinder
此方法用于返回当前Binder对象。
onTransact
这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型为public Boolean onTransact(int code,android.os.Parcel data,android.os.Parcel reply,int flags)。服务端通过
code可以确定客户端所请求的目标方法是什么
,接着
从data中取出目标方法所需的参数
(如果目标方法有参数的话),然后执行目标方法。当目标方法执行完毕后,就向
reply中写入返回值
(如果目标方法有返回值的话),
onTransact方法的执行过程就是这样的。需要注意的是,如果此方法返回false,那么客户端的请求会失败
,因此我们可以利用这个特性来做权限验证,毕竟我们也不希望随便一个进程都能远程调用我们的服务。
Proxy#getBookList
这个方法运行在客户端,当客户端远程调用此方法时,它的内部实现是这样的:首先创建该方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;然后把该方法的参数信息写入_data中(如果有参数的话);接着调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;最后返回_reply中的数据。
Proxy#addBook
这个方法运行在客户端,它的执行过程和getBookList是一样的,addBook没有返回值,所以它不需要从_reply中取出返回值。
AIDL文件并不是实现Binder的必需品,AIDL文件的本质是系统为我们提供了一种快速实现Binder的工具,仅此而已。
linkToDeath和unlinkToDeath
通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,这个时候我们就可以重新发起连接请求从而恢复连接.
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "binder died. tname:" + Thread.currentThread().getName());
if (mRemoteBookManager == null)
return;
mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mRemoteBookManager = null;
// TODO:这里重新绑定远程Service
}
};
2.3.4 Messager
Messenger是一种轻量级的IPC方案,它的底层实现是AIDL.
3.View 事件体系
3.1 view 位置
有左上角和右下角来确认view的位置 top left right botttom,这些都是相对父容器来说。
从Android 3.0 ,view增加了x ,y translationx,translationy ,x和y是左上角的坐标,而translationx,translationy 是左上角相对于父容器的偏移量。
注意:view平移的时候,top 、left是原始左上角的位置,其值并不会发生变化,变化的是x.y translationx和translationy这四个参数。
3.2 MotionEvent
getX 、getY是当前点击的位置相对于当前view的坐标。
getRawX 、 getRawY是相对于屏幕左上角的坐标。
3.3 TouchSlop
touchSlop是系统能够识别出被认为滑动的最小距离。
ViewConfiguration.get(getContent()).getScaledTouchSlop();
源码位置:frameworks/base/core/res/res/values/config.xml
<dimen name="config_viewconfigurationTouchSlop">8dp</dimen>
3.4 VelocityTracker 速度追踪
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。
VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
第一点,获取速度之前必须先计算速度,即getXVelocity和getYVelocity这两个方法的前面必须要调用computeCurrentVelocity 方法;
第二点,这里的速度是指一段时间内手指所滑过的像素数,比如将时间间隔设为1000ms时,在1s内,手指在水平方向从左向右滑过100像素,那么水 平速度就是100。注意速度可以为负数,当手指从右往左滑动时,水平方向速度即为负值
3.5 GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean consume = mGestureDetector.onTouchEvent(event);
Log.d(TAG, "onTouchEvent: ");
return consume;
}
@Override
public boolean onDown(MotionEvent e) {
Log.d(TAG, "onDown: ");
return false;
}
@Override
public void onShowPress(MotionEvent e) {
Log.d(TAG, "onShowPress: ");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.d(TAG, "onSingleTapUp: ");
return false;
}
/**
* 手指按下屏幕并滑动
* @param e1
* @param e2
* @param distanceX
* @param distanceY
* @return
*/
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.d(TAG, "onScroll: ");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.d(TAG, "onLongPress: ");
}
private static final String TAG = "MainActivity";
/**
* 快速滑动,down --move --up
* @param e1
* @param e2
* @param velocityX
* @param velocityY
* @return
*/
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.d(TAG, "onFling: ");
return false;
}
3.6 view 的滑动
1.view 提供的scrollTo/ scrollBy方法实现滑动
scrollTo()是绝对滚动,scrollby() 是相对滑动,是增量滑动。两者只能改变view 内容的位置,不能改变view在布局中的位置。
2.通过动画给view施加平移效果来实现滑动
3.通过改变view的layoutprams使得view重新布局从而实现滑动。
4.弹性滑动Scroller
Scroller scroller = new Scroller(getContext());
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int deltaX = destX = scrollX;
scroller.startScroll(scrollX,0,deltaX,0,1000);
invalidate();
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
scroller内部没有做什么,只是保存传入的几个参数。
注意这里的滑动使用的scrollTo,所以滑动的是view的内容而非view本身位置的改变。
// startX startY 滑动的起点,dx dy 表示要滑动的距离,duration 滑动时间
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
那么Scroller到底是如何到底滑动?
startScroll方法的invalidate(). invalidate()会导致view重绘,在view的draw方法会去调用computeScroll方法,computeScroll方法在view中是一个空实现,需要我们自己实现。
原理:当view重绘后在draw方法中调用computeScroll,而computeScroll又会向Scroller获取当前的scrollX scrollY,然后通过scrollTo方法实现滑动,接着调用postInvalidate进行第二次重绘,这次重绘和第一次一样,还会导致computeScroll被调用,继续向Scroller获取当前的scrollX scrollY,通过调用scrollTo方法滑动到新的位置,如此反复。
scroller的computeScrollOffset方法实现
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
4.事件分发机制
public boolean dispatchTouchEvent(MotionEvent ev)
public boolean onInterceptTouchEvent(motionEvent ev)
public boolean onTouchEvent(MotionEvent ev);
伪代码:
public boolean dispathcTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
8 windowManager
https://blog.csdn.net/chentaishan/article/details/122435355
11.Android线程
1.AsyncTask
AsyncTask是一个抽象的泛型类,它提供了Params、Progress和Result这三个泛型参数,其中Params表示参数的类型,Progress表示后台任务的执行进度的类型,而Result则表示后 台任务的返回结果的类型,如果AsyncTask确实不需要传递具体的参数,那么这三个泛型参数可以用Void来代替。
AsyncTask这个类的声明如下所示。
public abstract class AsyncTask<Params,Progress,Result>
AsyncTask提供了4个核心方法,它们的含义如下所示。
(1)onPreExecute(),在主线程中执行,在异步任务执行之前,此方法会被调用,一般可以用于做一些准备工作。
(2)doInBackground(Params…params),在线程池中执行,此方法用于执行异步任务,params参数表示异步任务的输入参数。在此方法中可以通过publishProgress方法来更新任务 的进度
,publishProgress方法会调用onProgressUpdate方法。另外此方法需要返回计算结果给onPostExecute方法。
(3)onProgressUpdate(Progress…values),在主线程中执行,当后台任务的执行进度发生改变时此方法会被调用。
(4)onPostExecute(Result result),在主线程中执行,在异步任务执行之后,此方法会被调用,其中result参数是后台任务的返回值,即doInBackground的返回值。
内部实现原理:
https://blog.csdn.net/chentaishan/article/details/104335445
2.HandlerThread
3.IntentService
4.Android 线程池
13 综合技术
通过CrashHandler来监视应用的crash信息,给程序设置一个CrashHandler,这样当程序crash时就会调用CrashHandler的uncaughtException方法。
在Android中,有一个限制,那就是整个应用的方法数不能超过65536,否则就会出现编译错误,并且程序也无法成功地安装到手机上。当项目日益庞大后这个问题就比较容易遇到,Google提供了multidex方案专门用于解决这个问题,通过将一个dex文件拆分为多个dex文件来避免单个dex文件方法数越界的问
题。
方法数越界的另一种解决方案是动态加载。动态加载可以直接加载一个dex形式的文件,将部分代码打包到一个单独的dex文件中(也可以是dex格式的jar
或者apk),并在程序运行时根据需要去动态加载dex中的类,这种方式既可以解决缓解方法数越界的问题,也可以为程序提供按需加载的特性,同时这还为
应用按模块更新提供了可能性。
13.1 使用CrashHandler来获取应用的crash信息
Android应用不可避免地会发生crash,也称之为崩溃, Android提供了处理这类问题的方法。
//Thread内部的uncaughtException方法
@FunctionalInterface
public interface UncaughtExceptionHandler {
void uncaughtException(Thread var1, Throwable var2);
}
public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh{
defaultUncaughtExceptionHandler = eh;
}
设置系统的默认异常处理器, 解决上面所提到的crash问题。当crash发生的时候,系统就 会回调UncaughtExceptionHandler的uncaughtException方法,在uncaughtException方法中就可以获取到异常信息。
首先需要实现一个UncaughtExceptionHandler对象,在它的uncaughtException方法中获取异 常信息并将其存储在SD卡中或者上传到服务器供开发人员分析,然后调用Thread的setDefaultUncaught-ExceptionHandler方法将它设置为线程默认的异常处理器, 由于默认异常处理器是Thread类的静态成员,因此它的作用对象是当前进程的所有线程。
public class CrashHandler implements UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/CrashTest/log/";
private static final String FILE_NAME = "crash";
private static final String FILE_NAME_SUFFIX = ".trace";
private static CrashHandler sInstance = new CrashHandler();
private UncaughtExceptionHandler mDefaultCrashHandler;
private Context mContext;
private CrashHandler() {
}
public static CrashHandler getInstance() {
return sInstance;
}
public void init(Context context) {
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context.getApplicationContext();
}
/**
* 这个是最关键的函数,当程序中有未被捕获的异常,系统将会自动调用#uncaughtException方法
* thread为出现未捕获异常的线程,ex为未捕获的异常,有了这个ex,我们就可以得到异常信息。
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
try {
//导出异常信息到SD卡中
dumpExceptionToSDCard(ex);
uploadExceptionToServer();
//这里可以通过网络上传异常信息到服务器,便于开发人员分析日志从而解决bug
} catch (IOException e) {
e.printStackTrace();
}
ex.printStackTrace();
//如果系统提供了默认的异常处理器,则交给系统去结束我们的程序,否则就由我们自己结束自己
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(thread, ex);
} else {
Process.killProcess(Process.myPid());
}
}
private void dumpExceptionToSDCard(Throwable ex) throws IOException {
//如果SD卡不存在或无法使用,则无法把异常信息写入SD卡
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
if (DEBUG) {
Log.w(TAG, "sdcard unmounted,skip dump exception");
return;
}
}
File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdirs();
}
long current = System.currentTimeMillis();
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(current));
File file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);
try {
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
pw.println(time);
dumpPhoneInfo(pw);
pw.println();
ex.printStackTrace(pw);
pw.close();
} catch (Exception e) {
Log.e(TAG, "dump crash info failed");
}
}
}
13.2 multidex解决方法数越界
在Android中单个dex文件所能够包含的最大方法数为65536,这包含Android FrameWork、依赖的jar包以及应用本身的代码中的所有方法。
com.android.dex.DexIndexOverflowException: method ID not in [0,0xffff]: 65536
另外一种情况有所不同,有时候方法数并没有达到65536,并且编译器也正常地完成了编译工作,但是应用在低版本手机安装时异常中止,异常信息如 下:
E/dalvikvm: Optimization failed E/installd: dexopt failed on '/data/dalvik-cache/data@app@com.ryg.
multidextest-2.apk@classes.dex' res = 65280
其实是这样的,dexopt是一个程序,应用在安装时,系统会通过dexopt来优化dex文件,在优化过程中dexopt采用一个固定大小 的缓冲区来存储应用中所有方法的信息,这个缓冲区就是LinearAlloc
。LinearAlloc缓冲区在新版本的Android系统中其大小是8MB或者16MB,但是在Android 2.2 和2.3中却只有5MB,当待安装的apk中的方法数比较多时,尽管它还没有达到65536这个上限,但是它的存储空间仍然有可能超出5MB,这种情况下dexopt程 序就会报错,从而导致安装失败。
解决:
- 删除无用的代码和第三方库
- 采用插件化的机制来动态加载部分dex,通过将一个dex拆分成两个 或多个dex,这就在一定程度上解决了方法数越界的问题。但是插件化是一套重量级的技术方案,并且其兼容性问题往往较多
- multidex的解决方案
在Android 5.0以前使用multidex需要引入Google提供的android-support-multidex.jar这个jar包,这个jar包可以在Android SDK目录下的 extras/android/support/multidex/library/ libs下
面找到。从Android 5.0开始,Android默认支持了multidex
13.3 指定主dex文件中所 要包含的类
在module下 build.gradle文件里,添加;
afterEvaluate {
println "afterEvaluate"
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
//引入配置文件,配置哪些类可以打包到dex文件里
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
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
15.性能优化
布局优化
尽量减少布局文件的层级
标签
将一个指定的布局文件加载到当前的布局文件中
标签
标签一般和标签一起使用从而减少布局的层级
ViewStub
ViewStub继承了View,它非常轻量级且宽/高都是0,因此它本身不参与任何的布局和绘制过程。
<ViewStub
android:id="@+id/stub_import"
android:inflatedId="@+id/panel_import"
android:layout="@layout/layout_network_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);
View importPanel = ((ViewStub)findViewById(R.id.stub_import)).inflate();
当ViewStub通过setVisibility或者inflate方法加载后,ViewStub就会被它内部的布局替换掉,这个时候ViewStub就不再是整个布局结构中的一部分了。另外,
目前ViewStub还不支持标签。
绘制优化
绘制优化是指View的onDraw方法要避免执行大量的操作
,onDraw中不要创建新的局部对象,这是因为onDraw方法可能会被频繁调用
onDraw方法中不要做耗时的任务,也不能执行成千上万次的循环操作
View的绘制帧率保证60fps是最佳的,这就要求每帧的绘制时间
不超过16ms(16ms = 1000 / 60),
内存泄漏优化
单例模式导致的内存泄露
属性动画导致的内存泄露