美文网首页
Android应用开发岗 面试汇总-Android进阶篇

Android应用开发岗 面试汇总-Android进阶篇

作者: hahaoop | 来源:发表于2022-03-25 13:47 被阅读0次

    背景

    最近在准备面试,结合之前的工作经验和近期在网上收集的一些面试资料,准备将Android开发岗位的知识点做一个系统的梳理,整理成一个系列:Android应用开发岗 面试汇总。本系列将分为以下几个大模块:
    Java基础篇Java进阶篇常见设计模式
    Android基础篇Android进阶篇性能优化
    网络相关数据结构与算法
    常用开源库、Kotlin、Jetpack

    注1:以上文章将陆续更新,直到我找到满意的工作为止,有跳转链接的表示已发表的文章。
    注2:该系列属于个人的总结和网上东拼西凑的结果,每个知识点的内容并不一定完整,有不正确的地方欢迎批评指正。
    注3:部分摘抄较多的段落或有注明出处。如有侵权,请联系本人进行删除。

    1、Handler原理

    1、使用

    一般用来在子线程中做耗时操作,执行完后通过Handler来发送执行结果到主线程。
    即:在主线程中实例化一个Handler,拿到引用后,在子线程中通过这个引用发送消息到主线程。

    //一般在Activity中实例化Handler,等同于在主线程中声明Handler,new Handler(Looper.getLooper())
    private Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what){
                    case UPDATE:
                        tv.setText(String.valueOf(msg.arg1));
                        break;
                }
            }
        };
    public void begin(){
            new Thread(new Runnable() {
                @Override
                public void run() {
                        try {
                            Thread.sleep(1000);//休眠1秒,模拟耗时操作
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        Message msg = new Message();
                        msg.what = UPDATE;
                        msg.arg1 = i;
                        handler.sendMessage(msg);
                }
            }).start();
        }
    

    2、Handler初始化流程

    构造方法的参数

    • Callback 传入该实例,实现handleMessage(Message msg)方法,可不传。等价于上面的handleMessage方法。
    • Looper 传入该实例,则会替换掉默认的Looper.myLooper()参数,即指定哪个线程的Looper。
    • async 很少使用到,默认是false。

    Handler在构造方法中初始化Looper和MessageQueue

    • Looper如果未在构造方法中指定,则拿到的是Handler被实例化那个线程的Looper。通过Looper.myLooper()方法拿到,该方法中调用ThreadLocal<Looper> sThreadLocal 的get方法拿到:mLooper
    • MessageQueue为消息队列,通过mQueue = mLooper.mQueue拿到,即mQueue实际是在Looper类中被实例化的。

    ActivityThread与Looper 的调用

    • 一般Android中,是由ActivityThread类的main方法创建Looper对象,即应用的主线程
    • 其中main方法中会初始化MainLooper、ActivityThread、mMainHandler
    • main方法中调用Looper.prepareMainLooper()、Looper.loop()
    • 以上是安卓主线程初始化的流程
    • 自定义使用Handler,流程如下:(跟主线程使用方式一样)
    class LooperThread extends Thread {
      *      public Handler mHandler;
      *
      *      public void run() {
      *          Looper.prepare();
      *
      *          mHandler = new Handler() {
      *              public void handleMessage(Message msg) {
      *                  // process incoming messages here
      *              }
      *          };
      *
      *          Looper.loop();
      *      }
    

    总结(UI线程中的Handler)

    ActivityThread类中的main方法,调用
    --1、Looper.prepareMainLooper()
    --2、prepareMainLooper 方法中调用prepare方法初始化Looper对象(主线程初始化,则为MainLooper),将该对象塞入ThreadLocal<Looper> sThreadLocal变量中
    --3、初始化主线程的Handler,该Handler会放入到线程池中
    --4、调用Looper.loop(),开始消息循环

    Handler的工作流程

    1、某个线程的handler实例(一般为UI线程)调用handler.sendMessage(msg)(任意线程中调用),将Message对象加入到Looper的MessageQueue中(MessageQueue的实例在Looper中,Handler中拿到的是它的引用),Looper的loop方法是一个死循环(for(;;)),会不停的消费掉msg对象,消费掉一个,就会调用msg.target.dispatchMessage(msg),同时将msg移出队列。target即为handler的引用。
    2、Handler的dispatchMessage方法中,调用了handleMessage的回调方法,供外部处理线程中生成的msg。

    4cf3b5423e960035a18fb6d75d3c6440.png

    参考链接

    • 面试题1:子线程中怎么使用 Handler?(即自定义使用Handler,参考上面↑)
      即在子线程中通过Looper.prepare()拿到一个ThreadLocal<Looper> sThreadLocal的Looper实例,再创建一个Handler,再开启死循环Looper.loop()

    • 面试题2:MessageQueue 如何等待消息?
      通过 Looper.loop 方法,MessageQueue.next() 来获取消息的,如果没有消息,那就会阻塞在这里,MessageQueue.next 是怎么等待的呢?next方法最终调用了native的epoll_wait 来进行等待

    • 面试题3:线程和 Handler Looper MessageQueue 的关系?
      一个线程对应一个 Looper 对应一个 MessageQueue 对应多个 Handler

    • 面试题4:多个线程给 MessageQueue 发消息,如何保证线程安全
      既然一个线程对应一个 MessageQueue,那多个线程给 MessageQueue 发消息时是如何保证线程安全的呢?答:加了锁 synchronized

    // MessageQueue.java
    boolean enqueueMessage(Message msg, long when) {
        synchronized (this) {
            // ...
        }
    }
    
    • 面试题5:Handler 消息延迟是怎么处理的?
    // Handler.java
    public final boolean postDelayed(Runnable r, long delayMillis) {
        return sendMessageDelayed(getPostMessage(r), delayMillis);
    }
    public final boolean sendMessageDelayed(Message msg, long delayMillis) {
        // 传入的 time 是 uptimeMillis + delayMillis
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        // ...
        return enqueueMessage(queue, msg, uptimeMillis);
    }
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        // 调用 MessageQueue.enqueueMessage
        return queue.enqueueMessage(msg, uptimeMillis);
    }
    

    最终是通过MessageQueue的next方法来实现的,等待时间调的是native的epoll_wait 来进行等待
    1.将我们传入的延迟时间转化成距离开机时间的毫秒数
    2.MessageQueue 中根据上一步转化的时间进行顺序排序
    3.在 MessageQueue.next 获取消息时,对比当前时间(now)和第一步转化的时间(when),如果 now < when,则通过 epoll_wait 的 timeout 进行等待
    4.如果该消息需要等待,会进行 idel handlers 的执行,执行完以后会再去检查此消息是否可以执行

    • 面试题6:View.post 和 Handler.post 的区别
      View.post 最终也是通过 Handler.post 来执行消息的

    2、Binder相关

    Android 是基于Linux系统来实现的,因此,我们有必要来了解一下,为什么Android 不使用Linux本身有的进程通信机制,而是要自己撸一个Binder 这玩意来实现进程间通信。
    首先,需要简要的了解下Linux 进程间通信的几种方式

    1、Linux IPC机制

    image.png
    1、管道通信的特性
    管道通信是一种1v1 的通信方式,相对来说比较安全的。缺点是:数据只能单向传输。
    • 打开管道必须由两端(两个进程)同时打开一个管道。分别为读(r)和写(w);读取端负责从管道中读取数据,写入端负责向管道中写入数据。
    • 数据一旦被读走,管道中便不存在,不可重复读取
    • 由于管道采用半双工通信方式,因此,数据只能再一个方向上流动
    • 只能在有公共祖先的进程中实现通信

    2、共享内存的特性
    共享内存就是允许两个或多个进程共享一定的存储区。当共享的这块存储区中的数据发生改变,所有共享这块存储区的进程都会察觉数据的改变,因为数据不需要在客户端和服务端进行数据拷贝,数据直接写到内存,不用若干次数据拷贝,所以这是最快的IPC。

    • 因为两个进程通过地址映射到同一片物理地址,所以,进程可以给那一片物理空间中写入数据,也可以读取数据----进程间双向通信。
    • 客户端和服务端都是从内存中直接读取、写入数据的,没有涉及到数据的拷贝,因此,速度最快。
    • 生命周期随内核,不会随着服务端或者客户端的断开而销毁。所有访问共享内存对象的进程都结束了,共享内存区域对象依然存在。
    • 共享内存并未实现同步机制:多个进程可以同时写入数据,这就会造成数据的混乱。

    3、Socket 通信的特性
    Socket通信是双向的通信,客户端与服务端要建立连接,然后读写数据,相当于在两个进程间各自拷贝数据,然后传输数据,这个效率是很慢的。

    • Socket 是一种双向多对多模式的IPC机制之一,但数据发生了两次拷贝,因此效率比较低。但是比较安全的。

    2、Android IPC通信:Binder

    1、内核空间和用户空间

    内核空间(内核进程):操作系统所占用的内存区域 -------只有一份
    用户空间(用户进程):用户进程所在的内存区域 ---------多份
    Q:为什么要这么划分?
    A:使用内核空间和用户空间这种分开来划分,可以做到,每个APP(用户空间)不会影响其他APP,也不会造成系统的崩溃。

    2、物理地址和虚拟地址

    • 虚拟内存
      实际上我们写的程序,都是面向虚拟内存的,我们在程序中写的变量的地址,实际上是虚拟内存中的地址,当CPU想要访问该地址的时候,内存管理单元MMU就会将虚拟地址翻译成物理地址。然后,CPU就可以从真实的物理地址处获取到数据。
    • MMU:内存管理单元
      它是一个硬件,不是软件。它用于将虚拟地址翻译成实际的物理内存地址,同时它还可以将特定的内存块设置成不同的读写属性,进而实现了内存保护。注意,MMU是硬件管理,不是软件实现内存管理。
      这里只要知道MMU是将虚拟地址转换成物理地址的过程是MMU的核心,MMU通过页和页表来实现内存映射(mmap):从虚拟内存找到物理内存所对应的地址编码,从而获取到物理内存中所对应的数据

    3、Binder IPC 通信模型

    以下是Binder IPC 通信模型的经典图,原本两个进程间数据的交互需要两次拷贝(发送数据方将数据拷贝到内核空间,然后内核空间再将数据拷贝到接收方),由于使用了mmap(内存映射)就减少了一次数据接收进程的数据拷贝(也可以说是服务端)。这样Binder只需要在数据发送进程(客户端)实现一次拷贝数据到内核空间即可。

    image.png
    参考链接

    4、Android中跨进程通信的几种方式

    四大组件:Activity、广播、ContentProvider、Service(AIDL)
    本地文件的读写,两个不同程序对同一个文件进行读写

    MVC、MVP、MVVM的特性:

    一篇不错的总结,需要单独消化

    MVC:

    Android提供的Activity、xml就是经典的MVC模式

    • Model: 负责管理业务数据逻辑,如网络请求、数据库处理;
    • View: Layout XML 文件;
    • Controller: Activity 负责处理表现逻辑,如获取用户输入的文字、向Model发送数据请求。
      缺点:Activity不可避免需要处理一些View层的逻辑,如显示控件的数据,这就造成了V和C层耦合。

    MVP:

    • Model: 负责管理业务数据逻辑,如网络请求、数据库处理;
    • View: Activity 和 Layout XML 文件;
    • Presenter: 负责处理表现逻辑。
      与MVC类似,差别不大,只是view层不能直接调用M层,而是通过P层去转发,将数据的业务处理逻辑放在了P层。但MVC中V层和C层存在一定的耦合

    以上两种模式的优点:

    • 按代码功能分层,将某一类代码模块抽取出来,如网络请求相关、数据库读取相关。使功能更加单一,便于维护和复用。比如某个列表数据需要在不同的页面展示,可直接复用同一个M
    • 按业务功能分层,将某一类业务的模块分出来,如同一个页面需要展示天气相关、路况相关、历史出行相关的业务,则可以根据不同的业务将业务逻辑层P层、M层分成多个P层和M层。这样其他页面需要展示天气相关的内容时,可复用P层和M层
    • 便于单元测试,以上两种分层后,更便于进行单元测试,但实际开发中,进行单元测试的公司不多。
    • Activity(V层)只处理生命周期的任务,业务性的代码和Android原生功能的代码分离,便于排查问题,且代码变得更加简洁
    • 把业务逻辑抽到Presenter中去,避免后台线程引用着Activity导致Activity的资源无法被系统回收从而引起内存泄露和OOM

    缺点:

    • MVP模式会增加额外的类/接口,①在功能简单的业务场景下滥用该模式,是完全没必要且多余的,增加了维护和开发的工作量。
    • V层和P层通过接口来通信,因此如果V层的业务发生变更,接口也需要变化,额外增加了接口定义的工作量。而在实际开发中,V层会进行频繁迭代。V和P是强关联的,需要手动维护
    • 在实际的开发中,应该合理使用设计模式。或不使用,或单独使用,或者混合起来使用(如MVP+MVVM)。

    MVVM:

    MVVM 模式改动在于中间的 Presenter 改为 ViewModel,MVVM 同样将代码划分为三个部分:

    • Model: 负责管理业务数据逻辑,如网络请求、数据库处理,与 MVP 中 Model 的概念相同;
    • View: Activity 和 Layout XML 文件,与 MVP 中 View 的概念相同;
    • ViewModel: 存储视图状态,负责处理表现逻辑,并将数据设置给可观察数据容器。

    在实现细节上,View 和 Presenter 从双向依赖变成 View 可以向 ViewModel 发指令,但 ViewModel 不会直接向 View 回调,而是让 View 通过观察者的模式去监听数据的变化,有效规避了 MVP 双向依赖的缺点。但 MVVM 本身也存在一些缺点:

    • 多数据流: View 与 ViewModel 的交互分散,缺少唯一修改源,不易于追踪;
    • LiveData 膨胀: 复杂的页面需要定义多个 MutableLiveData,并且都需要暴露为不可变的 LiveData。
    image

    DataBinding、ViewModel 和 LiveData 等组件是 Google 为了帮助我们实现 MVVM 模式提供的架构组件,它们并不是 MVVM 的本质,只是实现上的工具。
    Lifecycle: 生命周期状态回调;
    LiveData: 可观察的数据存储类;
    databinding: 可以自动同步 UI 和 data,不用再 findviewById();
    ViewModel: 存储界面相关的数据,这些数据不会在手机旋转等配置改变时丢失。

    App启动流程

    • 启动的起点发生在Launcher的Activity中,启动一个app说简单点就是启动一个Activity,那么我们说过所有组件的启动,切换,调度都由AMS来负责的,所以第一步就是Launcher响应了用户的点击事件,然后通知AMS(ActivityManagerService );

    • AMS得到Launcher的通知,就需要响应这个通知,主要就是新建一个Task去准备启动Activity,并且告诉Launcher你可以休息了(Paused);

    • Launcher得到AMS让自己“休息”的消息,那么就直接挂起,并告诉AMS我已经Paused了;

    • AMS知道了Launcher已经挂起之后,就可以放心的为新的Activity准备启动工作了,首先,APP肯定需要一个新的进程去进行运行,所以需要创建一个新进程,这个过程是需要Zygote参与的,AMS通过Socket去和Zygote协商,如果需要创建进程,那么就会fork自身,创建一个新进程,新的进程会导入ActivityThread类,这就是每一个应用程序都有一个ActivityThread与之对应的原因;

    • 在 ActivityThread 的 main 方法中会去创建 Looper 并且执行 loop 方法以及调用了 attach 方法。attach 方法中调用 ActivityManagerService 的 attachApplication 方法。

    • ActivityManagerService 的 attachApplication 中首先会调用 ApplicationThread 的 bindApplication 方法。ApplicationThread 是 ActivityThread 的一个内部类。在 bindApplication 中发送消息给 ActivityThread 的 H 类,调用 handleBindApplication 方法。

    • handleBindApplication 方法创建 application,并且调用 callApplicationOnCreate,执行 Application 的 onCreate 方法。

    • ActivityManagerService 中会调用 ActivityTaskManagerInternal 的 attachApplication,最终调用的是 ActivityTaskManagerService 的内部类 LocalService 的 attachApplication。

    • 接着调用到 ActivityStackSupervisor 的 realStartActivityLocked,经过一系列的调用后执行到 ActivityThread 的 handleLaunchActivity,最终调用到 Activity 的 onCreate 方法。

      image.png
      原文链接:https://blog.csdn.net/zzw0221/article/details/106716620

    启动模式相关(launchMode)

    Q:当手机执行菜单键,查看最近任务时,会出现多个正在运行的应用列表,此时这个列表里显示的是什么?
    A:显示的是一个个的Task,用户可见的是Task栈顶的Activity截图。

    • Standard:
      同一个Task中:在同一个Task中打开多次同一个Activity,Activity会被创建多个实例,放进同一个Task中
      当不同的Task中:当在不同的Task中打开同一个Activity,Activity会被创建多个实例,分别放进每个Task中,这是Android默认的规则。
    • SingleTop:
      在该模式下,如果栈顶Activity为我们要新建的Activity(目标Activity),那么就不会重复创建新的Activity,当启动这个Activity时,不会调用onCreate,而是调用onNewIntent方法。
    • SingleTask:
      同一个Task中:在同一个Task中创建Activity时,如果栈内存在该Activity的实例,则:1.将task内的对应Activity实例之上的所有Activity弹出栈;2.将对应Activity置于栈顶,获得焦点。
      当不同的Task中:在A应用中(A Task),启动B应用中(B Task)的Activity BB,BB的launchMode被标记为SingleTask,此时,BB先在自己的Task B中创建实例,再从B应用切换到A应用。此时按返回键,需要将B应用所有页面出栈后,才可看到A应用。即被SingleTask标记的Activity,始终只会创建在自己的Task中,不像Standard和SingleTop(跟随启动它的task)
    • SingleInstance:
      在该模式下,我们需要为目标Activity分配一个新的affinity,并创建一个新的Task栈,将目标Activity放入新的Task,并让目标Activity获得焦点。新的Task中有且只有这一个Activity实例。如果已经创建过目标Activity实例,则不会创建新的Task,而是将以前创建过的Activity唤醒(对应Task设为Foreground状态)

    事件传递机制

    事件在Android中的传递顺序

    • 事件传递会经过 Activity--> Window-->DecorView --> 布局View


      image.png
    • 事件的分发是:
      由Activity的dispatchTouchEvnent方法开始
      –>>然后调用PhoneWindow的superDispatchTouchEvent方法
      –>>接着调用DecorView的superDispatchTouchEvent方法
      –>>最后还是调用ViewGroup的dispatchTouchEvent方法

    事件的传递规则

    一个点击事件,或者说触摸事件,被封装为了一个MotionEvent。事件的分发主要由三个重要的方法来完成:
    1、分发:dispatchTouchEvent;
    2、拦截:onInterceptTouchEvent;
    3、处理:onTouchEvent;
    如果是ViewGroup容器类view,则以上三个方法都会用到。但是如果是View类型的,不能包含子view,那就没有第二个拦截方法,因为没有子view的话,拦截方法的就是多余的,只有ViewGroup才会有拦截。

    • public boolean dispatchTouchEvent(MotionEvent ev)
      此方法用来处理事件的分发,当事件传递给当前view时,首先就是通过调用此方法来进行传递的,如果当前view锁包含的子view的dispatchTouchEvent方法或者当前view的onTouchEvent处理了事件, 通常返回true, 表示事件已消费。如果没有处理则返回false。

    • public boolean onInterceptTouchEvent(MotionEvent ev)
      此方法用来判断某个事件是否需要拦截,如果某个view拦截了此事件,那么同一个事件序列中,此方法不会被再次调用,因为会把当前view赋值给mFirstTouchTarget对象(原本为null),后续父view判断mFirstTouchTarget != null时,就会去调用它的onTouchEvent方法,交给mFirstTouchTarget处理事件。

    • public boolean onTouchEvent(MotionEvent ev)
      用来处理事件,如果事件被消耗了,通常就返回true, 如果不做处理,则发挥false,并且在同一个时间序列中,当前view不会再接受到事件。
      完整流程如下:

      image.png
      参考链接

    View的绘制流程

    Android 中 Activity 是作为应用程序的载体存在,代表着一个完整的用户界面,提供了一个窗口来绘制各种视图,当 Activity 启动时,我们会通过 setContentView 方法来设置一个内容视图,这个内容视图就是用户看到的界面。


    image.png
    • PhoneWindow 是 Android 系统中最基本的窗口系统,每个 Activity 会创建一个。PhoneWindow 是 Activity 和 View 系统交互的接口。
    • DecorView 本质上是一个 FrameLayout,是 Activity 中所有 View 的祖先。

    整体流程

    当一个应用启动时,会启动一个主 Activity,Android 系统会根据 Activity 的布局来对它进行绘制。绘制会从根视图 ViewRoot 的 performTraversals() 方法开始,从上到下遍历整个视图树,每个 View 控制负责绘制自己,而 ViewGroup 还需要负责通知自己的子 View 进行绘制操作。视图操作的过程可以分为三个步骤,分别是测量(Measure)、布局(Layout)和绘制(Draw)

    Measure

    • 用来计算 View 的实际大小。页面的测量流程从 performMeasure 方法开始。
    • 具体操作是分发给 ViewGroup 的,由 ViewGroup 在它的 measureChild 方法中传递给子 View。ViewGroup 通过遍历自身所有的子 View,并逐个调用子 View 的 measure 方法实现测量操作。
    • View (ViewGroup) 的 Measure 方法,最终的测量是通过回调 onMeasure 方法实现的,这个通常由 View 的特定子类自己实现,可以通过重写这个方法实现自定义 View。

    Layout

    • 该过程用来确定 View 在父容器的布局位置,他是父容器获取子 View 的位置参数后,调用子 View 的 layout 方法并将位置参数传入实现的。

    Draw

    • 该操作用来将控件绘制出来,绘制的流程从 performDraw 方法开始。performDraw 方法在类 ViewRootImpl 内。
    • 最终调用到每个 View 的 draw 方法绘制每个具体的 View,绘制基本上可以分为六个步骤:
      public void draw(Canvas canvas) {
        ...
        // Step 1, draw the background, if needed
        if (!dirtyOpaque) {
          drawBackground(canvas);
        }
        ...
        // Step 2, save the canvas' layers
        saveCount = canvas.getSaveCount();
        ...
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
    
        // Step 4, draw the children
        dispatchDraw(canvas);
    
        // Step 5, draw the fade effect and restore layers
        canvas.drawRect(left, top, right, top + length, p);
        ...
        canvas.restoreToCount(saveCount);
        ...
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
      }
    

    链接:https://www.jianshu.com/p/c151efe22d0d

    解决滑动冲突的方式

    事件分发机制:Android中是从外向内分发,从内向外消耗的,记住这一点滑动冲突就很好解决。

    • 要求外部父控件滑动
      外部解决滑动冲突的方式就是当我们viewGrop分发事件的时候判断是否拦截,因为事件的分发机制就是从外向内分发,那么我们在viewGrop分发的时候就判断是否需要拦截就可以解决滑动冲突。
    • 要求内部子控件滑动
      内部解决滑动冲突的方式是首先让父view不拦截事件,然后在子view中判断是父view拦截呢还是子view拦截。

    自定义view

    HenCoder的教学视频和笔记

    概述

    • 自定义绘制的方式是重写绘制方法,其中最常用的是 onDraw()
    • 绘制的关键是 Canvas 的使用:
      ①Canvas 的绘制类方法: drawXXX() (关键参数:Paint)
      ②Canvas 的辅助类方法:范围裁切和几何变换
    • 可以使用不同的绘制方法来控制遮盖关系

    自定义绘制知识的4个级别

    • 1.Canvas 的 drawXXX() 系列方法及 Paint 最常见的使用
      Canvas.drawXXX() 是自定义绘制最基本的操作。掌握了这些方法,你才知道怎么绘制内容,例如怎么画圆、怎么画方、怎么画图像和文字。组合绘制这些内容,再配合上 Paint 的一些常见方法来对绘制内容的颜色和风格进行简单的配置,就能够应付大部分的绘制需求了。
    • 2.Paint 的使用
      Paint 可以做的事,不只是设置颜色,也不只是实心空心、线条粗细、有没有阴影、拐角的形状、开不开双线性过滤、
    • 3.Canvas 对绘制的辅助
      范围裁切、几何变换
    • 4.使用不同的绘制方法来控制绘制顺序
      控制绘制顺序解决的并不是「做不到」的问题,而是性能问题。同样的一种效果,你不用绘制顺序的控制往往也能做到,但需要用多个 View 甚至是多层 View 才能拼凑出来,因此代价是 UI 的性能;而使用绘制顺序的控制的话,一个 View 就全部搞定了。

    链接

    相关文章

      网友评论

          本文标题:Android应用开发岗 面试汇总-Android进阶篇

          本文链接:https://www.haomeiwen.com/subject/mzefjrtx.html