《Android开发艺术探索》小结

以前电子档大概看了一遍,就是没有去记录总结,时间久了,很多也忘了,这次认真看一遍,温故知新。
开始小结之前,先看下面这段代码,不用怀疑,不会报错:

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final TextView textView= (TextView) findViewById(R.id.tv); new Thread(new Runnable() { @Override public void run() { textView.setText("我不是UI线程"); } }).start(); }

原因是这样的,View的线程检查,是由ViewRoot来检查,而ViewRoot是在onResume的时候才创建的,所以在ViewRoot创建好之前,非UI线程操作View不会抛出异常。
1、Activity的生命周期和启动模式
1、生命周期
  • onStart和onResume都表示Activity已经可见了,但是onStart的时候Activity还在后台,onResume的时候Activity才显示到前台。
  • 在特殊情况下(很极端),如果在onPause的时候快速的回到当前的Activity,那么onResume而不是调用onStop。
  • 新Activity的显示,onPause必须先执行完,新Activity的onResume才会执行。
  • 当打开新Activity的时候,当前Activity会走onPause-->onStop,但有一个特殊情况,如果新Activity采用了透明主题,那么当前的Activity不会走onStop。
  • Activity在异常情况终止时,系统会调用onSaveInstanceState来保存当前Activity状态,这个方法的调用时机在onStop之前,它和onPause没有既定的时序关系,它既可能在onPause之前,也可能在之后。当Activity被重新创建后,系统会调用onRestoreInstanceState,并把Activity销毁时onSaveInstanceState方法保存的Bundle对象作为参数传递给onRestoreInstanceState和onCreate,onRestoreInstanceState的调用时机在onState之后。
  • 和Activity一样,每个View都有onSaveInstanceState和onRestoreInstanceState这个两个方法,所以系统能自动为每个View恢复数据,前提的要有ID。
  • onRestoreInstanceState只要被调用,参数Bundle savedInstanceState肯定不是null。
  • onSaveInstanceState只会在Activity即将被销毁并且有机会重新显示的情况下才会去调用它。
  • 配置configChanges属性
    其中locale、orientation、keyboardHidde最常用,注意screenSize,为了防止屏幕旋转重启,除了orientation还要加screenSize,Activity没有重新创建就不会调onSaveInstanceState和onRestoreInstanceState,而会调onConfigurationChanged,这样就可以自己做些特殊处理。
《Android开发艺术探索》小结
文章图片
configChanges.png
2、启动模式
  • standard:在这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。而非Activity的Context没有任务栈,必须指定FLAG_ACTIVITY_NEW_TASK标记位。
  • singleTop、singleTask:这种模式下,Activity会调onNewIntent,同时这个Activity的onCreate、onStart不会被系统调用。
  • TaskAffinity(AndroidMenifest中指定):
    命名要求:必须包含"."。
    当TaskAffinity和singleTask启动模式配对使用的时候,它是具有该模式的Activity的目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中。
    当TaskAffinity和allowTaskReparenting结合的时候,会是这种情况,当应用A启动了应用B的某个Activity后,如果这个Activity的allowTaskReparenting属性为true,那么应用B启动后,这个Activity会直接从应用A的任务栈转移到应用B的任务栈中。
  • 两种指定Activity的启动模式
    一是通过AndroidMenifest为Activity指定launchMode属性,二是通过Intent的标志位。
    区别:
    优先级上,第二种的优先级高于第一种;
    限定范围上,第一种无法直接为Activity设定FLAG_ACTIVITY_CLEAR_TOP标识,第二种无法直接为Activity指定singleInstance模式。
3、FLAG
有些时候FLAG虽然和launchMode同作用,但并不是效果一定相同。
  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    具有这个标记的Activity不会出现在历史Activity的列表中,当某些情况下我们不希望用户通过历史列表回到我们的Activity的时候这个标记比较有用,它等同于XML中指定Activity是属性android:excludeFromRecents="true"。
2、View的事件体系 1、View的基础知识
  • View在平移过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY。
  • TouchSlop
    TouchSlop是系统所能识别出的被认为是滑动的最小距离,换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在滑动。
    不同的设备这个值可能不同,通过ViewConfiguration.get(context).getScaledTouchSlop()获取。
  • VelocityTracker
    速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖值方向的速度。
    VelocityTracker velocityTracker = VelocityTracker.obtain(); velocityTracker.addMovement(event); velocityTracker.computeCurrentVelocity(1000); //计算速度,参数为时间间隔,这里表示1s float xVelocity = velocityTracker.getXVelocity(); //可以为负,从右往左滑的时候 float yVelocity = velocityTracker.getYVelocity(); //不使用时回收 velocityTracker.clear(); velocityTracker.recycle(); return super.onTouchEvent(event);

  • GestureDetector
    手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。
    public class GestureView extends View implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {private final GestureDetector mGestureDetector; public GestureView(Context context) { super(context); mGestureDetector = new GestureDetector(this); mGestureDetector.setIsLongpressEnabled(false); //解决长按屏幕后无法拖动的现象 }@Override public boolean onTouchEvent(MotionEvent event) { returnmGestureDetector.onTouchEvent(event); }@Override public boolean onDown(MotionEvent e) { //手指轻触屏幕的一瞬间,由一个ACTION_DOWN触发 return false; }@Override public void onShowPress(MotionEvent e) { //手指轻触屏幕,尚未松开或拖动 }@Override public boolean onSingleTapUp(MotionEvent e) { //单击行为 return false; }@Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //拖动 return false; }@Override public void onLongPress(MotionEvent e) { //长按 }@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { //按下。快速滑动后松开 return false; }@Override public boolean onSingleTapConfirmed(MotionEvent e) { //严格的单击 return false; }@Override public boolean onDoubleTap(MotionEvent e) { //双击,由2次连接单击组成,不能和onSingleTapConfirmed共存 return false; }@Override public boolean onDoubleTapEvent(MotionEvent e) { //双击行为 return false; } }

2、View的滑动
  • scrollTo/By看成是屏幕在滑动。
  • Scroller源码分析
    mScroller = new Scroller(context); private void smoothScrollTo(int destX,int destY){ int startX = mScroller.getStartX(); int delta = destX - startX; //1000ms慢慢滑向destX,效果是慢慢滑动 mScroller.startScroll(startX,0,delta,1000); invalidate(); }@Override public void computeScroll() { if (mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }

先概况:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来完成View的滑动。
先看startScroller:
可以看到,startScroller只是存参数,并没有滑动逻辑
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; //用于计算时间间距 }

再看computeScrollOffset就明白了:
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); //改变mCurrX,配合computeScroll mCurrY = mStartY + Math.round(x * mDeltaY); break; } } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; }

3、View的事件分发机制
  • 一张图看懂三个方法的关系:

    《Android开发艺术探索》小结
    文章图片
    微信截图_20170906101528.png
  • 一个View的OnTouchListener比onTouchEvent高,当OnTouchListener的方法onTouch被调用,返回false,当前的View的onTouchEvent才会被调用,而OnClickListener优先级最低。
  • 一个事件序列只能被一个View拦截且消耗。
  • 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
  • 通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但ACTION_DOWN事件除外。
  • 源码分析
    首先,事件从Activity开始:
    public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) {//事件交给Window,这里是PhoneWindow return true; } return onTouchEvent(ev); //window返回false就掉自己的onTouchEvent }

    看看PhoneWindow:
    @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); //事件交给了DecorView }

    DecorView就是ViewGroup了,看ViewGroup的分发过程:
    @Override public boolean dispatchTouchEvent(MotionEvent ev) { ....if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); //重置FLAG_DISALLOW_INTERCEPT,所以requestDisallowInterceptTouchEvent在ACTION_DOWN无效 resetTouchState(); }// Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //mFirstTouchTarget 表示有没有消费的子元素 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) {//子元素有没请求不拦截 intercepted = onInterceptTouchEvent(ev); //证实onInterceptTouchEvent不会多次调用 ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } ....final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; }if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; }newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; }resetCancelNextUpFlag(child); //遍历判断看谁能够接收事件 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //mFirstTouchTarget在这里被附值 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } ....

    View事件处理:
    public boolean dispatchTouchEvent(MotionEvent event) { ...boolean result = false; ...if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; //mOnTouchListener 优先 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //onClickListener在onTouchEvent里面调用 if (!result && onTouchEvent(event)) { result = true; } }if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); }// Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); }return result; }

3、View的工作原理
1、ViewRoot和DecorView
  • ViewRoot对应于ViewRootImp类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象与DecorView建立联系。
  • View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个流程才能最终将一个View绘制出来。
【《Android开发艺术探索》小结】2、MeasureSpec
  • MeasureSpec代表一个32位int值,高2位代表测量模式,低30位代表某种测量模式下的测量大小。
  • 在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。
  • MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。
  • 对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定,对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。
  • 子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin及padding有关。
3、View的工作流程
  • View最终的大小是在layout阶段确定的。
  • onWindowFocusChanged
    View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽高没问题的。注意的是,这个方法会被调用多次,当Activity得到焦点和失去焦点时均会调用一次。
  • view.post(runnable)
    代替handler
  • draw过程
    绘制背景-->绘制自己-->绘制children-->绘制装饰
  • setWillNotDraw
    如果一个View不需要绘制任何内容,那么设置这个标记位true以后,系统会进行相应的优化。默认情况下,View不会启用这个优化标记位,ViewGroup会。
4、自定义View
  • 直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。另外,直接继承子ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。
  • View中有线程和动画要及时停止,参考onDetachedFromWindow。
4、RemoteViews 1、RemoteViews的应用
  • RemoteViews用于跨进程更新界面,用在通知栏和桌面小部件。
  • RemoteViews只支持FrameLayout、LinearLayout、RelativeLayout、GridLayout、AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub
2、桌面小部件
  • 先在res/xml新建:

  • 创建实现类
    public class MyAppWidgetProvider extends AppWidgetProvider {@Override public void onReceive(final Context context, Intent intent) { super.onReceive(context, intent); //广播内置方法,用于分发具体的事件给其他方法 }@Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); //小部件被添加时或者每次小部件更新时都会调用 }@Override public void onDisabled(Context context) { super.onDisabled(context); //当最后一个该类型的桌面小部件被删除时调用该方法 }@Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); //每次删除一次桌面小部件就调用一次 }@Override public void onEnabled(Context context) { super.onEnabled(context); //小部件第一次添加到桌面时调用,添加多吃只调用一次 } }

  • 声明

3、PendingIntent
  • 匹配规则
    PendingIntent:如果两个PendingIntent它们内部的Intent相同并且requestCode也相同,那么两个PendingIntent就相同。
    Intent:如果两个Intent的ComponentName和intent-filter都相同,那么两个Intent相同。
  • FLAG
    FlAG_ONE_SHOT:当前描述的PendingIntent只能被使用一次,然后它就会被自动cancel,如果后续还有相同的PendingIntent,那么它们的send方法就会调用失败。对于通知栏消息来说,如果采用这个标记,那么同类的通知只能使用一次,后续的通知单击后无法打开。
    FlAG_NO_CREATE:当前的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity、getService、getBroadcast方法会直接返回null,即获取PendingIntent失败。
    FLAG_CANCEL_CURRENT:当前描述的PendingIntent如果已经存在,那么它们都会被cancel,然后系统会创建一个新的PendingIntent。对应通知栏消息来说,那些被cancel的消息单击后将无法打开。
    FLAG_UPDATE_CURRENT:当前描述PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换成最新的。
4、RemoteViews的内部机制
  • 通知栏和桌面小部件分别由NotificationManager和AppWidgetManager管理,而NotificationManager和AppWidgetManager通过Binder分别和SystemServer进程中的NotificationManagerService和AppWidgetService进行通信。
  • 首先RemoteViews会通过Binder传递到SystemServer进程,这是因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews中包名等信息去得到该应用的资源,然后会通过LayoutInflater去加载RemoteViews中的布局文件。
  • 通过setOnClickPendingIntent用于给普通View设置点击事件,但是不能给集合中的View去设置,比如ListView;要给ListView的item添加单击事件,必须将setPendingIntentTemplate和setOnClickFillInIntent组合使用才可以。
  • RemoteViews的apply会加载布局被更新界面,reApple只会更新界面。
5、Android的Drawable 1、Drawable
  • Drawable的内部宽/高这个参数比较重要,通过getIntrinsicWidth和getIntrinsicHeight来获取,当并不是所有的Drawable都有内部宽/高,比如一个颜色所形成的Drawable并没有这样概念。注意,内部宽/高不等同于它的大小,实际大小用getBounds来获取。
6、Android动画深入分析 1、layoutAnimation
  • 为ViewGroup指定动画,它的子元素出场时都有这个效果

2、Fragment切换动画
  • 通过FragmentTransaction中的setCustomAnimations来添加切换动画
    3、属性动画
  • 可用于改变View的背景颜色
  • Object必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc的方法;Object的setAbc对属性abc所做的改变必须能够反应出来。当然,可以用一个类包装原始对象,间接提供get和set。
  • View动画有时完成后无法隐藏View,调用view.clearAnimation。
  • 使用动画的过程中,建议开启硬件加速,这样会提高动画的流畅性。
7、Android的消息机制 1、概述
  • Handler的运行需要底层的MessageQueue和Looper的支撑,MessageQueue是消息队列,以队列的形式对外提供插入和删除工作,但它是采用单链表的数据结构来存储消息列表,Looper会以无限循环的形式去查找是否有新消息,有就处理,没就一直等待。
  • Looper中有一个特殊的概念ThreadLocal,ThreadLocal并不是线程,它的作用是可以在每个线程中存储数据,且互不干扰,在Handle中用于提供每个线程的Looper。
  • 不允许在子线程中访问UI是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件不可预期的状态。
  • 系统为什么不对UI控件的访问加上锁机制:首先加上锁机制会让UI访问的逻辑变得复杂;其次会降低UI访问效率。
  • Handler创建时会采用当前线程的Looper来构建内部的消息循环系统,如果当前线程没有Looper,那么会报错。
2、ThreadLocal
  • ThreadLocal是一个线程内部的数据存储类,一般来说,当某些数据是以线程为作用域并且不同线程具体不同的数据副本的时候,可以考虑用ThreadLocal。
  • set
    public void set(T value) { Thread currentThread = Thread.currentThread(); //拿到当前线程 Values values = values(currentThread); //拿到当前线程的数据集 if (values == null) { values = initializeValues(currentThread); } values.put(this, value); }

  • get
    public T get() { // Optimized for the fast path. Thread currentThread = Thread.currentThread(); //拿到当前线程 Values values = values(currentThread); //拿到当前线程的数据集 if (values != null) { Object[] table = values.table; int index = hash & values.mask; if (this.reference == table[index]) { return (T) table[index + 1]; } } else { values = initializeValues(currentThread); }return (T) values.getAfterMiss(this); }

3、MessageQueue
  • MessageQueue主要包含两个操作,插入和读取,读取操作本身伴随着删除操作,插入和读取方法对应enqueueMessage和next。
4、Looper
  • 扮演消息循环的角色,不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则就一直阻塞在那里。
  • 构造
    private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); //关连MessageQueue mThread = Thread.currentThread(); }

  • 创建Looper
    new Thread(new Runnable() { @Override public void run() { Looper.prepare(); //里面用ThreadLocal,会有一步set Handler handler = new Handler(); //里面有一步ThreadLocal get Looper.loop(); } }).start();

  • Looper提供了quit和quitSafely来退出一个Looper,二者区别是:quit会直接退出Looper,quitSafely只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。Looper退出后,Handler的send会发送失败,返回false,在子线中,不需要的时候终止Looper。
  • Looper.loop
    public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; ........ for (; ; ) {//无限循环 Message msg = queue.next(); // might block if (msg == null) {//没消息了跳出死循环 // No message indicates that the message queue is quitting. return; } ........... msg.target.dispatchMessage(msg); //target就是handler ........... }

3、Handler
  • Handler发送消息过程仅仅是向消息队列中插入一条消息,MessageQueue的next方法就会返回这消息给Looper,Looper收到消息后就开始处理了,最终消息由Looper交给Handler处理。
  • 构造
    public Handler(Callback callback, boolean async) { ............. mLooper = Looper.myLooper(); //拿到Looper if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; //拿到Looper的mQueue mCallback = callback; mAsynchronous = async; }

8、Android的线程和线程池 1、AsyncTask
public final AsyncTask execute(Params... params) { return executeOnExecutor(sDefaultExecutor, params); } public final AsyncTask executeOnExecutor(Executor exec, Params... params) { if (mStatus != Status.PENDING) {//AsyncTask只能执行一次 switch (mStatus) { case RUNNING: throw new IllegalStateException("Cannot execute task:" + " the task is already running."); case FINISHED: throw new IllegalStateException("Cannot execute task:" + " the task has already been executed " + "(a task can be executed only once)"); } }mStatus = Status.RUNNING; onPreExecute(); //执行前调用,UI线程mWorker.mParams = params; //将参数封装成callable exec.execute(mFuture); //线程池执行FutureTask,FutureTask关连了上面的callablereturn this; } //就是exec private static class SerialExecutor implements Executor { final ArrayDeque mTasks = new ArrayDeque(); Runnable mActive; public synchronized void execute(final Runnable r) { mTasks.offer(new Runnable() {//offer,在此deque的末尾插入指定的元素。 public void run() { try { r.run(); //调用了FutureTask的run,FutureTask实现了Runnable和Future } finally { scheduleNext(); } } }); if (mActive == null) { scheduleNext(); } } //THREAD_POOL_EXECUTOR是正在执行任务的线程池 protected synchronized void scheduleNext() { if ((mActive = mTasks.poll()) != null) {//拿出一个任务赋值给mActive THREAD_POOL_EXECUTOR.execute(mActive); //真正执行,所以SerialExecutor 是排队用 } } } //FutureTask的run中调了它的call mWorker = new WorkerRunnable() { public Result call() throws Exception { mTaskInvoked.set(true); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); //noinspection unchecked return postResult(doInBackground(mParams)); //此时是线程中了,调doInBackground } }; //很明显通过Handler回到主线程 private Result postResult(Result result) { @SuppressWarnings("unchecked") Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult(this, result)); message.sendToTarget(); return result; } private void finish(Result result) { if (isCancelled()) { onCancelled(result); //Cancel的情况 } else { onPostExecute(result); //主线程回调 } mStatus = Status.FINISHED; }

2、HandlerThread
  • HandlerThread在内部创建了消息队列,外景需要通过Handler的消息方式来通知HandlerThread执行一个具体的任务。
    public void run() { mTid = Process.myTid(); Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; }

3、IntentService
  • IntentService封装了HandlerThread和Handler:
    @Override public void onCreate() {super.onCreate(); HandlerThread thread = new HandlerThread("IntentService[" + mName + "]"); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); } //通过Handler发送消息,就在HandlerThread处理了 @Override public void onStart(Intent intent, int startId) { Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent; mServiceHandler.sendMessage(msg); }private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { super(looper); }@Override public void handleMessage(Message msg) { onHandleIntent((Intent)msg.obj); //子线程 stopSelf(msg.arg1); //等所以消息处理完终止服务 } }

4、ThreadPoolExecutor
  • 如果线程池中的线程数量已经达到或者超过核心线程的数量,那么任务会被插入到任务队列中排队等待执行,如果队列满了,就会启动非核心线程来执行,当线程池线程的数量达到最大值,就拒绝任务。
  • FixedThreadPool
    通过Executors的newFixedThreadPool创建,它是一种固定的线程池,核心线程数和最大线程数一样,任务队列没有大小限制。
    public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }

  • CachedThreadPool
    通过Executors的newCachedThreadPool创建,它是一种线程数量不定的线程池,只有非核心线程数,并且最大为Interget.MAX_VALUE。
    public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }

  • ScheduledThreadPool
    通过Executors的newScheduledThreadPool创建,它的核心线程是固定的,而非核心线程的没有限制的。
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }

  • SingleThreadExecutor
    通过Executors的newSingleThreadExecutor创建,只有一个核心线程。
    public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())); }

9、Bitmap的加载和Cache 1、Bitmap的高效加载
  • 通过BitmapFactory.Options来缩放图片,主要是用到了它的inSampleSize参数,即采样率。
    ①当inSampleSize为1时,采样后的图片的原始大小;
    ②当inSampleSize大于1时,比如2,采样后的图片其宽/高均为原图大小的1/2,而像素数为原图的1/4,其占内存大小也为原图的1/4;
    ③inSampleSize必须是大于1的整数图片才会有缩小效果,并且采样率同时作用去宽高,这将导致缩放后图片大小以采样率的2次方形式递减,即缩放比例为1/(inSampleSize的2次方);
    ④当inSampleSize小于1时,其作用相当于1,即无缩放效果;
    ⑤inSampleSize的取值应该总是为2的指数,比如1、2、4、8、16,如果外界传递给系统的inSampleSize的取值不为2的指数,那么系统会向下取整并选择一个最接近的2的指数来替代,但这个结论不是所有Android版本上都成立,因此可以当做是一个建议。
    ⑥获取采样率流程:将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片;从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数;根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize;将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
    ⑦inJustDecodeBounds参数为true时,BitmapFactory只会解析图片的原始宽高信息,并不会去真正的加载图片。
2、LruCache
  • LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的的缓存对象。
    int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024); //最大内存量 int cacheSize = maxMemory/8; LruCache memoryCache = new LruCache(cacheSize){ @Override protected int sizeOf(String key, Bitmap value) {//计算缓存对象的大小 return value.getRowBytes() * value.getHeight()/1024; }@Override//LruCache移除旧缓存时调用,可以完成一些回收工作 protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) { super.entryRemoved(evicted, key, oldValue, newValue); } }; memoryCache.put(key,bitmap); memoryCache.get(key);

3、DisLruCache
  • DisLruCache用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存效果;DisLruCache不属于AndroidSDK的一部分。
    ①DisLruCache的创建
    private static final long DISK_CACHE_SIZE = 1024*1024*50; //50M /** * 参数一,缓存路径 * 参数二,应用版本号,当版本发生改变,会清空之前的缓存文件 * 参数三,表示单个节点所对应的数据个数 * 参数四,缓存大小 */ DiskLruCache diskLruCache = DiskLruCache.open(file, 1, 1, DISK_CACHE_SIZE);

    ②DisLruCache缓存的添加
    DiskLruCache.Editor edit = diskLruCache.edit(key); OutputStream outputStream = edit.newOutputStream(0); //写到文件上 edit.commit(); diskLruCache.flush();

    ③DisLruCache缓存的查找
    DiskLruCache.Snapshot snapshot = diskLruCache.get(key); FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(0);

10、综合技术 1、使用CrashHandler来获取应用的crash信息
  • 当用户发生了crash,开发者无法得知为何crash,Android提供了处理这类问题的方法,请看下面Thread类中的方法:
    * Set the default handler invoked when a thread abruptly terminates * due to an uncaught exception, and no other handler has been defined * for that thread. public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) { defaultUncaughtExceptionHandler = eh; }

  • 这个方法可以解决上面的crash问题,当crash发生的时候,系统就会回调UncaughtException的uncaughtException方法,在uncaughtException方法中就可以获取到异常信息,可以选择把异常信息存储到SD卡中,然后在合适的时机通过网络将crash信息上传到服务器上。
    下面是一个异常处理器实现:
    public class CrashHandler implements Thread.UncaughtExceptionHandler {private static CrashHandler sInstance = new CrashHandler(); private Thread.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(); }/** *当程序中有未捕获的异常,系统将会自动调用 */ @Override public void uncaughtException(Thread t, Throwable e) { //做存储到SD卡等操作 } }

  • 使用的时候在Application初始化就可以了:
    @Override public void onCreate() { super.onCreate(); CrashHandler.getInstance().init(this); }

2、使用multidex来解决65535
  • 在Android系统中,一个App的所有代码都在一个Dex文件里面,Dex是一个类似Jar的存储了Java编译字节码的归档文件,因为Android系统使用Dalvik虚拟机,所以需要把Java编译之后的class文件转换成dex文件,而当Android系统启动一个应用是时候,有一步是对Dex进行优化,这个过程有一个专门的工具处理,叫DexOpt,这个DexOpt工具会把每一个类的方法id给保存起来,存在一个链表结构里面,这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65535个。
  • 官方推荐的解决方法:
    导入multidex包,设置为支持多dex输出模式:在Build.gradle文件中添加
    android { compileSdkVersion 21 buildToolsVersion "21.1.0"defaultConfig { ... minSdkVersion 14 targetSdkVersion 21 ... // Enabling multidex support. multiDexEnabled true } ... } afterEvaluate { tasks.matching { it.name.startsWith('dex') }.each { dx -> if (dx.additionalParameters == null) { dx.additionalParameters = [] } dx.additionalParameters += '--multi-dex' // enable multidex // optional // dx.additionalParameters += "--main-dex-list=$projectDir/".toString() // enable the main-dex-list } } dependencies { compile 'com.android.support:multidex:1.0.0' }

  • 在Application初始化:
    import android.support.multidex.MultiDex; @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); }

11、性能优化 1、优化方法
  • 布局优化
    首先删除布局中无用的控件和层级,其次是有选择的使用性能较低的ViewGroup。
    ①如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更多的CPU时间,FrameLayout和LinearLayout都是一种简单高效的ViewGroup。
    ②如果单纯一个LinearLayout或者FrameLayout无法实现产品效果,需要通过嵌套的方式来完成,这种情况下建议采用RelativeLayout,因为ViewGroup的嵌套就相当于增加了布局的层级,同样会降低程序的性能。
    布局优化另外一种手段是采用标签、标签和ViewStub。
    标签:主要用于布局重用。
    注意:标签只支持以android:layout_开头的属性,其他属性是不支持的,当然android:id这个属性是个特例,如果指定了这个id属性,同时被包含的布局文件的根元素也指定了id属性,那么以指定id为准。还有就是,如果指定了android:layout_这种属性,那么要求android:layout_width和android:layout_height必须存在,否则其他android:layout_形式的属性无效。
    标签:一般和配合使用,它可降低减少布局的层级。
    ③ViewStub:按需加载布局,它非常轻量且宽、高都是0,因此它本身不参与任何的布局和绘制过程。以下是个例子:

    《Android开发艺术探索》小结
    文章图片
    ViewStub.png
    其中,android:inflatedId这个属性是需要按需加载的布局的根布局的id,需要加载的时候有两种方式进行:
    一种是:((ViewStub)findViewById(R.id.stub_import)).setVisibility(View.Visibility);
    另一种是:View importPanel = ((ViewStub)findViewById(R.id.stub_import)).inflate();
  • 绘制优化
    View的onDraw方法要避免执行大量操作,因为onDraw会被频繁的调用,一来onDraw不要创建新的局部对象,这不仅占用过多内存还会导致系统更加频繁gc,二来不要做耗时的任务,不然会造成View的绘制过程不流畅,View的绘制帧率保证60fps是最佳的。
  • 内存泄露优化
    ①静态变量导致的内存泄露:如果一个Activity被静态变量sContext引用了,那么它无法正常销毁。
    ②单例模式导致的内存泄露:例如一个单例模式的TestManager,可以接收外部的注册并将外部的监听器存储起来,当Activity实现了接口并注册之后,由于单例模式的生命周期和Application保持一致,因此Activity对象无法被及时释放。
    ③属性动画导致的内存泄露:有些时候需要有无限循环动画,如果在Activity中播放此类动画且没有在onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看到动画效果了,这个时候Activity的View被动画持有,而View又持有了Activity,最终Activity无法释放。
  • 一些性能优化建议
    ①常量请使用static final来修饰;
    ②尽量采用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄露。

    推荐阅读