攒了一个月的Android面试题及详细解答,年底准备起来,冲刺大
一个月前呢,为了巩固下自己的基础以及为以后的面试做准备,每天去找一些大厂的面试真题,然后解答下,然后自己确实也在这个过程中能复习到不少以前没有重视的问题,今天就总结下之前一个多月总结的面试题,难度不大,大佬可以直接路过,当然发发善心点个赞也是可以的❤️。
webView与js通信
1) Android调用JS代码
主要有两种方法:
通过WebView的loadUrl()
// 调用javascript的callJS()方法
mWebView.loadUrl("javascript:callJS()");
但是这种不常用,因为它会自动刷新页面而且没有返回值,有点影响交互。
通过WebView的evaluateJavascript()
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
这种就比较全面了。调用方法并且获取返回值。
2) JS调用Android端代码
主要有两种方法:
通过WebView的addJavascriptInterface()进行对象映射
public class AndroidtoJs extends Object {
// 定义JS需要调用的方法
// 被JS调用的方法必须加入@JavascriptInterface注解
@JavascriptInterface
public void hello(String msg) {
System.out.println("JS调用了Android的hello方法");
}
}
mWebView.addJavascriptInterface(new AndroidtoJs(), "test");
//js中:
function callAndroid(){
// 由于对象映射,所以调用test对象等于调用Android映射的对象
test.hello("js调用了android中的hello方法");
}
这种方法虽然很好用,但是要注意的是4.2以后,对于被调用的函数以@JavascriptInterface进行注解,否则容易出发漏洞,因为js方可以通过反射调用一些本地命令,很危险。
通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url
这种方法是通过shouldOverrideUrlLoading回调去拦截url,然后进行解析,如果是之前约定好的协议,就调用相应的方法。
// 复写WebViewClient类的shouldOverrideUrlLoading方法
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri = Uri.parse(url);
// 如果url的协议 = 预先约定的 js 协议
if ( uri.getScheme().equals("js")) {
// 如果 authority = 预先约定协议里的 webview,即代表都符合约定的协议
if (uri.getAuthority().equals("webview")) {
System.out.println("js调用了Android的方法");
// 可以在协议上带有参数并传递到Android上
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
}
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
}
);
如何避免WebView内存泄露
WebView的内存泄露主要是因为在页面销毁后,WebView的资源无法马上释放所导致的。现在主流的是两种方法:
1)不在xml布局中添加webview标签,采用在代码中new出来的方式,并在页面销毁的时候去释放webview资源
//addview
private WeakReference<BaseWebActivity> webActivityReference = new WeakReference<BaseWebActivity>(this);
mWebView = new BridgeWebView(webActivityReference .get());
webview_container.addView(mWebView);
//销毁
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
mWebView.destroy();
mWebView=null;
2)另起一个进程加载webview,页面销毁后干掉这个进程。但是这个方法的麻烦之处就在于进程间通信。
使用方法很简单,xml文件中写出进程名即可,销毁的时候调用System.exit(0)
<activity android:name=".WebActivity"
android:process=":remoteweb"/>
System.exit(0)
webView还有哪些可以优化的地方
-
提前初始化或者使用全局WebView。首次初始化WebView会比第二次初始化慢很多。初始化后,即使WebView已释放,但一些多WebView共用的全局服务/资源对想仍未释放,而第二次初始化不需要生成,因此初始化变快。
-
DNS采用和客户端API相同的域名,DNS解析也是耗时比较多的部分,所以用客户端API相同的域名因为其DNS会被缓存,所以打开webView的时候就不会再耗时在DNS上了
-
对于JS的优化,尽量不要用偏重的框架,比如React。其次是高性能要求页面还是需要后端渲染。最后就是app中的网页框架要统一,这样就可以对js进行缓存和复用。
这里有美团团队的总结方案,如下:
- WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
- 后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。
- 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
- 同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。
- WebView初始化慢,就随时初始化好一个WebView待用。
- DNS和链接慢,想办法复用客户端使用的域名和链接。
- 脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。
Activity、View、Window 之间的关系。
每个 Activity 包含了一个 Window对象,这个对象是由 PhoneWindow做的实现。而 PhoneWindow 将 DecorView作为了一个应用窗口的根 View,这个 DecorView 又把屏幕划分为了两个区域:一个是 TitleView,一个是ContentView,而我们平时在 Xml 文件中写的布局正好是展示在 ContentView 中的。
说说Android的事件分发机制完整流程,也就是从点击屏幕开始,事件会怎么传递。
我觉得事件分发机制流程可以分为三部分,分别是从外传里,从里传外,消费之后。
1)首先,从最外面一层传到最里面一层:
如果当前是viewgroup层级,就会判断 onInterceptTouchEvent是否为true,如果为true,则代表事件要消费在这一层级,不再往下传递。接着便执行当前 viewgroup 的onTouchEvent方法。如果onInterceptTouchEvent为false,则代表事件继续传递到下一层级的 dispatchTouchEvent方法,接着一样的代码逻辑,一直到最里面一层的view。
伪代码解释:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean isConsume = false;
if (isViewGroup) {
if (onInterceptTouchEvent(event)) {
isConsume = onTouchEvent(event);
} else {
isConsume = child.dispatchTouchEvent(event);
}
} else {
//isView
isConsume = onTouchEvent(event);
}
return isConsume;
}
2)到最里层的view之后,view本身还是可以选择消费或者传到外面。
到最里面一层就会直接执行onTouchEvent方法,这时候,view有没有权利拒绝消费事件呢?按道理view作为最底层的,应该是没有发言权才对。但是呢,秉着公平公正原则,view也是可以拒绝的,可以在onTouchEvent方法返回false,表示他不想消费这个事件。那么它的父容器的onTouchEvent又会被调用,如果父容器的onTouchEvent又返回false,则又交给上一级。一直到最上层,也就是Activity的onTouchEvent被调用。
伪代码解释:
public void handleTouchEvent(MotionEvent event) {
if (!onTouchEvent(event)) {
getParent.onTouchEvent(event);
}
}
3)消费之后
当某一层viewGroup的onInterceptTouchEvent为true,则代表当前层级要消费事件。如果它的onTouchListener被设置了的话,则onTouch会被调用,如果onTouch的返回值返回true,则onTouchEvent不会被调用。如果返回false或者没有设置onTouchListener,则会继续调用onTouchEvent。而onClick方法则是设置了onClickListener则会被正常调用。
伪代码解释:
public void consumeEvent(MotionEvent event) {
if (setOnTouchListener) {
int tag = onTouch();
if (!tag) {
onTouchEvent(event);
}
} else {
onTouchEvent(event);
}
if (setOnClickListener) {
onClick();
}
}
解决滑动冲突的办法。
解决滑动冲突的根本就是要在适当的位置进行拦截,那么就有两种解决办法:
- 外部拦截:从父view端处理,根据情况决定事件是否分发到子view
- 内部拦截:从子view端处理,根据情况决定是否阻止父view进行拦截,其中的关键就是requestDisallowInterceptTouchEvent方法。
1)外部拦截法,其实就是在onInterceptTouchEvnet方法里面进行判断,是否拦截,见代码:
//外部拦截法:父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
//父view拦截条件
boolean parentCanIntercept;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
还是比较简单的,直接判断拦截条件,然后返回true就代表拦截,false就不拦截,传到子view。注意的是ACTION_DOWN状态不要拦截,如果拦截,那么后续事件就直接交给父view处理了,也就没有拦截不拦截的问题了。
2)内部拦截法,就是通过requestDisallowInterceptTouchEvent方法让父view不要拦截。
//父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
//子view.java
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//父view拦截条件
boolean parentCanIntercept;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
requestDisallowInterceptTouchEvent(true)的意思是阻止父view拦截事件,也就是传入true之后,父view就不会再调用onInterceptTouchEvent。反之,传入false就代表父view可以拦截,也就是会走到父view的onInterceptTouchEvent方法。所以需要父view拦截的时候,就传入flase,需要父view不拦截的时候就传入true。
Fragment生命周期,当hide,show,replace时候生命周期变化
1)生命周期:
- onAttach():Fragment和Activity相关联时调用。可以通过该方法获取Activity引用,还可以通过getArguments()获取参数。
- onCreate():Fragment被创建时调用。
- onCreateView():创建Fragment的布局。
- onActivityCreated():当Activity完成onCreate()时调用。
- onStart():当Fragment可见时调用。
- onResume():当Fragment可见且可交互时调用。
- onPause():当Fragment不可交互但可见时调用。
- onStop():当Fragment不可见时调用。
- onDestroyView():当Fragment的UI从视图结构中移除时调用。
- onDestroy():销毁Fragment时调用。
- onDetach():当Fragment和Activity解除关联时调用。
每个调用方法对应的生命周期变化:
- add(): onAttach()->…->onResume()。
- remove(): onPause()->…->onDetach()。
- replace(): 相当于旧Fragment调用remove(),新Fragment调用add()。remove()+add()的生命周期加起来
- show(): 不调用任何生命周期方法,调用该方法的前提是要显示的 Fragment已经被添加到容器,只是纯粹把Fragment UI的setVisibility为true。
- hide(): 不调用任何生命周期方法,调用该方法的前提是要显示的Fragment已经被添加到容器,只是纯粹把Fragment UI的setVisibility为false。
Activity 与 Fragment,Fragment 与 Fragment之间怎么交互通信。
- Activity 与 Fragment通信
Activity有Fragment的实例,所以可以执行Fragment的方法,或者传入一个接口。同样,Fragment可以通过getActivity()获取Activity的实例,也是可以执行方法。
- Fragment 与 Fragment之间通信
1)直接获取另一个Fragmetn的实例
getActivity().getSupportFragmentManager().findFragmentByTag("mainFragment");
2)接口回调 一个Fragment里面去实现接口,另一个Fragment把接口实例传进去。
3)Eventbus等框架。
Fragment遇到viewpager遇到过什么问题吗。
-
滑动的时候,调用setCurrentItem方法,要注意第二个参数smoothScroll。传false,就是直接跳到fragment,传true,就是平滑过去。一般主页切换页面都是用false。
-
禁止预加载的话,调用setOffscreenPageLimit(0)是无效的,因为方法里面会判断是否小于1。需要重写setUserVisibleHint方法,判断fragment是否可见。
-
不要使用getActivity()获取activity实例,容易造成空指针,因为如果fragment已经onDetach()了,那么就会报空指针。所以要在onAttach方法里面,就去获取activity的上下文。
-
FragmentStatePagerAdapter对limit外的Fragment销毁,生命周期为onPause->onStop->onDestoryView->onDestory->onDetach, onAttach->onCreate->onCreateView->onStart->onResume。也就是说切换fragment的时候有可能会多次onCreateView,所以需要注意处理数据。
-
由于可能多次onCreateView,所以我们可以把view保存起来,如果为空再去初始化数据。见代码:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (null == mFragmentView) {
mFragmentView = inflater.inflate(getContentViewLayoutID(), null);
ButterKnife.bind(this, mFragmentView);
isDestory = false;
initViewsAndEvents();
}
return mFragmentView;
}
ARouter的原理
首先,我们了解下ARouter是干嘛的?ARouter是阿里巴巴研发的一个用于解决组件间,模块间界面跳转问题的框架。所以简单的说,就是用来跳转界面的,不同于平时用到的显式或隐式跳转,只需要在对应的界面上添加注解,就可以实现跳转,看个案例:
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
//跳转
ARouter.getInstance().build("/test/activity").navigation();
使用很方便,通过一个path就可以进行跳转了,那么原理是什么呢?
其实仔细思考下,就可以联想到,既然关键跳转过程是通过path跳转到具体的activity,那么原理无非就是把path和Activity一一对应起来就行了。没错,其实就是通过注释,通过apt技术,也就是注解处理工具,把path和activity关联起来了。主要有以下几个步骤:
-
代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activity.class映射关系的类文件
-
app进程启动的时候会加载这些类文件,把保存这些映射关系的数据读到内存里(保存在map里)
-
进行路由跳转的时候,通过build()方法传入要到达页面的路由地址,ARouter会通过它自己存储的路由表找到路由地址对应的Activity.class
-
然后new Intent方法,如果有调用ARouter的withString()方法,就会调用intent.putExtra(String name, String value)方法添加参数
-
最后调用navigation()方法,它的内部会调用startActivity(intent)进行跳转
ARouter怎么实现页面拦截
先说一个拦截器的案例,用作页面跳转时候检验是否登录,然后判断跳转到登录页面还是目标页面:
@Interceptor(name = "login", priority = 6)
public class LoginInterceptorImpl implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
String path = postcard.getPath();
boolean isLogin = SPUtils.getInstance().getBoolean(ConfigConstants.SP_IS_LOGIN, false);
if (isLogin) {
// 如果已经登录不拦截
callback.onContinue(postcard);
} else {
// 如果没有登录,进行拦截
callback.onInterrupt(postcard);
}
}
@Override
public void init(Context context) {
LogUtils.v("初始化成功");
}
}
//使用
ARouter.getInstance().build(ConfigConstants.SECOND_PATH)
.withString("msg", "123")
.navigation(this,new LoginNavigationCallbackImpl());
// 第二个参数是路由跳转的回调
// 拦截的回调
public class LoginNavigationCallbackImpl implements NavigationCallback{
@Override
public void onFound(Postcard postcard) {
}
@Override
public void onLost(Postcard postcard) {
}
@Override
public void onArrival(Postcard postcard) {
}
@Override
public void onInterrupt(Postcard postcard) {
//拦截并跳转到登录页
String path = postcard.getPath();
Bundle bundle = postcard.getExtras();
ARouter.getInstance().build(ConfigConstants.LOGIN_PATH)
.with(bundle)
.withString(ConfigConstants.PATH, path)
.navigation();
}
}
拦截器实现IInterceptor接口,使用注解@Interceptor,这个拦截器就会自动被注册了,同样是使用APT技术自动生成映射关系类。这里还有一个优先级参数priority,数值越小,就会越先执行。
说说你对协程的理解
在我看来,协程和线程一样都是用来解决并发任务(异步任务)的方案。所以协程和线程是属于一个层级的概念,但是对于kotlin中的协程,又与广义的协程有所不同。kotlin中的协程其实是对线程的一种封装,或者说是一种线程框架,为了让异步任务更好更方便使用。
说下协程具体的使用
比如在一个异步任务需要回调到主线程的情况,普通线程需要通过handler切换线程然后进行UI更新等,一旦多个任务需要顺序调用,那更是很不方便,比如以下情况:
//客户端顺序进行三次网络异步请求,并用最终结果更新UI
thread{
iotask1(parameter) { value1 ->
iotask1(value1) { value2 ->
iotask1(value2) { value3 ->
runOnUiThread{
updateUI(value3)
}
}
}
}
}
简直是魔鬼调用,如果不止3次,而是5次,6次,那还得了。。
而用协程就能很好解决这个问题:
//并发请求
GlobalScope.launch(Dispatchers.Main) {
//三次请求并发进行
val value1 = async { request1(parameter1) }
val value2 = async { request2(parameter2) }
val value3 = async { request3(parameter3) }
//所有结果全部返回后更新UI
updateUI(value1.await(), value2.await(), value3.await())
}
//切换到io线程
suspend fun request1(parameter : Parameter){withContext(Dispatcher.IO){}}
suspend fun request2(parameter : Parameter){withContext(Dispatcher.IO){}}
suspend fun request3(parameter : Parameter){withContext(Dispatcher.IO){}}
就像是同一个线程中顺序执行的效果一样,再比如我要按顺序执行一次异步任务,然后完成后更新UI,一共三个异步任务。如果正常写应该怎么写?
thread{
iotask1() { value1 ->
runOnUiThread{
updateUI1(value1)
iotask2() { value2 ->
runOnUiThread{
updateUI2(value2)
iotask3() { value3 ->
runOnUiThread{
updateUI3(value3)
}
}
}
}
}
}
}
晕了晕了,不就是一次异步任务,一次UI更新吗。怎么这么麻烦,来,用协程看看怎么写:
GlobalScope.launch (Dispatchers.Main) {
ioTask1()
ioTask1()
ioTask1()
updateUI1()
updateUI2()
updateUI3()
}
suspend fun ioTask1(){
withContext(Dispatchers.IO){}
}
suspend fun ioTask2(){
withContext(Dispatchers.IO){}
}
suspend fun ioTask3(){
withContext(Dispatchers.IO){}
}
fun updateUI1(){
}
fun updateUI2(){
}
fun updateUI3(){
}
协程怎么取消
取消协程作用域将取消它的所有子协程。
// 协程作用域 scope
val job1 = scope.launch { … }
val job2 = scope.launch { … }
scope.cancel()
取消子协程
// 协程作用域 scope
val job1 = scope.launch { … }
val job2 = scope.launch { … }
job1.cancel()
但是调用了cancel并不代表协程内的工作会马上停止,他并不会组织代码运行。比如上述的job1,正常情况处于active状态,调用了cancel方法后,协程会变成Cancelling状态,工作完成之后会变成Cancelled 状态,所以可以通过判断协程的状态来停止工作。
Jetpack 中定义的协程作用域(viewModelScope 和 lifecycleScope)可以帮助你自动取消任务,下次再详细说明,其他情况就需要自行进行绑定和取消了。
之前大家应该看过我写的启动流程分析了吧,那篇文章里我说过分析源码的目的一直都不是为了学知识而学,而是理解了这些基础,我们才能更好的解决问题。所以今天就来看看通过分析app启动流程,我们该怎么具体进行启动优化。
- App启动流程中我们能进行优化的地方有哪些?
- 具体有哪些优化方法?
- 分析启动耗时的方法
具体有哪些启动优化方法?
障眼法之闪屏页
为了消除启动时的白屏/黑屏,可以通过设置android:windowBackground,让人感觉一点击icon就启动完毕了的感觉。
<activity android:name=".ui.activity.启动activity"
android:theme="@style/MyAppTheme"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<style name="MyAppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@drawable/logo</item>
</style>
预创建Activity
对象第一次创建的时候,java虚拟机首先检查类对应的Class 对象是否已经加载。如果没有加载,jvm会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。
第三方库懒加载
很多第三方开源库都说在Application中进行初始化,所以可以把一些不是需要启动就初始化的三方库的初始化放到后面,按需初始化,这样就能让Application变得更轻。
WebView启动优化
webview第一次启动会非常耗时,具体优化方法可以看我之前的文章,关于webview的优化。
线程优化
线程是程序运行的基本单位,线程的频繁创建是耗性能的,所以大家应该都会用线程池。单个cpu情况下,即使是开多个线程,同时也只有一个线程可以工作,所以线程池的大小要根据cpu个数来确定。
分析启动耗时的方法
Systrace + 函数插桩
也就是通过在方法的入口和出口加入统计代码,从而统计方法耗时
class Trace{
public static void i(String tag){
android.os.Trace.beginSection(tag);
}
public static void o(){
android.os.Trace.endSection();
}
}
void test(){
Trace.i("test");
System.out.println("doSomething");
Trace.o();
}
BlockCanary BlockCanary 可以监听主线程耗时的方法,就是在主线程消息循环打出日志的地入手, 当一个消息操作时间超过阀值后, 记录系统各种资源的状态, 并展示出来。所以我们将阈值设置低一点,这样的话如果一个方法执行时间超过200毫秒,获取堆栈信息。
而记录时间的方法我们之前也说过,就是通过looper()方法中循环去从MessageQueue中去取msg的时候,在dispatchMessage方法前后会有logging日志打印,所以只需要自定义一个Printer,重写println(String x)方法即可实现耗时统计了。
SharedPreferences是如何保证线程安全的,其内部的实现用到了哪些锁
SharedPreferences的本质是用键值对的方式保存数据到xml文件,然后对文件进行读写操作。
对于读操作,加一把锁就够了:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
对于写操作,由于是两步操作,一个是editor.put,一个是commit或者apply所以其实是需要两把锁的:
//第一把锁,操作Editor类的map对象
public final class EditorImpl implements Editor {
@Override
public Editor putString(String key, String value) {
synchronized (mEditorLock) {
mEditorMap.put(key, value);
return this;
}
}
}
//第二把锁,操作文件的写入
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
是进程安全的吗?如果是不安全的话我们作为开发人员该怎么办?
1) SharedPreferences是进程不安全的,因为没有使用跨进程的锁。既然是进程不安全,那么久有可能在多进程操作的时候发生数据异常。
2) 我们有两个办法能保证进程安全:
- 使用跨进程组件,也就是ContentProvider,这也是官方推荐的做法。通过ContentProvider对多进程进行了处理,使得不同进程都是通过ContentProvider访问SharedPreferences。
- 加文件锁,由于SharedPreferences的本质是读写文件,所以我们对文件加锁,就能保证进程安全了。
SharedPreferences 操作有文件备份吗?是怎么完成备份的?
- SharedPreferences 的写入操作,首先是将源文件备份:
if (!backupFileExists) {
!mFile.renameTo(mBackupFile);
}
- 再写入所有数据,只有写入成功,并且通过 sync 完成落盘后,才会将 Backup(.bak) 文件删除。
- 如果写入过程中进程被杀,或者关机等非正常情况发生。进程再次启动后如果发现该 SharedPreferences 存在 Backup 文件,就将 Backup 文件重名为源文件,原本未完成写入的文件就直接丢弃,这样就能保证之前数据的正确。
为什么需要插件化
我觉得最主要的原因是可以动态扩展功能。把一些不常用的功能或者模块做成插件,就能减少原本的安装包大小,让一些功能以插件的形式在被需要的时候被加载,也就是实现了动态加载。
比如动态换肤、节日促销、见不得人的一些功能,就可以在需要的时候去下载相应模式的apk,然后再动态加载功能。所以一般这个功能适用于一些平台类的项目,比如大众点评美团这种,功能很多,用户很大概率只会用其中的一些功能,而且这些模块单独拿出来都可以作为一个app运行。
但是现在用的却很少了,具体情况见第三点。
插件化的原理
要实现插件化,也就是实现从apk读取所有数据,要考虑三个问题:
- 读取插件代码,完成插件中代码的加载和与主工程的互相调用
- 读取插件资源,完成插件中资源的加载和与主工程的互相访问
- 四大组件管理
1)读取插件代码,其实也就是进行插件中的类加载。所以用到类加载器就可以了。Android中常用的有两种类加载器,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。
区别在于DexClassLoader多传了一个optimizedDirectory参数,表示缓存我们需要加载的dex文件的,并创建一个DexFile对象,而且这个路径必须为内部存储路径。而PathClassLoader这个参数为null,意思就是不会缓存到内部存储空间了,而是直接用原来的文件路径加载。所以DexClassLoader功能更为强大,可以加载外部的dex文件。
同时由于双亲委派机制,在构造插件的ClassLoader时会传入主工程的ClassLoader作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。而主工程调用插件则需要通过DexClassLoader去加载类,然后反射调用方法。
2)读取插件资源,主要是通过AssetManager进行访问。
具体代码如下:
/**
* 加载插件的资源:通过AssetManager添加插件的APK资源路径
*/
protected void loadPluginResources() {
//反射加载资源
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}
通过addAssetPath方法把插件的路径穿进去,就可以访问到插件的资源了。
3)四大组件管理 为什么单独说下四大组件呢?因为四大组件不仅要把他们的类加载出来,还要去管理他们的生命周期,在AndroidManifest.xml中注册。这也是插件化中比较重要的一部分。这里重点说下Activity。
主要实现方法是通过Hook技术,主要的方案就是先用一个在AndroidManifest.xml中注册的Activity来进行占坑,用来通过AMS的校验,接着在合适的时机用插件Activity替换占坑的Activity。
“
Hook 技术又叫做钩子函数,在系统没有调用该函数之前,钩子程序就先捕获该消息,钩子函数先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。简单来说,就是把系统的程序拉出来变成我们自己执行代码片段。
”
这里的hook其实就是我们常说的下钩子,可以改变函数的内部行为。
这里加载插件Activity用到hook技术,有两个可以hook的点,分别是:
Hook IActivityMana****ger上面说了,首先会在AndroidManifest.xml中注册的Activity来进行占坑,然后合适的时机来替换我们要加载的Activity。所以我们主要需要两步操作:第一步:使用占坑的这个Activity完成AMS验证。也就是让AMS知道我们要启动的Activity是在xml里面注册过的哦。具体代码如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".contains(method.getName())) {
//换掉
Intent intent = null;
int index = 0;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof Intent) {
//说明找到了startActivity的Intent参数
intent = (Intent) args[i];
//这个意图是不能被启动的,因为Acitivity没有在清单文件中注册
index = i;
}
}
//伪造一个代理的Intent,代理Intent启动的是proxyActivity
Intent proxyIntent = new Intent();
ComponentName componentName = new ComponentName(context, proxyActivity);
proxyIntent.setComponent(componentName);
proxyIntent.putExtra("oldIntent", intent);
args[index] = proxyIntent;
}
return method.invoke(iActivityManagerObject, args);
}
第二步:替换回我们的Activity。上面一步是把我们实际要启动的Activity换成了我们xml里面注册的activity来躲过验证,那么后续我们就需要把Activity换回来。
Activity启动的最后一步其实是通过H(一个handler)中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,最终会调用Activity的onCreate方法。最后会调用到Handler的dispatchMessage方法用于处理消息,如果Handler的Callback类型的mCallback不为null,就会执行mCallback的handleMessage方法。所以我们能hook的点就是这个mCallback。
public static void hookHandler() throws Exception {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Object currentActivityThread= FieldUtil.getField(activityThreadClass ,null,"sCurrentActivityThread");//1
Field mHField = FieldUtil.getField(activityThread,"mH");//2
Handler mH = (Handler) mHField.get(currentActivityThread);//3
FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
}
public class HCallback implements Handler.Callback{
//...
@Override
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
Object r = msg.obj;
try {
//得到消息中的Intent(启动SubActivity的Intent)
Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
//得到此前保存起来的Intent(启动TargetActivity的Intent)
Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
//将启动SubActivity的Intent替换为启动TargetActivity的Intent
intent.setComponent(target.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
mHandler.handleMessage(msg);
return true;
}
}
用自定义的HCallback来替换mH中的mCallback即可完成Activity的替换了。
Hook Instrumentation
这个方法是由于startActivityForResult方法中调用了Instrumentation的execStartActivity方法来激活Activity的生命周期,所以可以通过替换Instrumentation来完成,然后在Instrumentation的execStartActivity方法中用占坑SubActivity来通过AMS的验证,在Instrumentation的newActivity方法中还原TargetActivity。
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
private PackageManager mPackageManager;
public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
mInstrumentation = instrumentation;
mPackageManager = packageManager;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
if (infos == null || infos.size() == 0) {
intent.putExtra(HookHelper.TARGET_INTENsT_NAME, intent.getComponent().getClassName());//1
intent.setClassName(who, "com.example.liuwangshu.pluginactivity.StubActivity");//2
}
try {
Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,
target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
if (!TextUtils.isEmpty(intentName)) {
return super.newActivity(cl, intentName, intent);
}
return super.newActivity(cl, className, intent);
}
}
public static void hookInstrumentation(Context context) throws Exception {
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
Field mMainThreadField =FieldUtil.getField(contextImplClass,"mMainThread");//1
Object activityThread = mMainThreadField.get(context);//2
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mInstrumentationField=FieldUtil.getField(activityThreadClass,"mInstrumentation");//3
FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",new InstrumentationProxy((Instrumentation) mInstrumentationField.get(activityThread),
context.getPackageManager()));
}
更多面试复习资源
去好公司面试,能答出来只是第一步,延伸问答、灵活运用才是面试官的目的,你越能答,他们越能问。我希望读者们能知道深入了解的含义,这真的是一个过程。
自己的知识准备得怎么样,这直接决定了你能否顺利通过一面和二面,所以在面试前来一个知识梳理,看需不需要提升自己的知识储备是很有必要的。
关于知识梳理,这里再分享一下我面试这段时间的复习路线:(以下体系的复习资料是我从各路大佬收集整理好的)

知识梳理完之后,就需要进行查漏补缺,所以针对这些知识点,我手头上也准备了不少的电子书和笔记,这些笔记将各个知识点进行了完美的总结。
《379页Android开发面试宝典》
历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图,以上资源均免费分享,以上内容均放在了开源项目:【github】 中已收录,大家可以自行获取(或者关注主页扫描加微信获取)。