Android面试题(一)Java高频面试题
Android面试题(二)Android中高级/资深面试题
Android面试题(三)Java虚拟机原理面试题
Android面试题(四)常用设计模式面试题
Android面试题(五)数据结构/算法面试题
Android面试题(六)高频网络基础面试题
Android面试题(七)Kotlin高频面试题
Android面试题(八)Flutter高频面试题
目录
Activity/Fragment的生命周期(参考):
Activity的启动模式及使用场景:
Activity任务栈taskAffinity【任务相关性】属性:
APP的启动流程【Android 应用程序启动过程分析】:
Activity的启动流程(推荐1 推荐2):
Activity、Service、Context、Application的之间的关系【参考 参考2】:
一个App中Context的数量:
Service的生命周期【两种启动方式】(参考):
如何保证Service不被杀死(参考):
HandlerThread的原理及运用(参考1 参考2):
IntentService的原理及使用(参考):
BroadcastReceiver注册方式与区别 :
BroadcastReceiver与LocalBroadcastReceiver的区别:
ContentProvider是如何实现数据共享的(参考):
Activity、Window、View的关系(参考):
setContentView执行流程:
View的绘制流程(参考):
Touch事件分发机制,事件冲突解决(参考):
Android中动画分哪几类:
Handler机制及底层原理(参考1 参考2)
如何在子线程中更新UI (Android中子线程真的不能更新UI吗?)
跨进程通信(IPC)的几种方式:
Binder机制源码解析:
Binder通信原理(参考):
谈谈对AIDL的理解(参考):
开源图片框架Picasso、Fresco、Glide对比(参考):
网络请求框架OkHttp, Volley, Retrofit对比(参考):
Okhttp的基本实现原理【参考 参考】
SurfaceView与TextureView的区别:
Android开发框架模式(MVC、MVP、MVVM)区别(参考):
Android中数据存储方式:
SharedPreferences的apply()/commit()区别及应用:
LruCache 底层实现原理:
Bitmap内存占用的计算:
图片压缩的几种方式:
Java中Exception与Error的区别是什么:
内存泄漏和内存溢出区别:
内存泄漏的场景及解决方案(参考1 参考2 参考3):
内存泄漏检测分析工具:
线上内存(泄漏)监控方案参考【参考】:
Android性能分析及优化【Android 性能优化总结】:
ANR产生的原因及解决方案(参考):
RecyclerView四级缓存机制【缓存的对象是ViewHolder】(参考):
Gradle构建流程(参考):
AOP主流框架对比【一文读懂 AOP】:
对热修复的理解(参考 Android 热修复调研报告—流行方案选择):
模块化、组件化、插件化的理解(参考):
RN、Weex、Flutter框架的差异性:
-
Activity/Fragment的生命周期(参考):
onCreate:表示Activity正在创建,可以在该方法中做一些初始化工作,比如调用setContentView去加载界面布局资源、初始化Activity所需数据等。
onStart:表示Activity正在被启动,此时Activity已经可见了(但用户依然看不到),但是还没有出现在前台,还不能跟用户进行交互。
onResume:表示Activity已经可见了(用户可见),并且已经出现在前台,可以跟用户进行交互了。
onRestart:表示Activity正在重新启动,一般情况下,当当前Activity从不可见重新变为可见状态时,onRestart就会被调用。
onPause:表示Activity正在停止,已经不可见,此时可以做一些数据存储、停止动画等操作,但是注意不能太耗时,因为这会影响到新Activity的显示,onPause必须先执行完,新Activity的onResume方法才会执行。
onStop:表示Activity即将停止,此方法中可以做一些稍微重量级的回收工作,同样不能太耗时。
onDestory:表示Activity即将被销毁,在此方法中我们可以做一些回收工作和最终的资源释放。
-
Activity的启动模式及使用场景:
standard(标准模式):是系统的默认模式。每次启动一个Activity都会重新创建一个新的Activity实例,而不管这个实例是否已经存在。一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity的任务栈中。
singleTop(栈顶复用模式):在这种模式下,如果新Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法会被回调,通过此方法的参数我们可以取出当前请求的信息。如果新Activity的实例已存在但不是位于栈顶,那么新Activity仍然会被重新创建。
singleTask(栈内复用模式):这是一种单例模式,只要Activity在一个栈中存在,那么多次启动此Activity都不会重现创建实例,和singleTop一样,系统也会调用其onNewIntent方法。同时由于singleTask默认具有clearTop的效果,会导致栈内所有在该Activity上面的所有Activity出栈。不过这里是指在同一个App中启动这个singleTask的Activity;如果是其他App以singleTask模式来启动这个Activity,那么它将创建一个新的任务栈。需要注意的是,如果启动的模式为singleTask的Activity已经在后台的一个任务栈中了,那么启动后,后台的这个任务栈将一起被切换到前台。
***使用StartActivityForResult启动singleTask或singleInstance启动模式的Activity时,onActivityResult正常调用,不会产生冲突【Android5.0版本已经修复】,而使用Intent.FLAG_ACTIVITY_NEW_TASK【使用一个新的Task来启动Activity】来启动的时候,会产生冲突,其日志如下:
即系统将直接返回Activity.RESULT_CANCELED,而不会再去等待返回。
singleInstance(单实例模式):这是一种加强版的singleTask模式,它除了具有singleTask模式的所有特性外,还加强了一点,那就是具有此模式的Activity只能单独地位于一个任务栈中。当该模式的Activity启动后,系统会为它创建一个新的任务栈,然后该Activity独自在这个新的任务栈中,由于栈内复用的特性,后续的请求均不会创建新的Activity,除非这个独特的任务栈被系统销毁了。
启动模式 |
应用场景 |
standard(标准模式) | 文件中没有配置就使用该默认标准模式。 |
singleTop(栈顶复用模式) | 适合启动同类型的Activity,如接收通知启动的内容显示页面。 |
singleTask(栈内复用模式) | 适合作为程序入口,如应用主页面。 |
singleInstance(单实例模式) | 适合需要与程序分离开的页面,例如闹铃的响铃界面(不同APP调用此类Activity 时,首次创建实例,之后其他APP只能复用此实例)。 |
注:所有Activity所需的任务栈的名字为应用的包名,并且任务栈分为前台任务栈和后台任务栈,后台任务栈中的Activity处于暂停状态,用户可以通过切换将后台任务栈再次调到前台。
指令:adb shell dumpsys activity activities --- 将Activity的堆栈信息存至当前目录的文件中。
另外其他adb指令可以参考:Android 常用adb指令记录总结
-
Activity任务栈taskAffinity【任务相关性】属性:
TaskAffinity特点如下:
- TaskAffinity 参数标识着Activity所需要的任务栈的名称,默认情况下,一个应用中所有Activity所需要的任务栈名称都为该应用的包名。
- TaskAffinity 属性主要和singleTask模式或allowTaskReparenting属性配对使用,在其他情况下没有意义。
- TaskAffinity属性的值不能与当前应用包名相同,否则其值跟作废没两样。
TaskAffinity和singleTask启动模式结合使用:
当TaskAffinity和singleTask启动模式结合使用时,启动的Activity会运行在名字和TaskAffinity相同的任务栈中。
- 其应用场景:
我们的客户端app正处于后台运行,此时让微信调用我们客户端app的某个页面,用户完成相关操作后,我们不做任何处理,按下回退或者当前(),页面都会停留在自己的客户端(此时我们的app回退栈不为空),这样的用户体验是很差的。我们希望,回退必须回到微信客户端,而且要保证不杀死自己的app.这时候我们的处理方案就是,设置当前被调起Activity的属性为:
LaunchMode=""SingleTask" taskAffinity=""【微信包名】,就是把自己的Activity放到微信默认的Task栈里面,这样回退时就会遵循“Task只要有Activity一定从本Task剩余Activity回退”的原则,不会回到自己的客户端;而且也不会影响自己客户端本来的Activity和Task逻辑。
TaskAffinity和allowTaskReparenting结合使用:
当一个应用A启动了应用B的某个Activity后,如果这个Activity的allowTaskReparenting属性为true的话,那么当应用B被启动后,此Activity会直接从应用A的任务栈转移到应用B的任务栈中。
- 其应用场景:
一个e-mail应用消息包含一个网页链接,点击这个链接将出发一个activity来显示这个页面,虽然这个activity是浏览器应用定义的,但是activity由于e-mail应用程序加载的,所以在这个时候该activity也属于e-mail这个task。如果e-mail应用切换到后台,浏览器在下次打开时由于allowTaskReparenting值为true,此时浏览器就会显示该activity而不显示浏览器主界面,同时actvity也将从e-mail的任务栈迁移到浏览器的任务栈,下次打开e-mail时并不会再显示该activity。
-
APP的启动流程【Android 应用程序启动过程分析】:
APP启动会涉及到四个进程:
1、Launcher进程:
整个APP启动流程的起点,可看作一个总的Activity,实现了点击触摸等事件。
2、Zygote进程:
Android系统启动时Linux内核会通过init进程创建Zygote进程,Zygote进程负责后续Android应用其它进程的创建和启动工作。
3、SystemServer进程:
由Zygote进程创建,承载着整个Framework层的核心服务,如AMS、WMS、PMS。
4、App进程:
要启动的APP所运行的进程。
1)Launcher接收到Icon的点击事件之后,向SystemServer进程中的AMS发起启动应用的请求。
2)AMS收到请求之后,做一些启动Activity的准备工作,并告诉Launcher Pause。
3)Launcher 调用Pause保存状态,并在后台挂起,然后通知AMS Pause已执行完成。
4)随后AMS通过socket向 Zygote 发送创建新进程的请求。zygote收到请求后fork自身,创建一个新进程。
5)新进程中调用 ActivityThread 类的 main 方法(应用程序的入口),main方法主要做了两件事情:
- 首先是实例化ActivityThread,并执行其attach()方法。
- 开启主线程消息循环。
6)ActivityThread的attach方法,最终会调用attachApplication方法,表示App进程向AMS请求attach到AMS,这里主要做了两件事情:
- 将ApplicationThread这个当前应用的Binder对象传递给AMS,用于后续进程间通信。
- 将应用信息注册到AMS中,AMS再在堆栈顶部取得需要启动的Activity。
7)AMS拿到ApplicaitonThread这个Binder对象,便会链式调用:
- ()
- ()
最终会创建Application和AppContext实例,并调用Application的onCreate方法。
8)Application创建之后,便会通过一系列的链式调用,来创建启动Activity:
- ()
- ()
- 发送消息H.LAUNCH_ACTIVITY
- ()
- () -- 最后调用了Activity的onCreate.
完成APP的启动。
-
Activity的启动流程(推荐1 推荐2):
- ()
- ()
- ()
- 【AMS】startActivity()
- 一系列链式调用...
- ()
- ()
- 发送消息H.LAUNCH_ACTIVITY
- ()
- ActivityThread.performLaunchActivity()
performLaunchActivity方法主要完成了如下几件事:
- 从ActivityClientRecord中获取待启动的Activity的组建信息
- 通过()方法使用类加载器创建Activity对象
- 通过()方法来尝试创建Application对象,如果Application对象已经创建过了,则不会重复创建。Application创建完毕后,系统会调用Application的onCreate()方法。
- 创建ContextImpl对象并通过Activity的attach()方法来完成一些重要数据的初始化。
- 最后调用Activity的onCreate()方法。
这样就完成了Activity的整个启动过程。
1)ActivityManagerService:是Android中最核心的管理类,主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作,其职责与操作系统中的进程管理和调度模块相类似,它在Android中非常重要。
2)ActivityStackSupervisor:管理着整个手机任务栈,即ActivityStack,ActivityStackSupervisor在AMS中的构造方法中被创建。
3) ActivityStack:Activity的任务栈,用来管理系统中所有Activity的实例跟状态。
4)PackageManagerService:完成组件在清单文件里的扫描跟注册,它负责系统中Package的管理,应用程序的安装、卸载、信息查询等。定义了服务端和客户端通信的业务函数,还定义了内部类Stub,该类从Binder派生并实现了IPackageManager接口。因此PackageManagerService将作为服务端参与Binder通信。
5)ActivityThread:安卓java应用层的入口函数类,它会执行具体对Activity的操作,并将结果通知给ActivityManagerService。
-
Activity、Service、Context、Application的之间的关系【参考 参考2】:
如图所示:
可以看到,Activity是继承自ContextThemeWrapper,而Service和Application继承自ContextWrapper, 另外ContextThemeWrapper继承自ContextWrapper,而ContextWrapper最终继承自Context,Context实际上是一个抽象类,它的实现是交给ContextImpl类负责,所以App在启动时,Application、Activity和Service三者能够关联到Context,实际上都是在创建这三个要素的时候,同时实现了ContextImpl对象。
总结:
不论是启动App还是某个Activity或Service,最初都需要提交给AMS,AMS相当于总负责人,总负责人处理好消息后打包成数据(ApplicationInfo、ActivityInfo及ServiceInfo),并将数据通过跨进程传递的方式递交给ActivityThread进行处理,ActivityThread在接收时对外首先暴露ApplicationThread这个Binder接口,然后获取到相应的数据之后执行创建Application、Activity及Service的流程;同时在创建这三个要素时,通过ContextImpl的静态方法来创建ContextImpl对象,对象创建完成之后通过setOuterContext方法指定各类的外部代理人,但是该方法的参数是Context,而恰好Application、Activity及Service都继承自Context,所有可以有效充当ContextImpl的外部代理人,来调用ContextImpl中的方法,这就是Application、Activity及Service是Context的本质,本质在于以代理人的身份调用方法。
-
一个App中Context的数量:
- 一个 Application 只创建了一个 Context 对象。
- 每一个 Activity 分别创建了一个 Context 对象。
- 每一个 Service 分别创建了一个 Context 对象。
所以,我们可以得出以下结论:
Context 数量 = 1 + Activity 实例数量 + Service 实例数量
当然,这里是以单进程为例进行说明的,如果你的应用是多进程,那么 Application 对象会变成多个,相对应的 Context 对象也会有多个。
-
Service的生命周期【两种启动方式】(参考):
第一种通过startService方式启动启动Service:
通过startService启动后,service会一直运行下去,只有外部调用了stopService()或者stopSelf()的时候,该Service才会停止运行并销毁。
onCreate: 只会在第一次创建Service的时候调用,多次调用startService只会调用一次onCreate方法,适合做一些初始化的工作。
onStartCommand:如果多次执行了startService,那么该方法也会被执行多次,该方法很重要,我们可以在该方法中根据传入的intent参数进行实际的操作。
onBind:Service中的onBind方法是抽象方法,因为Service本身是抽象类【如果一个类集成一个抽象类,如果该子类不是抽象类,那么就要实现抽象类中的所有抽象方法】,所以onBind方法是必须重写(覆盖)的,即使我们用不到。
onDestory:在销毁的时候会执行该方法。
第二种方式bindService方式启动Service:
bindService启动服务特点
1、bindService启动的服务和调用者之间是典型的client-server模式。调用者是Client,Service则是Server端。Service只有一个,但绑定到Service上面的Client可以有一个或很多个。这里所提到的client指的是组件,比如某个Activity。
2、Client可以通过IBinder接口获取Service的实例,从而实现在Client端直接调用Service的方法以实现灵活交互,这在startService启动方式中是无法实现的。
3、bindService启动的服务的生命周期与其绑定的Client息息相关。当Client销毁时,Client会自动与Service接触绑定。当然,Client也可以明确的调用onUnbind方法,自动与Service接触绑定。当没有任何Client与Service保持绑定关系的话,Service会自动销毁。
-
如何保证Service不被杀死(参考):
1、在onStartCommand中返回START_STICKY:
onStartCommand可以返回的值如下:
1)START_NOT_STICKY:如果返回START_NOT_STICKY,表示当Service运行的进程被系统强制杀死之后,不会重新创建该Service。如果我们某个Service执行的工作被中断几次无关紧要或者对Android内存紧张的情况下需要被杀掉且不会立即重新创建这种行为也可接受,那么我们便可将 onStartCommand的返回值设置为START_NOT_STICKY。
2)START_STICKY:如果返回START_STICKY,表示当该Service运行的进程被系统强制杀死之后,系统仍会将该服务设置成started状态(运行状态),但是不再保存onStartCommand传入的intent对象,然后系统会重新创建该Service,并回调onStartCommand方法,但是onStartCommand回调方法的Intent参数为null,也就是onStartCommand方法虽然会执行但是获取不到intent信息。如果你的Service可以在任意时刻运行或结束都没什么问题,而且不需要intent信息,那么就可以在onStartCommand方法中返回START_STICKY,比如一个用来播放背景音乐功能的Service就适合返回该值。
3)START_REDELIVERY_INTENT:如果返回START_REDELIVERY_INTENT,表示当该Service运行的进程被系统强制杀死之后,与START_STICKY类似,系统仍会重新创建该服务,只是不同的是系统回调onStartCommand方法的时候,会将之前的intent对象传递回来。如果我们的Service需要依赖具体的Intent才能正常运行,并且在强制销毁之后有必要重新创建,那么就需要返回START_REDELIVERY_INTENT值。
2、提高Service优先级:
在文件中可以对intent-filter通过android:priority = "1000"这个属性设置最高优先级,1000是最高值,如果数字越小,优先级越低,同时适用于广播。
3、提高Service进程的优先级:
当系统资源紧张的时候,就会按照优先级回收进程,进程按照优先级由高到低依次为:
1)前台进程foreground_app
2) 可视进程visible_app
3)次要服务进程secondary_server
4)后台进程hidden_app
5)内容供应节点content_provider
6)空进程empty_app
可以使用startForeground将service放到前台状态,这样低内存时,被杀死的概率会低一些。
4、在onDestroy方法中重启Service(也可以使用双进程守护方式):
当service走到onDestroy()时,发送一个自定义广播,当收到广播时,重新启动service。
5、系统广播监听Service状态
6、将APK安装到/system/app,变身为系统级应用
-
HandlerThread的原理及运用(参考1 参考2):
1、HandlerThread出现的背景:
我们都知道Thread线程是一次消费品,当Thread线程执行完一个耗时任务之后,线程就会被自动销毁了。如果此时我又有一个耗时任务需要执行,那么久需要重新创建销毁线程。这样就存在一个性能问题:多次创建和销毁线程是很耗系统资源的。为了解决这个问题我们可以构建一个循环Looper Thread。当有该耗时任务投放到该循环线程中时,线程执行耗时任务,执行完之后循环线程处于等待状态,知道一个新的耗时任务被投放进来。这样一来就避免了多次创建Thread导致的性能问题了。
2、HandlerThread的主要作用(运用):
HandlerThread适用于构建循环线程。如每隔几秒钟更新数据或图片等。
3、HandlerThread的原理:
继承了Thread,实际上是一个使用Handler,Looper的线程。
1)继承了Thread,在run方法中通过()来创建消息队列,通过()来循环处理消息。
2)使用时创建HandlerThread,创建Handler并与HandlerThread的Looper绑定,Handler以消息的方式通知HandlerThread 来执行一个具体的任务。
4、HandlerThread的使用步骤:
1)创建HandlerThread实例对象。
2)启动HandlerThread线程。
3)构建循环消息处理机制:即将HandlerThread创建的Looper作为参数创建Handler对象,这样就完成了Handler对象与HandlerThread的Looper对象的绑定 (这里的Handler对象可以看做是绑定在HandlerThread中,所有handleMessage方法里面的操作是在子线程中运行的),重写 handleMessage方法处理耗时操作。
-
IntentService的原理及使用(参考):
IntentService是继承Service并处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时任务,启动IntentService的方式跟启动传统Service的方式一样,同时当任务执行完毕之后,IntentService会自动停止。而不需要我们手动去控制。另外,可以启动IntentService多次,而每一个耗时操作多会以工作队列的方式在IntentService的onHandleIntent回调方法中执行,并且每次只会执行一个工作线程,根据先后顺序,执行完第一个任务在执行第二个,依次类推。
使用IntentService的优点如下:
1)省去了在Service中手动开启线程的麻烦。
2)当操作完成后,我们不需要手动停止Service。
-
BroadcastReceiver注册方式与区别 :
BroadcastReceiver有两种注册方式:
1)静态注册---文件中注册:
-
<receiver android:name=".BroadcastReceiverDemo" >
-
<intent-filter>
-
<action android:name="" >
-
</action>
-
</intent-filter>
-
</receiver>
注册完成之后可以发送广播,可以使用(动态注册也适用)
()【正常发送广播】
() 【发送有序广播】
() 【可以将之前发送的广播存储起来,直到有人注册了该广播接收器】
三种方式来实现。
2)动态注册---使用代码进行注册
-
//注册广播
-
registerReceiver(BroadcastReceiver receiver, IntentFilter filter)
-
//注销广播
-
unregisterReceiver(BroadcastReceiver receiver)
动态注册/注销广播需要成对出现,在Activity中,一个在onResume(或onCreate)方法中注册广播,在onPause(或onDestroy)中注销广播。这里需要强调的是应用内的更建议使用LocalBroadcastReceiver,更加安全可靠。
区别:
静态注册:不管应用是否处于活动状态,都会进行监听。
动态注册:当应用程序关闭后,就不会在进行监听,当在Activity中进行的广播注册,那么当该Activity创建的时候开始监听,Activity销毁的时候,便停止了监听,生命周期跟Activity是绑定状态。对于那些没必要在某个Activity或者应用程序关闭后继续进行监听的Receiver来说,使用动态注册无疑是个比较好的选择。
-
BroadcastReceiver与LocalBroadcastReceiver的区别:
1、通信范围的比较:
1)LocalBroadcastReceiver即本地广播,BroadcastReceiver即全局广播。
2)LocalBroadcastReceiver只能用来接收本应用内的广播,通信范围比较小,但是安全性更好;而BroadcastReceiver它不仅针对应用内的广播有效,而且针对应用间,及应用和系统之间的广播通信均有效,它的通信范围更大。
2、通信效率的比较:
1)LocalBroadcastManager的核心实现其实还是 Handler,因此它是应用内的通信,自然安全性更好,运行效率更高。
2)BroadcastReceiver 是全局广播,可以跨进程通信,范围更广,从而导致它的运行效率没有本地广播高效,毕竟一个是本地的通信,一个是跨进程的通信方式,效率肯定相对较低点,但对于实时性不高的应用场景我们可以忽略不计。
3、注册方式的比较:
1)LocalBroadcastReceiver(本地广播)只能进行动态注册。
2)BroadcastReceiver(全局广播)可以进行静态注册和动态注册。
-
ContentProvider是如何实现数据共享的(参考):
ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式。ContentProvider的底层实现是Binder。
当一个应用程序需要把自己的数据共享给其他程序的时候,可以通过继承ContentProvider类来实现,其他程序通过ContentResolver来操作ContentProvider共享的数据。
如果应用程序A通过ContentProvider来暴露自己的数据操作接口,那么无论A是否启动,其他的程序都可以通过该接口来操作A的内部数据。
ContentProvider是以Uri的方式对外暴露数据,ContentResolver是以Uri的方式来获取数据。
ContentProvider使用步骤:
1)创建一个继承自ContentProvider的类,并实现其中的6个方法。
2)在 文件中注册ContentProvider,(四大组件的使用一般都需要在Manifest文件中注册) 注册时需要绑定一个Uri。
例如:
-
//uri
-
android:authorities=""
说明:
authorities就相当于为该ContentProvider指定Uri。 注册后,其他应用程序就可以通过该Uri来访问当前的ContentProvider所暴露的数据了。
3)其他程序使用ContentResolver来操作共享数据。
通过调用getContentResolver()方法来获取ContentProvider对象,通过该对象来进行共享数据的增删改查操作。
一般来说,ContentProvider是单例模式,也就是说,当多个应用程序通过ContentResolver来操作ContentProvider提供的数据时,ContentResolver调用的数据操作将会委托给同一个ContentResolver。
-
Activity、Window、View的关系(参考):
Activity负责界面展示、用户交互与业务逻辑处理(控制单元)
Window负责界面展示以及交互,就相当于Activity的下属(承载模型)
View就是放在Window容器的元素,Window是View的载体,View是Window的具体展示(显示视图)
三者之间的关系:
Activity通过Window来实现视图元素的展示,Window可以理解为一个容器,盛放着一个个的View,用来执行具体的展示工作。
具体来说:
Activity:可以看作一个容器,对于每个Activity都会拥有一个PhoneWindow。
PhoneWindow:是 Window 类的具体实现类,我们可以通过该类去绘制窗口。并且该类内部包含了一个DecorView对象。
DecorView:是应用窗口的根View,它本质上是一个FrameLayout。DecorView有一个唯一的子View,这个子View是一个垂直的LinearLayout,它包含两部分,一个TitleView(ActionBar 的容器),一个是ContentView(窗口内容的容器)。
ContentView:是一个 FrameLayout(),我们平常用的 setContentView 就是设置它的子View。
WindowManager:是一个接口,里面常用的方法就是addView、updateViewLayout、removeView。主要是用来管理Window的,WindowManager的具体实现类是WindowManagerImpl。WindowManagerImpl会将业务交给其代理类WindowManagerGlobal类来处理。
WindowManagerService(WMS):负责管理各App窗口的创建、更新、删除、显示顺序。运行在 system_server 进程。
ViewRootImpl:拥有DecorView的实例,通过该实例来控制DecorView绘制。在 里,通过 把 ViewRootImpl 设置为 DecorView 的 parent【注意ViewRootImpl本身并不是一个View】。
-
setContentView执行流程:
setContentView具体会调用 方法,主要做了两个事情:
1、创建DecorView。
2、根据layoutResId创建View,并添加到DecorView中。
-
View的绘制流程(参考):
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView简历关联。
View的绘制流程是从ViewRootImpl的performTraversals方法开始的,performTraversals会依次调用performMeasure、performLayout、performDraw三个方法。
即具体的View绘制流程主要分为三步:
1)onMeasure(测量):
作用是确定View的测量宽/高。从顶层View到子View递归调用measure方法,measure调用onMeasure方法,最终在各onMeasure方法中完成测量工作。
2)onLayout(布局):
作用是确定View的最终宽/高和四个顶点的位置。从顶层父View到子View递归调用layout()方法,父View根据上一步measure()方法得到的子View的布局大小和布局参数,将子View放在合适的位置上。
3)onDraw(绘制)【自定义 View 1-5 绘制顺序】:
作用是将View绘制在屏幕上。首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。onDraw()方法的绘制遵循如下几步:① 绘制视图背景。② 保存(save)画布的图层。 ③ 绘制View内容。④ 绘制子视图(如果有的话)。⑤ 还原(restore)图层。⑥ 绘制装饰(滚动条)。
绘制时下面三点需要注意:
1、在继承自 ViewGroup
的子类中重写除 dispatchDraw()
以外的绘制方法时,可能需要调用 setWillNotDraw(false)
;
2、ViewGroup没有背景绘制时,直接调用的是dispatchDraw()
方法,而绕过了onDraw
()
方法,因此在自定义ViewGroup中绘制东西的时候,往往重写的是dispatchDraw()
方法。
3、在重写的方法有多个选择时,优先选择 onDraw
()
【可以
提升开发效率】
。
-
Touch事件分发机制,事件冲突解决(参考):
-
Android中动画分哪几类:
1、View动画(补间动画):
View动画的作用对象是View,它支持四种动画效果,分别是TranslateAnimation(平移动画)、ScaleAnimation(缩放动画)、RotateAnimation(旋转动画)、AlphaAnimation(透明度动画)。
2、逐帧动画:
逐帧动画是顺序播放一组预先定义好的图片,类似于电影播放,不同与View动画,系统提供了另一个类AnimationDrawable来使用帧动画。
3、属性动画(推荐)
属性动画跟View动画不同,它对作用对象进行了扩展,属性对话可以对任意对象做动画,甚至还可以没有对象。属性动画常用的类有ValueAnimator、ObjectAnimator、AnimatorSet。
使用以上动画需要注意的几个事项:
1)帧动画容易引起OOM,所以避免使用过多尺寸较大的图片。
2)View动画是对View的影像做改变,并不会真正改变View的状态,有时候出现出现动画完成后View无法隐藏的问题,调用()清除动画即可解决问题。
3)动画移动后,view单击区域的区别是:View动画只能在原位置触发单击事件,而属性动画可以在移动后的位置触发单击事件。
4)属性动画中如果是无限循环的动画,需要在Activity退出时及时停止,否则造成内存泄露;而View动画不存在此问题。
-
Handler机制及底层原理(参考1 参考2)
-
如何在子线程中更新UI (Android中子线程真的不能更新UI吗?)
子线程在onCreate中可更新UI。
原因:ViewRootImpl的创建在onResume方法回调之后,而我们一开篇是在onCreate方法中创建了子线程并访问UI,在那个时刻,ViewRootImpl是没有创建的,无法检测【即checkThread方法】当前线程是否是UI线程,所以程序没有崩溃一样能跑起来。
-
跨进程通信(IPC)的几种方式:
1、使用Bundle:
四大组件的三大组件(Activity、Service、Receiver)都支持在Intent中传递Bundle数据,由于Bundle实现了Parcelable接口,所以它可以很方便的在不同的进程间传输。这是最简单的进程间通信方式。
2、使用文件共享:
两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程又从该文件中读取数据。文件共享方式适合在对数据同步要求不高的进程之间进行通信,且要处理并发读写的问题。
3、使用Messenger(信使---串行的方式):
通过Messenger可以在不同的进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松的实现进程间通信了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。
4、使用AIDL(Android接口定义语言):
AIDL底层的实现是Binder,下面会单独介绍AIDL的实现原理。
5、使用ContentProvider[ContentProvider基础应用]:
ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,ContentProvider的底层实现同样是Binder。
6、使用Socket:
Socket也称为“套接字”,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络传输控制层中的TCP和UDP协议。TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立需要进行“三次握手”才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;而UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能。在性能上,UDP具有更好的效率,其缺点是不能保证数据一定能够准确传输,尤其是在网络拥塞的情况下。
-
Binder机制源码解析:
Binder是Android中一种跨进程通信方式。从Android Framework的角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager等)和相应ManagerService的桥梁;从Android应用层来说,Binder是客户端与服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
Binder通信C/S架构如图:
我们先来了解一下这个类中每个方法的含义:
DESCRIPTOR:
Binder的唯一标识,一般用当前Binder的类名表示,比如
private static final .String DESCRIPTOR = "";
asInterface( obj):
用于将服务端的Binder对象转换成客户端所需要的 AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的对象。
asBinder:
此方法用于返回当前Binder对象。
onTransact:
这个方法运行在服务端中的Binder线程池中,当客户端发起请求的时候,远程请求系统底层封装后交由该方法处理,该方法的原型为public Boolean onTransact(int code, data, reply, int flags).服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需要的参数(如果有参数的话),然后执行目标方法。当目标方法执行完毕后,就往reply中写入返回值(如果有返回值的话),onTransact方法的执行过程就是这样的。需要注意的是,如果此方法返回false,那么客户端的请求会失败,因此我们可以通过这个特性来做权限验证,毕竟我们也不希望随便一个进程都能远程调用我们的服务。
使用Binder是需要注意:
1、客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是比较耗时的,那么不能在UI线程中发起此远程请求(避免引起ANR)。
2、由于服务端的Binder方法运行在Binder线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了(服务端要防止多个线程同时访问,可以使用并发处理)。
3、注册和解注册使用RemoteCallbackList。
4、需要进行权限验证。
-
Binder通信原理(参考):
1、用户空间与内核空间:
Linux的进程是相互独立的,一个进程是不能直接操作或者访问别一个进程空间的。个进程空间还分为用户空间和内核(Kernel)空间,相当于把Kernel和上层的应用程序抽像的隔离开。
这里有两个隔离,一个进程间是相互隔离的,二是进程内有用户和内核的隔离。进程间的交互就叫进程间通信(IPC,或称跨进程通信),而进程内的用户和内核的交互就是系统调用。
- 进程间,用户空间的数据不可共享,所以用户空间 = 不可共享空间
- 进程间,内核空间的数据可共享,所以内核空间 = 可共享空间
- 所有进程共用1个内核空间
用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是在内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。
进程内用户空间
与内核空间
进行交互需通过系统调用两个函数。
- copy_from_user():将用户空间的数据拷贝到内核空间
- copy_to_user():将内核空间的数据拷贝到用户空间
2、Binder进程通信详解:
Binder通信的详细流程如上图,可概要总结为:
- 首先Binder驱动在内核空间创建一个数据接收缓存区。
- 在内核空间开辟一个内核缓存区,建立内核缓存区和数据接收缓存区之间的映射关系。以及内核中数据接收缓存区和接收进程用户空间地址的映射关系。
- 发送方进程通过系统调用copy_from_user()将数据copy到内核中的内存缓冲区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
-
谈谈对AIDL的理解(参考):
AIDL是Android中最常用的进程间通信的一种方式,AIDL进行进程间通信的流程,分为服务端和客户端两个方面:
服务端:
1、服务端首先要创建一个Service用来监听客户端的连接请求。
2、创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明。
3、在Service中实现这个AIDL接口即可。
客户端:
客户端需要做的事情就稍微简单些:
1、首先需要绑定服务端创建的Service。
2、绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型。
3、接着就可以调用AIDL的方法了。
-
开源图片框架Picasso、Fresco、Glide对比(参考):
Picasso:
和Square的网络库配合使用最佳,因为他可以选择将网络请求的缓存部分交给Okhttp或者retrofit处理,如果项目中本来使用的网络库是Okhttp或者retrofit,并且没有Gif动图展示的情况下,Picasso无疑是个比较好的选择。
Glide:
模仿了Picasso的链式调用等API,并且在其基础上做了大量的优化,比如支持Gif图片的加载;并且将图片格式由原来的A ARGB_8888改成了RGB_565,内存使用上减少了一半;将只支持缓存全尺寸图片的方式,优化成了不仅可以缓存全尺寸的图,还可以将根据ImageView大小生成的图片缓存起来。Glide功能强大,上手比较快,使用起来比较简单,配置也很方便。所以普通的APP建议使用Glide。
其优点总结如下:
- <1>使用简单,链式调用比较方便.
- <2>占用内存较小,默认使用的是RGB_565格式
- <3>无代码侵入
- <4>支持gif
- <5>缓存优化
- <6>与Activity生命周期绑定,不会出现内存泄漏
Glide与普通的图片库区别:
- 1、Glide设计了一个弱引用缓存,当从内存里加载时,会先从弱引用里面获取图片资源。也就是说在没有触发GC的这段时间,可以重复利用图片资源,减少从LruCache里的操作。
Fresco:
最大的优势是在5.0以下系统的Bitmap加载,在5.0以下系统,Fresco将图片放置一个特殊的区域(Ashmem区)。在图片不显示的时候,该特殊区域的内存会被释放,这会使得APP加载更加流程,减少因为图片内存占用而引发的OOM。为什么说是5.0以下,因为在5.0以后系统默认就是存储在Ashmem区了。Fresco在前人的基础上做了大量的优化,功能比较强大,图片缓存上也使用了三级缓存,是图片加载更加流畅,但是带来的问题就是包比较大,API使用起来比较复杂。所以只建议在专业的图片APP中使用。
-
网络请求框架OkHttp, Volley, Retrofit对比(参考):
OKHttp:
OkHttp是Square公司开源的针对Java和Android程序,封装的一个高性能http请求库,它的职责跟HttpUrlConnection是一样的,而且 OkHttp 又封装了线程池,封装了数据转换,封装了参数使用、错误处理等,api使用起来更加方便。可以把它理解成是一个封装之后的类似HttpUrlConnection的东西,但是在使用的时候仍然需要自己再做一层封装,这样才能像使用一个框架一样更加顺手。
Volley:
Volley是Google官方出品的一款小而精巧的异步网络请求库,该框架封装的扩展性很强,支持HttpClient、HttpUrlConnection, 甚至支持OkHttp,而且Volley里面也封装了ImageLoader,所以如果你愿意你甚至不需要使用图片加载框架,不过这块功能没有一些专门的图片加载框架强大,对于简单的需求可以使用,稍复杂点的需求还是需要用到专门的图片加载框架。Volley也有缺陷,比如不支持post大数据,所以不适合上传文件。不过Volley设计的初衷本身也就是为频繁的、数据量小的网络请求而生。
Retrofit:
Retrofit是Square公司出品的默认基于OkHttp封装的一套RESTful网络请求框架,RESTful是目前流行的一套api设计的风格,并不是标准。Retrofit的封装可以说是很强大,里面涉及到一堆的设计模式,可以通过注解直接配置请求。可以使用不同的http客户端,虽然默认是用http ,可以使用不同Json Converter 来序列化数据,同时提供对RxJava的支持,使用Retrofit + OkHttp + RxJava + Dagger2 可以说是目前比较潮的一套框架,但是需要有比较高的门槛。
综上,如果以上三种网络库你都能熟练掌握,那么优先推荐使用Retrofit,前提是最好你们的后台api也能遵循RESTful的风格, 其次如果不想使用或者没能力掌握Retrofit ,那么推荐使用Volley ,毕竟Volley不需要做过多的封装,如果需要上传大数据, 那么不建议使用 Volley,该采用 OkHttp 。
-
Okhttp的基本实现原理【参考 参考】
OKhttp主要通过3个双端队列(2 个异步队列,1 个同步队列)和5个拦截器来进行工作。内部实现通过一个责任链模式完成,将网络请求的各个阶段封装到各个链条中,实现了各层的解耦。
1、异步请求入队列的时候,首先判断是否异步执行队列的数量是否小于最大值(64),并且当前主机的请求是否小于最大值(5):
- 如果均小于,则将当前任务添加至异步执行队列。并且调用线程池执行该任务, 每次执行完成之后,会将异步等待队列中的任务转移至异步执行队列中【符合条件的话】,并执行当前任务。
- 反之则将其加入到异步等待队列中。
2、同步请求时将其加入同步执行队列,然后执行后续操作。
3、5个拦截器分别是:
- RetryAndFollowUpInterceptor 用于错误重试和重定向
- BridgeInterceptor 应用层和网络层的桥接拦截器,主要工作是为了添加请求cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
- CacheInterceptor 缓存拦截器,如果命中缓存则不会发起网络请求。
- ConnectInterceptor 连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
- CallServerInterceptor 请求拦截器,在前置准备工作完成后,真正发起了网络请求。
OKhttp的底层是通过socket发送HTTP请求与接收响应,但是OKhttp实现了连接池的概念,即对于同一主机多个请求,可以共用同一个socket连接,而不是每次发送完Http请求就关闭底层的socket,这样就实现了连接池的概念。而 OkHttp 对 Socket 的读写操作使用的 OkIo 库进行了一层封装。
- ConnectionPool(连接池)的作用是缓存连接,当然,这个缓存是有时间限制的,空闲时间超过 5 分钟的连接就会被清除。所以连接池中连接的复用是为了避免出现在一段时间内频繁地创建和销毁连接而导致性能下降。
执行流程:
- 通过构建者构建出OkHttpClient对象,再通过newCall方法获得RealCall请求对象.
- 通过RealCall发起同步或异步请求,而决定是异步还是同步请求的是由线程分发器dispatcher来决定.
- 当发起同步请求时会将请求加入到同步队列中依次执行,所以会阻塞UI线程,需要开启子线程执行.
- 当发起异步请求时会创建一个线程池,并且判断请求队列是否大于最大请求队列64,请求主机数是否大于5,如果大于请求添加到异步等待队列中,否则添加到异步执行队列,并执行任务.
-
SurfaceView与TextureView的区别:
SurfaceView与TextureView都能在独立的线程中绘制和渲染,在专用的GPU线程中大大提高渲染的性能。
一、SurfaceView专门提供了嵌入视图层级的绘制界面,开发者可以控制该界面像Size等的形式,能保证界面在屏幕上的正确位置,但Surface是独立的一层View,更像是独立的一个Window,不能加上动画、平移、缩放,且两个SurfaceView不能相互覆盖。
二、TextureView更像是一般的View,像TextView那样能被缩放、平移,也能加上动画。TextureView只能在开启了硬件加速的Window中使用,并且消费的内存要比SurfaceView多,并伴随着1-3帧的延迟。
另外,两者更新画面的方式也有些不同,由于SurfaceView的双缓冲功能,可以是画面更加流畅的运行,但是由于其holder的存在导致画面更新会存在间隔。并且,由于holder的存在,SurfaceView也不能进行像View一样的setAlpha和setRotation方法,但是对于一些类似于坦克大战等需要不断告诉更新画布的游戏来说,SurfaceView绝对是极好的选择。但是比如视频播放器或相机应用的开发,TextureView则更加适合。
-
Android开发框架模式(MVC、MVP、MVVM)区别(参考):
MVC【Model(模型层)---View(视图层)---Controller(控制层)】:
三者形成一个闭环。
Model层:主要用于网络请求、数据库、业务逻辑处理等操作,操作完成后通知View层更新UI展示。
View层:主要用于UI的展示,一般使用xml文件来表示,并相应一些交互逻辑至控制层。
Controller层:控制层的重任落在了众多Activity上,Activity需要交割业务逻辑至Model层处理。
MVP【Model(模型层)---View(视图层)---Presenter(协调器/主持者)】:
MVP是由MVC开发框架模式演变而来的,MVP相对于MVC有如下优点:
1)MVP彻底解耦了Model层和View的交互,我们可以修改视图而不影响模型。
2)可以将大量的逻辑操作放到Presenter中,避免Activity的臃肿。
3)可以更高效地使用模型,因为所有的交互都发生在一个地方---Presenter内部。
4)可以选择将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。
MVVM【Model(模型层)---View(视图层)---ViewModel(视图模型层)】:
MVVM是MVP的又一种演变,将Presenter变换成ViewModel,关键在于View和Model的双向绑定,当View有数据输入时,ViewModel通知Model更新数据。同理,当Model有数据更新时,ViewModel通知View更新数据展示。
MVVM的优势:
1)View和Model双向绑定,一方的改变都会影响另一方,开发者不用再手动去修改UI数据。
2)不需要findViewById也不需要bufferKnife,不需要拿到具体的View
去设置数据绑定监听器等等,这些都可以通过DataBinding来完成。
3)View
和Model
的双向绑定是支持生命周期检测的,不会担心页面销毁了还有回调发生,这个由lifeCycle
完成。
4)不会像MVC一样使Activity变得很臃肿;也不会像MVP一样,在Presenter层使用较多的接口回调。项目结构更加低耦合。
-
Android中数据存储方式:
1)SQLite存储:
SQLite是一种轻量级的数据库,支持基本的SQL语法,是最常用的一种存储方式,Android为此数据库提供了一个名为SQLiteDatabase的类,封装了一些操作数据库的API。
2)SharedPreferences存储:
其本质就是一个xml文件,常用于存储较简单的键值对数据。
3)File存储:
文件(I/O)存储方法,常用语存储大数量的数据,但是缺点是更新数据将是一件困难的事情。
4)ContentProvider:
是Android中实现数据共享的一种存储方式,由于数据通常在各应用间的是互相私密的,所以此存储方式较少使用,但是其又是必不可少的一种存储方式。如图片、音视频、短信、日历、通讯录等,一般都可以采用此种方式进行存储。每个Content Provider都会对外提供一个公共的URI(包装成Uri对象),如果应用程序有数据需要共享时,就需要使用Content Provider为这些数据定义一个URI,其他应用使用ContentResolver来进行操作。
5)网络存储:
将数据通过网络请求将数据存储在服务器端。
-
SharedPreferences的apply()/commit()区别及应用:
区别:
- commit()是把内容同步提交到硬盘;而apply()先把修改提交到内存,然后开启异步线程提交到硬盘;
- commit()有返回值表明修改是否提交成功;而apply()没有返回值(即使提交失败)。
应用:
- 在不关心提交结果是否成功的前提下,优先考虑使用apply()方法;
- 使用commit()方法时需要考虑到耗时及ANR问题;
- 每次添加添加键值对的时候,都会重新写入整个文件的数据,所以不适合大量数据存储;
- 不要把较多数据存储到同一个name对应的SharedPreferences中,最好根据规则拆分为多个SharedPreferences文件;
- 频繁修改的数据修改后统一提交,而不是修改单个数据后马上提交;
- 每次调用edit()方法都会创建一个新的EditorImpl()对象,所以不要频繁调用edit()方法;
- 在跨进程中不建议使用SharedPreferences。
-
LruCache 底层实现原理:
LruCache的底层实现就是通过LinkedHashMap来实现的,LinkedHashMap继承于HashMap,它使用了一个双向链表来存储Map中的Entry顺序关系。对于get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,还要做调整Entry顺序链表的工作。
LruCache中将LinkedHashMap的顺序设置为LRU(Least Recently Used)顺序来实现LRU缓存,每次调用get(从内存中获取图片)或调用put(新插入一个图片对象)都会将该对象元素放置链表尾端,这样当缓存值达到最大限定值时,就会将链表头部的对象(最近最少使用的对象)元素删除。
-
Bitmap内存占用的计算:
Android中一张图片占用的内存主要和如下几种因素有关系:图片的长度、图片的宽度、每个像素值所占用的字节数。所以每张图片占用的内存可以用如下公式来计算:
一张图片(Bitmap)占用的内存=图片长度*图片宽度*单位像素占用的字节数
下面来距离说明:有以下几个常量:
|
分别占用1个字节、2个字节、4个字节、2个字节。那么这几种色彩模式下记载一张100*100大小的图片是所占用的内存大小为:
图片格式() |
占用内存的计算方向 |
一张100*100的图片占用内存的大小 |
ALPHA_8 |
图片长度*图片宽度 |
100*100=10000字节 (10kb) |
ARGB_4444 |
图片长度*图片宽度*2 |
100*100*2=20000字节 (20kb) |
ARGB_8888 |
图片长度*图片宽度*4 |
100*100*4=40000字节 (40kb) |
RGB_565 |
图片长度*图片宽度*2 |
100*100*2=20000字节(20kb) |
注:A(透明度)、R(红色)、G(绿色)、B(蓝色)
-
图片压缩的几种方式:
图片压缩大体上分两种:质量压缩和尺寸压缩。
我们知道Bitmap占用的内存大小 = 图片的宽度 * 图片的高度 * 每个像素点包含的字节数。
质量压缩---不会改变Bitmap占用内存大小,无法避免OOM,但是当把其转成File文件时,确实变小了。
尺寸压缩---可以改变Bitmap所占内存大小,可以避免OOM。
1、质量压缩(应用于图片上传/分享等):
1)质量压缩不会减少图片的像素,它是在保证图片像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的。
2)由于质量压缩后的长、宽、像素均不变,那么Bitmap所占用的内存也是不变的,故无法避免OOM,但可以改变图片在磁盘或者File文件的大小(注:PNG是无损的,所以不能进行质量压缩)。
-
public static Bitmap pressBimmapInQuality(Bitmap bitmap) {
-
-
byte[] bytes;
-
int maxSize = 100;
-
-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
-
//这里的100表示不压缩,将压缩后的数据放到baos中
-
(, 100, baos);
-
bytes = ();
-
int quality = 100;
-
while (().length / 1024 > maxSize && quality > 20) {
-
//清空baos
-
();
-
//每次都减少10
-
quality -= 10;
-
(, quality, baos);
-
}
-
-
bytes = ();
-
bitmap = (bytes, 0, );
-
return bitmap;
-
}
2、采样率压缩:
-
public static Bitmap pressBitmapInSampleSize(Resources res, int resId, int reqWidth, int reqHeight) {
-
Options options = new Options();
-
= true;
-
Bitmap preBitmap = (res, resId, options);
-
int width = ;
-
int height = ;
-
int inSampleSize = 1;
-
if (width > reqWidth || height > reqHeight) {
-
//计算出实际宽高和目标宽高的比率
-
final int halfHeight = height / 2;
-
final int halfWidth = width / 2;
-
-
while ((halfHeight / inSampleSize > reqHeight) && (halfWidth / inSampleSize > reqWidth)) {
-
//按照宽度的比例去取样
-
inSampleSize *= 2;
-
}
-
}
-
= false;
-
= inSampleSize;
-
-
Bitmap afterBitmap = (res, resId, options);
-
return afterBitmap;
-
}
3、缩放法压缩:
-
public static Bitmap pressBitmapInScale(Bitmap bitmap, float widthScale, float heightScale) {
-
Matrix matrix = new Matrix();
-
(widthScale, heightScale);
-
bitmap = (bitmap, 0, 0, (), (), matrix, true);
-
return bitmap;
-
}
4、RGB_565法:
-
public static Bitmap pressBitmapInRGB(Bitmap bitmap, Resources res, int resId) {
-
Options options = new Options();
-
= Config.RGB_565;
-
bitmap = (res, resId, options);
-
return bitmap;
-
}
5、createScaledBitmap法:
-
public static Bitmap pressBitmapInScaledBitmap(Bitmap bitmap) {
-
bitmap = (bitmap, 192 * 1, 108 * 1, true);
-
return bitmap;
-
}
-
Java中Exception与Error的区别是什么:
如下图所示:
Exception和Error都是继承自Throwable类,两者的区别一个表示异常,一个表示错误。他们的区别是:
Exception:表示程序可以处理的异常,可以捕获且可以恢复。遇到这种问题,应该及时去处理,使程序恢复运行,而不应该随意终止异常。Exception又分为运行时异常(Runtime Exception)和受检查异常(Checked Exception)。运行时异常如:ArithmeticException、IllegalArgumentException等,编译时不会有异常,但是一运行就会出现异常,程序不会处理运行时异常,这种异常出现,程序就会终止;受检查异常如:如开启一个线程睡眠的时候,必须要捕获的InterruptedException等,受检查异常,在编译的时候必须要处理,使用try...catch捕获或者使用Throwable向上抛出给其父类处理,否则不会编译通过。
Error:一般表示是与虚拟机有关的问题,如系统崩溃、虚拟机异常、内存溢出、方法调用栈溢出等。对于这类Error问题,我们不应该进行捕获处理。要做的是应该分析找到错误的原因,尽量避免这类错误的发生。如内存溢出(OOM),一般由于内存中瞬间加载了较多超高清的大图引起的,即使我们使用try...catch捕获错误,也没有具体的实际意义,需要的是进行高清大图的处理(如压缩、分块加载等),减少内存的使用,这样才能从根源上避免OOM错误的产生。
综上,两者的区别可以简单的概述为:我们可以从异常中恢复程序,但是不应该尝试从错误中恢复程序。
-
内存泄漏和内存溢出区别:
内存泄漏(Memory Leak):
当一个对象不再使用了,应该被垃圾回收器(JVM)回收,但是这个对象被正在使用的对象所持有,造成无法被回收的结果,即指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终会导致内存溢出。
内存溢出(Out of Memory):
系统会给每个APP分配堆内存,当APP占用的内存加上我们申请需要的内存大小超过了虚拟机所分配的最大内存时就会抛出的Out Of Memory异常。
-
内存泄漏的场景及解决方案(参考1 参考2 参考3):
产生的原因:一个长生命周期的对象持有一个短生命周期的对象的引用。
1)单例模式(如Context)导致的内存泄漏:
单例的生命周期往往跟我们App的生命周期是一致的,如果往单例模式中传入一个生命周期比较短的对象(如Activity),这个时候Activity引用就一直被该单例模式所持有,就会导致内存泄漏。所以此时可以考虑使用Application的Context 来代替 Activity的Context。
2)Handler内存泄漏:
Handler导致的内存泄漏可以归纳为非静态内部类导致的,Handler内部Message是存储在MessageQueue中的,有些Message不能被马上处理,尤其是发送消息时需要Delayed一段时间再执行的Message,这些消息就会存活比较长的时间,导致Handler无法被回收,如果Handler是非静态的,就会导致它的外部类无法被回收,解决方法是<1>使用静态的Handler,外部类引用使用弱引用处理 (Android Handler造成内存泄露的分析和解决)。<2>在退出页面时移除消息队列中的消息。
3)非静态内部类的静态变量/实例:
非静态内部类会持有外部类的引用,如果非静态内部类的变量/实例是静态的,就会长期的维持着外部类的引用,阻止被系统回收,解决办法是:<1>尽量避免静态变量/实例的使用; <2>使用静态内部类。
4)多线程相关的匿名内部类和非静态内部类:
匿名内部类同样会持有外部类的引用,如果在线程中执行耗时操作就有可能发生内存泄漏,导致外部类无法被回收,直到耗时任务结束,解决办法是在页面退出时结束线程中的任务。
5)资源未释放导致的内存泄漏:
资源性对象比如(Cursor、File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们并将引用置位null,以便它们的缓冲及时回收内存。而不是等待GC来处理。尤其是音视频播放的时候,一定要在退出播放的时候进行音视频资源的释放。
6)静态View导致内存泄漏:
使用静态View可以避免每次启动Activity的时候都去读取并渲染View,但是静态View会持有Activity的引用,导致无法被回收,解决方法是当Activity销毁时,将静态View置位null(View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity)。
7)Bitmap导致内存泄漏:
bitmap是比较占内存的,所以一定要在不使用的时候及时调用recycle()释放内存,避免静态变量持有大的bitmap对象。
8)集合中对象没清理造成的内存泄露:
我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。解决方案是在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。
9)注册未取消造成的内存泄露:
很多需要register和unregister的系统服务要在合适的时候进行unregister,手动添加的listener也需要及时移除。
10)WebView内存泄露 [WebView性能优化]:
WebView只要使用一次,内存就不会被释放,所以WebView都存在内存泄漏的问题,通常的解决办法是为WebView单开一个进程,使用AIDL进行通信,根据业务需求在合适的时机释放掉。
-
内存泄漏检测分析工具:
1、Profiler:
打开Profiler --> 选中要检测的APP --> 点击MEMORY模块 ---> 就可以看到变化的内存占用 ---> force GC ---> 点击Dump Java heap ---> 生成.hprof文件。
2、 LeakCanary的工作原理:
- LeakCanary的检测机制,用到了WeakReference(弱引用)和ReferenceQueue(引用队列)。被弱引用关联着的Activity对象如果被回收,该弱引用会被放到引用队列中,通过监测引用队列里面的内容就能检测到Activity是否能够被回收【即在引用队列中,说明可以被回收,不存在泄漏;否则,可能出现内存泄漏,LeakCanary执行一遍GC,若还在没有在引用队列中,就会认定为内存泄漏】。
- 如果Activity被认定为内存泄漏了,则把heap(堆)内存dump到.hprof 文件中。之后通过特定的类分别进行内存文件分析、内存泄漏分析、内存泄漏的展示。
3、KOOM [支持线上监控]【快手开源KOOM浅析,一个高性能线上内存监控方案】:
1) 监控触发时机:
LeakCanary 和 Matrix 都是在 时触发泄漏检测,KOOM 有点另辟蹊径,KOOM 是用阈值检测法来触发。
2) dump 内存堆栈:
Dump hprof是通过虚拟机提供的 API dumpHprofData 实现的,这个过程会 “冻结” 整个应用进程,造成数秒甚至数十秒内用户无法操作,这也是LeakCanary 无法线上部署的最主要原因。
KOOM 使用 fork dump 操作,从当前主进程 fork 出一个子进程,由于 linux 的 copy-on-write 机制,子进程和父进程共享的是一块内存,那么我们就可以在子进程中进行 dump 堆栈,不影响主进程的运行。
3) 分析 hprof 文件:
对于解析,KOOM 做了如下优化:
1、GC root 剪枝,由于我们搜索 Path to GC Root 时,是从 GC Root 自顶向下 BFS,如JavaFrame、MonitorUsed等此类 GC Root 可以直接剪枝。
2、基本类型、基本类型数组不搜索、不解析。
3、同类对象超过阈值时不再搜索。
4、增加预处理,缓存每个类的所有递归 super class,减少重复计算。
5、将object ID的类型从long修改为int,Android虚拟机的object ID大小只有32位,目前shark里使用的都是long来存储的,OOM时百万级对象的情况下,可以节省10M内存。
-
线上内存(泄漏)监控方案参考【参考】:
方案一:
思路:
- 在特定场景下,比如当内存占用超过单个APP可用内存的80%、检测到内存泄漏时,抓取一次内存快照存在问题。
- 在合适的时机将dump文件传到服务器,因为dump文件比较大,不要在应用出于前台的时候上传。
- 使用MAT对其进行分析。
问题:
- 文件会比较大,和对象数量直接正相关。需要考虑dump文件裁剪。
- 上传失败的问题比较严重
- 分析比较困难,可能会存在多个dump文件。
方案二:
思路:
- 将LeakCanary带到线上,在怀疑点使用LeakCanary进行监控,
- 监控到问题的时候,上报相关信息。
问题:
- 需要人工寻找怀疑点,容错性低,监控不全面
- LeakCanery分析性能比较低,内存占用比较高,存在LeakCanary本身发生OOM的风险
方案三:
思路:
定制LeakCanay
LeakCanary分为监控组件和分析组件:
- 使用LeakCanary的监控组件,自动寻找怀疑点,寻找大对象。
- 针对分析组件的分析行为占用内存大的问题,只对大对象进行分析,大对象往往是内存泄漏的元凶,从而解决分析性能低的问题。
- 针对LeakCanary可能会发生OOM问题。解决方案对HPROF文件进行裁剪,分析的时候,不把堆栈信息全部加载到内存,只把相应同一种类型的对象只记录数量和占用内存的情况记录下来(如上图)我们只记录这条GC链路上对象甲乙的数量和内存占用,而不是对象甲A、甲B对记录分析,从而降低其内存占用。
综合方案
- 监控记录App占用内存情况,重点模块的内存占用情况。
- 整体GC次数,重点模块GC次数,GC耗费时间。
- 对LeakCanary改造带到线上。当然关于HPROF文件的裁剪要找到准确性和效率的平衡,对在在客户段分析失败的情况以回传裁剪过的dump文件作为补充。
-
Android性能分析及优化【Android 性能优化总结】:
App的性能优化主要包含以下几个方面:
- 操作流畅度优化
- 内存优化
- 稳定性优化
- 启动速度优化
- APK 瘦身优化
操作流畅度优化【参考】:
在界面绘制的过程中,CPU的主要任务是计算出屏幕上所有 View 对应的图形和向量、纹理等息。 GPU 的主要任务就是把 CPU 计算出的图形栅格化并转化为位图,可以简单理解为屏幕像素点对应的值。而在这过程中需要涉及到内存的分配跟占用。所以造成 UI 卡顿分为从以下这 3 个方面去分析原因:
- CPU耗时
- GPU耗时
- 内存不足【一般有内存泄漏/或占用内存太高(如图片)引起】
1、尽量保持布局的扁平化(减少布局文件的层级)【解决过度绘制问题】:
1)一般单层布局可以实现的UI效果,建议使用FrameLayout或者LinearLayout,如果单层布局,这两种无法实现的,可以考虑使用RelativeLayout来实现。
2)考虑使用<include>标签、<merge>标签和ViewStub。
3)去除冗余背景。
【检查 GPU 渲染速度和过度绘制】
- 手机检测工具:开发者选项 -> 调试GPU过度绘制 -> 显示GPU过度绘制
- Android Studio检测工具:Tools-> Layout Inspector
2、绘制优化【解决渲染卡顿问题】:
1)在onDraw中不要创建新的局部对象,因为它会迅速占用大量内存,产生内存抖动,进而引起频繁的GC。
2)在onDraw方法中不要做耗时的任务,View的绘制帧率保证60fps是最佳的,这就要求每帧的绘制时间不要超过16ms(1000/60)。
3) 代码里是否有不必要的 notifyDataSetChanged,invalidate,requestLayout 等行为;这都会导致对应 View/ViewGroup 重新绘制;
4)通过()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源。
【Android渲染机制】
- 手机检测工具:开发者选项 -> GPU呈现模式分析 -> 在屏幕上显示为条形图
3、卡顿检测工具(类):
- 使用AS中的CPU Profiler:
可以检测主线程中每个函数的执行周期,进而判断是否进行了耗时操作。
- 使用Choreographer【编舞者】:
可以通过接收到的VSYNC(垂直同步)信号,来判定各帧的耗时,进而判断超时长短和掉帧的数量。
- 使用消息分发机制日志原理(BlockCanary原理):
通过观测各消息分发前后的日志打印信息计算出差值,超过设定的阈值,即可打印出具体的堆栈(stacktrace)信息。
内存优化:
1、Bitmap相关的优化:
1)对Bitmap进行采样压缩。
2)超大图采用BitmapRegionDecoder进行分块加载。
3)使用三级缓存策略。
4)使用完Bitmap后,需要及时释放Bitmap资源。
5)选择合适的图片解码方式。
2、线程优化:
使用线程池。线程池可以重用内部的线程,从而避免了线程的创建和销毁的所带来的性能开销;同时线程还能控制最大并发数,避免众多线程,抢占系统资源从而导致阻塞现象的发生。
3、内存泄漏优化:
参考 内存泄漏的场景及解决办法 一题。
4、更加轻量的数据结构:
如使用SparseArray/ArrayMap替代HashMap【以时间换空间】:
- SparseArray更加高效,因为它避免了存取元素时的装箱和拆箱,且节省内存空间【如果key的类型已经确定为int类型,那么优先使用SparseArray -- 数据量不大,最好在千级以内】
- ArrayMap是在容量满的时机触发容量扩大至原来的1.5倍,在容量不足1/3时触发内存收缩至原来的0.5倍,更节省的内存扩容机制。而HashMap是在容量的0.75倍时触发容量扩大至原来的2倍,且没有内存收缩机制。【如果key类型为其它的类型,则使用ArrayMap -- 数据量不大,最好在千级以内】。
【深度解读ArrayMap优势与缺陷】
5、StringBuilder替代String:
代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。
6、避免过多使用枚举:
枚举占用的内存空间比整形大。
7、可以考虑使用软引用和弱引用:
这样可以保证无用对象及时的被回收。
稳定性优化
1、Crash治理
2、ANR分析及解决方案
参考 ANR产生的原因及解决方案 一题。
启动速度优化
//TODO
APK 瘦身优化
//TODO
-
ANR产生的原因及解决方案(参考):
1、ANR 产生的原因:
只有当应用程序的UI线程响应超时才会引起ANR,超时产生的原因有两种:
<1>当前事件没有机会处理,例如UI线程正在响应另外的事件,当前事件被某个事件给阻塞掉了。
<2>当前事件正在处理,但是由于耗时太长没有能及时的完成。
2、ANR的分类:
根据产生原因不同超时时间也不尽相同,可以分为 Activity(5s),Service(20s),Broadcast(10s)。
Service与Bradcast只会打印trace信息,不会提示用户ANR弹窗,大部分可感知的ANR都是由于InputEvent。
3、Android对ANR的监控机制:
Android应用程序是通过消息来驱动的,Android某种意义上也可以说成是一个以消息驱动的系统,UI、事件、生命周期都和消息处理机制息息相关。Android的ANR监测方案也是一样,大部分就是利用了Android的消息机制。
InputEvent的ANR与上图有些许不同,是在Native监控,但同样会堵塞主线程的消息队列,后面会讲到一部分监测场景。
4、应用ANR检测(解决)方案:
- 当发生ANR时,开发者可以结合logcat日志和在手机沙箱目录下的生成文件/data/anr/进行分析和定位。
- 目前流行的ANR检测方案有开源的BlockCanary 、ANR-WatchDog、SafeLooper, 还有根据谷歌原生系统接口监测的方案:FileObserver。
-
RecyclerView四级缓存机制【缓存的对象是ViewHolder】(参考):
1、Scrap:是屏幕内的缓存,一般不需要我们做处理。通过position去找ViewHolder可以直接复用。
2、Cache:Cache可直接拿来复用的缓存(默认大小是2个),性能高效,同样是通过position去找ViewHolder可以直接复用。
3、ViewCacheExtension:需要开发者自定义的缓存,API设计比较奇怪,慎用。
4、RecycledViewPool:四级缓存(默认的缓存数量是5个),通过Type来获取ViewHolder,获取的ViewHolder是全新的,需要重新绑定数据。也就是说可以避免用户调用onCreateViewHolder()方法,提高性能。在ViewPager+RecyclerView的应用场景下可以大有作为
-
Gradle构建流程(参考):
Gradle 构建流程分为三部分:初始化阶段、配置阶段和执行阶段。
1、初始化阶段:
Gradle支持单工程或者多工程构建,初始化阶段的任务是确定有多少工程(Project)需要构建,创建整个项目的层次结构,并且为每一个项目创建一个Project实例对象。
如果是多工程构建,一般都会在根工程目录下声明一个脚本,在脚本中include所有需要参与构建的子工程,通过解析脚本,读取include信息,确定有多少个Project需要构建。
2、配置阶段:
配置阶段的主要任务是生成整个构建过程的有向无环图。
确定了所有需要参与构建的工程后,通过读取解析各个工程对应的脚本,构造Task任务,并根据Task的依赖关系,生成一个基于Task的有向无环图TaskExecutionGraph
3、执行阶段:
通过读取配置阶段生成有向无环图TaskExecutionGraph,按顺序依此执行各个Task,像流水线一样,一步一步构建整个工程,这也是构建过程中最耗时的阶段。
-
快速多渠道打包(VasDolly):
多渠道打包工具对比 | VasDolly | packer-ng-plugin | Walle |
---|---|---|---|
V1签名方案 | 支持 | 支持 | 不支持 |
V2签名方案 | 支持 | 不支持 | 支持 |
已有注释块的APK | 支持 | 不支持 | 不支持 |
根据已有APK生成渠道包 | 支持 | 不支持 | 不支持 |
命令行工具 | 支持 | 支持 | 支持 |
强校验 | 支持 | 不支持 | 不支持 |
多线程加速打包 | 支持 | 不支持 | 不支持 |
-
1、VasDolly基于V1签名的多渠道打包方案:
在APK文件的注释字段,添加渠道信息。 整个方案包括以下几步:
- 复制APK
- 找到EOCD数据块
- 修改注释长度
- 添加渠道信息
- 添加渠道信息长度
- 添加魔数
该方案的最大优点就是:不需要解压缩APK,不需要重新签名,只需要复制APK,在注释字段添加渠道信息。每个渠道包仅需几秒的耗时,非常适合渠道较多的APK。
但是好景不长,Android7.0之后新增了V2签名,该签名会校验整个APK的数据摘要,导致上述渠道打包方案失效。所以如果想继续使用上述方案,需要关闭Gradle Plugin中的V2签名选项,禁用V2签名。
- VasDolly基于V2签名的多渠道打包方案:
由于Android系统只会关注ID为0x7109871a的V2签名块,并且忽略其他的ID-Value,同时V2签名只会保护APK本身,不包含签名块。
因此,基于V2签名的多渠道打包方案就应运而生:在APK签名块中添加一个ID-Value,存储渠道信息。 整个方案包括以下几步:
- 找到APK的EOCD块
- 找到APK签名块
- 获取已有的ID-Value Pair
- 添加包含渠道信息的ID-Value
- 基于所有的ID-Value生成新的签名块
- 修改EOCD的*目录的偏移量(上面已介绍过:修改EOCD的*目录偏移量,不会导致数据摘要校验失败)
- 用新的签名块替代旧的签名块,生成带有渠道信息的APK
该方案的最大优点就是:支持7.0之上新增的V2签名,同时兼有V1方案的所有优点。
-
AOP主流框架对比【一文读懂 AOP】:
-
对热修复的理解(参考 Android 热修复调研报告—流行方案选择):
1、什么是热修复技术:
简单来说,就是通过下发补丁包,让已安装的客户端动态更新,用户不用重新安装APP,就能修复应用缺陷的一种技术。
2、为什么要使用热修复技术:
1)可快速修复bug,避免线上bug带来的业务损失,把损失影响降到最低。
2)保证用户的更新率,无需用户版本升级安装。
3)良好的用户体验,无感知修复异常。
3、国内主流的热修复技术方案:
1)阿里系
名称 | 说明 |
---|---|
AndFix | 开源,实时生效 |
HotFix | 阿里百川,未开源,免费、实时生效 |
Sophix | 未开源,商业收费,实时生效/冷启动修复 |
HotFix是AndFix的优化版本,Sophix是HotFix的优化版本。目前阿里系主推是Sophix。
2)腾讯系
名称 | 说明 |
---|---|
Qzone超级补丁 | QQ空间,未开源,冷启动修复 |
QFix | 手Q团队,开源,冷启动修复 |
Tinker | 微信团队,开源,冷启动修复。提供分发管理,基础版免费 |
3)其他
名称 | 说明 |
---|---|
Robust | 美团, 开源,实时修复 |
Nuwa | 大众点评,开源,冷启动修复 |
Amigo | 饿了么,开源,冷启动修复 |
4、热修复方案如何选择:
- 公司综合实力较强,完全可以考虑自研,灵活可控性较强,但需要考虑到开发及维护成本。
- 如果公司规模较小,建议使用现有的热修复方案。
- 如果考虑付费,可以选择阿里的Sophix,Sophix是综合优化的产物,功能完善、开发简单透明,提供分发及监控管理。
- 如果不考虑付费,只需支持方法级别的Bug修复,不支持资源及so,推荐使用Robust。
- 如果考虑需要同时支持资源及so,推荐使用Tinker。
-
模块化、组件化、插件化的理解(参考):
1、模块化:
主要解决问题是重用问题,不强调单独编译,模块化相互引入,需要引入依赖的module。
2、组件化【参考】:
主要解决问题是功能拆分,强调单独编译,组件化通讯方式分为隐式和路由。组件化常用到的一个路由框架是ARouter【探索Android路由框架-ARouter之基本使用】
- ARouter路由框架原理:注解[APT] + 反射 -----优化-----> 注解[APT] + 插装的方式
【ARouter原理与缺陷解析】
3、插件化【参考】:
是所有组件都为apk的特殊组件化,它是一种动态加载(热插拔)技术。插件化本身是不同进程,因此是binder机制进程间通讯。【Android插件化技术调研】
插件化要解决的3个核心问题:
1、类加载:
1)、宿主apk加载外部的插件,生成这个插件对应的ClassLoader。
2)、根据生成的ClassLoader通过反射,加载插件apk中的类。
【注意:不同插件的ClassLoader可以放到HashMap中,这样就可以保证不同插件中的类彼此互补干扰】
-
//apk文件地址
-
private String dexPath = null;
-
//插件apk名称
-
private String pluginApkName = "";
-
private DexClassLoader dexClassLoader;
-
//释放目录
-
private File fileRelease = null;
-
-
//【加载APK插件】
-
File extractFile = this.getFileStreamPath(pluginApkName);
-
dexPath = ();
-
fileRelease = getDir("dex", 0);
-
dexClassLoader = new DexClassLoader(dexPath, (), null, getClassLoader());
-
-
//【类加载】其中这里Dynamic类实现了IDynamic接口。
-
Class mLoadClassDynamic = (".");
-
Object dynamicObject = ();
-
IDynamic dynamic = (IDynamic)dynamicObject;
2、资源加载:
1)、通过反射调用AssetManager中的addAssetPath方法,并将插件apk的路径传给它,资源就加载到AssetManager了中了。
2)、然后再通过AssetManger来创建一个新的Resources对象,通过这个对象我们就可以访问插件apk中的资源了。
-
// 创建AssetManager对象
-
AssetManager assetManager = ();
-
// 将apk路径添加到AssetManager中
-
Method addAssetPath = ().getMethod("addAssetPath", );
-
(assetManager, dexPath);
-
-
-
// 创建插件Resource对象
-
Resources pluginResources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
3、四大组件生命周期管理(Activity为例):
管理Activity的生命周期的方法有:
1)、反射方式【有性能开销】:
首先通过Java的反射去获取Activity的各种生命周期方法,然后在代理Activity中去调用插件Activity对应的生命周期的方法。
-
@Override
-
protected void onResume() {
-
super.onResume();
-
Method onResume = ("onResume");
-
if(onResume != null) {
-
try {
-
(mRemoteActivity, new Object[] {});
-
} catch(Exception e) {
-
();
-
}
-
}
-
}
2)、接口方式:
将Activity的生命周期的方法提取出来作为一个接口(比如DLPlugin),然后通过代理Activity去调用插件Activity的生命周期方法,这样就完成了插件Activity的生命周期管理。
-
public interface DLPlugin {
-
public void onStart();
-
public void onResume();
-
...
-
...
-
}
-
-
@Override
-
protected void onStart() {
-
//mRemoteActivity就是DLPlugin的实现
-
();
-
super.onStart();
-
}
以下内容为大前端相关:
-
RN、Weex、Flutter框架的差异性:
【理解 Flutter 和 RN/Weex 的差异】【移动端跨平台开发的深度解析】
1、RN / Weex 的代码需要先跑到 Native 层处理一下,然后经过 Native 层渲染到屏幕【控件映射】:
由于 Native 组件可能会随着系统的升级跟着一起升级(API 增、删或变化),RN/Weex 需要写很多胶水层代码来适配不同版本、不同平台的 Native 组件,而 Flutter 就不存在这个问题。
2、Flutter 的代码经过 Flutter 引擎直接就渲染到了屏幕上:
所以很显然Flutter效率会更高,但是Flutter 却不能像 RN/Weex 那般可以直接使用 Native 提供的丰富组件和属性,它需要使用 Flutter 引擎暴露出来的底层 API 做封装,Flutter 引擎之上有一层是 Dart,事实上它就提供了上面我们所说的 Flex 布局能力、类 React 的 DSL 能力、各种动画、CSS rule 等,其实现方式就利用 Flutter 引擎提供的比较底层的可以直接在 GPU 上渲染的 API 能力。
三者整体对比如下:
参考:
Android面试题
给高级Android工程师的进阶手册(扔物线)