Android高级进阶Android进阶之路

攒了一个月的Android面试题及详细解答,年底准备起来,冲刺大

2020-11-20  本文已影响0人  小小小小怪兽_666

一个月前呢,为了巩固下自己的基础以及为以后的面试做准备,每天去找一些大厂的面试真题,然后解答下,然后自己确实也在这个过程中能复习到不少以前没有重视的问题,今天就总结下之前一个多月总结的面试题,难度不大,大佬可以直接路过,当然发发善心点个赞也是可以的❤️。

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还有哪些可以优化的地方

这里有美团团队的总结方案,如下:

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();
    }
}

解决滑动冲突的办法。

解决滑动冲突的根本就是要在适当的位置进行拦截,那么就有两种解决办法:

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)生命周期:

每个调用方法对应的生命周期变化:

Activity 与 Fragment,Fragment 与 Fragment之间怎么交互通信。

Activity有Fragment的实例,所以可以执行Fragment的方法,或者传入一个接口。同样,Fragment可以通过getActivity()获取Activity的实例,也是可以执行方法。

1)直接获取另一个Fragmetn的实例

getActivity().getSupportFragmentManager().findFragmentByTag("mainFragment");

2)接口回调 一个Fragment里面去实现接口,另一个Fragment把接口实例传进去。

3)Eventbus等框架。

Fragment遇到viewpager遇到过什么问题吗。

@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关联起来了。主要有以下几个步骤:

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启动流程,我们该怎么具体进行启动优化。

具体有哪些启动优化方法?

障眼法之闪屏页

为了消除启动时的白屏/黑屏,可以通过设置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) 我们有两个办法能保证进程安全:

SharedPreferences 操作有文件备份吗?是怎么完成备份的?

if (!backupFileExists) {
    !mFile.renameTo(mBackupFile);
}

为什么需要插件化

我觉得最主要的原因是可以动态扩展功能。把一些不常用的功能或者模块做成插件,就能减少原本的安装包大小,让一些功能以插件的形式在被需要的时候被加载,也就是实现了动态加载。

比如动态换肤、节日促销、见不得人的一些功能,就可以在需要的时候去下载相应模式的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 中已收录,大家可以自行获取(或者关注主页扫描加微信获取)。

上一篇 下一篇

猜你喜欢

热点阅读