Android进阶之路Android面试相关《Java、Android等等面试》收集

Android面试题收录及解答10月刊

2020-10-26  本文已影响0人  积木zz

前言

嗨,大家好,好久不见。一个月没写过文章了,这里跟大家侃侃这中间发生了什么。

一个月前呢,想准备面试,就网上随便找找面试题什么的,发现要么就是卖课的,要么就是不给详细回答的或者回答不够深的(也许是我没找到😢)。反正稍微有点苦恼,因为我毕竟是个懒人,就想看看面试题,然后自己思考下,顺便看看一些参考回答,看看自己回答的全不全面等等。

于是,我就想干脆我自己做这个事吧,就算没人看,也当我自己每天复习下了。于是,我就建了一个小小公众号(小到确实没人看,哈哈哈),每天去找一些大厂的面试真题,然后解答下,然后自己确实也在这个过程中能复习到不少以前没有重视的问题,今天就总结下之前一个多月总结的面试题,难度不大,大佬可以直接路过,当然发发善心点个赞也是可以的❤️。

进入正题,下面为10月刊内容,每三个问题为一个小节,也就是一个专题文章,我就不具体区分了,由于字数问题,也只节选了一些问题,大家见谅。另外答的不好的地方大家也可以留言敲敲我,感谢。

10月刊内容

网页中输入url,到渲染整个界面的整个过程,以及中间用了什么协议?

1)过程分析:主要分为三步

2)其中涉及到TCP/IP协议簇,包括DNS,TCP,IP,HTTP协议等等。

具体介绍下TCP/IP

TCP/IP一般指的是TCP/IP协议簇,主要包括了多个不同网络间实现信息传输涉及到的各种协议
主要包括以下几层:

TCP的三次握手和四次挥手,为什么不是两次握手?为什么挥手多一次呢?

客户端简称A,服务器端简称B
1)TCP建立连接需要三次握手

2)TCP断开连接需要四次挥手

3)为什么挥手多一次
其实正常的断开和连接都是需要四次

但是连接中,第二步和第三步是可以合并的,因为连接之前A和B是无联系的,所以没有其他情况需要处理。而断开的话,因为之前两端是正常连接状态,所以第二步的时候不能保证B之前的消息已经发送完毕,所以不能马上告诉A要断开的消息。这就是连接为什么可以少一步的原因。

4)为什么连接需要三次,而不是两次。
正常来说,我给你发消息,你告诉我能收到,不就代表我们之前通信是正常的吗?

TCP 协议为了实现可靠传输, 通信双方需要判断自己已经发送的数据包是否都被接收方收到, 如果没收到, 就需要重发。

TCP是怎么保证可靠传输的?

所以握手过程中,比如A发送syn信号给B,初始序列号为120,那么B收到消息,回复ack消息,序列号为120+1。同时B发送syn信号给A,初始序列号为256,如果收不到A的回复消息,就会重发,否则丢失这个序列号,就无法正常完成后面的通信了。

这就是三次握手的原因。

TCP和UDP的区别?

TCP提供的是面向连接,可靠的字节流服务。即客户和服务器交换数据前,必须现在双方之间建立一个TCP连接(三次握手),之后才能传输数据。并且提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。

UDP 是一个简单的面向数据报的运输层协议。它不提供可靠性,只是把应用程序传给IP层的数据报发送出去,但是不能保证它们能到达目的地。由于UDP在传输数据报前不用再客户和服务器之间建立一个连接,且没有超时重发等机制,所以传输速度很快。

所以总结下来就是:

可以看到TCP适用于稳定的应用场景,他会保证数据的正确性和顺序,所以一般的浏览网页,接口访问都使用的是TCP传输,所以才会有三次握手保证连接的稳定性。
而UDP是一种结构简单的协议,不会考虑丢包啊,建立连接等。优点在于数据传输很快,所以适用于直播,游戏等场景。

HTTP的几种请求方法具体介绍

常见的有四种:

HTTP请求和响应报文的格式,以及常用状态码

1)请求报文:

   //请求行(包括method、path、HTTP版本)
   GET /s HTTP/1.1
   //Headers
   Host: www.baidu.com
   Content-Type: text/plain
   //Body
   搜索****

2)响应报文

   //状态行 (包括HTTP版本、状态码,状态信息)
   HTTP/1.1 200 OK
   //Headers
   Content-Type: application/json; charset=utf-8
   //Body
   [{"info":"xixi"}]

3)常用状态码

主要分为五种类型:

介绍对称加密和非对称加密

1)对称加密,即加密和解密算法不同,但是密钥相同。比如DES,AES算法。

数据A --> 算法D(密钥S)--> 加密数据B
加密数据B --> 算法E(密钥S)--> 数据A

优点:
缺点:密钥有可能被破解,容易被伪造。传输过程中一旦密钥被其他人获知则可以进行数据解密。

2)非对称加密,即加密和解密算法相同,但是密钥不同。私钥自己保存,公钥提供给对方。比如RSA,DSA算法。

数据A --> 算法D(公钥)--> 加密数据B
加密数据B --> 算法D(私钥)--> 数据A

优点:安全,公钥即使被其他人获知,也无法解密数据。
缺点:需要通信双方都有一套公钥和私钥

数字签名的原理

1)首先,为什么需要数字签名?
防止被攻击,被伪造。由于公钥是公开的,别人截获到公钥就能伪造数据进行传输,所以我们需要验证数据的来源。

2)怎么签名?
由于公钥能解密 私钥加密的数据,所以私钥也能解密 公钥加密的数据。(上图非对称加密A和B代号互换即可)
所以我们用公钥进行加密后,再用私钥进行一次加密,那么私钥的这次加密就叫签名,也就是只有我自己可以进行加密的操作。所以传输数据流程就变成了加密数据和签名数据,如果解出来都是同样的数据,那么则数据安全可靠

数据A --> 算法D(公钥)--> 加密数据B
数据A --> 算法D(私钥)--> 签名数据C

加密数据B --> 算法D(私钥)--> 数据A
签名数据C --> 算法D(公钥)--> 数据A

Base64算法是什么,是加密算法吗?

为什么多线程同时访问(读写)同个变量,会有并发问题?

说说原子性,可见性,有序性分别是什么意思?

实际项目过程中,有用到多线程并发问题的例子吗?

有,比如单例模式
由于单例模式的特殊性,可能被程序中不同地方多个线程同时调用,所以为了避免多线程并发问题,一般要采用volatile+Synchronized的方式进行变量,方法保护。

    private volatile static Singleton singleton;

    public static Singleton getSingleton4() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }

        }
        return singleton;
    }

介绍几种启动模式。

Activity依次A→B→C→B,其中B启动模式为singleTask,AC都为standard,生命周期分别怎么调用?如果B启动模式为singleInstance又会怎么调用?B启动模式为singleInstance不变,A→B→C的时候点击两次返回,生命周期如何调用。

1)A→B→C→B,B启动模式为singleTask

2)A→B→C→B,B启动模式为singleInstance

3)A→B→C,B启动模式为singleInstance,点击两次返回键

屏幕旋转时Activity的生命周期,如何防止Activity重建。

线程的三种启动方式

1)继承thread类

    class MyThread :Thread(){
        override fun run() {
            super.run()
        }
    }

    fun test(){
        var t1=MyThread()
        t1.start()
    }

2)实现runnable接口

    class MyRunnable : Runnable {
        override fun run() {

        }
    }

    fun test() {
        var t1 = Thread(MyRunnable(),"test")
        t1.start()
    }

3)实现 Callable 接口

    class MyCallThread : Callable<String> {
        override fun call(): String {
            return "i got it"
        }

    }

    fun test() {
        var task = FutureTask(MyCallThread())
        var t1 = Thread(task, "test")
        t1.start()
        try {
            //获取结果
            var result = task.get()
        } catch (e: Exception) {
        }
    }

也有人表示其实是两个方法,因为第三个方法FutureTask也是实现了Runnable的方法,只不过表现方法不一样,然后带返回值。这个大家面试的时候可以都说上,然后说说自己的见解,毕竟要让面试官多多看到你的知识面。

线程run和start的区别

简单的说就是:
调用start方法方可启动线程,而run方法只是thread类中的一个普通方法调用,不用启动新线程,还是在主线程里执行。

线程的几种状态,相互之间是如何转化的

1) 初始状态(New)。新创建了一个线程对象就进入了初始状态,也就是通过上述新建线程的几个方法就能进入该状态。

2) 可运行状态,就绪状态(RUNNABLE)。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权。以下几种方式会进入可运行状态:

3)运行状态(RUNNING)。可运行状态(runnable)的线程获得了cpu 时间片 ,执行程序代码。线程调度程序从可运行池中选择一个线程作为当前线程,就会进入运行状态。

4)阻塞状态(BLOCKED)。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。wait,sleep,suspend等方法都可以导致线程阻塞。

5)死亡状态(DEAD)。线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

String是java中的基本数据类型吗?是可变的吗?是线程安全的吗?

为什么要设计成不可变的呢?如果String是不可变的,那我们平时赋值是改的什么呢?

1)为什么设计不可变

2)平时使用双引号方式赋值的时候其实是返回的字符串引用,并不是改变了这个字符串对象

浅谈一下String, StringBuffer,StringBuilder的区别?String的两种创建方式,在JVM的存储方式相同吗?

String是不可变类,每当我们对String进行操作的时候,总是会创建新的字符串。操作String很耗资源,所以Java提供了两个工具类来操作String - StringBuffer和StringBuilder

StringBuffer和StringBuilder是可变类,StringBuffer是线程安全的,StringBuilder则不是线程安全的。所以在多线程对同一个字符串操作的时候,我们应该选择用StringBuffer。由于不需要处理多线程的情况,StringBuilder的效率比StringBuffer高。

1) String常见的创建方式有两种

2)存储方式不同

线程池是干嘛的,优点有哪些?

线程池主要用作管理子线程,优点有:

线程池的构造方法每个参数是什么意思,执行任务的流程

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {}

其中,拒绝策略有四种:

执行任务流程:

Android线程池主要分为哪几类,分别代表了什么?

主要有四类:FixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledTheadPool

1) FixedThreadPool——可重用固定线程数的线程池

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  1. CachedThreadPool——按需创建的线程池
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  1. SingleThreadExecutor——单线程的线程池
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  1. ScheduledThreadPool——定时和周期性的线程池
    private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

索引是什么,优缺点

数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询,更新数据库中表的数据.索引的实现通常使用B树和变种的B+树(mysql常用的索引就是B+树)

优点

缺点

事务四大特性

数据库事务必须具备ACID特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的英文缩写。

一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

讲讲几个范式

范式的英文名称是Normal Form,它是英国人E.F.Codd(关系数据库的老祖宗)在上个世纪70年代提出关系数据库模型后总结出来的。范式是关系数据库理论的基础,也是我们在设计数据库结构过程中所要遵循的规则和指导方法。通常所用到的只是前三个范式,即:第一范式(1NF),第二范式(2NF),第三范式(3NF)

Recycleview和listview区别

Recycleview有几级缓存,缓存过程?

Recycleview有四级缓存,分别是mAttachedScrap(屏幕内),mCacheViews(屏幕外),mViewCacheExtension(自定义缓存),mRecyclerPool(缓存池)

四级缓存按照顺序需要依次读取。所以完整缓存流程是:

  1. 保存缓存流程:
  1. 获取缓存流程:

需要注意的是,如果从缓存池找到缓存,还需要重新bindview。

说说RecyclerView性能优化。

void onItemsInsertedOrRemoved() {
   if (hasFixedSize) layoutChildren();
   else requestLayout();
}
new LinearLayoutManager(this) {
    @Override
    protected int getExtraLayoutSpace(RecyclerView.State state) {
        return size;
    }
};

说说双重校验锁,以及volatile的作用

先回顾下双重校验锁的原型,也就是单例模式的实现:

public class Singleton {
    private volatile static Singleton mSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == mSingleton) {
            synchronized (Singleton.class) {
                if (null == mSingleton) {
                    mSingleton = new Singleton();
                }
            }
        }
        return mSingleton;
    }
}

有几个疑问需要解决:

接下来一一解答:

如果进行了指令重排,由于不影响结果,所以2和3有可能被调换。所以就变成了:

1)分配内存空间
2)将对象指向分配的空间
3)初始化对象

就有可能会导致,假如线程A中已经进行到第二步,线程B进入第二次判空的时候,判断mSingleton不为空,就直接返回了,但是实际此时mSingleton还没有初始化。

synchronized和volatile的区别

synchronized修饰static方法和修饰普通方法有什么区别

内存泄漏是什么,为什么会发生?

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
简单点说,手机给我们的应用提供了一定大小的堆内存,在不断创建对象的过程中,也在不断的GC(java的垃圾回收机制),所以内存正常情况下会保持一个平稳的值。
但是出现内存泄漏就会导致某个实例,比如Activity的实例,应用被某个地方引用到了,不能正常释放,从而导致内存占用越来越大,这就是内存泄漏

内存泄漏发生的情况有哪些?

主要有四类情况

1)集合类泄漏

集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露。

static List<Object> mList = new ArrayList<>();
   for (int i = 0; i < 100; i++) {
       Object obj = new Object();
      mList.add(obj);
       obj = null;
    }

解决办法就是把集合也释放掉。

  mList.clear();
  mList = null;

2)单例/静态变量造成的内存泄漏

单例模式具有其静态特性,它的生命周期等于应用程序的生命周期,正是因为这一点,往往很容易造成内存泄漏。


public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

    private SingleInstance(Context context){
        this.mContext = context;
    }

    public static SingleInstance newInstance(Context context){
        if(mInstance == null){
            mInstance = new SingleInstance(context);
        }
        return sInstance;
    }
}


比如这个单例模式,如果我们调用newInstance方法时候把Activity的context传进去,那么就是生命周期长的持有了生命周期短的引用,造成了内存泄漏。要修改的话把context改成context.getApplicationContext()即可。

3)匿名内部类/非静态内部类

非静态内部类他会持有他外部类的强引用,所以就有可能导致非静态内部类的生命周期可能比外部类更长,容易造成内存泄漏,最常见的就是Handler

public class TestActivity extends Activity {
private TextView mText;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);


        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    
        mHandler. sendEmptyMessageDelayed(0, 100000);
    }

怎么修改呢?改成静态内部类,然后弱引用方式修饰外部类

public class TestActivity extends Activity {
    private TextView mText;
    private MyHandler myHandler = new MyHandler(TestActivity.this);
    private MyThread myThread = new MyThread();

    private static class MyHandler extends Handler {

        WeakReference<TestActivity> weakReference;

        MyHandler(TestActivity testActivity) {
            this.weakReference = new WeakReference<TestActivity>(testActivity);

        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            weakReference.get().mText.setText("do someThing");
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
    }

4)资源未关闭造成的内存泄漏

比如:

该怎么发现和解决内存泄漏?

1、使用工具,比如Memory Profiler,可以查看app的内存实时情况,捕获堆转储,就生成了一个内存快照,hprof文件。通过查看文件,可以看到哪些类发生了内存泄漏。

2、使用库,比较出名的就是LeakCanary,导入库,然后运行后,就可以发现app内的内存泄漏情况。

这里说下LeakCanary的原理:

什么是类加载机制?

我们编写的java文件会在编译后变成.class文件,类加载器就是负责加载class字节码文件,class文件在文件开头有特定的文件标识,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎Execution Engine决定。

简单来说类加载机制就是从文件系统将一系列的 class 文件读入 JVM 内存中为后续程序运行提供资源的动作。

类加载器种类。

类加载器种类主要有四种:

属于依次继承关系,也就是上一级是下一级的父加载器。

什么是双亲委派机制,为什么这么设计?

当一个类加载器收到了类加载的请求,它不会直接去加载这类,而是先把这个请求委派给父加载器去完成,依次会传递到最上级也就是启动类加载器,然后父加载器会检查是否已经加载过该类,如果没加载过,就会去加载,加载失败才会交给子加载器去加载,一直到最底层,如果都没办法能正确加载,则会跑出ClassNotFoundException异常。

举例:

这么设计的原因是为了防止危险代码的植入,比如String类,如果在AppClassLoader就直接被加载,就相当于会被篡改了,所以都要经过老大,也就是BootstrapClassLoader进行检查,已经加载过的类就不需要再去加载了。

webView与js通信

1) Android调用JS代码

主要有两种方法:

// 调用javascript的callJS()方法
mWebView.loadUrl("javascript:callJS()");

但是这种不常用,因为它会自动刷新页面而且没有返回值,有点影响交互。

mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此处为 js 返回的结果
        }
    });

这种就比较全面了。调用方法并且获取返回值。

2) JS调用Android端代码

主要有两种方法:



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方可以通过反射调用一些本地命令,很危险。

这种方法是通过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做的实现。而 PhoneWindowDecorView作为了一个应用窗口的根 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处理了,也就没有拦截不拦截的问题了。

  1. 内部拦截法,就是通过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,那么原理无非就是把pathActivity一一对应起来就行了。没错,其实就是通过注释,通过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,数值越小,就会越先执行。

怎么应用到组件化中

首先,在公用组件的build.gradle中添加依赖:

dependencies {
    api 'com.alibaba:arouter-api:1.4.0'
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
}

其次,必须在每个业务组件,也就是用到了arouter的组件中都声明annotationProcessorOptions,否则会无法通过apt生成索引文件,也就无法正常跳转了:

//业务组件的build.gradle
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}
dependencies {
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
    implementation '公用组件'
}

这个arguments是用来设置给编译处理器的一些参数,这里就把[AROUTER_MODULE_NAME: project.getName()]键值对传了过去,方便Arouter使用apt的时候进行数据处理,也是Arouter库所规定的配置。

然后就可以正常使用了。

说说你对协程的理解

在我看来,协程和线程一样都是用来解决并发任务(异步任务)的方案。
所以协程和线程是属于一个层级的概念,但是对于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>

对象第一次创建的时候,java虚拟机首先检查类对应的Class 对象是否已经加载。如果没有加载,jvm会根据类名查找.class文件,将其Class对象载入。同一个类第二次new的时候就不需要加载类对象,而是直接实例化,创建时间就缩短了。

很多第三方开源库都说在Application中进行初始化,所以可以把一些不是需要启动就初始化的三方库的初始化放到后面,按需初始化,这样就能让Application变得更轻。

webview第一次启动会非常耗时,具体优化方法可以看我之前的文章,关于webview的优化。

线程是程序运行的基本单位,线程的频繁创建是耗性能的,所以大家应该都会用线程池。单个cpu情况下,即使是开多个线程,同时也只有一个线程可以工作,所以线程池的大小要根据cpu个数来确定。

由于65536方法限制,所以一般class文件要生成多个dex文件,Android5.0以下,ClassLoader加载类的时候只会从class.dex(主dex)里加载,所以要执行MultiDex.install(context)方法才能正常读取dex类。

而这个install方法就是耗时大户,会解压apk,遍历dex文件,压缩dex、将dex文件通过反射转换成DexFile对象、反射替换数组。

这里需要的方案就是今日头条方案:

1、在Application的attachBaseContext方法里,启动另一个进程的LoadDexActivity去异步执行MultiDex逻辑,显示Loading。
2、然后主进程Application进入while循环,不断检测MultiDex操作是否完成
3、MultiDex执行完之后主进程Application继续走,ContentProvider初始化和Application onCreate方法,也就是执行主进程正常的逻辑。

所以重点就是单开进程去执行MultiDex逻辑,这样就不影响APP的启动了。

分析启动耗时的方法

也就是通过在方法的入口和出口加入统计代码,从而统计方法耗时

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

而记录时间的方法我们之前也说过,就是通过looper()方法中循环去从MessageQueue中去取msg的时候,在dispatchMessage方法前后会有logging日志打印,所以只需要自定义一个Printer,重写println(String x)方法即可实现耗时统计了。

Activity、View、Window三者如何关联?

Activity包含了一个PhoneWindow,而PhoneWindow就是继承于Window的,Activity通过setContentView将View设置到了PhoneWindow上,而View通过WindowManager的addView()、removeView()、updateViewLayout()对View进行管理。Window的添加过程以及Activity的启动流程都是一次IPC的过程。Activity的启动需要通过AMS完成;Window的添加过程需要通过WindowSession完成。

onCreate,onResume,onStart里面,什么地方可以获得宽高

如果在onCreate、onStart、onResume中直接调用View的getWidth/getHeight方法,是无法得到View宽高的正确信息,因为view的measure过程与Activity的生命周期是不同步的,所以无法保证在这些生命周期里view
的measure已经完成。所以很有可能获取的宽高为0。

所以主要有以下三个方法来获取view的宽高:

在该方法里的runnable对象,能保证view已经绘制完成,也就是执行完measure、layout和draw方法了。

        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getWidth();
                int hight = view.getHeight();
            }
        });

Activity中可以重写onWindowFocusChanged方法,该方法表示Activity的窗口得到焦点或者失去焦点的时候,所以Activitiy获取焦点时,view肯定绘制完成了,这时候获取宽高也是没问题的:

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            int width = view.getWidth();
            int hight = view.getHeight();
        }
    }

ViewTreeObserver是一个观察者,主要是用来观察视图树的各种变化。OnGlobalLayoutListener的作用是当View树的状态发生改变或者View树中某view的可见性发生改变时,OnGlobalLayoutListener的onGlobalLayout方法将会被回调。因此,此时获取view的宽高也是可以的。

        ViewTreeObserver observer = title_name.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
               int width = view.getWidth();
               int hight = view.getHeight();
            }
        });

为什么view.post可以获得宽高,有看过view.post的源码吗?

能获取宽高的原因肯定就是因为在此之前view 绘制已经完成,所以View.post() 添加的任务能够保证在所有 View 绘制流程结束之后才被执行。

看看post的源码:

 public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action); 
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().post(action);  
        return true;
    }

//RunQueue .class
     void post(Runnable action) {
            postDelayed(action, 0);
        }
 
        void postDelayed(Runnable action, long delayMillis) {
            HandlerAction handlerAction = new HandlerAction();
            handlerAction.action = action;
            handlerAction.delay = delayMillis;
 
            synchronized (mActions) {
                mActions.add(handlerAction);
            }
        }

        void executeActions(Handler handler) {
            synchronized (mActions) {
                final ArrayList<HandlerAction> actions = mActions;
                final int count = actions.size();
 
                for (int i = 0; i < count; i++) {
                    final HandlerAction handlerAction = actions.get(i);
                    handler.postDelayed(handlerAction.action, handlerAction.delay);
                }
 
                actions.clear();
            }
        }

所以在执行View.post()的方法时,那些Runnable并没有马上被执行,而是保存到RunQueue里面,然后通过executeActions方法执行,也就是通过handler,post了一个延时任务Runnable。而executeActions方法什么时候会执行呢?


private void performTraversals() {
     getRunQueue().executeActions(attachInfo.mHandler);
    ...
    performMeasure();
    ...
    performLayout();
    ...
    performDraw();
 }

可以看到在performTraversals方法中执行了,但是在view绘制之前,这是因为在绘制之前就把需要执行的runnable封装成Message发送到MessageQueue里排队了,但是Looper不会马上去取这个消息,因为Looper会按顺序取消息,主线程还有什么消息没执行完呢?其实就是当前的这个performTraversals所在的任务,所以要等下面的·performMeasure,performLayout,performDraw·都执行完,也就是view绘制完毕了,才会去执行之前我们post的那个runnable,也就是我们能在view.post方法里的runnable能获取宽高的主要原因了。

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类的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中常用的有两种类加载器,DexClassLoaderPathClassLoader,它们都继承于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的点,分别是:

  @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的替换了。

这个方法是由于startActivityForResult方法中调用了Instrumentation的execStartActivity方法来激活Activity的生命周期,所以可以通过替换Instrumentation来完成,然后在InstrumentationexecStartActivity方法中用占坑SubActivity来通过AMS的验证,在InstrumentationnewActivity方法中还原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()));
    }

市面上的一些插件化方案以及你的想法

前几年插件化还是很火的,比如Dynamic-Load-Apk(任玉刚),DroidPlugin,RePlugin(360),VirtualApk(滴滴),但是现在机会都没怎么在运营了,好多框架都最多只支持到Android9。

这是为什么呢?我觉得一个是维护成本太高难以兼容,每更新一次源码,就要重新维护一次。二就是确实插件化技术现在用的不多了,以前用插件化框架干嘛?主要是比如增加新的功能,让功能模块之间解耦。现在有RN可以进行插件化功能,有组件化可以进行项目解耦。所以用的人就不多咯。

虽然插件化用的不多了,但是我觉得技术还是可以了解的,而且热更新主要用的也是这些技术。方案可以被淘汰,但是技术不会。

参考

多线程
内存泄露
启动优化
view.post
view.post
SharedPreferences

总结

希望给大家一点帮助吧,当然文章我也会继续写的,感觉大家之前给我点的赞,嘿嘿。

大家一起加油吧!共勉!爱你们!

有一起学习的小伙伴可以关注下公.众.号❤️——码上积木。

上一篇下一篇

猜你喜欢

热点阅读