一次次的挂于一面让我筋疲力竭…意识到自己存在眼高手低的状态…准备重新上路夯实基础
本片文章部分 Fork from android-interview
并且在此基础上对一些问题进行了补充、深入剖析
手画一下Android系统架构图,描述一下各个层次的作用?
Android系统架构图
从上到下依次分为六层:
- 应用框架层
- 进程通信层
- 系统服务层
- Android运行时层
- 硬件抽象层
- Linux内核层
Activity、View、Window 三者关系
这个问题真的很不好回答,所以这里先来个算是比较恰当的比喻来形容下它们的关系。
Activity 像一个工匠(Control Unit),Window 像窗户(承载模型),View 像窗花(显示视图),LayoutInflater 像剪刀,Xml 配置像窗花图纸。
- Activity 构造的时候会初始化一个 Window,准确的说是 PhoneWindow。
- 这个 PhoneWindow 有一个“ViewRoot”,这个“ViewRoot”是一个 View 或者说 ViewGroup,是最初试的根视图
- “ViewRoot”通过 addView 方法来一个个的添加 View。比如 TextView,Button 等
- 这些 View 的事件监听,是由 WindowManagerService 来接受消息,并且回调 Activity 函数。比如 onClickListener,onKeyDown 等。
四种 LaunchMode 及其使用场景
standard 模式
这是默认模式,每次激活 Activity 时都会创建 Activity 实例,并放入任务栈中。
使用场景:大多数 Activity
singleTop 模式
如果在任务的栈顶正好存在该 Activity 的实例,就重用该实例(会调用实例的 onNewIntent()),否则就会创建新的实例并放入栈顶,即使栈中已经存在该 Activity 的实例,只要不在栈顶,就会创建新的实例。
使用场景如新闻类或者阅读类 App 的内容页面。
singleTask 模式
如果在栈中已经有该 Activity 的实例,就重用该实例(会调用实例的 onNewIntent() )。重用时,会让该实例回到栈顶,因此在它上面的实例将会被移出栈。如果栈中不存在该实例,将会创建新的实例放入栈中。
使用场景如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。
singleInstance 模式
在一个新栈中创建该 Activity 的实例,并让多个应用共享该栈中的该 Activity 实例。一旦该模式的 Activity 实例已经存在于某个栈中,任何应用再激活该 Activity 时都会重用该栈中的实例(会调用实例的 onNewIntent())。其效果相当于多个应用共享一个应用,不管谁激活该 Activity 都会进入同一个应用中。
使用场景:如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance 不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B(singleInstance) -> C,完全退出后,再次启动,首先打开的是 B。
Activity如与Service通信?
可以通过bindService的方式,先在Activity里实现一个ServiceConnection接口,并将该接口传递给bindService()方法,在ServiceConnection接口的onServiceConnected()方法
里执行相关操作。
注:只有activities,services,和contentproviders可以绑定到一个service—你不能从一个broadcastreceiver绑定到service.
Service的生命周期与启动方法有什么区别?
- startService():开启Service,调用者退出后Service仍然存在。
- bindService():开启Service,调用者退出后Service也随即退出。
Service生命周期:
- 只是用startService()启动服务:onCreate() -> onStartCommand() -> onDestory
- 只是用bindService()绑定服务:onCreate() -> onBind() -> onUnBind() -> onDestory
- 同时使用startService()启动服务与bindService()绑定服务:onCreate() -> onStartCommnad() -> onBind() -> onUnBind() -> onDestory
Service先start再bind如何关闭service,为什么bindService可以跟Activity生命周期联动?
通过IBinder
从你的客户端绑定到一个service,你必须:
1实现ServiceConnection.
你的实现必须重写两个回调方法:
onServiceConnected()
系统调用这个来传送在service的onBind()中返回的IBinder.
OnServiceDisconnected()
Android系统在同service的连接意外丢失时调用这个.比如当service崩溃了或被强杀了.当客户端解除绑定时,这个方法不会被调用.
2调用bindService(),传给它ServiceConnection的实现.
3当系统调用你的onServiceConnected()方法时,你就可以使用接口定义的方法们开始调用service了.
4要与service断开连接,调用unbindService().
当你的客户端被销毁,它将从service解除绑定,但是你必须总是在你完成与service的交互时或当你的activity暂停于是service在不被使用时可以关闭此两种情况下解
除绑定.(下面会讨论更多在适当的时候绑定和解除绑定的问题.
使用这个ServiceConnection,客户端可以绑定到一个service,通过把它传给bindService().例如:
12Intent intent = new Intent(this, LocalService.class);bindService(intent,mConnection, Context.BIND_AUTO_CREATE);第一个bindService()的参数是一个明确指定了要绑定的service的Intent.
第二个参数是ServiceConnection对象.
第三个参数是一个标志,它表明绑定中的操作.它一般应是BIND_AUTO_CREATE,这样就会在service不存在时创建一个.其它可选的值是BIND_DEBUG_UNBIND和BIND_NOT_FOREGROUND,不想指定时设为0即可。
补充事项
下面是一些关于绑定到service的重要事项:
你总是需要捕获DeadObjectException异常.它会在连接被打断时抛出.这是被远程方法抛出的唯一异常.
对象引用计数是跨进程的作用的.
你应该在客户端的生命期内使绑定和解除绑定配对进行,例如:
如果你需要在你的activity可见时与service交互,你应该在onStart()绑定并在onStop()中解除绑定.
如果你想让你的activity即使在它停止时也能接收回应,那么你可以在onCreate()中绑定并在onDestroy()中解除绑定.注意这意味着你的activity需要使用在自己整
个运行期间使用service(即使位于后台),所以如果service在另一个进程中,那么你增加了这个进程的负担而使它变得更容易被系统杀掉.
注:你一般不应该在你的activity的onResume()和onPause()中绑定和解除绑定到service,因为这些回调方法,出现在每个生命期变化中,并且你需要使发生在这
些变化中的处理最小化.还有,如果你应用中的多个activity绑定到同一个service,并且有一个变化发生在其中两个activity之间,service可能在当前activity解除绑
定(pause中)和下一个绑定前(rusume中)被销毁又重建.
bindService和startService混合使用时
如果先bindService,再startService:
在bind的Activity退出的时候,Service会执行unBind方法而不执行onDestory方法,因为有startService方法调用过,所以Activity与Service解除绑定后会有一个与调用者没有关连的Service存在如果先bindService,再startService,再调用Context.stopService
Service的onDestory方法不会立刻执行,因为有一个与Service绑定的Activity,但是在Activity退出的时候,会执行onDestory,如果要立刻执行stopService,就得先解除绑定
把上面的”如果先bindService,再startService”换成”如果先startService,再bindService”,结果是一样的
- 问题:
如果在一个Activity的onCreate方法中,
先bindService(serviceIntent,conn,Context.BIND_AUTO_CREATE);
再startService(serviceIntent);
退出这个Activity时,会执行onUnBind
但是再次进入这个Activity的时候,为什么不执行onBind方法了?
只有在这个Service销毁后(执行onDestory),再进这个Activity才会执行onBind,还有就是当有两个客户端时,在第一个客户端startServie启动服务再bindService绑定服务,这时跳到第二个客户端里(启动时会调用onBind()),再客户端startServie启动服务再bindService绑定服务,启动时不会调用用onBind()了(因为之前客户端已经启动后没有onDestory()销毁Service,所以再客户端第二次绑定服务时,只会返回IBinder对象给onServiceConnected()),而且要注意的是当,当第一个服务启动并绑定一个服务时,再跳去第二个服务端启动并绑定这个服务时,第二个服务端再解绑时,不会调用onUnbind(),只有回到第一个客户端时,解绑这是才会调用onUnbind(),顺序反过来结果是一样的。得出一个结论是:当一个服务没被onDestory()销毁之前,只有第一个启动它的客户端能调用它的onBind()和onUnbind()。
广播分为哪几种,应用场景是什么?
- 普通广播:调用sendBroadcast()发送,最常用的广播。
- 有序广播:调用sendOrderedBroadcast(),发出去的广播会被广播接受者按照顺序接收,广播接收者按照Priority属性值从大-小排序,Priority属性相同者,动态注册的广播优先,广播接收者还可以选择对广播进行截断和修改。
广播的两种注册方式有什么区别?
- 静态注册:常驻系统,不受组件生命周期影响,即便应用退出,广播还是可以被接收,耗电、占内存。
- 动态注册:非常驻,跟随组件的生命变化,组件结束,广播结束。在组件结束前,需要先移除广播,否则容易造成内存泄漏。
广播发送和接收的原理了解吗?
- 继承BroadcastReceiver,重写onReceive()方法。
- 通过Binder机制向ActivityManagerService注册广播。
- 通过Binder机制向ActivityMangerService发送广播。
- ActivityManagerService查找符合相应条件的广播(IntentFilter/Permission)的BroadcastReceiver,将广播发送到BroadcastReceiver所在的消息队列中。
- BroadcastReceiver所在消息队列拿到此广播后,回调它的onReceive()方法。
广播传输的数据是否有限制,是多少,为什么要限制?
ContentProvider、ContentResolver与ContentObserver之间的关系是什么?
- ContentProvider:管理数据,提供数据的增删改查操作,数据源可以是数据库、文件、XML、网络等,ContentProvider为这些数据的访问提供了统一的接口,可以用来做进程间数据共享。
- ContentResolver:ContentResolver可以不同URI操作不同的ContentProvider中的数据,外部进程可以通过ContentResolver与ContentProvider进行交互。
- ContentObserver:观察ContentProvider中的数据变化,并将变化通知给外界。
遇到过哪些关于Fragment的问题,如何处理的?
getActivity()空指针:这种情况一般发生在在异步任务里调用getActivity(),而Fragment已经onDetach(),此时就会有空指针,解决方案是在Fragment里使用
一个全局变量mActivity,在onAttach()方法里赋值,这样可能会引起内存泄漏,但是异步任务没有停止的情况下本身就已经可能内存泄漏,相比直接crash,这种方式
显得更妥当一些。Fragment视图重叠:在类onCreate()的方法加载Fragment,并且没有判断saveInstanceState==null或if(findFragmentByTag(mFragmentTag) == null),导致重复加载了同一个Fragment导致重叠。(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)
|
|
Android里的Intent传递的数据有大小限制吗,如何解决?
Intent传递数据大小的限制大概在1M左右,超过这个限制就会静默崩溃。处理方式如下:
- 进程内:EventBus,文件缓存、磁盘缓存。
- 进程间:通过ContentProvider进行款进程数据共享和传递。
描述一下Android的事件分发机制?
Android事件分发机制的本质:事件从哪个对象发出,经过哪些对象,最终由哪个对象处理了该事件。此处对象指的是Activity、Window与View。
Android事件的分发顺序:Activity(Window) -> ViewGroup -> View
Android事件的分发主要由三个方法来完成,如下所示:
|
|
- Touch 事件分发中只有两个主角:ViewGroup 和 View。ViewGroup 包含 onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent 三个相关事件。View 包含 dispatchTouchEvent、onTouchEvent 两个相关事件。其中 ViewGroup 又继承于 View。
- ViewGroup 和 View 组成了一个树状结构,根节点为 Activity 内部包含的一个 ViewGroup。
- 触摸事件由 Action Down、Action Move、Action Up 组成,其中一次完整的触摸事件中,Down 和 Up 都只有一个,Move 有若干个,可以为 0 个。
- 当 Activity 接收到 Touch 事件时,将遍历子 View 进行 Down 事件的分发。ViewGroup 的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的 View,这个 View 会在 onTouchEvent 结果返回 true。
- 当某个子 View 返回 true 时,会终止 Down 事件的分发,同时在 ViewGroup 中记录该子 View。接下去的 Move 和 Up 事件将由该子 View 直接进行处理。由于子 View 是保存在 ViewGroup 中的,多层 ViewGroup 的节点结构时,上级 ViewGroup 保存的会是真实处理事件的 View 所在的 ViewGroup 对象:如 ViewGroup0-ViewGroup1-TextView 的结构中,TextView 返回了 true,它将被保存在 ViewGroup1 中,而 ViewGroup1 也会返回 true,被保存在 ViewGroup0 中。当 Move 和 Up 事件来时,会先从 ViewGroup0 传递至 ViewGroup1,再由 ViewGroup1 传递至 TextView。
- 当 ViewGroup 中所有子 View 都不捕获 Down 事件时,将触发 ViewGroup 自身的 onTouch 事件。触发的方式是调用 super.dispatchTouchEvent 函数,即父类 View 的 dispatchTouchEvent 方法。在所有子 View 都不处理的情况下,触发 Activity 的 onTouchEvent 方法。
- onInterceptTouchEvent 有两个作用:1.拦截 Down 事件的分发。2.终止 Up 和 Move 事件向目标 View 传递,使得目标 View 所在的 ViewGroup 捕获 Up 和 Move 事件。
描述一下View的绘制原理?
View的绘制流程主要分为三步:
- onMeasure:测量视图的大小,从顶层父View到子View递归调用measure()方法,measure()调用onMeasure()方法,onMeasure()方法完成绘制工作。
- onLayout:确定视图的位置,从顶层父View到子View递归调用layout()方法,父View将上一步measure()方法得到的子View的布局大小和布局参数,将子View放在合适的位置上。
- onDraw:绘制最终的视图,首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。onDraw()方法的绘制流程为:① 绘制视图背景。② 绘制画布的图层。 ③ 绘制View内容。④ 绘制子视图,如果有的话。⑤ 还原图层。⑥ 绘制滚动条。
requestLayout()、invalidate()与postInvalidate()有什么区别?
- requestLayout():该方法会递归调用父窗口的requestLayout()方法,直到触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为true,会触发onMesaure()与onLayout()方法,不一定会触发onDraw()方法。
- invalidate():该方法递归调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为false,不会触发onMesaure()与onLayout()方法,当时会触发onDraw()方法。
- postInvalidate():该方法功能和invalidate()一样,只是它可以在非UI线程中调用。
一般说来需要重新布局就调用requestLayout()方法,需要重新绘制就调用invalidate()方法。
Scroller用过吗,了解它的原理吗?
|
|
libcore.io.DiskLruCache
1
1
1
DIRTY 1517126350519
CLEAN 1517126350519 5325928
REMOVE 1517126350519
我们来分析下这个文件的内容:
- 第一行:libcore.io.DiskLruCache,固定字符串。
- 第二行:1,DiskLruCache源码版本号。
- 第三行:1,App的版本号,通过open()方法传入进去的。
- 第四行:1,每个key对应几个文件,一般为1.
- 第五行:空行
- 第六行及后续行:缓存操作记录。
第六行及后续行表示缓存操作记录,关于操作记录,我们需要了解以下三点:
- DIRTY 表示一个entry正在被写入。写入分两种情况,如果成功会紧接着写入一行CLEAN的记录;如果失败,会增加一行REMOVE记录。注意单独只有DIRTY状态的记录是非法的。
- 当手动调用remove(key)方法的时候也会写入一条REMOVE记录。
- READ就是说明有一次读取的记录。
- CLEAN的后面还记录了文件的长度,注意可能会一个key对应多个文件,那么就会有多个数字。
Android 中跨进程通讯的几种方式
Android 跨进程通信,像 Intent,contentProvider,broadcast,service 都可以跨进程通信。
Android 中的几种动画
- 帧动画:指通过指定每一帧的图片和播放时间,有序的进行播放而形成动画效果,比如音乐的律动条
- 补间动画:指通过指定 View 的初始状态、变化时间、方式,通过一系列的算法去进行图形变换,从而形成动画效果,主要有 Alpha、Scale、Translate、Rotate 四种效果。注意:只是在视图层实现了动画效果,并没有真正改变 View 的属性,比如滑动列表,改变标题栏的透明度。
- 属性动画:在 Android3.0 的时候才支持,通过不断的改变 View 的属性,不断的重绘而形成动画效果。相比于视图动画,View 的属性是真正改变了。比如 View 的旋转,放大,缩小。
PathClassLoader与DexClassLoader有什么区别?
- PathClassLoader:只能加载已经安装到Android系统的APK文件,即/data/app目录,Android默认的类加载器。
- DexClassLoader:可以加载任意目录下的dex、jar、apk、zip文件。
WebView优化了解吗,如何提高WebView的加载速度?
为什么WebView加载会慢呢?
这是因为在客户端中,加载H5页面之前,需要先初始化WebView,在WebView完全初始化完成之前,后续的界面加载过程都是被阻塞的。
优化手段围绕着以下两个点进行:
- 预加载WebView。
- 加载WebView的同时,请求H5页面数据。
因此常见的方法是:
- 全局WebView。
- 客户端代理页面请求。WebView初始化完成后向客户端请求数据。
- asset存放离线包。
除此之外还有一些其他的优化手段:
- 脚本执行慢,可以让脚本最后运行,不阻塞页面解析。
- DNS与链接慢,可以让客户端复用使用的域名与链接。
- React框架代码执行慢,可以将这部分代码拆分出来,提前进行解析。
保存 Activity 状态
Java和JS的相互调用怎么实现,有做过什么优化吗?
jockeyjs:https://github.com/tcoulter/jockeyjs
对协议进行统一的封装和处理。
JNI了解吗,Java与C++如何相互调用?
Java调用C++
- 在Java中声明Native方法(即需要调用的本地方法)
- 编译上述 Java源文件javac(得到 .class文件)
- 通过 javah 命令导出JNI的头文件(.h文件)
- 使用 Java需要交互的本地代码 实现在 Java中声明的Native方法
- 编译.so库文件
- 通过Java命令执行 Java程序,最终实现Java调用本地代码
C++调用Java
- 从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象。
- 获取类的默认构造方法ID。
- 查找实例方法的ID。
- 创建该类的实例。
- 调用对象的实例方法。
|
|
了解插件化和热修复吗,它们有什么区别,理解它们的原理吗?
- 插件化:插件化是体现在功能拆分方面的,它将某个功能独立提取出来,独立开发,独立测试,再插入到主应用中。依次来较少主应用的规模。
- 热修复:热修复是体现在bug修复方面的,它实现的是不需要重新发版和重新安装,就可以去修复已知的bug。
利用PathClassLoader和DexClassLoader去加载与bug类同名的类,替换掉bug类,进而达到修复bug的目的,原理是在app打包的时候阻止类打上CLASS_ISPREVERIFIED标志,然后在
热修复的时候动态改变BaseDexClassLoader对象间接引用的dexElements,替换掉旧的类。
目前热修复框架主要分为两大类:
- Sophix:修改方法指针。
- Tinker:修改dex数组元素。
如何做性能优化?
- 节制的使用Service,当启动一个Service时,系统总是倾向于保留这个Service依赖的进程,这样会造成系统资源的浪费,可以使用IntentService,执行完成任务后会自动停止。
- 当界面不可见时释放内存,可以重写Activity的onTrimMemory()方法,然后监听TRIM_MEMORY_UI_HIDDEN这个级别,这个级别说明用户离开了页面,可以考虑释放内存和资源。
- 对图片本身进行操作。尽量不要使用 setImageBitmap、setImageResource、BitmapFactory.decodeResource 来设置一张大图,因为这些方法在完成 decode 后,最终都是通过 java 层的 createBitmap 来完成的,需要消耗更多内存。 避免在Bitmap浪费过多的内存,使用压缩过的图片,也可以使用Fresco等库来优化对Bitmap显示的管理。不用的图片记得调用图片的 recycle()方法回收。
- 使用优化过的数据集合SparseArray代替HashMap,HashMap为每个键值都提供一个对象入口,使用SparseArray可以免去基本对象类型转换为引用数据类想的时间。
ANR
ANR 全名 Application Not Responding,也就是“应用无响应”。当操作在一段时间内系统无法处理时,系统层面会弹出 ANR 对话框
产生原因&解决方法:
5s 内无响应用户输入事件(例如键盘输入,触摸屏幕等)。
不要在主线程中做耗时操作,而应在子线程中来实现。如 onCreate()和 onResume()里尽可能少的去做创建操作。BroadcastReceiver 在 10s 内无法结束
应用程序应该避免在 BroadcastReceiver 里做耗时操作或计算。
避免在 IntentReceiver 里启动一个 Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点Service 20s 内无法结束
service 是运行在主线程的,所以再 service 中做耗时操作,必须要放在子线程中。
如果防止过度绘制,如何做布局优化?
- 使用include复用布局文件。
- 使用merge标签避免嵌套布局。
- 使用stub标签仅在需要的时候在展示出来。
如何提交代码质量?
- 避免创建不必要的对象,尽可能避免频繁的创建临时对象,例如在for循环内,减少GC的次数。
- 尽量使用基本数据类型代替引用数据类型。
- 静态方法调用效率高于动态方法,也可以避免创建额外对象。
- 对于基本数据类型和String类型的常量要使用static final修饰,这样常量会在dex文件的初始化器中进行初始化,使用的时候可以直接使用。
- 多使用系统API,例如数组拷贝System.arrayCopy()方法,要比我们用for循环效率快9倍以上,因为系统API很多都是通过底层的汇编模式执行的,效率比较高。
有没有遇到64k问题,为什么,如何解决?
- 在DEX文件中,method、field、class等的个数使用short类型来做索引,即两个字节(65535),method、field、class等均有此限制。
- APK在安装过程中会调用dexopt将DEX文件优化成ODEX文件,dexopt使用LinearAlloc来存储应用信息,关于LinearAlloc缓冲区大小,不同的版本经历了4M/8M/16M的限制,超出
缓冲区时就会抛出INSTALL_FAILED_DEXOPT错误。
解决方案是Google的MultiDex方案,具体参见:配置方法数超过 64K 的应用。
MVC、MVP与MVVM之间的对比分析?
- MVC:PC时代就有的架构方案,在Android上也是最早的方案,Activity/Fragment这些上帝角色既承担了V的角色,也承担了C的角色,小项目开发起来十分顺手,大项目就会遇到
耦合过重,Activity/Fragment类过大等问题。 - MVP:为了解决MVC耦合过重的问题,MVP的核心思想就是提供一个Presenter将视图逻辑I和业务逻辑相分离,达到解耦的目的。
- MVVM:使用ViewModel代替Presenter,实现数据与View的双向绑定,这套框架最早使用的data-binding将数据绑定到xml里,这么做在大规模应用的时候是不行的,不过数据绑定是
一个很有用的概念,后续Google又推出了ViewModel组件与LiveData组件。ViewModel组件规范了ViewModel所处的地位、生命周期、生产方式以及一个Activity下多个Fragment共享View
Model数据的问题。LiveData组件则提供了在Java层面View订阅ViewModel数据源的实现方案。