补:《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

目录
相关文章
|
1月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
67 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
28天前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
26 8
|
26天前
|
Android开发 开发者
Android经典面试题之SurfaceView和TextureView有什么区别?
分享了`SurfaceView`和`TextureView`在Android中的角色。`SurfaceView`适于视频/游戏,独立窗口低延迟,但变换受限;`TextureView`支持复杂变换,视图层级中渲染,适合动画/视频特效,但性能略低。两者在性能、变换、使用和层级上有差异,开发者需按需选择。
15 1
|
29天前
|
SQL Java Unix
Android经典面试题之Java中获取时间戳的方式有哪些?有什么区别?
在Java中获取时间戳有多种方式,包括`System.currentTimeMillis()`(毫秒级,适用于日志和计时)、`System.nanoTime()`(纳秒级,高精度计时)、`Instant.now().toEpochMilli()`(毫秒级,ISO-8601标准)和`Instant.now().getEpochSecond()`(秒级)。`Timestamp.valueOf(LocalDateTime.now()).getTime()`适用于数据库操作。选择方法取决于精度、用途和时间起点的需求。
32 3
|
1月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
29 6
|
1月前
|
XML Android开发 数据格式
Android面试题之DialogFragment中隐藏导航栏
在Android中展示全屏`DialogFragment`并隐藏状态栏和导航栏,可通过设置系统UI标志实现。 记得在布局文件中添加内容,并使用`show()`方法显示`DialogFragment`。
35 2
|
1月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
27 1
|
27天前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
27 0
|
1月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式有哪些用法
Kotlin的Lambda表达式是匿名函数的简洁形式,常用于集合操作和高阶函数。基本语法是`{参数 -&gt; 表达式}`。例如,`{a, b -&gt; a + b}`是一个加法lambda。它们可在`map`、`filter`等函数中使用,也可作为参数传递。单参数时可使用`it`关键字,如`list.map { it * 2 }`。类型推断简化了类型声明。
14 0
|
1月前
|
Android开发 Kotlin
Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
**Kotlin中的匿名函数与Lambda表达式概述:** 匿名函数(`fun`关键字,明确返回类型,支持非局部返回)适合复杂逻辑,而Lambda(简洁语法,类型推断)常用于内联操作和高阶函数参数。两者在语法、返回类型和使用场景上有所区别,但都提供无名函数的能力。
15 0