JingBin's Home

《Android开发艺术探索》读书笔记

《Android开发艺术探索》 - 一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。

《Android开发艺术探索》从三个方面来组织内容。第一,介绍Android开发者不容易掌握的一些知识点;第二,结合Android源代码和应用层开发过程,融会贯通,介绍一些比较深入的知识点;第三,介绍一些核心技术和Android的性能优化思想。

《Android开发艺术探索》侧重于Android知识的体系化和系统工作机制的分析,通过《Android开发艺术探索》的学习可以极大地提高开发者的Android技术水平,从而更加高效地成为高级开发者。而对于高级开发者来说,仍然可以从《Android开发艺术探索》的知识体系中获益。

完善中,借鉴了他人的读书笔记。

Activity的生命周期和启动模式

用户正常使用情况下的生命周期 & 由于Activity被系统回收或者设备配置改变导致Activity被销毁重建情况下的生命周期。

Activity的生命周期全面分析

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

Activity生命周期.png

  • 1.Activity第一次启动:onCreate->onStart->onResume。
  • 2.Activity切换到后台( 用户打开新的Activity或者切换到桌面),onPause->onStop。
  • 3.Activity从后台到前台,重新可见,onRestart->onStart->onResume。
  • 4.用户退出Activity,onPause->onStop->onDestroy。
  • 5.onStart开始到onStop之前,Activity可见。onResume到onPause之前,Activity可以接受用户交互。
  • 6.在新Activity启动之前,栈顶的Activity需要先onPause后,新Activity才能启动。所以不能在onPause执行耗时操作。

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

系统配置变化导致Activity销毁重建

例如Activity处于竖屏状态,如果突然旋转屏幕,由于系统配置发生了改变,Activity就会被销毁并重新创建。

  • 在异常情况下系统会在onStop之前调用onSaveInstanceState来保存状态。Activity重新创建后,会在onStart之后调用onRestoreInstanceState来恢复之前保存的数据。
  • 保存数据的流程: Activity被意外终止,调用onSaveIntanceState保存数据-> Activity委托Window,Window委托它上面的顶级容器一个ViewGroup( 书上说很可能就是DecorView) 。然后顶层容器在通知所有子元素来保存数据。 每个View都有 onSaveInstanceState 和 onRestoreInstanceState 方法。查看TextView 源码可以发现保存了文本选中状态和文本内容。
  • 系统只在Activity异常终止的时候才会调用 onSaveInstanceState 和onRestoreInstanceState 方法。其他情况不会触发。
资源内存不足导致低优先级的Activity被回收
  • 1.前台- 可见非前台( 被对话框遮挡的Activity) -后台,这三种Activity优先级从高到低。
  • 2.android:configChanges=”orientation” 在manifest中指定 configChanges 在系统配置变化后不重新创建Activity,也不会执行onSaveInstanceState 和 onRestoreInstanceState 方法,而是调用 onConfigurationChnaged 方法。
  • 3.configChanges 一般常用三个选项:
    • locale 系统语言变化
    • keyborardHidden 键盘的可访问性发生了变化,比如用户调出了键盘
    • orientation 屏幕方向变化

Activity的启动模式

Activity的LaunchMode

Android使用栈来管理Activity。

standard
  • 每次启动都会重新创建一个实例,不管这个Activity在栈中是否已经存在。
  • 谁启动了这个Activity,那么Activity就运行在启动它的那个Activity所在的栈中。
  • 用Application去启动Activity时会报错,提示非Activity的Context没有所谓的任务栈。解决
    办法是为待启动Activity制定FLAG_ACTIVITY_NEW_TASH标志位,这样就会为它创建
    一个新的任务栈。
singleTop
  • 如果新Activity位于任务栈的栈顶,那么此Activity不会被重新创建,同时回调 onNewIntent 方法。
  • 如果新Activity已经存在但不是位于栈顶,那么新Activity仍然会被创建。
singleTask
  • 这是一种单实例模式
  • 只要Activity在栈中存在,那么多次启动这个Activity都不会重新创建实例,同时也会回调 onNewIntent 方法。
  • 同时会导致在Activity之上的栈内Activity出栈。
singleIntance
  • 具有singleTask模式的所有特性,同时具有此模式的Activity只能单独的位于一个任务栈中
TaskAffinity属性

TaskAffinity参数标识了一个Activity所需要的任务栈的名字。为字符串,且中间必须包含包名分隔符“.”。默认情况下,所有Activity所需的任务栈名字为应用包名。TashAffinity属性主要和singleTask启动模式或者 allowTaskReparenting 属性配对使用,其他情况下没有意义。 应用A启动了应用B的某个Activity后,如果Activity的allowTaskReparenting属性为true的话,那么当应用B被启动后,此Activity会直接从应用A的任务栈转移到应用B的任务栈中。 打个比方就是,应用A启动了应用B的ActivityX,然后按Home回到桌面,单击应用B的图标,这时并不会启动B的主Activity,而是重新显示已经被应用A启动的ActivityX。这是因为ActivityX的TaskAffinity值肯定不和应用A的任务栈相同( 因为包名不同) 。所以当应用B被启动以后,发现ActivityX原本所需的任务栈已经被创建了,所以把ActivityX从A的任务栈中转移过来了。

设置启动模式:

  • 1.manifest中 设置下的 android:launchMode 属性。
  • 2.启动Activity的 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 。
  • 3.两种同时存在时,以第二种为准。第一种方式无法直接为Activity添加FLAG_ACTIVITY_CLEAR_TOP标识,第二种方式无法指定singleInstance模式。
  • 4.可以通过命令行 adb shell dumpsys activity 命令查看栈中的Activity信息。

Activity的Flags

这些FLAG可以设定启动模式、可以影响Activity的运行状态。

  • FLAG_ACTIVITY_CLEAR_TOP 具有此标记位的Activity启动时,同一个任务栈中位于它上面的Activity都要出栈,一般和FLAG_ACTIVITY_NEW_TASK配合使用。效果和singleTask一样。
  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 如果设置,新的Activity不会在最近启动的Activity的列表(就是安卓手机里显示最近打开的Activity那个系统级的UI)中保存。

IntentFilter的匹配规则

启动Activity分为两种:

  • 1.显示调用 明确指定被启动对象的组件信息,包括包名和类名
  • 2.隐式调用 不需要明确指定组件信息,需要Intent能够匹配目标组件中的IntentFilter中所设置的过滤信息。
  • 3.IntentFilter中的过滤信息有action、category、data。
  • 4.只有一个Intent同时匹配action类别、category类别、data类别才能成功启动目标Activity。
  • 5.一个Activity可以有多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可成功启动对应的Activity。

action

  • 1.action是一个字符串。
  • 2.一个intent-filter可以有多个aciton,只要Intent中的action能够和任何一个action相同即可成功匹配。匹配是指与action的字符串完全一样。
  • 3.Intent中如果没有指定action,那么匹配失败。

category

  • category是一个字符串。
  • 2.Intent可以没有category,但是如果你一旦有category,不管有几个,每个都能够与
    intent-filter中的其中一个category相同。
  • 3.系统在startActivitystartActivityForResult的时候,会默认为Intent加上 android.intent.category.DEFAULT 这个category,所以为了我们的activity能够接收隐式调用,就必须在intent-filter中加上 android.intent.category.DEFAULT 这个category。

data

  • 1.data的匹配规则与action一样,如果intent-filter中定义了data,那么Intent中必须要定义可匹配的data。
  • 2.intent-filter中data的语法:

    <data android:scheme="string"
        android:host="string"
        android:port="string"
        android:path="string"
        android:pathPattern="string"
        android:pathPrefix="string"
        android:mimeType="string"/>
    
  • 3.Intent中的data有两部分组成:mimeType和URI。mimeType是指媒体类型,比如
    image/jpeg、audio/mpeg4-generic和video/等,可以表示图片、文本、视频等不同的媒
    体格式。

    • URI的结构:<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
      //实际例子
      content://com.example.project:200/folder/subfolder/etc
      http://www.baidu.com:80/search/info
      • scheme:URI的模式,比如http、file、content等,默认值是 file 。
      • host:URI的主机名
      • port:URI的端口号
      • path、pathPattern和pathPrefix:这三个参数描述路径信息。
        • path、pathPattern可以表示完整的路径信息,其中pathPattern可以包含通配符 * ,表示0个或者多个任意字符。
        • pathPrefix只表示路径的前缀信息。
    • Intent指定data时,必须调用 setDataAndType 方法, setData 和 setType 会清除另一方的值。

隐式调用需注意:

  • 1.当通过隐式调用启动Activity时,没找到对应的Activity系统就会抛出 android.content.ActivityNotFoundException 异常,所以需要判断是否有Activity能够匹配我们的隐式Intent。

    • i. 采用 PackageManager 的 resloveActivity 方法
      public abstract List queryIntentActivityies(Intent intent,int flags);
      public abstract ResolveInfo resloveActivity(Intent intent,int flags);
      以上的第二个参数使用 MATCH_DEFAULT_ONLY ,这个标志位的含义是仅仅匹配那些在intent-filter中声明了 android.intent.category.DEFAULT 这个category的Activity。因为如果把不含这个category的Activity匹配出来了,由于不含DEFAULT这个category的Activity是无法接受隐式Intent的从而导致startActivity失败。
    • ii. 采用 Intent 的 resloveActivity 方法
  • 2.下面的action和category用来表明这是一个入口Activity并且会出现在系统的应用列表中,二者缺一不可。


理解RemoteViews

RemoteViews提供了一组基础的操作,用于跨进程更新它的界面。RemoteViews在Andriod中的使用场景有两种:通知栏和桌面小部件(都运行在SystemServer进程)。

RemoteViews的应用

通知栏主要通过NotificationManager的notify方法来实现,除了默认效果外还可以自定义布局。
桌面小工具主要通过AppWidgetProvider来实现,AppWidgetProvider本质上是一个广播。
两者都会用到RemoteViews,两者都运行在其他进程中,准确的说是系统的SystemServer进程。

RemoteViews在通知栏上的应用

关于PendingIntent,它表示的是一种特定的Intent,这个Intent中所包含的意图必须由用户来触发。

RemoteViews在桌面小部件上的应用

AppWidgetProvider实现桌面小工具的类,本质是一个广播即BroadcastReceiver。
具体使用看系统自动生成的桌面小工具。
桌面小部件上不管是初始化界面还是后续的更新界面都必须使用RemoteViews来完成。

PendingIntent概述

PendingIntent和Intent的区别在于,PendingIntent是在将来的某个环节的不确定的时刻发生,而Intent是立刻发生。

RemoteViews的内部机制

  • 大部分的set方法的确是通过发射来完成的。
  • NotificationManager和AppWidgetProvider通过Binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetServer进行通信。
  • RemoteViews会通过Binder传递到SystemServer进程中,这是因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews中的包名等信息去得到该应用的资源。

RemoteViews的意义

模拟通知栏效果实现跨进程跨进程的UI更新。
我们可以像系统一样使用Binder来实现,但是为了简单起见就采用广播。
实际:

  • 现在用两个应用,一个应用需要能够更新另一个应用的某个界面,这个时候我们当然可以选择AIDL去实现(跨应用更新UI),但是如果对界面的更新比较频繁,这个时候就会有效率的问题,如果采用RemoteViews来实现就没有这个问题了。(RemoteViews只支持一些常用的View,对于自定义的View是不支持的。)

Android动画深入分析

Android动画分为三种:

  • 1.View动画(平移、缩放、旋转、透明度)
  • 2.帧动画(图片切换动画)
  • 3.属性动画(动态的改变对象的属性从而达到动画的效果)

View动画

View动画的作用对象是View,支持四种动画效果:

  • 1.平移
  • 2.缩放
  • 3.旋转
  • 4.透明

View动画的种类

上述四种变换效果对应着Animation四个子类: TranslateAnimation 、 ScaleAnimation 、 RotateAnimation 和 AlphaAnimation 。这四种动画皆可以通过XML定义,也可以通过代码来动态创建。

xml定义动画:

  • 1. 标签表示动画集合,对应AnimationSet类,可以包含一个或若干个动画,内部还可以嵌套其他动画集合。两个属性:

    • i. android:interpolator 表示动画集合所采用的插值器,插值器影响动画速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。
    • ii. android:shareInterpolator 表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或默认值。
  • 2.<translate><scale><rotate><alpha>这几个子标签分别代表四种变换效果。

  • 3.定义完View动画的xml后,通过以下代码应用动画:

    1
    2
    Aniamation anim = AnimationUtils.loadAnimation(context,R.anim.animation_test);
    view.startAnimation(anim);

代码动态创建动画:

1
2
3
AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(1500);
view.startAnimation(alphaAnimation);

自定义View动画

需要继承 Animation 这个抽象类,重写它的 initialize 和 applyTransformation 方法。在 initialize 方法中做一些初始化工作,在 applyTransformation 中进行相应的矩阵变换即可,很多时候需要采用 Camera 来简化矩阵变换的过程。自定义View动画的过程主要是矩阵变换的过程。

帧动画

帧动画是顺序播放一组预先定义好的图片,使用简单,但容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。

View动画的特殊使用场景

LayoutAnimation

作用于ViewGroup,为ViewGroup指定一个动画,当它的子元素出场时都会具有这种动画效果,一般用在ListView上。

Activity的切换效果

我们可以自定义Activity的切换效果,主要通过在 startActivity 或者 finish 的后面增加overridePendingTransition(int enterAnim , int exitAnim)方法

属性动画

API 11后加入,可以在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。因此与
View动画相比,属性动画几乎无所不能,只要对象有这个属性,它都能实现动画效果。API11
以下可以通过 nineoldandroids 库来兼容以前版本。

属性动画有以下三种使用方法:

  • ObjectAnimator:
    • ObjectAnimator.ofFloat(view,"translationY",values).start();
  • ValueAnimator

    • 1
      2
      3
      4
      5
      6
      ValueAnimator colorAnim = ObjectAnimator.ofInt(view,"backgroundColor",/*red*/0xffff8080,/*blue*/0xff8080ff);
      colorAnim.setDuration(2000);
      colorAnim.setEvaluator(new ArgbEvaluator());
      colorAnim.setRepeatCount(ValueAnimator.INFINITE);
      colorAnim.setRepeatMode(ValueAnimator.REVERSE);
      colorAnim.start();
  • AnimatorSet

    • 1
      2
      3
      AnimatorSet set = new AnimatorSet();
      set.playTogether(animator1,animator2,animator3);
      set.setDuration(3*1000).start();
    • 也可以通过在xml中定义在 res/animator/ 目录下。具体如下:

      • 1
        2
        3
        4
        5
        6
        7
        <?xml version="1.0" encoding="utf-8"?>
        <set xmlns:android="http://schemas.android.com/apk/res/android">
        <objectAnimator
        ....../>
        <animator
        ....../>
        </set>
      • 1
        2
        3
        4
        5
        AnimatorSet set = (AnimatorSet)AnimatorInflater.loadAnimator(context , R.animator.anim);
        set.setTarget(view);
        set.start();
        <set> 标签对应 AnimatorSet,<animator>对应ValueAnimator,
        而<objectAnimator>则对应 ObjectAnimator。

理解差值器和估值器

  • 时间插值器( TimeInterpolator) 的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator(线性插值器:匀速动画),AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快),DecelerateInterpolator(减速插值器:动画越来越慢)。

  • 估值器( TypeEvaluator) 的作用是根据当前属性改变的百分比来计算改变后的属性值。
    系统预置有IntEvaluator 、FloatEvaluator 、ArgbEvaluator。

  • 具体来说 对于一个作用在view上改变其宽度属性、持续40ms的属性动画来说,就是当时间t=20ms时,时间流逝了50%,那么view的宽度属性应该改变了多少呢?这个就由Interpolator和Evaluator的算法来决定。

属性动画的监听器

1
2
3
4
5
6
public static interface AnimatorListener {
void onAnimationStart(Animator animation); //动画开始
void onAnimationEnd(Animator animation); //动画结束
void onAnimationCancel(Animator animation); //动画取消
void onAnimationRepeat(Animator animation); //动画重复播放
}

为了方便开发,系统提供了AnimatorListenerAdapter类,它是AnimatorListener的适配器类,可以有选择的实现以上4个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Implementors of this interface can add themselves as update listeners
* to an <code>ValueAnimator</code> instance to receive callbacks on every animation
* frame, after the current frame's values have been calculated for that
* <code>ValueAnimator</code>.
*/
public static interface AnimatorUpdateListener {
/**
* <p>Notifies the occurrence of another frame of the animation.</p>
* *
@param animation The animation which was repeated.
*/
void onAnimationUpdate(ValueAnimator animation);
}

AnimatorUpdateListener会监听整个动画的过程,动画由许多帧组成的,每播放一帧,onAnimationUpdate就会调用一次。

对任意属性做动画

  • 1.属性动画要求作用的对象提供该属性的get和set方法,属性动画根据外界传递的该属性的初始值和最终值,通过多次调用set方法来实现动画效果。

  • 2.如果被作用的对象没有set/get方法,可以:

    • i.请给你的对象加上get和set方法,如果你有权限的话( 对于SDK或者其他第三方类库
      的类无法加上的)
    • ii.用一个类来包装原始对象,间接为其提供get和set方法
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //包装View类 用于给属性动画调用 从而包装了set get
      public class ViewWrapper {
      private View target;
      public ViewWrapper(View target) {
      this.target = target;
      }
      public int getWidth() {
      return target.getLayoutParams().width;
      }
      public void setWidth(int width) {
      target.getLayoutParams().width = width;
      target.requestLayout();
      }
      }
      //使用:
      ViewWrapper wrapper = new ViewWrapper(mButton);
      ObjectAnimator.ofInt(mButton,"width",500).setDuration(3000).start();
    • iii.采用ValueAnimator,监听动画过程,自己实现属性的改变;

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        private void performAnimate(final View target, final int start, final int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
        // 持有一个IntEvaluator对象,方便下面估值的时候使用
        private IntEvaluator mEvaluator = new IntEvaluator();
        @Override
        public void onAnimationUpdate(ValueAnimator animator) {
        // 获得当前动画的进度值,整型,1-100之间
        int currentValue = (Integer) animator.getAnimatedValue();
        Log.d(TAG, "current value: " + currentValue);
        // 获得当前进度占整个动画过程的比例,浮点型,0-1之间
        float fraction = animator.getAnimatedFraction();
        // 直接调用整型估值器通过比例计算出宽度,然后再设给Button
        target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
        target.requestLayout();
        }
        });
        valueAnimator.setDuration(5000).start();
        }

属性动画的工作原理

属性动画需要运行在有Looper的线程中,系统通过反射调用被作用对象get/set方法。

使用动画的注意事项

  • 1.使用帧动画时,当图片数量较多且图片分辨率较大的时候容易出现OOM,需注意,尽量
    避免使用帧动画。
  • 2.使用无限循环的属性动画时,在Activity退出时即使停止,否则将导致Activity无法释放从而造成内存泄露。
  • 3.动画在3.0以下的系统存在兼容性问题,特殊场景可能无法正常工作,需做好适配工作。
  • 4.View动画是对View的影像做动画,并不是真正的改变了View的状态,因此有时候会出现动画完成后View无法隐藏( setVisibility(View.GONE) 失效),这时候调用 view.clearAnimation() 清理View动画即可解决。
  • 5.不要使用px,使用px会导致不同设备上有不同的效果。
  • 6.View动画是对View的影像做动画,View的真实位置没有变动,动画完成后的新位置是无法触发点击事件的。属性动画是真实改变了View的属性,所以动画完成后的位置可以接受触摸事件。
  • 7.使用动画的过程中,使用硬件加速可以提高动画的流畅度。

相关资料