补:《Android面试题思考与解答》2021年3月刊(四)

简介: 回来啦,《Android面试题思考与解答21年3月刊》送给大家。

ActivityThread中做了哪些关于Handler的工作?(为什么主线程不需要单独创建Looper)


主要做了两件事:


  • 1、在main方法中,创建了主线程的LooperMessageQueue,并且调用loop方法开启了主线程的消息循环。


public static void main(String[] args) {
        Looper.prepareMainLooper();
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        Looper.loop();
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }


  • 2、创建了一个Handler来进行四大组件的启动停止等事件处理


final H mH = new H();
class H extends Handler {
        public static final int BIND_APPLICATION        = 110;
        public static final int EXIT_APPLICATION        = 111;
        public static final int RECEIVER                = 113;
        public static final int CREATE_SERVICE          = 114;
        public static final int STOP_SERVICE            = 116;
        public static final int BIND_SERVICE            = 121;


IdleHandler是啥?有什么使用场景?


之前说过,当MessageQueue没有消息的时候,就会阻塞在next方法中,其实在阻塞之前,MessageQueue还会做一件事,就是检查是否存在IdleHandler,如果有,就会去执行它的queueIdle方法。


private IdleHandler[] mPendingIdleHandlers;
    Message next() {
        int pendingIdleHandlerCount = -1;
        for (;;) {
            synchronized (this) {
                //当消息执行完毕,就设置pendingIdleHandlerCount
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                //初始化mPendingIdleHandlers
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                //mIdleHandlers转为数组
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
            // 遍历数组,处理每个IdleHandler
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                //如果queueIdle方法返回false,则处理完就删除这个IdleHandler
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;
        }
    }


当没有消息处理的时候,就会去处理这个mIdleHandlers集合里面的每个IdleHandler对象,并调用其queueIdle方法。最后根据queueIdle返回值判断是否用完删除当前的IdleHandler


然后看看IdleHandler是怎么加进去的:


Looper.myQueue().addIdleHandler(new IdleHandler() {  
    @Override  
    public boolean queueIdle() {  
        //做事情
        return false;    
    }  
});
    public void addIdleHandler(@NonNull IdleHandler handler) {
        if (handler == null) {
            throw new NullPointerException("Can't add a null IdleHandler");
        }
        synchronized (this) {
            mIdleHandlers.add(handler);
        }
    }


ok,综上所述,IdleHandler就是当消息队列里面没有当前要处理的消息了,需要堵塞之前,可以做一些空闲任务的处理。


常见的使用场景有:启动优化


我们一般会把一些事件(比如界面view的绘制、赋值)放到onCreate方法或者onResume方法中。但是这两个方法其实都是在界面绘制之前调用的,也就是说一定程度上这两个方法的耗时会影响到启动时间。


所以我们可以把一些操作放到IdleHandler中,也就是界面绘制完成之后才去调用,这样就能减少启动时间了。


但是,这里需要注意下可能会有坑。


如果使用不当,IdleHandler会一直不执行,比如在View的onDraw方法里面无限制的直接或者间接调用View的invalidate方法


其原因就在于onDraw方法中执行invalidate,会添加一个同步屏障消息,在等到异步消息之前,会阻塞在next方法,而等到FrameDisplayEventReceiver异步任务之后又会执行onDraw方法,从而无限循环。


具体可以看看这篇文章:https://mp.weixin.qq.com/s/dh_71i8J5ShpgxgWN5SPEw


HandlerThread是啥?有什么使用场景?


直接看源码:


public class HandlerThread extends Thread {
    @Override
    public void run() {
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
    }


哦,原来如此。HandlerThread就是一个封装了Looper的Thread类。


就是为了让我们在子线程里面更方便的使用Handler。


这里的加锁就是为了保证线程安全,获取当前线程的Looper对象,获取成功之后再通过notifyAll方法唤醒其他线程,那哪里调用了wait方法呢?


public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }


就是getLooper方法,所以wait的意思就是等待Looper创建好,那边创建好之后再通知这边正确返回Looper。


IntentService是啥?有什么使用场景?


老规矩,直接看源码:


public abstract class IntentService extends Service {
    private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }
    @Override
    public void onCreate() {
        super.onCreate();
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();
        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }
    @Override
    public void onStart(@Nullable Intent intent, int startId) {
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    }


理一下这个源码:


  • 首先,这是一个Service
  • 并且内部维护了一个HandlerThread,也就是有完整的Looper在运行。
  • 还维护了一个子线程的ServiceHandler
  • 启动Service后,会通过Handler执行onHandleIntent方法。
  • 完成任务后,会自动执行stopSelf停止当前Service。


所以,这就是一个可以在子线程进行耗时任务,并且在任务执行后自动停止的Service

BlockCanary使用过吗?说说原理


BlockCanary是一个用来检测应用卡顿耗时的三方库。


上文说过,View的绘制也是通过Handler来执行的,所以如果能知道每次Handler处理消息的时间,就能知道每次绘制的耗时了?那Handler消息的处理时间怎么获取呢?

再去loop方法中找找细节:


public static void loop() {
    for (;;) {
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        msg.target.dispatchMessage(msg);
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
    }
}


可以发现,loop方法内有一个Printer类,在dispatchMessage处理消息的前后分别打印了两次日志。


那我们把这个日志类Printer替换成我们自己的Printer,然后统计两次打印日志的时间不就相当于处理消息的时间了?


Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }


这就是BlockCanary的原理。


具体介绍可以看看作者的说明:http://blog.zhaiyifan.cn/2016/01/16/BlockCanaryTransparentPerformanceMonitor/


说说Hanlder内存泄露问题。


这也是常常被问的一个问题,Handler内存泄露的原因是什么?


"内部类持有了外部类的引用,也就是Hanlder持有了Activity的引用,从而导致无法被回收呗。"


其实这样回答是错误的,或者说没回答到点子上。


我们必须找到那个最终的引用者,不会被回收的引用者,其实就是主线程,这条完整引用链应该是这样:


主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity


具体分析可以看看我之前写的这篇文章:https://juejin.cn/post/6909362503898595342


利用Handler机制设计一个不崩溃的App?


主线程崩溃,其实都是发生在消息的处理内,包括生命周期、界面绘制。


所以如果我们能控制这个过程,并且在发生崩溃后重新开启消息循环,那么主线程就能继续运行。


Handler(Looper.getMainLooper()).post {
        while (true) {
            //主线程异常拦截
            try {
                Looper.loop()
            } catch (e: Throwable) {
            }
        }
    }


还有一些特殊情况处理,比如onCreate内发生崩溃,具体可以看看文章

《能否让APP永不崩溃》https://juejin.cn/post/6904283635856179214


JVM中如何决定对象是否可以回收


JVM中通过可达性分析算法来决定对象是否可以回收。


具体做法就是把内存中所有对象之间的引用关系看做一条关系链,比如A持有B的引用,B持有C的引用。而在JVM中有一组对象作为GC Root,也就是根节点,然后从这些节点开始往下搜索,查看引用链,最后判断对象的引用链是否可达来决定对象是否可以被回收。


为了方便大家理解,我画了一张图来说明:


20.png


很明显,ABCD四个引用都是GCRoot可达的,通俗点讲,就是跟GCRoot直接或间接有关系,有线连着的。而EF虽然直接连着线,但是他们和GCRoot是没关系的,也就是GCRoot不可达的对象组。


所以当GC发生的时候,EF就会被回收。


GC发生的内存区域


在说GC发生的内存区域之前,我们先聊聊JVM中的内存分配。


在JVM中,主要有内存分成了五个数据区域:


  • 程序计数器:线程私有,主要用作记录当前线程执行的位置。
  • 虚拟机栈:线程私有,描述Java方法执行的内存模型。
  • 本地方法栈:线程私有,描述本地(native)方法执行的内存模型。
  • :存放对象实例。
  • 方法区:存放类信息、常量、静态变量等


通过上面的介绍,我们了解到前三个都是线程私有,所以会随着线程的死亡而消失。

而后面两块内存区域,也就是堆和方法区是所有线程共有的,如果不处理可能内存就会一直增长,直到超出可用内存。所以需要借助GC机制对这些区域内的无用内存进行回收,特别是堆区的内存,因为堆区就是存储对象实例的。


GC发生的时机


那具体什么时候会被回收呢?主要有两种情况:


  • 在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。
  • 在应用层,开发者可以调用System.gc()来请求一次 GC。


GCRoot的类型


刚才说过了可达性分析算法,所以大家应该知道GCRoot的重要性了。


GCRoot,说白了就是JVM认证的可以作为老大的人选,只有这些对象是可以作为引用链的头头,掌管并保护着有用的引用。


在Java中,有以下几种对象可以被作为GCRoot,这些对象是不会被GC的:


  • Java 虚拟机栈(局部变量表)中的引用的对象。


这里又涉及到一个问题了,什么是局部变量表。


刚才说过虚拟机栈是用于支持方法调用或者执行的数据结构,具体是怎么操作的呢?


当某个方法被执行,就会在虚拟机栈中创建一个栈帧,也就是一个方法就对应着一个栈帧,栈帧会管理方法调用和执行所有的数据结构。


而栈帧中又分为几块存储空间,进行存储方法对应的不同的数据结构,比如局部变量表就是用于存储方法参数和方法内创建的局部变量。


所以这第一个GC Root 指得就是方法的参数或者方法中创建的参数。


public class GCTest {
    public static void test1(){
        //局部变量作为GCRoot
        GCRoot root=new GCRoot();
        System.gc();
    }
}


顺便说下栈帧中其他几个内存结构:


  • 局部变量表:存储方法参数和方法内创建的局部变量
  • 操作数栈:后入先出栈。当方法执行过程中,就会通过操作数栈来进行参数传递,又或者进行加数
  • 动态连接:支持方法调用过程中的动态连接。
  • 返回地址:在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态,而这个返回地址区域就是用于存储返回地址信息的。一般方法正常退出时,是可以将调用者的PC计数器值作为返回地址。
  • 方法区中静态引用指向的对象。


这个很好理解,指得就是静态变量。


public class GCTest {
    private static GCRoot root2;
    public static void main(String[] args) {
        //静态变量作为GCRoot
        root2=new GCRoot();
        System.gc();        
    }
}


  • 仍处于存活状态中的线程对象。


活着的线程,比如主线程,上一篇文章就说过Handler内存泄露的原因就是被主线程所引用,所以无法被回收。


Thread root3=new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    public void test2(){
        //活着的线程作为GCRoot
        root3.start();
        System.gc();
    }


  • Native 方法中 JNI 引用的对象。


在JNI中有如下三种引用类型可供使用:


  • 局部引用
  • 全局引用
  • 弱全局引用


其中局部引用和全局引用都可以作为GC Root,不会被GC回收。


编译打包的过程中有哪些task会执行


//aidl 转换aidl文件为java文件
> Task :app:compileDebugAidl
//生成BuildConfig文件
> Task :app:generateDebugBuildConfig
//获取gradle中配置的资源文件
> Task :app:generateDebugResValues
// merge资源文件
> Task :app:mergeDebugResources
// merge assets文件
> Task :app:mergeDebugAssets
> Task :app:compressDebugAssets
// merge所有的manifest文件
> Task :app:processDebugManifest
//AAPT 生成R文件
> Task :app:processDebugResources
//编译kotlin文件
> Task :app:compileDebugKotlin
//javac 编译java文件
> Task :app:compileDebugJavaWithJavac
//转换class文件为dex文件
> Task :app:dexBuilderDebug
//打包成apk并签名
> Task :app:packageDebug


简单介绍v1、v2、v3、v4签名


之前大家比较熟知的签名工具是JDK提供的jarsigner,而apksigner是Google专门为Android提供的签名和签证工具。


其区别就在于jarsigner只能进行v1签名,而apksigner可以进行v2、v3、v4签名。


  • v1签名


v1签名方式主要是利用META-INFO文件夹中的三个文件。


首先,将apk中除了META-INFO文件夹中的所有文件进行进行摘要写到 META-INFO/MANIFEST.MF;然后计算MANIFEST.MF文件的摘要写到CERT.SF;最后计算CERT.SF的摘要,使用私钥计算签名,将签名和开发者证书写到CERT.RSA。


所以META-INFO文件夹中这三个文件就能保证apk不会被修改。


但是缺点也很明显,META-INFO文件夹不会被签名,所以美团针对这种签名方式设计了一种多渠道打包方案:


利用pythone在META-INFO文件夹中创建一个文件,其名称就是渠道名,然后用java去读取文件名获取渠道。


  • v2签名


Android7.0之后,推出了v2签名,为了解决v1签名速度慢以及签名不完整的问题。

apk本质上是一个压缩包,而压缩包文件格式一般分为三块:


文件数据区,中央目录结果,中央目录结束节。


而v2要做的就是,在文件中插入一个APK签名分块,位于中央目录部分之前,如下图:


19.png


这样处理之后,文件就完成无法修改了。


  • v3签名


Android 9 推出了v3签名方案,和v2签名方式基本相同,不同的是在v3签名分块中添加了有关受支持的sdk版本和新旧签名信息,可以用作签名替换升级。


  • v4签名


Android 11 推出了v4签名方案。


v4 签名基于根据 APK 的所有字节计算得出的 Merkle 哈希树。它完全遵循 fs-verity 哈希树的结构,将签名存储在单独的.apk.idsig 文件中。


参考


《Android开发艺术探索》


https://juejin.cn/post/6896751245722615815

https://juejin.cn/post/6891911483379482637

https://mp.weixin.qq.com/s/kQmH2GnwW8FK-yNmWcheTA

https://segmentfault.com/a/1190000021357383

https://blog.csdn.net/lmj623565791/article/details/72859156

https://developer.android.google.cn/guide/components/services#Lifecycle

http://gityuan.com/2017/03/10/job_scheduler_service/

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1856

https://www.zhihu.com/question/34652589

https://segmentfault.com/a/1190000003063859

https://juejin.cn/post/6844904150140977165

https://juejin.cn/post/6893791473121280013

https://www.jianshu.com/p/bfb13eb3a425

https://segmentfault.com/a/1190000020386580

https://www.jianshu.com/p/02db8b55aae9

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc

https://www.runoob.com/design-pattern/design-pattern-tutorial.html

https://www.jianshu.com/p/ae2fe5481994

https://juejin.cn/post/6895369745445748749

目录
相关文章
|
7月前
|
XML Java Android开发
Android面试官最喜欢问15道面试题
Android面试官最喜欢问15道面试题
91 0
|
1月前
|
存储 安全 Java
Android 面试题及答案整理,最新面试题
Android 面试题及答案整理,最新面试题
79 2
|
7月前
|
XML 网络协议 Java
Android最新面经,面试题,附答案,赶紧收藏起来吧
Android最新面经,面试题,附答案,赶紧收藏起来吧
|
消息中间件 存储 IDE
Android体系课--Handler-Handler面试题
面试官:说说Handler基本使用原理
|
安全 API Android开发
Android 面试题:说一下 PendingIntent 和 Intent 的区别
Android 面试题:说一下 PendingIntent 和 Intent 的区别
392 0
Android 面试题:说一下 PendingIntent 和 Intent 的区别
|
前端开发 网络协议 安全
Android 面试题二
Android 面试题二
150 0
|
消息中间件 存储 设计模式
Android 面试题
Android 面试题
149 0
|
Android开发
关于Android Service服务的面试题
距离上一次面试的已经过去两个月了,想起来面试的题目,想给大家分享一下,希望大家能用到。
259 0
|
Android开发
Android面试题:bindService获取代理是同步还是异步
Android面试题:bindService获取代理是同步还是异步
374 0
Android面试题:bindService获取代理是同步还是异步