android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info">
注:第一个Action用于识别小部件的单击行为,第二个Action作为小部件的标识必须存在。
onDisabled:当最后一个该类型的桌面小部件被删除时调用此方法。
5.1.3 PendingIntent 概述
PendingIntent的匹配规则:如果两个PendingIntent内部的Intent相同并且requestCode也相同,那么这两个PendingIntent相同。
Intent的匹配规则:如果两个Intent的ComponentName和intent_filter相同,那么这两个Intent相同。
flags常见的类型有4种:
FLAG_ONE_SHOT
当前的PendingInent只能被使用一次,然后它就会被自动cancel。
FLAG_NO_CREATE
无法单独使用,不常见。
FLAG_CANCEL_CURRENT
如果当前描述的PendingIntent已经存在,那么它们都会被取消,然后就会创建一个新的PendingIntent。
FLAG_UPDATE_CURRENT
PendingIntent的flags和notify方法的id对通知间的影响:
如果id是常量,那么不管PendingIntent是否匹配,后面的通知将会直接替换掉前面的通知。
如果id每次都不同,且当PendingIntent不匹配时,不管采用何种标志位,通知间都会互不干扰。
当PendingIntent匹配时,如果采用了FLAG_ONE_SHOT标记位,后续的通知会和第一条通知保持一致,单击任何一条通知后,后续的通知均无法打开。
如果采用了FLAG_CANCEL_CURRENT标记位,那么只有最新的通知才可以打开,之前弹出的通知均无法打开。
如果采用了FLAG_UPDATE_CURRENT标记位,那么之前弹出的通知中的PendingIntent会被更新,最终它们会和最新的通知保持一致,并且这些通知都是可以打开的。
5.2 RemoteViews 的内部机制
RemoteViews无法使用自定义View,它的一系列set方法大部分是通过反射来完成的,其它set方法一般直接设置即可。
通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService和AppWidgetService中被加载的,而它们运行在系统的SystemServer中,因而构成了跨进程通信的场景。
RemoteViews的apply方法在内部调用了每个Action的apply方法,具体的View的更新操作是由Action的apply方法来完成的。
Action对象的apply方法就是真正操作View的地方。
apply会加载布局和更新界面,而reApply只会更新界面。
首先,setOnClickPending只适合给普通View设置单击事件,其次,如果要给ListView和StackView中的item添加点击事件,则必须将setPendingIntentTemplate和setOnClickFillInIntent组合使用才可以。
5.3 RemoteViews 的意义
一个应用需要能够更新另一个应用中的某个界面,如果界面中的View都是一些简单的且都被RemoteViews支持的View,那么可以使用RemoteViews。
如果A和B是两个不同的应用,那么B中的布局文件资源id传输到A中以后很有可能是无效的。既然资源id不相同,那我们就通过资源名称来加载布局文件。两个应用要约定好布局文件中的资源名称,比如“layout_simulated_notification"。
int layoutId = getResources().getIdentifier("layout_simulated_notification", "layout", getPackageName());
View view = getLayoutInflater().inflater(layoutId, mRemoteViewContent, false);
remoteViews.reApply(this, view);
mRemoteViewContent.addView(view);
第六章 Android 的 Drawable
6.1 Drawable 简介
优点:使用简单,比自定义View的成本要低;非图片类型的Drawable占用空间较小,这对减小apk的大小也很有帮助。
Drawable的内部宽/高这个参数比较重要,通过getIntrinsicWidth和getIntrinsicHeight这两个方法可以获取到它们。
一般来说,Drawable是没有大小概念的,当用作View的背景时,Drawable会被用作View的同等大小。
6.2 Drawable 的分类
6.2.1 BitmapDrawable
android:antialias
开启后会让图片变得平滑,同时也会在一定程度上降低图片的清晰度,但是降低的幅度可以忽略,因此应该开启。
android:dither
当图片的像素配置和手机屏幕的像素配置不一致时,开启这个选项可以让高质量的图片在低质量的屏幕上还能保持较好的显示效果。
android:filter
当图片的尺寸被拉伸或者压缩时,开启过滤效果可以保持较好的显示效果。
android:mipMap
这是一种图像相关的处理技术,也叫纹理映射。
android:tileMode
当开启平铺模式后,gravity属性会被忽略。
repeat表示的是简单的水平和竖直方向上的平铺效果;mirror表示一种在水平和竖直方向的镜面投影效果;而clamp表示的效果就更加奇特,图片四周的像素会扩展到周边区域。
.9图片可以自动地根据所需的宽/高进行相应的缩放并保证不失真。
在实际使用中发现在bitmap标签中也可以使用.9图,即BitmapDrawable也可以代表一个.9格式的图片。
6.2.2 ShapeDrawable
android:shape
line和ring需要通过标签来制定线的宽度和颜色等信息,否则无法达到预期的显示效果。
px表示。
solid表示纯色填充,而gradient表示渐变效果。
1.android:angle——渐变的角度,默认为0,其值必须为45的倍数,0表示从左到右,90表示从上到下。
2.android:useLevel——一般为false,当Drawable作为StateListDrawble使用时为true。
3.android:type——渐变类型,linear、radial(径向渐变)、sweep(扫面线渐变)。
4.android:size——标签设置的宽/高就是ShapeDrawable的固有宽/高,但是作为View背景时,Shape还会被View拉伸或者缩小到View的大小。
6.2.3 LayerDrawable
比较常用的属性有android:top、android:bottom、android:left和android:right,它们分别表示Drawable相对于View的上下左右的偏移量,单位为像素。
6.2.4 StateListDrawable
StateListDrawable对应于标签。
android:constantSize
True表示它的固有大小是不变的,它的固有大小是内部所有Drawable的固有大小的最大值,false则会随着状态的改变而改变。此选项默认为false。
android:variablePadding
ture表示会随着状态的改变而改变,false则表示StateListDrawable的padding是内部所有Drawable的padding的最大值。
注意:因为默认的View不附带任何状态,所以它可以匹配View的任何状态。
6.2.5 LevelListDrawable
LevelListDrawable对应于标签。
Drawable的等级是有范围的,即0~10000,最小的等级是0,这也是默认值。
6.2.6 TransitionDrawable
TransitionDrawable对应于标签,它用于实现两个Drawable之间的淡入淡出效果。
6.2.7 InsetDrawable
InsetDrawable对应于标签。当一个View希望自己的背景比实际区域小的时候,可以采用InsetDrawable来实现。
其中android:insetTop、android:insetBottom、android:insetRight、android:insetLeft分别表示顶部、底部、右边、左边内凹的大小。
6.2.8 ScaleDrawable
ScaleDrawable对应于标签。
android:scaleWidth和android:scaleHeight分别表示对指定Drawable宽和高的缩放比例。
等级0表示ScaleDrawable不可见,这是默认值。
如果ScaleDrawable的级别越大,那么内部的Drawble看起来越大;如果ScaleDrawable的XML中所定义的缩放比例越大,那么内部的Drawable看起来越小。
我们可以武断地把ScaleDrawable的等级设为20000,虽然也能正常工作,但是不推荐这么做,但是系统内部Drawable的等级范围为0到10000。
6.2.9 ClipDrawable
ClipDrawable对应于标签。裁剪方向由android:clipOrientation和android:gravity两个属性共同控制。
Drawable的等级是有范围的,即0~10000,最小的等级是0,即完全裁剪,最大是10000,即不裁剪。
6.3 自定义 Drawable
自定义的Drawable无法再XML中使用。另外,getIntrinsicWidth和getIntrinsicHeight这两个方法需要注意,当自定义的Drawable有固有大小时,最好重写一下这两个方法,因为它会影响到View的wrap_content布局。
需要注意的是,内部大小不等于Drawable实际区域的大小,Drawable的实际区域大小可以通过它的getBounds方法来获得,一般来说它和View的尺寸相同。
第七章 Android 动画深入分析
7.1 View 动画
7.1.1 View 动画的分类
对于View动画来说,建议采用XML来定义动画,这是因为XML动画的可读性更好。
7.1.2 自定义 View 动画
只需要基础Animation这个抽象类,然后重写它的initialize方法和applyTransformation方法,在initialize中做一些初始化工作,在applyTransformation中进行相应的矩阵变换即可,很多时候需要采用Camera来简化矩阵变换的过程。
7.1.3 帧动画
不同于View动画,系统提供了另一个类AnimationDrawable来使用帧动画。
7.2 View 动画的特殊使用场景
7.2.1 LayoutAnimation
LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时都会具有这种动画效果。
使用LayoutAnimation的步骤:
1. 定义LayoutAnimation
android:delay
如果时间周期为300ms,那么0.5就表示第一个动画需要延迟150ms才能进入动画,第二个动画需要延迟300ms才能进入动画。
2. 为子元素指定具体的入场动画
3. 为ViewGroup指定android:layoutAnimation属性。
除了在XML中使用LayoutAnimation属性外,还可以通过LayoutAnimationController来实现。
7.2.2 Activity 的切换效果
overridePendingTransition(int enterAnim, int exitAnim),必须在startActivity(Intent)或者finish()之后被调用后才能生效。
Fragment也可以添加切换动画,需要使用support—v4兼容包,通过FragmentTransaction中的setCustomAnimations()方法来添加切换动画。
7.3 属性动画
7.3.1 使用属性动画
Nineoldandroids对属性动画做了兼容,在API 11以前的版本其内部是通过代理View动画来实现的,因此在Android低版本上,它的本质还是View动画。
属性动画需要定义在res/animator/目录下。
android:startOffset——表示动画的延迟时间。
android:valueType——如果android:propertyName所指定的属性表示的是颜色,那么不需要指定android:valueType,系统会自动对颜色类型的属性做处理。
在实际开发中建议采用代码来实现属性动画,这是因为通过代码实现比较简单。
7.3.2 理解插值器和估值器
自定义插值器需要实现Interpolator或者TimeInterpolator,自定义估值算法需要实现TypeEvaluator。如果要对其它类型(非int、float、Color)做动画,那么必须要自定义类型估值算法。
7.3.3 属性动画的监听器
动画是由许多帧组成的,每播放一帧,onAnimationUpdate就会被调用一次。
7.3.4 对任意属性做动画
总结:我们队object的属性abc做动画,如果想让动画生效,要同时满足两个条件:
1.object必须要提高setAbc方法,如果动画的时候没有传递初值,那么还要提高getAbc方法,因为系统要去取abc属性的初始值(如果这条不满足,程序直接Crash)。
2.object的setAbc对属性abc所做的改变必须能够通过某种方法反映出来,比如会带来UI的改变之类的。
TextView的宽度对应XML中的android:layout_width属性,而TextView还有一个属性android:width,这个属性就对应了TextView的setWidth方法。
解决方法:
1. 给你的对象加上get和set方法,如果你有权限的话。
简单,往往不可行。
2. 用一个类来包装原始对象,间接为其提供get和set方法。
3. 采用ValueAnimator,监听动画过程,自己实现属性的改变。
7.3.5 属性动画的工作原理
AnimationHandler并不是Handler,它是一个Runnable。
calculateValue方法就是计算每帧动画所对应的属性的值。
PropertyValuesHolder的setupValue方法,其get方法是通过反射来调用的。
当动画的下一帧到来时,PropertyValuesHolder中的setAnimatedValue会将新的属性值设置给对象,调用其set方法。
7.4 动画使用的注意事项
1. OOM问题
2.内存泄漏
在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄漏,通过验证后发现View动画并不存在此问题。
3.兼容性问题54
4. View动画问题
当setVisibility(View.GONE)失效时,只要调用View.clearAnimation()清除View动画杰克解决此问题。
5. 不要使用px
要尽量使用dp,使用px会导致在不同的设备上有不同的效果。
6. 动画元素的交互
7. 硬件加速
建议开启,会提高动画的流畅性。
第八章 理解 Window 和 WindowManger
8.1 Window 和 WindowManager
Window常用的Flags如下:
1. FLAG_NOT_FOUSABLE
表示Window不需要获取焦点,也不需要接受各种输入事件。
2. FLAG_NOT_TOUCH_MODEL
系统会将Window以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理。
3. FLAG_SHOW_WHEN_LOCKED
Window有三种类型,分别是应用Window、子Window、系统Window。
在三类Window中,应用Window的层级范围是1~99,子Window的层级范围是1000~1999,系统Window的层级范围是2000~2999。
WindowManager提供了三种功能,addView、updateViewLayout、removeView。
WindowManger操作Window的过程更像是操作Window中的View。
8.2 Window 中的内部机制
Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。
8.2.1 Window 的添加过程
WindowManagerImpl这种工作模式是典型的桥接模式,它将工作全部委托给WindowManagerGlobal来实现。
WindowManagerGlobal的addView方法主要分为如下几步:
1. 检查参数是否合法,如果是子Window那么还需要调整一些布局参数。
2. 创建ViewRootImpl并将View添加到列表中。
在addView中通过如下方式将Window的一系列对象添加到列表中:
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
3. 通过ViewRootImpl来更新界面并完成Window的添加过程。
在setView内部会通过requestLayout来完成异步刷新请求。scheduleTraversals实际是View绘制的入口。
mWindowSession的类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,也就是Window的添加过程是一次IPC调用。
在Session内部会通过WindowManagerService来实现Window的添加。
8.2.2 Window 的删除过程
在WindowManager中提供了两种删除接口RemoveView和RmoveViewImmediate,它们分别表示异步删除和同步删除。具体的删除操作由ViewRootImpl的die方法来完成。
在dodie方法中会调用dispatchDetachedFromWindow,它主要做以下四件事:
1. 垃圾回收相关工作,比如清除数据和消息、移除回调。
2.通过Session的remove方法来删除Window:mWindowSession.remove(mWindow,这同样是一个IPC调用过程,最终会调用WindowManagerService的removeWindow方法。
3. 调用View的dispatchDetachedFromWindow方法,在内部会调用View的onDetachedFromWindow()和onDetachedFromWindowInternal()。可以再
onDetachFromWindow()方法中做一些资源回收工作,比如终止动画、停止线程等。
4. 调用WindowManagerGlobal的doRemoveView方法刷新刷新数据,需要将当前Window所关联的这三类对象mRoots、mParams、mDyingViews从列表中删除。
8.2.3 Window 的更新过程
8.3 Window 的创建过程
8.3.1 Activity 的创建过程
Activity的启动过程很复杂,最终会由ActivityThread中的PerformLaunchActivity()来完成整个启动过程。
Window对象的创建时通过PolicyManger的makeNewWidnow方法实现的。
Window的具体实现的确是PhoneWindow。
PhoneWindow的setContentView的步骤如下:
1. 如果没有DecorView,那么就创建它。
2. 将View添加到DecorView的mContentParent中。
因为Activity的布局文件只是被添加到DecorView的mContentParent中,因此叫它setContentView更准确。
3. 回调Activity中的mContentChanged方法通知Activity视图已经发生改变。
在ActivityThread的handleResumeActivity方法中,首先会调用Activity的onResume方法,接着会调用Activity的makeVisible(),在makeVisible方法中,DecorView真正完成了添加和显示这两个过程。
8.3.2 Dialog 的 Window 的创建过程
Dialog的Window的创建步骤如下:
1. 创建Window。
2. 初始化DecorView并将Dialog的视图添加到DecorView中。
3. 将DecorView添加到Window中并显示。
在Dialog的show方法中,会通过WindowManger将DecorView添加到Window中。
普通的Dialog有一个特殊之处,那就是必须采用Activity的Context,如果采用Application的Conte xt,那么就会报错。
只要指定对话框的Window为系统Window,那么就会正常弹出对话框。
8.3.3 Toast 的 Window 创建过程
在Toast的内部有两类IPC调用过程,第一类是Toast访问NotificationManagerService,第二类是NotificationManagerService回调Toast里的TN接口。
需要注意的是TN这个类,它是一个Binder类,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示和隐藏时会跨进程回调TN中的方法,这个时候由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程中。对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,这样做是为了防止DOS。
第九章 四大组件的工作过程
9.1 四大组件的运行状态
隐式Intent则指向一个或多个目标Activity组件,当然也可能没有任何一个Activity组件可以处理这个隐式Intent。
尽管Service是用于执行后台技计算的,但是它本身是运行咋主线程中的,因此耗时的后台计算仍然需要在单独的线程中去完成。
Service组件也是可以停止的,停止一个Service组件稍显复杂,需要灵活运用stopService和unBindService这两个方法才能完全停止一个Service组件。
静态注册是指在AndroidManifest中注册广播,这种广播在应用安装时会被系统解析,此种形式的广播不需要应用启动就可以收到相应的广播。
由于BroadcastReceiver的特性,它不适合用来执行耗时操作。BroadcastReceiver不需要停止,它也没有停止的概念。
需要注意的是,ContentProvider内部的query、delete、update、insert方法需要处理好线程同步,因为这几个方法是在Binder线程池中被调用的,
另外ContentProvider组件也不需要手动停止。
9.2 Activity 的工作过程
startActivity方法有好几种重载方式,但它们最终都会调用startActivityForResult方法。
ApplicationThread和ActivityThread在Activity的启动过程中发挥着很重要的作用。接着看一下Instrumentation的execStartActivity方法。
启动Activity真正的实现由ActivityManagerNative.getDefault()的startActivity方法来完成。
由于ActivityManagerNative.getDefault()其实是一个IActivityManager类型的Binder对象,因此它的具体实现是AMS。
在AMS的startActivity中,又调用了ActivityStackSupervisor的startActivityMayWait方法,又调用了startActivityUncheckedLocked方法,
又
调用了ActivityStack的resumeTopActivitiesLocked方法,又调用了resumeTopActivityInnerLocked,又调用了ActivityStackSupervisor
的startSpecificActivityLocked方法,又调用了realStartActivityLocked方法。
从接口方法的命名可以猜测,IApplicationThread这个Binder接口的实现者完成了大量和Activity以及Service启动/停止相关的功能。
IApplicationThread的实现者就是ActivityThread中的内部类ApplicationThread。
ApplicationThread通过scheduleLaunchActivity方法来启动Activity。
scheduleLaunchActivity的实现很简单,就是发送一个启动Activity的消息交由Handler(H)处理。
接下来Activity的启动过程由ActivityThread的handleLaunchActivity方法来实现。
然后在performLaunchActivity方法最终完成了Activity对象的创建和启动过程。
performLaunchActivity这个方法主要完成了如下几件事。
1. 从ActivityClientRecord中获取待启动的Activity的组件信息。
2. 通过Instrumentation的newActivity方法使用类加载器创建Activity对象。
3. 通过LoadedApk的makeApplication方法来尝试创建Application对象。
Application对象的创建也是通过Instrumentation来完成的,这个过程和Activity对象的创建一样,都是通过类加载器来实现的。
4. 创建ContextImp对象并通过Activity的attach方法来完成一些重要数据的初始化。
ContexImpl是通过Activity的attach方法来和Activity建立关联的,除此之外,在attach方法中Activity还会完成Window的创建并建立自己
和Window的关联,这样当Window接收到外部输入事件后就可以将事件传递给Activity。
5. 调用Activity的onCreate方法。
9.3 Service 的工作过程
Service分为两种工作状态,一种是启动状态,主要用于执行后台计算;另一种是绑定状态,主要用于其他组件和Service的交互。
Service既可以处于启动状态也可以同时处于绑定状态。
9.3.1 Service 的启动过程
Service的启动过程从ContextWrapper的startService开始。其中采用了桥接模式,在ContextImpl中,startService方法会调用startServiceCommon
方法,其中又会通过ActivityManagerNative.getDefault()这个对象来启动一个服务。
ActiveServices是一个辅助AMS进行Service管理的类,包括Service的启动、绑定和停止等。在ActiveServices的startServiceLocked方法的尾部会调用startServiceInnerLocked方法。
后续的工作由bringUpServiceLocked方法处理,又调用了realStartServiceLocked方法。首先通过app.thread的scheduleCreateService方法来创建Service对象并调用其onCreate,接着在通过sendServiceArgsLocked方法来调用Service的其它方法,比如onStartCommand。接下来只需要看ApplicationThread对Service启动过程的处理即可,即scheduleCreateService方法。
H会接收这个CREATE_SERVICE消息并通过ActivityThread的handleCreateService方法来完成Service的最终启动。
handleCreateService主要完成了如下几件事:
1. 通过类加载器创建Service的实例 。
2. 创建Application对象并调用其onCreate。
3. 接着创建ContextImpl对象并通过Service的attach方法建立二者之间的关系,这个过程和Activity的过程是类似的,毕竟Servcie和Activity都是一个Context。
最后调用Service的onCreate方法并将Service对象存储到ActivityThread中的一个列表中。
除此之外,ActivityThread中还会调用handleServiceArgs方法调用Service的onStartCommand方法。
9.3.2 Service 的绑定过程
bindServiceCommon方法主要完成如下两件事情:qqq
首先将客户端的ServiceConnection对象转化为ServiceDispatcher.InnerConnection对象。ServiceDispatcher的内部类InnerConnection刚好
充当了Binder这个角色。其实ServiceDispatcher起着连接ServiceConnection和InnerConnection的作用。
当Service和客户端连接以后,系统会通过InnerConnection来调用ServiceConnection中的onServiceConnected方法,这个过程有可能是跨进程的。
当ServiceDispatcher创建好了以后,getServiceDispatcher会返回保存的ServiceConnection对象。
接着bindServiceCommon方法会通过AMS来完成Service的具体的绑定过程。AMS会调用ActiveServices的bindServiceLocked方法,又调用了bringUpServiceLocked,又调用了realStartServiceLocked方法。和启动Service不同的是,Service的绑定过程会调用app.thread的scheduleBindService
方法,这个过程的实现在ActiveServices的requestServiceBindingLocked方法中。
在H内部,接收到BIND_SERVICE这类消息时,会交给ActivityThread的handleBindService方法来处理。首先会根据Servcie的token取出Service对象,
然后调用Service的onBind方法,Service的onBind方法会返回一个Binder对象给客户端使用。为了确定客户端知道已经成功连接Service了,所以还必须
调用客户端的ServiceConnection的onServiceConnected,这个过程是由ActivityManagerNative.getDefault()的publishService方法来完成的。
Service有一个特性,当多次绑定同一个Servcie时,Service的onBind方法只会执行一次,除非Service被终止了。当Service的onBind方法执行了以后,
系统还要告诉客户端已经成功连接Service了。根据上面的分析,这个过程由AMS的publishService方法来实现的。
核心代码为c.conn.connected(r.name, service)。
对于Service的绑定过程来说,ServiceDispatcher的mActivityThread就是ActivityThread中的H。
ServiceDispatcher内部保存了客户端的ServiceConnected对象,因此可以很方便地调用ServiceConnection对象的onServiceConnected方法。
9.4 BroadcastReceiver 的工作过程
9.4.1 广播的注册过程
ContextImpl的registerReceiver方法调用了自己的registerReceiverInternal方法。BroadcastReceiver作为Android的一个组件是不能直接跨进程传递的,所以需通过IIntentReceiver来中转一下。
9.4.2 广播的发送和接受过程
在Android 5.0中,默认情况下广播不会发送给已经停止的应用。因为系统在Android 3.1中为Intent添加了两个标记位,分别是FLAG_INCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES,用来控制广播是否要对处于停止状态的应用起作用。
Android 3.1中广播的这个特性同样会影响开机广播,从Android 3.1开始,处于停止状态的应用同样无法接受到开机广播。
在broadcastIntentLocked的内部,会根据intent-filter查出匹配的广播接受这并经过一系列的条件过滤,最终会将满足条件的广播接收者添加到
BroadcastQueue中,接着BroadcastQueue就会把广播发送给相应的广播接收者。
无序广播存储在mParallelBroadcasts中,系统会遍历mParallelBroadcast并将其中的广播发送给它们所有的接收者。deliverToRegisteredReceiverLocked
方法负责将一个广播发送给一个特定的接收者,它内部调用了performReceiveLocked方法来完成具体的发送过程。
ApplicationThread的scheduleRegisteredReceiver的实现比较简单,它通过InnerReceiver来实现广播的接收。
InnerReceiver的performReceive方法会调用LoadedApk.ReceivierDispatcher的performReceive方法,其中会创建一个Args对象并通过mActivityThread的post方法来执行Args中的逻辑,而Args实现了Runnable接口。在run方法中,onReceive方法被执行了。
9.5 ContentProvider 的工作过程
当ContentProvider所在的进程启动时,ContentProvider会同时启动并被发布到AMS中。需要注意的是,这个时候ContentProvider的onCreate要先于
Application的onCreate执行,这在四大组件中是一个少有的现象。
ApplicationThread是一个Binder对象,它的Binder接口是IApplicationThread,它主要用于ActivityThread和AMS之间的通信。
这四个方法都是通过Binder来调用的,外界无法直接访问ContentProvider,它只能通过AMS根据Uri来获取对应的ContentProvider的Binder接口的IContentProvider,然后通过IContentProvider来访问CntentProvider中的数据源。
ContentProvider可以避免进程间通信的开销,但是这在实际开发中仍然缺少使用价值。
ContentProvider被启动时会伴随着进程的启动,在AMS中,首先会启动ContentProvider所在的进程,然后在启动ContentProvider。
ActivityThread的handleBindApplication则完成了Application的创建以及ContentProvider的创建,分为如下四个步骤:
1. 创建ContentImpl和Instrumentation。
2. 创建Application对象。
3. 启动当前进程的ContentProvider并调用其onCreate方法。
在installProvider方法中通过类加载器完成了ContentProvider对象的创建。
4. 调用Application的onCreate方法。
需要注意的是,这里的ContentProvider的具体实现是ContentProviderNative和ContENTProvider.Transport,其中ContentProvider.Transport继承了ContentProviderNative。
第十章 Android 的消息机制
MessageQueue,虽然叫消息队列,但是它的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。
Looper中还有一个特殊的概念,那就是ThreadLocal,ThreadLocal并不是线程,它的作用是可以在每个线程中存储数据。
当然需要注意的是,线程是默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。
10.1 Andorid 的消息机制
Handler的主要作用是将一个任务切换到指定的线程中去执行。
Android规定访问UI只能在主线程中进行,如果是在子线程中访问UI,那么程序就会抛出异常。
系统为什么不允许在子线程中访问UI呢?这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:首先加上锁机制会让UI访问的逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
10.2 Android 的消息机制分析
10.2.1 ThreadLocal 的工作原理
一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以采用ThreadLocal。
其实获取的方法也是很简单的,在Thread类的内部有一个成员专门用于存储线程的ThreadLocal的数据:ThreadLocal.Values localValues,因此获取当前线程的ThreadLocal数据就变得异常简单了。如果LocalValues的值为null,那么就需要对其进行初始化,初始化在将ThreadLocal的值进行存储。
在localValues内部有一个数组:private Object[] table。
ThreadLocal的值在table数组中的存储位置总是为ThreadLocal的reference字段所标识的对象的下一个位置。
从ThreadLocal的get和set方法可以看出,它们所操作的对象都是当前线程的localValues对象的table数组,因此在不同线程中访问同一个ThreadLocal的set和get方法,它们对于ThreadLocal所做的读/写操作仅限于自己的线程内部,这就是为什么ThreadLocal可以在多个线程中互不干扰地存储和修改。
10.2.2 消息队列的工作原理
MessageQueue主要包含两个操作:插入和读取。读取操作本身会伴随着删除操作,插入和读取对应的方法分别为enqueueMessage和next,其中enqueueMessage的作用是向消息队列中插入一条消息,而next的作用是从消息队列中取出一条消息并将其从消息队列中移除。
可以发现next是一个无限循环的方法,如果消息队列中没有消息,那么next方法就会一直阻塞在这里。当有消息到来时,next方法会返回这条消息并将其从单链表中移除。
10.2.3 Looper 的工作原理
Looper除了prepare方法外,还提供了prepareMainLooper方法,这个方法主要是给主线程,也就是ActivityThread创建Looper使用的,其本质也是通过prepare方法来实现的。由于主线程的Looper比较特殊,所以Looper提供了一个getMainLooper方法,通过它可以在任何地方获取到主线程的Looper。
10.2.4 Handler 的工作原理
而Callback给我听了另外一种使用Handler的方式,当我们不想派生子类时,就可以通过Callback来实现。
10.3 主线程的消息循环
ActivityThread通过ApplicationThread和AMS进行进程间通信,AMS以进程间通信的方式完成ActivityThread的请求后会回调Application的Binder方法,然后Application会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到ActivityThread中去执行,即切换到主线程中query执行,这个过程就是主线程的消息循环模型。
第十一章 Android 的线程和线程池
对于AsyncTask来说,它的底层用到了线程池,对于IntentService和HandlerThread来说,它们的底层则直接使用了线程。但是它们的本质仍然是传统的线程。
从任务执行的角度来看,IntentService的作用很像一个后台线程,但是IntentService是一种服务,它不容易被系统杀死从而能够尽量保证任务的执行,而如果是一个后台线程,由于这个时候进程中没有活动的四大组件,那么这个进程的优先级就会非常低,会很容易被系统杀死,这就是IntentService的优点。
Android中的线程池来源于Java,主要是通过Executor来派生特定的线程池,不同种类的线程池又具有各自的特性。
11.1 主线程和子线程
子线程也叫工作线程,除了主线程以外的线程都是子线程。
11.2 Android 中的线程形态
11.2.1 AsyncTask
AsyncTask并不适合进行特别耗时的后台任务,对于特别耗时的后台任务来说,建议采用线程池。
尽管如此,在Android 3.0以及后续的版本中,我们仍然可以通过AsyncTask的executeOnExecutor方法来并行地执行任务。
11.2.2 AsyncTask 的工作原理
AsyncTask中有两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler),其中线程池SerialExecutor用于任务的排队,而线程池THREAD_POOL_EXECUTOR用于执行真正的任务,InternalHandler用于将执行环境从线程池切换到主线程。
从Android 3.0开始,默认情况下AsyncTask的确是串行执行的。
为了让AsyncTask可以在Android 3.0及以上的版本上并行,可以采用AsyncTask的executeOnExecutor方法,需要注意的是这个方法是Android 3.0新添加的方法,并不能在低版本下使用。
11.2.3 HandlerThread
由于HandlerThread的run方法是一个无限循环,因此当明确不需要再使用HandlerThread时,可以通过它的quit或者quitSafely方法来终止线程的执行,这是一个良好的编程习惯。
11.2.4 IntentService
IntentService是一种特殊的Servcie,他继承了Servcie并且它是一个抽象类,因此必须创建它的子类才能使用IntentService。
在实现上,IntentService封装了HandlerThread和Handler。
一般来说,stopSelf(int startId)在尝试停止服务之前会判断最近启动服务的次数是否和startId相同,如果相同就立即停止服务,不相同就不停止服务。
IntentService的onHandleIntent方法是一个抽象方法,它需要我们在子类中实现,它的作用是从Intent参数中区分具体的任务并执行这些任务。
这就意味着IntentService也是顺序执行后台任务的,当有多个后台任务同时存在时,这些后台任务会按照外界发起的顺序排队执行。
11.3 Android 中的线程池
11.3.1 ThreadPoolExecutor
下面是ThreadPoolLocal各个参数的含义:
corePoolSize
如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的线程在等待新任务到来时会有超时策略,这个时间间隔由keepAliveTime所指定,当等待时间超过keepAliveTiem所指定的时长后,核心线程就会被终止。
maximumPoolSize
线程池所能容纳的最大线程数。
keepAliveTime
非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。
unit
用于指定keepAliveTime参数的时间单位。
workQueue
线程池中的任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中。
threadFactory
threadFactory是一个接口,它只有一个方法new Thread(Runnable r)。
ThreadPoolExecutor为RejectedExecutionHandler提供了几个可选值:
CallerRunsPolicy、AbortPolicy、DiscardPolicy和DiscardOldestPolicy,其中AbortPolicy是默认值。
11.3.2 线程池的分类
1. FixedThredPool
FixedThreadPool中只有核心线程并且这些核心线程没有超时机制,另外任务队列也是没有大小限制的。
2. CachedThreadPool
它是一种线程数量不定的线程池,它只有非核心线程,并且其最大线程数为Integer.MAX_VALUE.由于Integer.MAX_VALUE可以是一个很大的数,实际上可以相当于它可以是任意大。
3. ScheduledThreadPool
它的核心线程数量是固定的,而非核心数量是没有限制的,并且当非核心限制时会立即被回收。
4. SingleThreadExecutor
这类线程池内部只有一个核心线程,它确保所欲的任务都在同一个线程中按顺序执行。
第十二章 Bitmap 的加载和 Cache
12.1 Bitmap 的高效加载
如何加载一个图片?
BitmapFactory类提供了四类方法:decodeFile、decodeStream、decodeResource、decodeByteArray,其中decodeFile和decodeResource又间接调用了decodeStream方法。
如何高效地加载Bitmap呢?
其实核心思想也很简单,那就是采用BitmapFactory.Options来加载所需尺寸的图片。
通过BitmapFactory.Options来缩放图片,主要用到了它的inSampleSize参数,即采样率。
有一种特殊情况,那就是当insampleSize小于1的时候,其作用相当于1,即无缩放效果。另外最新的官方文档指出,inSampleSize的取值应该总是为2的指数,当不为2的指数时,系统就会向下取整。
获取采样率也很简单,遵循如下流程:
1. 将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片。
2. 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
4. 将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
这里说明一下inJustDecodeBounds参数,当此参数设为true时,BitmapFactory只会解析图片的原始宽/高信息,并不会去真正加载图片,所以这个操作是轻量级的。
12.2 Android 中的缓存策略
如何定义缓存的新旧这是一个策略,不同的策略就对应着不同的缓存算法。
12.2.1 LruCache
为了能够兼容Android 2.2版本,在使用LruCache时建议采用support-v4兼容包中提供的LruCache,而不要直接使用Android 3.1提供的LruCache。
LruCache是一个泛型类,它内部采用了一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作。
一些特殊情况下,还需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用entryRemoved方法,因此可以在entryRemoved方法中完成一些资源回收工作(如果需要的话)。
12.2.2 DiskLruCache
它通过将缓存对象写入文件系统从而实现缓存的效果。
1. DisLruCache 的创建。
如果应用卸载后就希望删除缓存文件,那么就选择SD卡上的缓存目录,如果希望保持缓存数据就选择SD上的其它目录。
2. DisLruCache 的缓存添加。
DiskLruCache的缓存添加操作是通过Editor来完成的,Editor表示一个缓存对象的编辑对象。这里仍然以图片缓存举例,首先需要获取图片url所对应的key,
然后根据key就可以通过edit()来获取Editor对象,如果这个缓存正在被编辑,那么edit()会返回null,即DisLruCache不允许同时编辑一个缓存对象。
通过Editor就可以得到一个文件输出流。
当从网络上下载图片时,图片就可以通过这个文件输出流写入到文件系统上。
经过上面的步骤,其实并没有真正将图片写入文件系统,还必须通过Editor的commit()来提交写入操作,如果图片下载过程发生了异常,那么还可以通过Editor的abort()来回退整个操作。
3. DisLruCache 的缓存查找
和缓存的添加过程类似,缓存查找过程也需要将url转换为key,然后通过DiskLruCache的get方法得到一个SnapShot对象,接着再通过SnapShot对象即可得到缓存的文件输入流。
通过BitmapFactory.Options来加载一张压缩后的图片,但是那种方法对FileInputStream的缩放存在问题,原因是FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属性,这导致了第二次decodeStream得到的是null。为了解决这个问题,可以通过文件流来得到它所对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张压缩后的图片。
除此之外,DiskLruCache还提供了remove,delete等方法用于磁盘缓存的删除操作。
12.2.3 ImageLoader 的实现
一般来说,一个优秀的ImageLoader应该具备如下的功能:
1. 图片的同步加载。
2. 图片的异步加载。
3. 图片压缩。
4. 磁盘缓存。
5. 内存缓存。
6. 网路拉取。
ImageLoader的实现步骤:
1. 图片压缩功能的实现。
2. 内存缓存和磁盘缓存的实现。
在创建磁盘缓存是,这里做了一个判断,即有可能磁盘剩余空间小于磁盘缓存所需的大小,一般是指用户的手机空间已经不租了,因此没有办法创建磁盘缓存,这个时候磁盘缓存就会失效。
3. 同步加载和异步加载接口的设计。
通过检查当前线程的Looper是否是主线程的Looper来判断当前线程是否是主线程,如果是主线程则直接抛出异常终止程序。
如果直接采用普通的线程去直接加载图片,随着列表的滑动这有可能会产生大量的线程,这样并不利于整体效率的提升。
在这里选择了线程池和Handler来提供ImageLoader的并发能力和访问UI的能力。
为了解决View复用所导致的列表错位问题,在给ImageView设置图片之前会检查它的url有没有发生变化,如果变化了就不给它设置图片,这样就解决了列表错位问题。
12.3 ImageLoader 的使用
12.3.1 照片墙效果
12.3.2 优化列表的卡顿现象
首先,不要在getView中进行耗时操作。
其次,控制异步任务的执行频率。
如果用户刻意地频繁上下滑动,这就会在一瞬间产生上百个异步任务,如何解决这个问题呢?
可以考虑在列表滑动的时候停止加载图片,尽管这个过程是异步的,等列表停下来以后再加载仍然可以获得良好的用户体验。具体实现时,可以给ListView和GridView设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态,如果是则停止加载图片。
一般来说,经过上述两个步骤,列表都不会有卡顿现象,但是在某些特殊情况下,列表还是会有偶尔的卡顿现象,这个时候还可以开启硬件加速。通过设置
android:hardwareAccelerated=“true"即可为Activity开启硬件加速。
第十三章 综合技术
CrashHandler可以用来监视应用的crash信息,给程序设置一个CrashHandler,这样当程序crash时就会调用程序的unCaughtException方法了。
在Android中有一个限制,那就是应用的方法数不能超过65536,,否则就会出现编译错误,并且程序也无法成功地安装到手机上。
Google提供了multidex用来专门解决这个问题,通过将一个dex文件拆分为多个dex文件来避免单个dex文件方法数越界的问题。
方法数解决的另一种方式是动态加载。动态加载可以直接加载一个dex形式的文件,将部分代码单独打包到一个dex文件中(也可以是dex格式的jar或者apk),并在程序运行时根据需要去动态加载dex中的类,这种方式既可以解决方法数越界的问题,也可以给程序提供按需加载的特性,同时还为应用按模块更新提供了可能性。
13.1 使用 CrashHandler 来获取应用的 Crash 信息
首先需要实现一个UncaughtExceptionHandler对象,在它的uncaughtException方法中获取异常信息将其存储到SD卡上或者将其上传到服务器供开发人员来分析,然后调用Thread的setDefaultUncaughtExceptionHandler方法将它设置为线程默认的异常处理器,由于默认的异常处理器是Thread类的静态成员,
因此它的作用对象是当前进程中的所有线程。
如何使用上面的CrashHandler呢?
可以选择在Application初始化的时候为线程其设置CrashHandler。
13.2 只用 multidex 来解决方法数越界
dexpot是一个程序,应用在安装时,系统会通过dexpot来优化dex文件,在应用优化的过程中dexpot采用一个固定大小的缓冲区来存储应用中所有方法的信息,这个缓冲区就是LinearAlloc。LinearAlloc在新版本Android中的大小是8MB或者16MB。而在Android 2.2 和Android 2.3 中只有5MB,当待安装的应用中的方法数比较多时,尽管它的方法数还没有超过65536,但是它的存储信息仍然有可能超过5MB,这种情况下dexpot就有可能报错,从而导致安装失败,这种情况主要在Android 2.x 的手机上出现。
为了解决这个问题,Google在2014年提出了multidex的解决方案,通过multidex可以很好地解决方法数越界的问题,并且很容易使用。
在Android 5.0 之前使用multidex需要引入Google提供的android-support-multidex.jar这个jar包,这个jar包可以在Android SDK目录下的extras/android/support/multidex/library/lib下面找到。
在AndroidStudio和Gradle编译环境中,如果想要使用multidex,首先要使用Android SDK Build Tools 21.1 及以上版本,接着修改工程中app目录下的build.gradle文件,在defaultConfig中添加multidexEnabled true这个配置选项。
接着还需要在dependencies中添加multidex的依赖。
最后在代码中加上支持multidex的功能,有三种方案可以选择。
第一种方案,在manifest文件中指定Application为MutilDexApplication。
第二种方案,让应用的Application继承MultiDexApplication。
第三种方案,重写Application的attachBaseContext方法,这个方法比Application的onCreate要先执行。
在有些情况下,可能需要指定主dex文件中要包含那些类,这个时候可以通过multi-dex-list选项来实现这个功能。在bulid.gradle文件中添加afterEvaluate区域,在multi-dex-list区域内部用afterEvaluate指定了那些是主dex要包含的类。
注意maindexlist.txt这个文件名是可以修改的,但是它的内容必须遵守一定的格式。
需要注意的是,multidex中的jar包中的9个类也必须打包到主dex中,否则程序运行时会抛出异常,告知无法找到multide相关的类。
multidex可能带来的问题:
1. 应用启动速度会降低,要避免生成过大的dex文件。
2. 由于Dalvik linearAlloc的bug,可能会导致multidex的应用无法再Android 4.0 以前的手机上运行,因此需要做大量的兼容性测试。同时由于Dalvik linearAlloc的bug,有可能出现应用在运行中由于采用了multidex方案从而产生大量的内存消耗的情况,这会让应用崩溃。
在实际的项目中,1是客观存在的,2出现的可能性极少。
13.3 Android 的动态加载技术
不同的插件化方案各有各的特色,但是它们必须都要解决三个基础性的问题:资源访问,Activity生命周期的管理,ClassLoader的管理。
宿主是指普通的apk,而插件是指经过处理的dex后者apk。
1. 资源访问。
插件化的目的就是要减小宿主程序apk包的大小,同时降低宿主程序的更新频率并做到自由装载模块。
由于addAssetPath是隐藏API我们无法调用,所以只能通过反射。
传递的路径可以是一个zip文件也可以是一个资源目录,而apk是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了。然后再通过AssetManager来创建一个新的Resources对象,通过这个对象我们就可以访问插件apk中的资源了。
2. Activity 生命周期的管理。
管理Activity生命周期的方式各种各样,这里只介绍两种:反射方式和接口方式。
反射方式的缺点:
反射代码写起来比较复杂,过多使用反射会有一定的性能开销。
3. 插件 ClassLoader 的管理
使同一个插件可以采用同一个ClassLoader去加载类,通过将不同插件的ClassLoader存储在一个HashMap中,这样就可以保证不同插件中的类彼此可以互不干扰。
13.4 反编译初步
13.4.1 使用 dex2jar 和 jd-gui 反编译 apk
Dex2jar是一个将dex文件转换为jar包的工具,然后jd-gui将jar包进一步转换为java代码。
q
13.4.2 使用 apktool 对 apk 进行二次打包
作用:反编译出apk中的二进制的数据资源或者二次打包。
需要注意的是,由于Windows系统的兼容性问题,有时候会导致apktool.bat无法再Windows的一些版本上正常工作,比如Windows 8,这个时候可以安装Cygwin,然后采用Linux的方式解压即可。apktool在Linux上的打包成功率要比Windows高。
smali是dex文件反编译的结果(不同于dex2jar反编译的结果)。smali有自己的语法可以修改,然后可以被二次打包为apk。
需要注意的是,apk经过二次打包后并不能直接安装,必须经过签名后才能安装。
在实际开发中,很多产品都会做签名校验,简单的二次打包后得到的山寨版apk安装后无法运行。尽管如此,还是可以通过修改smali的方式来绕过签名校验,这就是市面中仍然还有这么多山寨版应用的原因。
第十四章 JNI 和 NDK 编程
Java JNI的本意是Java Native Interface(Java本地接口),它是为了Java方便调用C、C++等本地接口时所封装的一层接口。
NDK是Android所提供的一个工具集合,通过NDK可以在Android中更加方便地通过JNI来访问本地代码。
NDK还提供了交叉编译器,开发人员只需要简单地修改mk文件就可以生成特定平台的CPU动态库。使用NDK有如下好处:
1. 提高程序的安全性,由于so文件反编译比较困难,因此NDK文件提高了Android程序的安全性。
2. 可以很方便地使用目前已有的C/C++开源库。
3. 便于平台间的移植。
4. 提高程序在某些特定情形下的执行效率,但是不能明显提升Android程序的性能。
JNI和NDK主要用于底层开发,在Android的应用层开发比较少,本身更加侧重于C/C++方面的编程。
14.1 JNI 的开发流程
1. 在Java中声明native方法。
2. 编译Java文件得到class文件,然后通过javah命令导出JNI的头文件。
JNIEnv*:表示一个指向JNI环境的指针,可以用它来访问JNI提供的接口方法。
jobject:表示java对象中的this。
JNIEXPORT和JNICALL:它们是JN中定义的宏,可以再jni.h这个头文件中查找到。
3. 实现JNI的方法。
JNI方法是Java中声明的native方法,可以选择采用C++/C来实现。
4. 编译so库并在Java中调用。
libjni-test.so是生成的so库的名字,在Java中可以通过如下方式加载:System.LoadLibrary("jni-test"),其中so库的名字中的lib和.so是不需要指出的。
14.2 NDK 的开发流程
1. 下载并配置NDK。
2. 创建一个Android项目,并声明所需的native方法。
3. 实现Android项目中所声明的native方法。
Application.mk中常用的配置是APP_ABI,它表示CPU的架构平台的类型,目前市面上常见的架构平台有armeabi、x86和mips,其中在移动设备中占据主要地位的是armeabi,这也是大部分平台只包含armeabi类型的so库的原因。
4. 切换到jni的父目录,然后通过ndk-build命令编译产生so库。
需要注意的是,ndk-bulid默认指定jni目录为本地源码的目录,如果源码存放的目录名不是jni,那么ndk-bulid则无法成功编译。
14.3 JNI 的数据类型和类型签名
类的签名比较简单,它采用“L+包名+类名+;"的形式,只需要将其中得到.替换为/即可。比如java.lang.String,它的签名为Ljava/lang/String;,注意末尾的;也是签名的一部分。
对于对象来说,它的签名就是所属类的签名,比如String对象,就是Ljava/lang/String。
对于多维数组来说,它的签名就是n个[+类型签名,其中n表示数组的维度。
方法的签名为(参数类型签名)+返回值类型签名。
14.4 JNI 调用 Java 方法的流程
JNI调用Java方法的流程是先通过类名找到类,然后再根据方法名找到方法的id,最后就可以调用这个方法了。如果是调用Java中的非静态方法,那么需要构造出类的对象后才能调用它。
第十五章 Android 性能优化
过多地使用内存会导致程序内存溢出,即OOM,而过多地使用CPU资源,一般是指做大量的耗时任务,会导致手机变得卡顿甚至应用程序无法响应的情况,即ARN。
15.1 Android 的性能优化方法
15.1.1 布局优化
如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更多CPU的时间。
标签
标签只支持android:layout开头的属性,不如android:layout_width、android:layout_height,其它属性是不支持的,如果android:background。当然,android:id这个属性是个特例。
需要注意的是,如果标签指定了android:layout_*这种属性,那么哟啊球android:layout_widht、android:layout_height必须存在,否则其它android:layout_*形式的属性无法生效。
标签
如果当前布局的方向是一个竖直方向上的LinearLayout,并且它所包含的布局中的方向也是一个竖直方向上的LinearLayout,那么可以使用标签,这样就可以减少布局间的层级。
ViewStub
ViewStub的作用在于按需加载所需的布局。
注意:目前ViewStub暂时不支持标签。
15.1.2 绘制优化
首先,在onDraw中不要创建新的局部对象。
另一方面,onDraw方法中不要做耗时操作,也不能执行成千上万的循环操作。
View的绘制频率保持在60fps时是最佳的,这就要求每帧的绘制时间不超过16ms(16ms=1000/60)。
15.1.3 内存泄漏优化
内存泄漏的优点分为两个方面,一方面是在开发中避免写出有内存泄漏的代码,另一方面是通过一些分析工具比如MAT来找出潜在的内存泄漏继而解决。
场景1:静态变量导致的内存泄漏。
场景2:单例模式导致的内存泄漏。
单例模式的特点就是其生命浊气的Application的生命周期保持一致。
场景3:属性动画导致的内存泄漏。
解决方法是在Activity的onDestroy方法中通过animator.cancel()来停止动画。
15.1.4 响应速度优化和ANR日志分析
响应速度过慢更多地体现在Activity的启动速度上面,如果在主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至出现ANR。
当一个进程发生了ANR以后,系统会在data/anr目录下创建一个traces.txt,通过分析这个文件就能定位出ANR的原因。
15.1.5 ListView 和 Bitmap 的优化
注意ListView的优化策略完全适应于GridView。
15.1.6 线程优化
15.1.7 一些性能优化建议
1. 避免创建过多的对象。
2. 不要过多使用枚举,枚举占用的内存空间要比整形大。
3. 常量请使用static final来修饰。
4. 使用Android中特有的一些数组结构,比如SparseArray和Pair等,它们都具有更好的性能。
5. 适当使用软引用和弱引用。
6. 尽量地使用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄漏。
15.2 内存泄漏分析值 MAT 工具
通过Histogram可以很直观地看出内存中不同类型的buffer数量和占用的内存大小。而Dominator Tree则把内存中的对象按照从大到小的顺序进行排序,并且可以分析对象之间的引用关系,内存泄漏分析就是通过Dominator Tree来分析的。
15.3 提供程序的可维护性
1. 私有成员以m开头,静态成员以s开头,常量则全部用大写字母来表示。
2. 代码的排版上要留出合理的空白来区分不同的代码块,其中同类变量的声明要留在一组,两类变量之间要留出一行空白用来作区分。
3. 仅为非常关键的地方添加注释,其他地方不写注释。
4. 单一职责和层次性相关联的,代码分层后,每一层仅仅关注少量的逻辑,这样就做到了单一职责。