Markdown版本笔记 | 我的GitHub首页 | 我的博客 | 我的微信 | 我的邮箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
Android 面试题 MD
Android系统架构图
https://github.com/BeesAndroid/BeesAndroid/raw/master/art/android_system_structure.png
从上到下依次分为四层:
可以通过bindService的方式,先在Activity里实现一个ServiceConnection
接口,并将该接口传递给bindService()
方法,在ServiceConnection接口的onServiceConnected()
方法里执行相关操作。
Service生命周期:
onCreate() -> onStartCommand() -> onDestory
onCreate() -> onBind() -> onUnBind() -> onDestory
onCreate() -> onStartCommnad() -> onBind() -> onUnBind() -> onDestory
Priority
属性值从大-小排序,Priority属性相同者,动态注册的广播优先,广播接收者还可以选择对广播进行截断和修改。常驻系统,不受组件生命周期影响
,即便应用退出,广播还是可以被接收,耗电、占内存。在组件结束前,需要先移除广播
,否则会造成内存泄漏。通过Binder机制向ActivityManagerService注册广播
。通过Binder机制向ActivityMangerService发送广播
。消息队列
中。onReceive()
方法。Intent
携带需要传递的数据的Binder
机制实现的1M
ContentProvider为这些数据的访问提供了统一的接口,可以用来做进程间数据共享
。URI
操作不同的ContentProvider中的数据,外部进程可以通过ContentResolver与ContentProvider进行交互。getActivity()空指针:这种情况一般发生在异步任务里调用getActivity(),而Fragment已经onDetach()
,此时就会有空指针。(我对此方案持保留意见)解决方案是在Fragment里使用一个全局变量mActivity,在onAttach()方法里赋值,这样可能会引起内存泄漏,但是异步任务没有停止的情况下本身就已经可能内存泄漏,相比直接crash,这种方式显得更妥当一些。
Fragment视图重叠:在类onCreate()的方法加载Fragment,并且没有判断saveInstanceState==null
或findFragmentByTag(mFragmentTag) == null
,导致重复加载了同一个Fragment导致重叠。(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠 ;
if(saveInstanceState == null){
// 或者 if(findFragmentByTag(mFragmentTag) == null)
// 正常情况下去 加载根Fragment
}
}
Intent传递数据大小的限制大概在1M
左右,超过这个限制就会静默崩溃。处理方式如下:
ContentProvider
进行进程间数据共享和传递。Android事件分发机制的本质:事件从哪个对象发出,经过哪些对象,最终由哪个对象处理了该事件。此处对象指的是Activity、Window与View。
Android事件的分发顺序:Activity(Window) -> ViewGroup -> View
Android事件的分发主要由三个方法来完成,如下所示:
// 父View调用dispatchTouchEvent()开始分发事件
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
// 父View决定是否拦截事件
if(onInterceptTouchEvent(event)){
// 父View调用onTouchEvent(event)消费事件,如果该方法返回true,表示
// 该View消费了该事件,后续该事件序列的事件(Down、Move、Up)将不会在传递给其他View。
consume = onTouchEvent(event);
}else{
// 调用子View的dispatchTouchEvent(event)方法继续分发事件
consume = child.dispatchTouchEvent(event);
}
return consume;
}
View的绘制流程主要分为三步:
测量视图的大小
,从顶层父View到子View递归调用measure()方法
,measure()调用onMeasure()方法,onMeasure()方法完成测量工作。确定视图的位置
,从顶层父View到子View递归
调用layout()方法,父View将上一步measure()方法得到的子View的布局大小和布局参数,将子View放在合适的位置上。Canvas
对象,然后调用onDraw()方法进行绘制。onDraw()方法的绘制流程为:① 绘制视图背景。② 绘制画布的图层。 ③ 绘制View内容。④ 绘制子视图,如果有的话。⑤ 还原图层。⑥ 绘制滚动条。递归
调用父窗口的requestLayout()方法,直到触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为true,会触发onMesaure()与onLayout()方法,不一定会触发onDraw()方法
。递归
调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为false,不会触发onMesaure()与onLayout()方法,但是会触发onDraw()方法
。一般说来需要重新布局
就调用requestLayout()方法,需要重新绘制
就调用invalidate()方法。
Android的包文件APK分为两个部分:代码和资源
,所以打包方面也分为资源打包和代码打包两个方面,这篇文章就来分析资源和代码的编译打包原理。
APK整体的的打包流程如下图所示:
https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/native/vm/apk_package_flow.png
具体说来:
AAPT
工具进行资源文件
(包括AndroidManifest.xml、布局文件、各种xml资源等)的打包,生成R.java
文件。AIDL
工具处理AIDL文件,生成相应的Java文件。Javac
工具编译项目源码,生成Class文件。DX
工具将所有的Class文件转换成DEX文件,该过程主要完成Java字节码转换成Dalvik字节码,压缩常量池以及清除冗余信息等工作。ApkBuilder
工具将资源文件、DEX文件打包生成APK文件。KeyStore
对生成的APK文件进行签名。ZipAlign
工具进行对齐处理,对齐的过程就是将APK文件中所有的资源文件距离文件的起始距离都偏移4字节的整数倍,这样通过内存映射访问APK文件的速度会更快。APK的安装流程如下所示:
/data/app
目录下,解压并扫描安装包。/data/data/
目录下创建对应的应用数据目录。对dex文件进行优化
,并保存在dalvik-cache目录下。PackageManagerService
中。点击应用图标后会去启动应用的LauncherActivity,如果LancerActivity所在的进程没有创建,还会创建新进程,整体的流程就是一个Activity的启动流程。
Activity的启动流程图(放大可查看)如下所示:
https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/activity_start_flow.png
整个流程涉及的主要角色有:
组件管理调度中心
,什么都不干,但是什么都管。任务栈
。注:这里单独提一下ActivityStackSupervisior,这是高版本才有的类,它用来管理多个ActivityStack,早期的版本只有一个ActivityStack对应着手机屏幕,后来高版本支持多屏以后,就有了多个ActivityStack,于是就引入了ActivityStackSupervisior用来管理多个ActivityStack。
整个流程主要涉及四个进程:
Launcher
应用进程。Zygote进程
,该进程主要用来fork新进程。有了以上的理解,整个流程可以概括如下:
Launcher
进程将启动Activity的请求以Binder
的方式发送给了AMS
。ActivityStarter
处理Intent和Flag等信息,然后再交给ActivityStackSupervisior/ActivityStack
处理Activity进栈相关流程。同时以Socket
方式请求Zygote
进程fork新进程
。ActivityThread
对象,新创建的进程就是应用的主线程,在主线程里开启Looper消息循环
,开始处理创建Activity。跨应用广播
,利用Binder
机制实现。应用内广播
,利用Handler
实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率比较高。Android消息循环流程图如下所示:
https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/native/process/android_handler_structure.png
主要涉及的角色如下所示:
消息分发
给相应的处理者。消息处理
器,主要向消息队列发送
各种消息以及处理
各种消息。整个消息的循环流程还是比较清晰的,具体说来:
target handler
来处理。handleMessage()
方法来处理Message。Android Binder是用来做进程通信的,Android的各个应用以及系统服务都运行在独立的进程中,它们的通信都依赖于Binder。
为什么选用Binder,在讨论这个问题之前,我们知道Android也是基于Linux内核,Linux现有的进程通信手段有以下几种:
既然有现有的IPC方式,为什么重新设计一套Binder机制呢。主要是出于以下三个方面的考量:
并发同步问题
,控制复杂,容易出现死锁和资源竞争,稳定性较差。而Binder基于C/S架构,客户端与服务端彼此独立,稳定性较好。Activity与Fragment生命周期如下所示:
https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/complete_android_fragment_lifecycle.png
读者可以从上图看出,Activity有很多种状态,状态之间的变化也比较复杂,在众多状态中,只有三种是常驻状态:
其他的状态都是中间状态。
我们再来看看生命周期变化时的整个调度流程,生命周期调度流程图如下所示:
https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/activity_lifecycle_structure.png
所以你可以看到,整个流程是这样的:
注:这里提到了主线程ActivityThread,更准确来说ActivityThread不是线程,因为它没有继承Thread类或者实现Runnable接口,它是运行在应用主线程里的对象,那么应用的主线程到底是什么呢?从本质上来讲启动启动时创建的进程就是主线程,线程和进程处理是否共享资源外,没有其他的区别,对于Linux来说,它们都只是一个struct结构体。
Context类图如下所示:
https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/context_uml.png
可以发现Context是个抽象类,它的具体实现类是ContextImpl
,ContextWrapper
是个包装类,内部的成员变量mBase
指向的也是个ContextImpl
对象,ContextImpl
完成了实际的功能,Activity、Service与Application都直接或者间接的继承ContextWrapper
。
一个安装的应用对应一个LoadedApk
(hide类)对象,对应一个Application对象,对于四大组件,Application的创建和获取方式也是不尽相同的,具体说来:
makeApplication()
方法创建。makeApplication()
方法创建。onReceive()
方法的第一个参数指向Application。常见的产生内存泄漏的情况如下所示:
静态的 Context
的引用,持有静态的View引用内部类&匿名内部类
实例无法释放(有延迟时间等等),而内部类又持有外部类的强引用,导致外部类无法释放,这种匿名内部类常见于监听器、Handler、Thread、TimerTask
finalize()
方法,finalize()方法执行不确定,可能会导致引用无法被释放。查找内存泄漏可以使用Android Profiler
工具或者利用LeakCanary
工具。
Android的进程主要分为以下几种:
前台进程
用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:
onResume()
方法)startForeground()
)onCreate()、onStart() 或 onDestroy()
) ServiceonReceive()
方法的 BroadcastReceiver通常,在任意给定时间,前台进程都为数不多
,只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。
可见进程
没有任何前台组件、但仍会影响用户在屏幕上所见内容
的进程。 如果一个进程满足以下任一条件,即视为可见进程:
可见进程被视为是极其重要的进程,除非为了维持所有前台进程
同时运行而必须终止,否则系统不会终止这些进程。
服务进程
正在运行已使用 startService()
方法启动的服务,且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程
同时运行,否则系统会让服务进程保持运行状态。
后台进程
包含目前对用户不可见的 Activity 的进程
(已调用 Activity 的 onStop()
方法)。这些进程对用户体验没有直接影响
,系统可能随时
终止它们,以回收内存供前台进程、可见进程或服务进程使用。
通常会有很多后台进程在运行,因此它们会保存在 LRU
列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复
其所有可见状态。
空进程
不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存
,以缩短下次在其中运行组件所需的启动时间。
为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
ActivityManagerService
负责根据各种策略算法计算进程的adj
值,然后交由系统内核进行进程的管理。
在Android中, SharePreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data/package_name/shared_prefs
目录下.
之所以说SharedPreference是一种轻量级的存储方式,是因为它在创建的时候会把整个文件全部加载进内存,如果SharedPreference文件比较大,会带来以下问题:
优化建议
MODE_MULTI_PROCESS
只是保证了在API 11以前的系统上,如果sp已经被读取进内存,再次获取这个SharedPreference的时候,如果有这个flag,会重新读一遍文件,仅此而已。数据库升级增加表和删除表都不涉及数据迁移,但是修改表涉及到对原有数据进行迁移。升级的方法:
重写
如果是跨版本数据库升级,可以由两种方式:
所谓序列化就是将对象变成二进制流
,便于存储和传输。
频繁的IO
操作,效率比较低,适合将对象存储到磁盘
上的情况。共享内存
中,其他对象可以从这块共享内存中读出字节流,并反序列化成对象。因此效率比较高,适合在对象间或者进程间
传递信息。Bitamp 占用内存大小 = 宽度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一个像素所占的内存
注:这里inDensity表示目标图片的dpi(放在哪个资源文件夹下),inTargetDensity表示目标屏幕的dpi,所以你可以发现inDensity和inTargetDensity会对Bitmap的宽高进行拉伸,进而改变Bitmap占用内存的大小。
在Bitmap里有两个获取内存占用大小的方法。
getByteCount()
:API12 加入,代表存储 Bitmap 的像素需要的最少内存。getAllocationByteCount()
:API19 加入,代表在内存中为 Bitmap 分配的内存大小,代替了 getByteCount() 方法。在不复用 Bitmap 时,getByteCount() 和 getAllocationByteCount 返回的结果是一样的。在通过复用 Bitmap 来解码图片时,那么 getByteCount() 表示新解码图片占用内存的大小,getAllocationByteCount() 表示被复用 Bitmap真实占用的内存大小(即 mBuffer 的长度)。
为了保证在加载Bitmap的时候不产生内存溢出,可以使用BitmapFactory进行图片压缩,主要有以下几个参数:
使用BitmapRegionDecoder
进行布局加载。
内存缓存基于LruCache
实现,磁盘缓存基于DiskLruCache
实现。这两个类都基于Lru
算法和LinkedHashMap
来实现。
LRU算法可以用一句话来描述:
LR2U是
Least Recently Used
的缩写,最近最久未使用算法,从它的名字就可以看出,它的核心原则是如果一个数据在最近一段时间没有使用到,那么它在将来被访问到的可能性也很小,则这类数据项会被优先淘汰掉。
LruCache的原理是利用LinkedHashMap
持有对象的强引用,按照Lru算法进行对象淘汰。具体说来假设我们从表尾访问数据,在表头删除数据
,当访问的数据项在链表中存在时,则将该数据项移动到表尾
,否则在表尾新建一个数据项
。当链表容量超过一定阈值,则移除表头的数据。
为什么会选择LinkedHashMap呢?
这跟LinkedHashMap的特性有关,LinkedHashMap的构造函数里有个布尔参数accessOrder
,当它为true时,LinkedHashMap会以访问顺序为序排列元素,否则以插入顺序为序排序元素。
DiskLruCache与LruCache原理相似,只是多了一个journal
文件来做磁盘文件的管理和延伸,如下所示:
libcore.io.DiskLruCache
1
1
1
DIRTY 1517126350519
CLEAN 1517126350519 5325928
REMOVE 1517126350519
注:这里的缓存目录是应用的缓存目录/data/data/pckagename/cache
,未root的手机可以通过以下命令进入到该目录中或者将该目录整体拷贝出来:
adb shell
run-as com.your.packagename
cp /data/data/com.your.packagename/
//将/data/data/pckagename目录拷贝出来
adb backup -noapk com.your.packagename
我们来分析下这个文件的内容:
第六行及后续行表示缓存操作记录,关于操作记录,我们需要了解以下三点:
已经安装到Android系统的APK文件
,即/data/app
目录,Android默认
的类加载器。任意目录下的dex、jar、apk、zip文件
。为什么WebView加载会慢呢?
这是因为在客户端中,加载H5页面之前,需要先初始化WebView
,在WebView完全初始化完成之前,后续的界面加载过程都是被阻塞的。
优化手段围绕着以下两个点进行:
因此常见的方法是:
离线包
。除此之外还有一些其他的优化手段:
独立开发,独立测试,再插入到主应用中
。依此来减少主应用的规模。利用PathClassLoader
和DexClassLoader
去加载与bug类同名的类,替换掉bug类
,进而达到修复bug的目的,原理是在app打包的时候阻止类打上CLASS_ISPREVERIFIED
标志,然后在热修复的时候动态改变BaseDexClassLoader
对象间接引用的dexElements
,替换掉旧的类。
目前热修复框架主要分为两大类:
IntentService
,执行完成任务后会自动停止。onTrimMemory()
方法,然后监听TRIM_MEMORY_UI_HIDDEN
这个级别,这个级别说明用户离开了页面,可以考虑释放内存和资源。SparseArray
代替HashMap,HashMap为每个键值都提供一个对象入口,使用SparseArray可以免去基本对象类型转换为引用数据类型的时间。System.arrayCopy()
方法,要比我们用for循环效率快9倍以上,因为系统API很多都是通过底层的汇编模式执行的,效率比较高。dexopt
将DEX文件优化成ODEX文件,dexopt使用LinearAlloc来存储应用信息,关于LinearAlloc缓冲区大小,不同的版本经历了4M/8M/16M的限制,超出缓冲区时就会抛出INSTALL_FAILED_DEXOPT错误。解决方案是Google的MultiDex方案,具体参见:配置方法数超过 64K 的应用。
提供一个Presenter将视图逻辑和业务逻辑相分离
,达到解耦的目的。数据与View的双向绑定
,这套框架最早使用的data-binding
将数据绑定到xml里,这么做在大规模应用的时候是不行的,不过数据绑定是一个很有用的概念,后续Google又推出了ViewModel
组件与LiveData
组件。ViewModel组件规范了ViewModel所处的地位、生命周期、生产方式以及一个Activity下多个Fragment共享 ViewModel 数据的问题。LiveData组件则提供了在Java层面View订阅ViewModel数据源的实现方案。原文:https://www.cnblogs.com/baiqiantao/p/10452032.html