Android面试题集锦--中

3,786 阅读48分钟

4. Activity 这么“简单”相关笔记

  1. Activity的启动流程是怎样的?
    面试官视角:这道题想考察什么?
        1. 是否熟悉Activity启动过程中与AMS的交互过程(高级)
        2. 是否熟悉 Binder 通信机制(高级)
        3. 是否了解插件化框架如何Hook Activity启动(高级)
        4. 阐述 Activity 转场动画的实现原理可加分(中级)
        5. 阐述 Activity 的窗口显示流程可加分(高级)
    题目剖析:
        1. 与AMS交互
        2. Activity的参数和结果如何传递
        3. Activity如何实例化
        4. Activity生命周期如何流转
        5. Activity的窗口如何展示
        6. Activity转场动画的实现机制
    题目结论:
        Activity跨进程启动:
            请求进程A:startActivity——(AMP,ActivityManager代理对象)——>
            system_server进程:AMS(ActivityManagerService)
                解析Activity信息、处理启动参数
                启动目标进程——> Zygote --> 进程B --> 绑定新进程
                ATP(ApplicationThread在system_server进程的代理) --scheduleLaunchActivity/mH中EXECUTE_TRANSACTION消息执行任务(Android P)-->
            新进程B:ApplicationThread --> ActivityThread --> Activity生命周期
        Activity进程内启动:
            请求进程A:startActivity—(hook插入点1)—(AMP,ActivityManager代理对象)——>
            system_server进程:AMS(ActivityManagerService)
                解析Activity信息、处理启动参数、scheduleLaunchActivity/mH中EXECUTE_TRANSACTION消息处理(Android P)-->
            回到请求进程A:ApplicationThread --> ActivityThread -(hook插入点2)-> Activity生命周期
        Activity的参数传递:Activity之间切换交互,需要system_server进程作为中介,而两者之间的交流是通过Binder机制,而Binder机制依赖于Android内核中的Binder缓冲区,因此参数传递的大小依赖于Binder缓冲区的大小并且数据必须是可序列化的。
            传递大数据的方法:
                EventBus
                单例数据格式对象(注意内存泄露或者内存溢出的,考虑使用WeakReferences将数据包装起来)
                持久化 数据库、ACache(ASimpleCache)、文件之类的(缺点:数据量很大的时候读写时间慢,效率低下,IO容易出问题)
        Activity实例化:Activity实际上是在nstrumentation类的newActivity方法中被反射创建的。
        Fragment为什么不能添加有参数的构造方法?虽然Fragment可以通过new的方式创建,但是若涉及Activity状态的保存和恢复则可能会出问题。比如:Activity A可能由于长时间处于不可见而被杀死,则此时就涉及Activity状态的保存和恢复问题,而Activity中的FragmentManager会在Activity被销毁时,将所有Fragment按照android:fragments为key的数据里存储现在有哪些fragment显示、顺序、位置如何等等,当Activity需要恢复时则还是通过反射创建所以根本不知道需要构造参数如何赋值,因此无法给Activity或者Fragment添加有参数的构造方法,若fragment存在有参构造则最好有默认值处理。
        Activity窗口如何展示:
            newActivity
            activity-attach--> createPhoneWindow
            activity-create--> installDecor\addContentView\setContentView
            activity-start-->
            activity-restoreState
            activity-postCreate
            activity-resume--> 测量、布局、绘制
            activity-makeVisible--> 显示DecorView
        Activity转场动画的实现机制:参考于https://www.jianshu.com/p/69d48f313dc4
            1. 内容过渡动画的原理
                1). Activity A 调用 startActivity()
                    系统遍历 A 的视图节点,找到将要运行退退出转换的所有过渡视图
                    A 的退出转换记录所有过渡视图的开始状态
                    系统将所有过渡视图的可见性设置为INVISIBLE
                    在下一帧,A 的退出转换记录所有过渡视图的结束状态
                    A 的退出转换比较每个过渡视图的开始状态和结束状态,然后创建 Animator 作为退出动画,运行该动画。
                2). Activity B 启动了
                    系统遍历 B 的视图节点,找到将要运行进入转换的所有过渡视图,设置这些过渡视图的可见性为INVISIBLE
                    B 的进入转换记录所有过渡视图的开始状态
                    系统将所有过渡视图的可见性设置为VISIBLE
                    在下一帧,B 的进入转换记录所有过渡视图的结束状态
                    B 的进入转换比较每个过渡视图的开始状态和结束状态,然后创建 Animator 作为进入动画,运行该动画。
                注:所有的内容转换都需要记录每个过渡视图的开始状态和结束状态。而抽象类Visibility已经做了这部分内容了,Visibility的子类只需要实现 onAppear() 和 onDisappear() 方法,创建过渡视图进入或退出场景的 Animator。Android 5.0 中Visibility有三个子类 -- Fade、Slide、Explode,如果有需要的话也可以自定义Visibility子类。
            2. 共享元素过渡动画的原理
               A 调用startActivity(intent, bundle)后,B 启动时,窗口的背景是透明的。
               系统以 A 为标准重新设置 B 的每个共享元素视图的大小和位置,过一会 B 的进入转换会记录 B 中所有共享元素的开始状态,而对于内容过渡来说,其他的 transitioning view 的可见性都是 INVISIBLE。
               系统再重新将 B 的每个共享元素视图的大小和位置设置为原来的样子,过一会 B 的进入转换会记录 B 中所有共享元素的结束状态。
               B 的进入转换比较每个共享元素的开始状态和结束状态,创建 Animator 作为共享元素动画。
               系统将隐藏 A 的所有共享元素视图,然后开始运行 B 的共享元素动画。在 B 的共享元素动画过程中,B 的窗口背景会逐渐变为不透明的。
    
               注:对比内容过渡动画,内容过渡动画中系统会修改 transition views 的可见性,而共享元素过渡动画中系统会修改 shared element views 的位置、大小和显示。而且我们也可以看出实际上共享元素的 view 其实并没有在 Activity/ Fragment 之间共享,事实上,我们看到的进入或者返回的共享元素过渡动画都是直接在 B 的视图中运行的。
        注:Android P中创建新Activity由以前的scheduleLaunchActivity方法变成mH中EXECUTE_TRANSACTION消息执行ClientTransaction类型任务(实际为LaunchActivityItem类型),继而执行client.handleLaunchActivity。
           client实际类型为ClientTransactionHandler,而在Android P中,ActivityThread extends ClientTransactionHandler,而ClientTransactionHandler封装了handlexxxActivity的方法。因此Android P中最后也是执行ActivityThread中的handleLaunchActivity方法执行创建Activity。
           EXECUTE_TRANSACTION消息由ActivityThread中sendActivityResult方法调用mAppThread.scheduleTransaction(clientTransaction)--> ActivityThread.this.scheduleTransaction(transaction) --> ClientTransactionHandler(隐藏抽象类,ActivityThread是其子类)中scheduleTransaction方法--> sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);
           简单列以下Android P的流程:
           1. startActivity--> Activity(startActivity-> startActivityForResult)--> Instrumentation(execStartActivity)--> ActivityManager(getService.startActivity)--> ActivityManagerService(startActivity)--> ActivityStartController(obtainStarter工厂方法模式)--> ActivityStarter(execute--> startActivityMayWait--> startActivity--> startActivityUnchecked)
           2. --> ActivityStackSupervisor(resumeTopActivityUncheckedLocked)--> ActivityStack(resumeTopActivityUncheckedLocked--> resumeTopActivityInnerLocked)--> ActivityStartSupervisor(startSpecificActivityLocked--> realStartActivityLocked)--> ClientLifecycleManager(scheduleTransation)--> ClientTransation(schedule)
           3. --> ActivityThread(ApplicationThread(scheduleTransation)--> scheduleTransation)--> ClientTransationHandler(scheduleTransation--> sendMessage(ActivityThread.H.EXECUTE_TRANSATION))--> ActivityThread(H(handleMessage))--> TransationExceutor(execute)--> LaunchActivityItem(excute)--> ClientTransationHandler(handleLaunchActivity)
           4(最后使用反射创建Activity). --> ActivityThread(handleLaunchActivity--> performLaunchActivity)--> Instrumentation(newActivity--> getFactory(pkg))--> ActivityThread(peekPackageInfo)--> LoadedApk(getAppFactory)--> AppComponentFactory(instantiateActivity(cl, className, intent)--> (Activity) cl.loadClass(className).newInstance())--> Activity(performCreate--> onCreate)
        小技巧:若在代码中无法找到隐藏类,则可在文件顶部,先查找此类包名,再想办法进行查找。
    
  2. 如何跨App启动Activity?有哪些注意事项?
    面试官视角:这道题想考察什么?
        1. 是否了解如何启动外部应用的Activity(初级)
        2. 是否了解如何防止自己的Activity被外部非正常启动(中级)
        3. 是否对拒绝服务漏洞有了解(高级)
        4. 如何在开发时规避拒绝服务漏洞(高级)
    题目结论:
        初级:
            1. 共享uid的App(应用于系统级应用或者"全家桶"应用),示例:
                <?xml version="1.0" encoding="utf-8"?>
                <manifest xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    package="com.android.customwidget"
                    android:sharedUserId="com.demo">
                    ...
                </manifest>
              共享uid不仅能启动其Activity,系统对于流量的计算等等都是共享的。
            2. 使用exported,示例:
                <activity android:name=".BActivity" android:exported="true"/>
            3. 使用IntentFilter,配置action等
                <activity android:name=".BActivity"
                    android:permission="com.demo.b">
                    <intent-filter>
                        <action android:name="com.demo.intnet.Test"/>
                        <category android:name="android.intent.category.DEFAULT"/>
                    </intent-filter>
                </activity>
              使用:
                Intent it = new Intent();
                it.setAction("com.demo.intnet.Test");
                startActivity(it);
        中级:
            App B为允许外部启动的Activity B加权限,示例:
                <activity android:name=".BActivity"
                    android:permission="com.demo.b">
                    <intent-filter>
                        <action android:name="com.demo.intnet.Test"/>
                        <category android:name="android.intent.category.DEFAULT"/>
                    </intent-filter>
                </activity>
              App A若想启动App B的ActivityB,则需要声明权限:<uses-permission android:name="com.demo.b">
        高级:
            什么是服务漏洞?
            答:说App A的ActivityA启动App B的ActivityB时,传过来一个Bundle数据,此数据是一个被Serializable修饰的类SerializableA。
            若App B中没有SerializableA这个类,只要App B的ActivityB中访问了Intent的Extra(getIntent().getExtras())则就会发生类找不到异常。此种情况就是服务漏洞
            如何解决服务漏洞?
            答:try{}catch(Exception e){}
    
  3. 如何解决Activity参数的类型安全及接口繁琐的问题?
    面试官视角:这道题想考察什么?
        1. 是否有代码优化和代码重构的意识(高级)
        2. 是否对反射、注解处理器有了解(高级)
        3. 是否具备一定的框架设计能力(高级)
    题目剖析:
        1. 类型安全:Bundle的Key-Value不能在编译期保证类型
        2. 接口繁琐:启动Activity时参数和结果传递都依赖Intnet
        3. 等价的问法:设计一个框架,解决上述问题
        4. 面试不需要实现,只管合理大胆的想
    题目结论:
        初级:为什么Activity的参数存在类型安全问题?
            设置值:intent.putExtra("id", 0);
            获取值:String id = getIntent().getStringExtra("id");
          参数类型安全需要人工保证,容易出错。
        中高级:
            常规写法:
                intent.putExtra("id", 0);
                intent.putExtra("name", 0);
                intent.putExtra("age", 0);
                intent.putExtra("title", 0);
                ...
              参数若很多时,则需要写一堆代码,若此时又多一个参数,则又需要维护一遍。
            期望写法:
                UserActivityBuilder.builder(age, name).title(title).start(context);
                必传参数直接当作builder参数传入,否则通过方法传入。通过注解处理器生成Builder
                注入逻辑调用时机:ActivityLifrcycleCallbacks的onActivityCreated方法中
                注意需要手动处理onNewIntent方法:onNewIntent没有对应的生命周期回调
                注解处理器程序的开发注意事项:
                    1. 注意注解标注的类的继承关系
                    2. 注意注解标注的类为内部类的情况
                    3. 注意kotlin与Java的类型映射问题
                    4. 把握好代码生成和直接依赖的边界
        满分答案:框架设计,元编程(用代码写代码)
            compile编译期:
                1. APT:即注解处理器
                2. Bytecode:RePlugin
                3. Generic泛型:介于编译和运行期之间
            runtime运行期:
                4. Reflect:反射
                5. Proxy:动态代理
    
  4. 如何在代码的任意位置为当前Activity添加View?
     面试官视角:这道题想考察什么?
        1. 如何在任意位置获取当前Activity(中级)
        2. 是否对Activity的窗口有深入认识(高级)
        3. 潜在的内存泄漏的风险以及内存回收机制(高级)
        4. 是否能够深入需求评估技术方案的合理性(高级)
    题目剖析:
        1. 如何获取当前Activity?
        2. 如何在不影响正常View展示的情况下添加View?
        3. 既然能添加,就应当能移除,如何移除?
        4. 这样作的目的是什么?添加全局View是否更合适?
    题目结论:
        1. 获取当前Activity:Application.ActivityLifecycleCallbacksgais回调中获取
           注意内存泄漏:private static WeakReference<Activity> currentActivityRef;
           onActivityCreated回调中:currentActivityRef = new WeakReference<>(activity);
        2. 内存回收机制
           GC Roots包括:虚拟机栈帧(栈桢中的本地变量表)引用的对象、类静态属性引用的对象、常量引用的对象、Native方法引用的对象
           对象若无 GC Roots引用,则表示可以被回收。软引用SoftRef内存不足时回收,弱引用WeakRef发生gc时(内存即便充足)回收。
        3. 添加View
            Activity中真实的根布局为DecorView(FrameLayout子类)
            DecorView包含一个线性布局LinearLayout,LinearLayout其分为上下两部分:titleBar和mContentParent。
            而mContentParent实际上就是我们在布局文件中绘制布局显示的区域。mContentParent的id即android.R.id.content
          示例扩展类:https://github.com/Endless5F/JcyDemoList/blob/master/CustomWidget/src/main/java/com/android/customwidget/ext/ActivityExt.java
        4. 添加全局View:https://github.com/yhaolpz/FloatWindow
    
    Android全局悬浮窗:github.com/yhaolpz/Flo…
  5. 如何实现类似微信右滑返回的效果?
    面试官视角:这道题想考察什么?
        1. 是否熟练掌握手势和动画的运用(中级)
        2. 是否了解窗口绘制的内部原理(高级)
        3. 是否对Activity的窗口有深入了解(高级)
    题目剖析:
        1. 没有明说UI的类型,Activity还是Fragment?
        2. Fragment实现简单,重点回答Activity
        3. 考虑如何设计这样一个组件
        4. 考虑如何降低接入成本
    题目结论:
        一星:Fragment的实现
            1. 对于Fragment的控制相对简单
            2. 不涉及Window的控制,只是View级别的操作
            3. 实现View跟随手势滑动移动的效果
            4. 实现手势结束后判断取消或返回执行归位动画
        二星:Activity的实现
            1. 首先需要将最上层Activity的window设置成透明色
                <style name="AppTranslucentTheme" parent="AppTheme">
                    <item name="android:windowBackground">@android:color/transparent</item>
                    <item name="android:windowIsTranslucent">true</item> <!--窗口是半透明的-->
                </style>
              若上层Activity不设置成半透明,则下层Activity则不会被绘制,就会显示黑色
            2. Activity联动--多Task(不同堆栈的Activity)
               多Task之间Activity切换时,会先切换一个任务堆栈,然后再显示Activity
               场景举例:ActivityA和ActivityC是同一个任务栈Task#0,ActivityB是单独任务栈Task#1
                 若此时ActivityC的window的背景是透明的,而ActivityA在ActivityC下面,此时若由ActivityB切换到ActivityC,
                 则就会出现,先显示ActivityA再显示ActivityC,这是由于ActivityB和C之间是两个Task栈,因此先栈切换,而ActivityC是透明的,因此先显示了一下A,最后才显示C。
               此种场景可以先判断跳转之间是否是同一Task栈,然后给ActivityC“拍照”,然后放在ActivityC下面,给用户一种假象。
               如何获取Activity栈?
               答:根据Application.ActivityLifecycleCallbacksgais回调中,使用Activity#getTaskId()获取。
            3. Activity透明对生命周期的影响(为了性能)
               若上层Activity的window背景是透明的,则该Activity下面的Activity生命周期则为Started的状态,以此类推,直到一个Activity的window背景不透明,则此下面的Activity的生命周期为Created。
        三星:设计SDK
            1. 现有方案(SwipeBackLayout):Activity需要继承自SwipeBackActivity,若此时也需要继承业务的BaseActivity则会产生冲突。
            2. 用接口代替父类:通过实现接口把逻辑移到外部类中,通过组合而不是继承来实现。
            3. 动态切换窗口透明状态:滑动过程中可通过反射调用
                // @hide
                @SystemApi
                convertToTranslucent(TranslucentConversionListener callback, ActivityOptions options) // 转换为半透明
    
                // @hide
                @SystemApi
                public void convertFromTranslucent()    // 从半透明转换
    
    SwipeBackLayout:github.com/ikew0ng/Swi…

5. Handler UI相关笔记

  1. Android中为什么非UI线程不能更新UI?
    概念引入1:ViewRootImpl,ViewRoot和View关系
        ViewRoot对应ViewViewRootImpl类,它是连接WindowManager和DecorView的纽带,
        View的三大流程(measure,layout,draw)均是通过ViewRoot来完成的。ViewRootIml是View的根类,其控制着View的测量、绘制等操作
        ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象并和DecorView建立联系。
        DecorView作为顶级的View一般情况下它内部会包含一个竖向的LinearLayout,这个LinearLayout里面有上下两个部分(具体情况和Android版本及主题有关),上面是标题栏,下面是内容栏。
    概念引入2:SurfaceView
        SurfaceFlinger服务负责绘制Android应用程序的UI,它的实现相当复杂,SurfaceFlinger服务运行在Android系统的System进程中,它负责管理Android系统的帧缓冲区(Frame Buffer)。
        Android设备的显示屏被抽象为一个帧缓冲区,而Android系统中的SurfaceFlinger服务就是通过向这个帧缓冲区写入内容来绘制应用程序的用户界面的。Android系统在硬件抽象层中提供了一个Gralloc模块,封装了对帧缓冲区的所有访问操作。
        Linux内核在启动的过程中会创建一个类别和名称分别为“graphics”和“fb0”的设备,用来描述系统中的第一个帧缓冲区,即第一个显示屏,其中,数字0表示从设备号。注意,系统中至少要存在一个显示屏,因此,名称为“fb0”的设备是肯定会存在的,否则的话,就是出错了。Android系统和Linux内核本身的设计都是支持多个显示屏的,不过,在Android目前的实现中,只支持一个显示屏。
        init进程在启动的过程中,会启动另外一个进程ueventd来管理系统的设备文件。当ueventd进程启动起来之后,会通过netlink接口来Linux内核通信,以便可以获得内核中的硬件设备变化通知。而当ueventd进程发现内核中创建了一个类型和名称分别为“graphics”和“fb0”的设备的时候,就会这个设备创建一个/dev/graphics/fb0设备文件。这样,用户空间的应用程序就可以通过设备文件/dev/graphics/fb0来访问内核中的帧缓冲区,即在设备的显示屏中绘制指定的画面。注意,用户空间的应用程序一般是通过内存映射的方式来访问设备文件/dev/graphics/fb0的。
        用户空间的应用程序在使用帧缓冲区之间,首先要加载Gralloc模块,并且获得一个gralloc设备和一个fb设备。有了gralloc设备之后,用户空间中的应用程序就可以申请分配一块图形缓冲区,并且将这块图形缓冲区映射到应用程序的地址空间来,以便可以向里面写入要绘制的画面的内容。最后,用户空间中的应用程序就通过fb设备来将前面已经准备好了的图形缓冲区渲染到帧缓冲区中去,即将图形缓冲区的内容绘制到显示屏中去。相应地,当用户空间中的应用程序不再需要使用一块图形缓冲区的时候,就可以通过gralloc设备来释放它,并且将它从地址空间中解除映射
        每一个Android应用程序与SurfaceFlinger服务都有一个连接,这个连接都是通过一个类型为Client的Binder对象来描述的。这些Client对象是Android应用程序连接到SurfaceFlinger服务的时候由SurfaceFlinger服务创建的,而当Android应用程序成功连接到SurfaceFlinger服务之后,就可以获得一个对应的Client对象的Binder代理接口了。有了这些Binder代理接口之后,Android应用程序就可以通知SurfaceFlinger服务来绘制自己的UI了。
        Android应用程序在通知SurfaceFlinger服务来绘制自己的UI的时候,需要将UI元数据传递给SurfaceFlinger服务,例如,要绘制UI的区域、位置等信息。一个Android应用程序可能会有很多个窗口,而每一个窗口都有自己的UI元数据,因此,Android应用程序需要传递给SurfaceFlinger服务的UI元数据是相当可观的。在这种情况下,通过Binder进程间通信机制来在Android应用程序与SurfaceFlinger服务之间传递UI元数据是不合适的,这时候Android系统的匿名共享内存机制(Anonymous Shared Memory)就派上用场了。
    
        一般来说,每一个窗口在SurfaceFlinger服务中都对应有一个Layer,用来描述它的绘图表面。对于那些具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。
        无论是LayerBuffer,还是Layer,它们都是以LayerBase为基类的,也就是说,SurfaceFlinger服务把所有的LayerBuffer和Layer都抽象为LayerBase,因此就可以用统一的流程来绘制和合成它们的UI。
        注意,用来描述SurfaceView的Layer或者LayerBuffer的Z轴位置是小于用来其宿主Activity窗口的Layer的Z轴位置的,但是前者会在后者的上面挖一个“洞”出来,以便它的UI可以对用户可见。实际上,SurfaceView在其宿主Activity窗口上所挖的“洞”只不过是在其宿主Activity窗口上设置了一块透明区域。
        SurfaceView有以下三个特点:
            A. 具有独立的绘图表面;
            B. 需要在宿主窗口上挖一个洞(即需要在宿主窗口的绘图表面上设置一块透明区域)来显示自己;
            C. 它的UI绘制可以在独立的线程中进行,这样就可以进行复杂的UI绘制,并且不会影响应用程序的主线程响应用户输入。
    概念引入3:SurfaceTexture,TextureView, SurfaceView和GLSurfaceView的区别(https://blog.csdn.net/m475664483/article/details/52998445)
        SurfaceView:
            从Android 1.0(API level 1)时就有 。它继承自类View,因此它本质上是一个View。但与普通View不同的是,它有自己的Surface。我们知道,一般的Activity包含的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的。这个DecorView在WMS中有一个对应的WindowState。相应地,在SF中对应的Layer。而SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,在SF中也会有自己的Layer。
            虽然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。
        GLSurfaceView:
            从Android 1.5(API level 3)开始加入,作为SurfaceView的补充。它可以看作是SurfaceView的一种典型使用模式。在SurfaceView的基础上,它加入了EGL的管理,并自带了渲染线程。另外它定义了用户需要实现的Render接口,提供了用Strategy pattern更改具体Render行为的灵活性。作为GLSurfaceView的Client,只需要将实现了渲染函数的Renderer的实现类设置给GLSurfaceView即可。
            其中SurfaceView中的SurfaceHolder主要是提供了一坨操作Surface的接口。GLSurfaceView中的EglHelper和GLThread分别实现了上面提到的管理EGL环境和渲染线程的工作。GLSurfaceView的使用者需要实现Renderer接口。
        SurfaceTexture:
            从Android 3.0(API level 11)加入。和SurfaceView不同的是,它对图像流的处理并不直接显示,而是转为GL外部纹理,因此可用于图像流数据的二次处理(如Camera滤镜,桌面特效等)。比如Camera的预览数据,变成纹理后可以交给GLSurfaceView直接显示,也可以通过SurfaceTexture交给TextureView作为View heirachy中的一个硬件加速层来显示。首先,SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,
            根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,接下来,就可以像操作普通GL纹理一样操作它了。从下面的类图中可以看出,它核心管理着一个BufferQueue的Consumer和Producer两端。Producer端用于内容流的源输出数据,Consumer端用于拿GraphicBuffer并生成纹理。SurfaceTexture.OnFrameAvailableListener用于让SurfaceTexture的使用者知道有新数据到来。JNISurfaceTextureContext是OnFrameAvailableListener从Native到Java的JNI跳板。其中SurfaceTexture中的attachToGLContext()和detachToGLContext()可以让多个GL context共享同一个内容源。
            Android 5.0中将BufferQueue的核心部分分离出来,放在BufferQueueCore这个类中。BufferQueueProducer和BufferQueueConsumer分别是它的生产者和消费者实现基类(分别实现了IGraphicBufferProducer和IGraphicBufferConsumer接口)。它们都是由BufferQueue的静态函数createBufferQueue()来创建的。Surface是生产者端的实现类,提供dequeueBuffer/queueBuffer等硬件渲染接口,和lockCanvas/unlockCanvasAndPost等软件渲染接口,使内容流的源可以往BufferQueue中填graphic buffer。GLConsumer继承自ConsumerBase,是消费者端的实现类。它在基类的基础上添加了GL相关的操作,如将graphic buffer中的内容转为GL纹理等操作。
        TextureView:
            在4.0(API level 14)中引入。它可以将内容流直接投影到View中,可以用于实现Live preview等功能。和SurfaceView不同,它不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。值得注意的是TextureView必须在硬件加速的窗口中。它显示的内容流数据可以来自App进程或是远端进程。
            TextureView继承自View,它与其它的View一样在View hierachy中管理与绘制。TextureView重载了draw()方法,其中主要把SurfaceTexture中收到的图像数据作为纹理更新到对应的HardwareLayer中。SurfaceTexture.OnFrameAvailableListener用于通知TextureView内容流有新图像到来。SurfaceTextureListener接口用于让TextureView的使用者知道SurfaceTexture已准备好,这样就可以把SurfaceTexture交给相应的内容源。Surface为BufferQueue的Producer接口实现类,使生产者可以通过它的软件或硬件渲染接口为SurfaceTexture内部的BufferQueue提供graphic buffer。
    面试官视角:这道题想考察什么?
        1. 是否理解线程安全的概念(中级)
        2. 是否能够理解UI线程的工作机制(高级)
        3. 是否熟悉SurfaceView实现高帧率的原理(高级)
    题目剖析:
        1. UI线程的工作机制
        2. 为什么UI设计成线程不安全的?
        3. 非UI线程一定不能更新UI吗?
    题目结论:
        1. 首先需要了解UI更新是非线程安全的。
        2. 非UI线程更新UI的异常是从哪抛出的呢?
           答:android.view.ViewRootImpl#checkThread,因此刷新View时只要不触发checkThread()就不会抛出异常。
        一星:UI线程是什么?
            答:Android的App进程是由zygote进程fork出的新进程,此进程会执行Android的入口函数ActivityThread#main()方法,并开启Kooper#loop(),此时应用就启动并运行于前台。因此UI线程即main函数运行的线程。
        二星:主线程如何工作?
            答:主线程通过Looper#loop开启死循环,一直轮询从Handler发送到MessageQueue消息队列中的消息,然后再分发给Handler进行处理,来保证程序一直运行于前台。
        三星:
            UI为什么不设计成线程安全的?
                1. UI具有可变性,甚至是高频可变性
                2. UI对响应时间的敏感性要求UI操作必须高效
                3. UI组件必须批量绘制来保证效率
                4. 若设计成线程安全,则需要频繁的加锁,开销太大
            非UI线程一定不能更新UI吗?
                场景:IO线程(网络请求)  UI线程(刷新UI)
                0. 正常操作:网络请求回来后,通过Handler#post/sendMessage发送消息,然后主线程Handler中处理View刷新
                1. 间接在非UI线程刷新:调用View#postInvalidate
                2. ViewRootImpl未初始化前在非UI线程更新:ViewRootIml是View的根类,是在onResume生命周期创建,因此在onCreate和onStart生命周期中在子线程(线程不能睡眠)里可以更改UI。
                3. 通过Looper实现在子线程使用Toast:
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                Thread.sleep(2000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            Looper.prepare();
                            Toast.makeText(mContext, "子线程弹Toast", Toast.LENGTH_SHORT).show();
                            Looper.loop();
                        }
                    }).start();
            SurfaceView非UI线程刷新及绘制:SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。常用于画面内容更新频繁的场景,比如游戏、视频播放和相机预览。
                使用SurfaceView的三步骤:
                    1、获取SurfaceHolder对象,其是SurfaceView的内部类。添加回调监听Surface生命周期。
                        mSurfaceHolder = getHolder();
                        mSurfaceHolder.addCallback(this);
                    2、surfaceCreated 回调后启动绘制线程。只有当native层的Surface创建完毕之后,才可以调用lockCanvas(),否则失败。
                        @Override
                        public void surfaceCreated(SurfaceHolder holder) {
                            mDrawThread = new DrawThread();
                            mDrawThread.start();
                        }
                    3、绘制
                        Canvas canvas = mSurfaceHolder.lockCanvas();
                        // 使用canvas绘制内容
                        mSurfaceHolder.unlockCanvasAndPost(canvas);
                使用SurfaceView不显示问题:发生这种问题的原因是多层嵌套被遮挡
                    setZOrderOnTop(boolean onTop) // 在最顶层,会遮挡一切view
                    setZOrderMediaOverlay(boolean isMediaOverlay)// 如已绘制SurfaceView则在surfaceView上一层绘制。
                黑色背景问题:mHolder.setFormat(PixelFormat.TRANSPARENT); //设置背景透明
    
  2. Handler发送消息的delay可靠吗?
    概念引入:Linux的epoll模型
        epoll模型 :当没有消息的时候会epoll.wait,等待句柄写的时候再唤醒,这个时候其实是阻塞的。
    面试官视角:这道题想考察什么?
        1. 是否清楚UI时间相关的任务如动画的设计实现原理(中级)
        2. 是否对Looper的消息机制有深刻的理解(高级)
        3. 是否做过UI过度绘制(UI消息过多)或者其它消息机制的优化(高级)
    题目剖析:
        1. 答案肯定不可靠,但需要深入分析原理
        2. 给出基于原理的实践案例
    题目结论:
        一星:主线程压力过大(待处理消息过多)
            若发送的消息过多,主线程处理较慢,导致堆积很多待处理消息,会导致主线程卡顿
        二星:
            1. handler.postDelayed(run, delay)的消息,调用时间非delay
            2. MessageQueue如何处理消息:
                Working Thread(工作线程)-->enquequeMessage(MessageQueue,入队一条消息)-->wake(Native层:NativeMessageQueue,唤醒)-->write(mWakeEventFd,写入消息)--↓(唤醒mEpollFd)
                Looper-->next(MessageQueue,处理下一条消息)-->pollOnce(Native层:NativeMessageQueue,轮询一条)-->epoll_wait(mEpollFd,若此时消息队列中无消息,则在此等待,唤醒后返回一条消息)
        三星:队列优化
               重复消息过滤:主要针对运行时高频发送的事件类型。通过一定手段判断一个合适的频率,通过handler.removeCallbacksAndMessages(msg)移除重复消息。
               互斥消息取消:主要针对后面的事件与前面消息的互斥。通过handler.removeCallbacksAndMessages(msg)移除前面互斥的消息。
               复用消息:Message.obtain();防止消息对象创建过多引发gc。
               消息空闲IdleHandler:
                    class RunOnceHandler implements MessageQueue.IdleHandler {
                       @Override
                       public boolean queueIdle() {
                           Log.d(TAG, "RunOnceHandler.queueIdle()...只运行一次");
                           return false;
                       }
                    }
                    Looper.myQueue().addIdleHandler(new RunOnceHandler());
                 完整示例:https://github.com/Endless5F/JcyDemoList/blob/master/PerformanceAnalysis/src/main/java/com/android/performanceanalysis/activity/IdleHandlerActivity.java
               使用独享的Looper:
                   Handler只能在主线程中创建吗?不是的只要有Looper就可以,比如HandlerThread
                   private HandlerThread handlerThread = new HandlerThread("独享Looper");
                   {handlerThread.start();}
                   private Handler sigleLooperHandler = new Handler(handlerThread.getLooper);
    
  3. 主线程的Looper为什么不会导致应用ANR?
    概念引入1:进程/线程
        进程:每个app运行时前首先创建一个进程,该进程是由Zygote fork出来的,用于承载App上运行的各种Activity/Service等组件。
             进程对于上层应用来说是完全透明的,这也是google有意为之,让App程序都是运行在Android Runtime。
             大多数情况一个App就运行在一个进程中,除非在AndroidManifest.xml中配置Android:process属性,或通过native代码fork进程。
        线程:线程对应用来说非常常见,比如每次new Thread().start都会创建一个新的线程。
             该线程与App所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,
             都是一个task_struct结构体,在CPU看来进程或线程无非就是一段可执行的代码,CPU采用CFS调度算法,保证每个task都尽可能公平的享有CPU时间片。
    概念引入2:为什么主线程不会因为Looper.loop()方法造成阻塞
        1. epoll模型:当没有消息的时候会epoll.wait,等待句柄写的时候再唤醒,这个时候其实是阻塞的。
        2. 所有的ui操作都通过handler来发消息操作。比如屏幕刷新16ms一个消息,你的各种点击事件,所以就会有句柄写操作,唤醒上文的wait操作,所以不会被卡死了。
        死循环问题:
            对于线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。
            而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,
            死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。
        这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。没看见哪里有相关代码为这个死循环准备了一个新线程去运转?
            事实上,会在进入死循环之前便创建了新binder线程,在代码ActivityThread.main()中:
            thread.attach(false);便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件)
            ,该Binder线程通过Handler将Message发送给主线程,具体过程可查看http://gityuan.com/2016/03/06/start-service/,简单说Binder用于进程间通信,采用C/S架构。
        另外,ActivityThread实际上并非线程,不像HandlerThread类,ActivityThread并没有真正继承Thread类,只是往往运行在主线程,给人以线程的感觉,其实承载ActivityThread的主线程就是由Zygote fork而创建的进程。
    面试官视角:这道题想考察什么?
        1. 是否了解ANR产生的条件(中级)
        2. 是否对Looper的消息机制有深刻的理解(高级)
        3. 是否对Android App的进程运行机制有深入了解(高级)
        4. 是否对IO多路复用有一定的认识(高级)
    题目剖析:
        1. ANR是如何产生的
        2. Looper的工作机制是什么?
        3. Looper不会导致ANR本质原因是什么?
        4. Looper的死循环为什么不会导致CPU占用率高?
    题目结论:
        一星:ANR类型
            Service超时:前台服务 20s / 后台服务 200s
            BroadcaseQueue超时:前台广播 10s / 后台广播 60s
            ContentProvider超时:10s
            InputDispatching(输入事件,最常见)超时:5s
        二星:
            主线程究竟在干什么?
                ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的程序也就可以退出了。
                从消息队列中取消息可能会阻塞,取到消息会做出相应的处理。如果某个消息处理时间过长,就可能会影响UI线程的刷新速率,造成卡顿的现象。
            Looper和ANR的关系:Looper是针对整个应用进程,而ANR只针对Looper循环中对应消息的一环而已。
        三星:主线程的死循环一直运行是不是特别消耗CPU资源呢?
            其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,
            便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,
            通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,
            当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
    
    概念引入参考链接:www.cnblogs.com/chenxibobo/…
  4. 如何自己实现一个简单的Handler - Looper框架?
    面试官视角:这道题想考察什么?
        1. 是否对Looper的消息机制有深刻的理解(高级)
        2. 是否对Java并发包中提供的队列有较为清楚的认识(高级)
    题目剖析:
        1. “简单”表明可以运用Java标准库当中的组件
        2. 覆盖实现的关键路径即可,突出重点
        3. 分析Android为什么要单独实现一套
        4. 仍然着眼于阐述Handler-Looper的原理
    题目结论:
        Handler的核心能力:
            1. 线程间通信
            2. 延迟任务执行
        Looper的核心能力:
            1. 准备prepare
            2. 获取消息队列
            3. loop轮询分发处理消息
        MessageQueue的核心能力:
            1. 持有消息
            2. 消息按时间排序(优先级)
            3. 队列为空时阻塞读取
            4. 头结点有延时可以定时阻塞(DealyQueue)
        Message的实现:
            1. 仿照Android即可,保存信息,持有handler等等
            2. 若MessageQueue使用DealyQueue,则此Message需要实现Delayed接口
        Android为什么不直接复用DelayQueue?
            1. DelayQueue没有提供合适的remove机制
            2. 更大的自由度,可以定制许多功能,特别是与Native层结合
            3. Android的MessageQueue可以针对单线程读取的场景做优化(DelayQueue很多地方加了锁,而MessageQueue只需要在入队时加锁,因为读时只是自己的Looper读)
    

6. 内存优化相关笔记

  1. 如何避免OOM内存溢出的产生?
    面试官视角:这道题想考察什么?
        1. 是否对Java内存管理机制有一定认识(中级)
        2. 是否对Android内存有过优化经验(高级)
        3. 是否在代码编写时有良好的习惯避免内存消耗(高级)
    题目剖析:
        1. OOM如何产生
        2. 如何优化程序减少内存占用?
    题目结论:
        一星:
            OOM的产生:
                以使用内存 + 新申请内存 > 可分配内存
                OOM几乎覆盖所有的内存区域,通常指堆内存
                Natvie Heap在物理内存不够时也会抛OOM
            使用合适的数据结构:
                HashMap:元素个数多(>1000)或者增删频繁推荐使用。特点:2倍扩容、无缩容机制、额外的Entry对象、查询插入接近O(1)
                SparseArray:元素个数少(<1000)并且key是整型推荐使用
                ArrayMap:元素个数少(<1000)并且key是非整型推荐使用。特点:小数组复用池(减少gc)、无额外对象开销、0.5倍缩容机制、1.5倍扩容机制
            避免使用枚举:类型安全、可读性强,占用内存大
            Bitmap的使用
                1. 尽量根据实际需求选择合适的分辨率
                2. 注意原始分件分辨率与内存缩放的结果
                3. 不使用帧动画,使用代码实现动效
                4. 考虑对Bitamp的重采样(例如:采样缩略图加载)和复用配置
        二星:
            谨慎的使用多进程:每一个进程fork出来后先天的带有一些公共的资源(比如:系统预加载的资源)
            谨慎的使用Large Heap:大堆本身会影响垃圾回收的效率
                Java虚拟机:-Xmx4096m
                Android虚拟机:android:largeHeap="true"
            使用NDK
                Native Heap没有专门的使用限制(物理内存)
                内存大户的核心逻辑主要在Native层
                    各类基于Cocos2dx、Unity3D等框架的游戏
                    游戏以外的OpenGL重度用户,例如各大地图App
        三星:内存优化 5R 法则
            1. Reduce缩减:降低图片分辨率/重采样/抽稀策略(例如:地图路线,抽取几个关键点,将大概轮廓描述出来)
            2. Reuse复用:池化策略/避免频繁创建对象,减小GC压力
            3. Recycle回收:主动销毁、结束,避免内存泄漏/生命周期(一定要)闭环
            4. Refactor重构:更合适的数据结构/更合理的程序架构
            5. Revalue重申:谨慎使用Large Heap/多进程/第三方框架
    
  2. 如何对图片进行缓存
    面试官视角:这道题想考察什么?
        1. 是否对缓存淘汰算法有一定的研究(高级)
        2. 是否对常见的图片加载框架有深入研究(高级)
        3. 是否对算法效果有验证闭环的意识(高级)
    题目剖析:
        1. 网络/磁盘/内存缓存
        2. 缓存算法分析
        3. 以熟悉的框架为例分析它的缓存机制
        4. 要有验证算法效果的意识
    题目结论:
        一星:
            缓存算法注意点:
                1. 哪些应该保存?
                2. 哪些应该丢弃?
                3. 什么时候丢弃?
            缓存算法的评价:
                1. 获取成本
                2. 缓存成本
                3. 缓存价值(命中率)
                4. 时间
        二星:
            LFU(Least Frequently Used最少使用算法)
            LRU(Least Recently Used最近最少使用算法)原理:最近使用的移动到最后,直到缓存媒介添满,丢弃第一个
                1、LruCache 是基于 Lru算法实现的一种缓存机制;
                2、Lru算法的原理是把近期最少使用的数据给移除掉,当然前提是当前数据的量大于设定的最大值。
                3、LruCache 没有真正的释放内存,只是从 Map中移除掉数据,真正释放内存还是要等等gc释放或者手动释放。
        三星:android.util.LruCache和com.jakewharton.disklrucache.DiskLruCache(implementation 'com.jakewharton:disklrucache:2.0.2')
            DiskLruCache和LruCache内部都是使用了LinkedHashMap去实现缓存算法的,只不过前者针对的是将缓存存在本地,而后者是直接将缓存存在内存。
            DiskLruCache会自动生成journal文件,这个文件是日志文件,主要记录的是缓存的操作:
                第一行是个固定的字符串“libcore.io.DiskLruCache”,标志着我们使用的是DiskLruCache技术。
                第二行是DiskLruCache的版本号,这个值是恒为1的。
                第三行是应用程序的版本号,我们在open()方法里传入的版本号是什么这里就会显示什么。
                第四行是valueCount,这个值也是在open()方法中传入的,通常情况下都为1。
                第五行是一个空行。前五行也被称为journal文件的头
                第六行是以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。通常我们看到DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。
                    没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,
                    但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,
                    调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。
                    也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
                DiskLruCache使用:
                    // 1. 首先打开DiskLruCache
                    // 参数:diskCacheFile 是本地可写的文件目录 valueCount 是表示一个key对应多少个文件,一般是1 maxSize 最大容量
                    DiskLruCache mDiskCache = DiskLruCache.open(diskCacheFile, appVersion, valueCount, maxSize);
                    // 2. 写入缓存
                    DiskLruCache.Editor editor = mDiskCache.edit(key);
                    OutputStream outputStream = editor.newOutputStream(0);// 0表示第一个缓存文件,不能超过valueCount
                    outputStream.write(data);
                    outputStream.close();
                    editor.commit();
                    mDiskCache.flush();
                    // 3. 读取缓存
                    DiskLruCache.Snapshot snapshot = mDiskCache.get(key);
                    FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(0);// 0表示第一个缓存文件,不能超过valueCount
            LruCache的使用:
                //获取系统分配给每个应用程序的最大内存
                int maxMemory=(int)(Runtime.getRuntime().maxMemory()/1024);
                int cacheSize=maxMemory/8;
                private LruCache<String, Bitmap> mMemoryCache;
                //给LruCache分配1/8
                mMemoryCache = new LruCache<String, Bitmap>(mCacheSize){
                    //重写该方法,来测量Bitmap的大小
                    @Override
                    protected int sizeOf(String key, Bitmap value) {
                        return value.getRowBytes() * value.getHeight()/1024;
                    }
                };
    
                // 把Bitmap对象加入到缓存中
                public void addBitmapToMemory(String key, Bitmap bitmap) {
                    if (getBitmapFromMemCache(key) == null) {
                        lruCache.put(key, bitmap);
                    }
                }
    
                // 从缓存中得到Bitmap对象
                public Bitmap getBitmapFromMemCache(String key) {
                    Log.i(TAG, "lrucache size: " + lruCache.size());
                    return lruCache.get(key);
                }
    
                // 从缓存中删除指定的Bitmap
                public void removeBitmapFromMemory(String key) {
                    lruCache.remove(key);
                }
    
  3. 如何计算图片内存大小?
    概念引入1:基本概念
        1. dip/dp(Density independent pixels):设备无关像素(也叫设备独立像素)。计算公式:dp = px / (设备DPI / (160dpi))  = px / density
        2. px: 像素。计算公式:px = density * dp
        3. dpi(dots per inch):一英寸多少个像素点。常见取值 120,160,240。我一般称作像素密度,简称密度
        4. 分辨率: 横纵2个方向的像素点的数量,常见取值 480X800 ,320X480
        5. 屏幕尺寸:屏幕对角线的长度。电脑电视同理。
        6. 屏幕比例的问题。因为只确定了对角线长,2边长度还不一定。所以有了4:3、16:9这种,这样就可以算出屏幕边长了。
        7. metrics.densityDpi; 就是我们常说的dpi。
        8. density(metrics.density)    密度比:以160dpi(标准dpi)为基,根据不同设备屏幕dpi,以公式:density = dpi/(160dpi); 计算所得。
            其实是 公式:DPI / (160像素/英寸) 后得到的值。从公式中就看得出了,DPI本身的单位也是 像素/英寸,所以density其实是没单位的,他就是一个比例值。
            而dpi的单位是 像素/英寸,比较符合物理上面的密度定义,密度不都是单位度量的值么,所以我更喜欢把dpi叫像素密度,简称密度,density还是就叫density。
            代码获取:
                DisplayMetrics metric = new DisplayMetrics();
                getWindowManager().getDefaultDisplay().getMetrics(metric);
                float density = metric.density;
    概念引入2:为什么标准dpi = 160像素/英寸?
        1. Android Design [1] 里把主流设备的 dpi 归成了四个档次,120 dpi、160 dpi、240 dpi、320 dpi
           实际开发当中,我们经常需要对这几个尺寸进行相互转换(比如先在某个分辨率下完成设计,然后缩放到其他尺寸微调后输出)
           一般按照 dpi 之间的比例即 2 :1.5 :1 :0.75 来给界面中的元素来进行尺寸定义。
           也就是说如果以 160 dpi 作为基准的话,只要尺寸的 DP 是 4 的公倍数,XHDPI 下乘以 2,HDPI 下乘以 1.5,LDPI 下乘以 0.75 即可满足所有尺寸下都是整数 pixel 。
           但假设以 240 dpi 作为标准,那需要 DP 是 3 的公倍数,XHDPI 下乘以 1.333,MDPI 下乘以 0.666 ,LDPI 下除以 2而以 LDPI 和 XHDPI 为基准就更复杂了,所以选择 160 dpi
        2. 这个在Google的官方文档中有给出了解释,因为第一款Android设备(HTC的T-Mobile G1)是属于160dpi的。
    概念引入3:Bitmap内存模型
        在Android 2.2(API8)之前,当GC工作时,应用的线程会暂停工作,同步的GC会影响性能。而Android2.3之后,GC变成了并发的,意味着Bitmap没有引用的时候其占有的内存会很快被回收且不会影响主线程性能。
        在Android 2.3.3(API10)之前,Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik Heap中,Native内存中的像素数据并不会以可预测的方式进行同步回收,有可能会导致Native内存升高。而在Android3.0之后8.0之前,Bitmap的像素数据也被放在了Dalvik Heap中,这样Bitmap 内存也会随着对象一起被回收。而在Android 8.0 之后,Bitmap的像素数据又被放到 Native 内存中。当然此时Google做了改进,在Native层的Bitmap的像素数据可以做到和Java层的对象一起快速释放。Android 8.0 还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提升绘制效率。
        Android3.0(API 11之后)引入了BitmapFactory.Options.inBitmap字段,设置此字段之后解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。不过,使用这个字段有几点限制:
            声明可被复用的Bitmap必须设置inMutable为true;
            Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用;
            Android4.4(API 19)之前被复用的Bitmap的inPreferredConfig会覆盖待分配内存的Bitmap设置的inPreferredConfig;
            Android4.4(API 19)之后被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存;
            Android4.4(API 19)之前待加载Bitmap的Options.inSampleSize必须明确指定为1。
    概念引入4:drawable微技巧
        1. mipmap
            mipmap开头的文件夹,它们的命名规则和drawable文件夹很相似,也是hdpi、mdpi、xhdpi等等,并且里面也放的图片。
            mipmap文件夹只是用来放置应用程序的icon的,仅此而已。防止我们把应用程序的icon图标和普通的图片资源一起放到drawable文件夹下显的杂乱。
            将icon放置在mipmap文件夹还可以让我们程序的launcher图标自动拥有跨设备密度展示的能力,比如说一台屏幕密度是xxhdpi的设备可以自动加载mipmap-xxxhdpi下的icon来作为应用程序的launcher图标,这样图标看上去就会更加细腻。
        2. drawable-nodpi文件夹:这个文件夹是一个密度无关的文件夹,放在这里的图片系统就不会对它进行自动缩放,原图片是多大就会实际展示多大。
            但是要注意一个加载的顺序,drawable-nodpi文件夹是在匹配密度文件夹和更高密度文件夹都找不到的情况下才会去这里查找图片的,因此放在drawable-nodpi文件夹里的图片通常情况下不建议再放到别的文件夹里面。
        3. 根据1中的内容,如果将一张图片放在低密度文件夹下,那么在高密度设备上显示图片时就会被自动放大,而如果将一张图片放在高密度文件夹下,
            那么在低密度设备上显示图片时就会被自动缩小。那我们可以通过成本的方式来评估一下,一张原图片被缩小了之后显示其实并没有什么副作用,
            但是一张原图片被放大了之后显示就意味着要占用更多的内存了。因为图片被放大了,像素点也就变多了,而每个像素点都是要占用内存的。
            因此图片资源应该尽量放在高密度文件夹下,这样可以节省图片的内存开支,而UI在设计图片的时候也应该尽量面向高密度屏幕的设备来进行设计。就目前来讲,最佳放置图片资源的文件夹就是drawable-xxhdpi。那么有的朋友可能会问了,不是还有更高密度的drawable-xxxhdpi吗?干吗不放在这里?这是因为,市面上480dpi到640dpi的设备实在是太少了,如果针对这种级别的屏幕密度来设计图片,图片在不缩放的情况下本身就已经很大了,基本也起不到节省内存开支的作用了。
    面试官视角:这道题想考察什么?
        1. 是否了解图片加载到内存的过程与变换(高级)
        2. 是否清楚如何对图片内存占用大小进行优化(高级)
    题目剖析:
        1. 注意是占用内存,不是文件大小
        2. 可以运行时获取
        3. 重要的是能够直接掌握计算方法
    题目结论:
        运行时获取Bitmap大小:
            图片理论占用大小:Bitmap#getByteCount()
            图片实际占用大小:Bitmap#getAllocationByteCount()
            getByteCount()与getAllocationByteCount()的区别:
                一般情况下(不使用Bitmap复用时)两者是相等的;
                通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)。
        图片有哪些来源?
            assets:等同于文件系统
            raw:不经过任何处理
            drawable目录:注意dpi类型的影响
        一个像素占用多大内存?
            Bitmap.Config用来描述图片的像素是怎么被存储的?
            ARGB_8888: 每个像素4字节. 共32位,默认设置。
            Alpha_8: 只保存透明度,共8位,1字节。
            ARGB_4444: 共16位,2字节。
            RGB_565:共16位,2字节,只存储RGB值。
        dpi:像素密度
            ldpi:0dpi ~ 120dpi
            mdpi:120dpi ~ 160dpi
            hdpi:160dpi ~ 240dpi
            xhdpi:240dpi ~ 320dpi
            xxhdpi:320dpi ~ 480dpi
            xxxhdpi:480dpi ~ 640dpi
        dip/dp:
            drawable:1
            drawable-hdpi:1.5
            drawable-mdpi:1
            drawable-xhdpi:2
            drawable-xxhdpi:3
            drawable-xxxhdpi:4
        直接计算图片大小:
            assets:不考虑压缩,只是加载一张Bitmap,那么它占用的内存 = width * height * 一个像素所占的内存。
            // inDensity默认为图片所在文件夹对应的密度。inDensity = 对应drawable目录的dpi值
            // inTargetDensity为当前系统密度。inTargetDensity = getResources().getDisplayMetrics().densityDpi
            // 例如:红米Note7手机 density = 2.75    densityDpi = 440
            drawable目录:加载一张本地资源图片,占用的内存 = width * inTargetDensity/inDensity * height * inTargetDensity/inDensity * 一个像素所占的内存。
        Drawable中的图片加载流程:
            当我们使用资源id来去引用一张图片时,Android会使用一些规则来去帮我们匹配最适合的图片。
            什么叫最适合的图片?比如我的手机屏幕密度是xxhdpi,那么drawable-xxhdpi文件夹下的图片就是最适合的图片。
            因此,当我引用ic_launcher这张图时,如果drawable-xxhdpi文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的。
            但是,如果drawable-xxhdpi文件夹下没有这张图时,系统就会自动去其它文件夹下找这张图了,优先会去更高密度的文件夹下找这张图片,
            若我们当前的场景就是drawable-xxxhdpi文件夹,然后发现这里也没有ic_launcher这张图,接下来会尝试再找更高密度的文件夹,
            发现没有更高密度的了,这个时候会去drawable-nodpi文件夹找这张图,发现也没有,那么就会去更低密度的文件夹下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。
            总体匹配规则就是这样,那么比如说现在终于在drawable-mdpi文件夹下面找到ic_launcher这张图了,但是系统会认为你这张图是专门为低密度的设备所设计的,如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作。
            那么同样的道理,如果系统是在drawable-xxxhdpi文件夹下面找到这张图的话,它会认为这张图是为更高密度的设备所设计的,如果直接将这张图在当前设备上使用就有可能会出现像素过高的情况,于是会自动帮我们做一个缩小的操作。
        图片内存体积优化:
            1. 跟文件存储格式无关,无论是jpg、png等,加载过后都是ARGB_8888。
            2. 使用 inSimpleSize采样,大图 -> 小图
                核心思想:降低图片占用内存大小
                BitmapFactory根据实际控件大小对图片进行采样,用到 BitmapFactory.Options 的 insimpleSize 和 inJustDecodeBounds 参数
                纯色尽量使用color;规则图形尽量用shape;稍微复杂使用9patch图;如果不能使用9patch针对几种主流分辨率机型进行切图。
            3. 使用矩阵变换来放大图片:小图 -> 大图
            4. 使用RGB_565来加载不透明图片
            5. 使用.9(9-patch)图片做背景
            6. 不使用图片,比如:帧动画、贝塞尔曲线动画、纯色使用color
                优先使用VectorDrawable
                时间和技术允许的前提下使用代码编写动画
    

笔记中部分源码