撒花,2022Android最新面试专题:完结篇

Posted 初一十五啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了撒花,2022Android最新面试专题:完结篇相关的知识,希望对你有一定的参考价值。

前言

面试专题前面的百度篇,腾讯篇,阿里篇,京东篇,bilibili篇,网易篇,字节篇,小红书,小米,携程十大板块已经更新完了,还剩下最后个专题~持续更新中。
1.12W字;2022最新Android11位大厂面试专题(一)百度篇

2.12W字;2022最新Android11位大厂面试专题(二)阿里篇

3.12W字;2022最新Android11位大厂面试专题(三)腾讯篇

4.面霸养成记;50万字Android面试文档(四五)字节,京东篇

5.面霸养成记;50万字Android面试文档(六七)网易,Bilibili篇

6.面霸养成记;50万字Android面试文档(八九)小米,小红书

7.含泪刷128道面试题,50万字2022最新Android11位大厂面试专题(七)

一共50W字的文档,面试专题12W字只是一小部分,字数限制,分几篇更。

关注公众号:初一十五a
提前解锁 《整套50W字Android体系PDF》,让学习更贴近未来实战。
50W字,腾讯T10级Android高级工程师成长;内容全在这里了

总共囊括

1.腾讯Android开发笔记(33W字)
2.2022最新Android十一位大厂面试专题(12W字)
3.音视频经典面试题(6W字)
4.Jetpack全家桶
5.Android 性能监控框架Matrix
6.JVM
7.车载应用开发

共十一模块,今天来更新第11专题爱奇艺篇,完结啦🤣

十一丶爱奇艺

1.android布局层级过深为什么会对性能有影响?为什么Compose没有布局嵌套问题?

做过布局性能优化的同学都知道,为了优化界面加载速度,要尽可能的减少布局的层级。这主要是因为布局层级的增加,可能会导致测量时间呈指数级增长。

而Compose却没有这个问题,它从根本上解决了布局层级对布局性能的影响: Compose界面只允许一次测量。这意味着随着布局层级的加深,测量时间也只是线性增长的.

下面我们就一起来看看Compose到底是怎么只测量一次就把活给干了的,本文主要包括以下内容:

  • 布局层级过深为什么影响性能?
  • Compose为什么没有布局嵌套问题?

①布局层级过深为什么影响性能?

我们总说布局层级过深会影响性能,那么到底是怎么影响的呢?主要是因为在某些情况下ViewGroup会对子View进行多次测量

举个例子

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <View
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@android:color/holo_red_dark" />

    <View
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@android:color/black" />
</LinearLayout>
  • LinearLayout宽度为wrap_content,因此它将选择子View的最大宽度为其最后的宽度
  • 但是有个子View的宽度为match_parent,意思它将以LinearLayout的宽度为宽度,这就陷入死循环了
  • 因此这时候, LinearLayout 就会先以0为强制宽度测量一下子View,并正常地测量剩下的其他子View,然后再用其他子View里最宽的那个的宽度,二次测量这个match_parent的子 View,最终得出它的尺寸,并把这个宽度作为自己最终的宽度。
  • 这是对单个子View的二次测量,如果有多个子View写了match_parent ,那就需要对它们每一个都进行二次测量。
  • 除此之外,如果在LinearLayout中使用了weight会导致测量3次甚至更多,重复测量在Android中是很常见的

上面介绍了为什么会出现重复测量,那么会有什么影响呢?不过是多测量了几次,会对性能有什么大的影响吗?

之所以需要避免布局层级过深是因为它对性能的影响是指数级的

  • 如果我们的布局有两层,其中父View会对每个子View做二次测量,那它的每个子View一共需要被测量 2 次
  • 如果增加到三层,并且每个父View依然都做二次测量,这时候最下面的子View被测量的次数就直接翻倍了,变成 4 次
  • 同理,增加到 4 层的话会再次翻倍,子 View 需要被测量 8 次

也就是说,对于会做二次测量的系统,层级加深对测量时间的影响是指数级的,这就是Android官方文档建议我们减少布局层级的原因

②Compose为什么没有布局嵌套问题?

我们知道,Compose只允许测量一次,不允许重复测量。

如果每个父组件对每个子组件只测量一次,那就直接意味着界面中的每个组件只会被测量一次

这样即使布局层级加深,测量时间却没有增加,把组件加载的时间复杂度从O(2ⁿ) 降到了 O(n)。

那么问题就来了,上面我们已经知道,多次测量有时是必要的,但是为什么Compose不需要呢?

Compose中引入了固有特性测量(Intrinsic Measurement)

固有特性测量即Compose允许父组件在对子组件进行测量之前,先测量一下子组件的「固有尺寸」

我们上面说的,ViewGroup的二次测量,也是先进行这种「粗略测量」再进行最终的「正式测量」,使用固有特性测量可以产生同样的效果

而使用固有特性测量之所以有性能优势,主要是因为其不会随着层级的加深而加倍,固有特性测量也只进行一次

Compose会先对整个组件树进行一次Intrinsic测量,然后再对整体进行正式的测量。这样开辟两个平行的测量过程,就可以避免因为层级增加而对同一个子组件反复测量所导致的测量时间的不断加倍了。

总结成一句话就是,在Compose里疯狂嵌套地写界面,和把所有组件全都写进同一层里面,性能是一样的!所以Compose没有布局嵌套问题

2.kotlin协程

kotlin(完整版)→33W字开发笔记中有详细版

3.HashMap原理(第三章第10题)

4.算法:手写快排

快速排序算法思路

从小到大排序时

快速排序算法原理
快速排序利用了分治的思想,采用递归来实现
如果要排序一个数组,先取一个参照数(这里取数组最后一个数作为参照)把数组从分成以参数为”中间“结点的前后2部分,数组中参照数之前的都小于参照数,参照数之后的都大于参照数,
然后,对参照数的前后两部分分别排序 这样整个数组就有序了。

递推公式:

  • quickSort(p,r) = partition(p,r)+quickSortCore(p,q-1)+quickSortCore(q+1,r);
  • q = partition(p,r);
  • 终止条件:p>=r 不用继续分解
  • p: 每段的起始下标
  • r: 每段的末尾下标
  • q: 每段的"中间"下标

最好情况下 :

最坏情况下

时间复杂度:O(N*logN) ~ O(n 2 n^2n 2 )

1.快速排序算法的耗时是在拆分这里(partition),通过上图可以看出是个完全二叉树,当n规模足够大时就可以近似看成是满二叉树,由partition函数来看合并的时间复杂度是O(n)

  • 最好情况下 O(nlogn):即拆分时左右2部分均分,时间复杂度由上图可知每层处理的数据规模都是n 假设每层耗时常量t 树高为h,那么总耗时=nth, T(n) = ntlog ⁡ 2 n \\log_2 nlog 2 n (参考:完全二叉树的树高H和结点数N的关系)

  • 最坏情况下 O(n 2):即当数组数据已经有序时例如1,2,3,4,5,此时无法均分,取5为参照数,每次就只会执行quickSortCore(p,q-1)方法,partition处理的数据规模就是每次:T(n) = (n+n-1+ …1)t = tn*(n-1)/2 (t指单个数据处理耗时,是个常量值,n指数据规模)

    空间复杂度:O(logn)~O(n nn),快速排序虽然没有申请额外的空间,但是有递归函数调用栈的内存消耗。

  • 最好情况下 O(logn):即拆分时左右2部分均分时树深度最深也就是树的高度可表示log ⁡ 2 n \\log_2 nlog 2 n。

  • 最坏情况下 O(n nn):即当数组数据已经有序时例如1,2,3,4,5,此时无法均分,取5为参照数,每次就只会执行quickSortCore(p,q-1)方法一直往下递归

    快速排序是不稳定的排序算法。 例如6,8,7,6,3,5,9,4,取末尾4为参照数,拆分时,当下标j=4指向数3时 此时下标i=-1,r=7 ,arr[j] < arr[r] 进入交换导致第一个6在第二个6的后面顺序发生改变。

总结:快速排序算法时间复杂度:O(N*logN) ~ O(n 2 n^2n 2), 空间复杂度:O(logn) ~ O(n nn),该算法可以进行进一步优化:出发点就是尽量使其每次左右均分数据规模

①核心代码

private static int[] quickSort(int[] arr) 
        if (null != arr && arr.length > 0) 
            quickSortCore(arr, 0, arr.length - 1);
        
        return arr;
    

    private static void quickSortCore(int[] arr, int p, int r) 
        if (p >= r) 
            return;
        
        int q = partition(arr, p, r);
        quickSortCore(arr, p, q - 1);
        quickSortCore(arr, q + 1, r);
    

    /**
     * [p,i] 小于 arr[r]的区域
     * [i+1,j-1] 大于 arr[r]的区域
     * [j,r-1]  未处理的区域
     * r 下标默认是分区点参照的元素
     * p,j,i,r 均指下标,arr[r]指参照数
     *
     * @param arr
     * @param p
     * @param r
     * @return
     */
    private static int partition(int[] arr, int p, int r) 
        // 初始时设置i= -1,表示小于参照数的区域是空,i标识小于参照数的区域末尾位置
        int i = p - 1;
        // 扫描未处理区域和挨个和参照数进行比较
        for (int j = p; j < r; j++) 
            // 比参照数小的放到[p,i]区域,[p,i]区域就开始扩大了
            if (arr[j] < arr[r]) 
                swap(arr, i + 1, j);
                i++;
            
        
        // 再把参照数放到比它小的区域的后一个下标位置,这样 参照数左侧就是全部小于参照数的数,右侧就是大于参照数的数,可以继续往下拆分左右2侧递归了
        swap(arr, i + 1, r);
        return i + 1;
    

    private static void swap(int[] arr, int i, int j) 
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    

②测试用例

package arithmetic.ecut.com.排序.a_快速排序;

/**
 * 快速排序
 *
 * @author 起凤
 * @description: TODO
 * @date 2022/4/11
 */
public class QuickSort 
    public static void main(String[] args) 
        int[] arr = 1, 5, 6, 2, 3, 4;
        print(quickSort(arr));

        int[] arr1 = 2, 3, 1, 4, -1, 8, -1;
        print(quickSort(arr1));

        int[] arr2 = -1, 7, 1, 4, 5, 8, 7;
        print(quickSort(arr2));
    

    private static int[] quickSort(int[] arr) 
        if (null != arr && arr.length > 0) 
            quickSortCore(arr, 0, arr.length - 1);
        
        return arr;
    

    private static void quickSortCore(int[] arr, int p, int r) 
        if (p >= r) 
            return;
        
        int q = partition(arr, p, r);
        quickSortCore(arr, p, q - 1);
        quickSortCore(arr, q + 1, r);
    

    /**
     * [p,i] 小于 arr[r]的区域
     * [i+1,j-1] 大于 arr[r]的区域
     * [j,r-1]  未处理的区域
     * r 下标默认是分区点参照的元素
     * p,j,i,r 均指下标,arr[r]指参照数
     *
     * @param arr
     * @param p
     * @param r
     * @return
     */
    private static int partition(int[] arr, int p, int r) 
        // 初始时设置i= -1,表示小于参照数的区域是空,i标识小于参照数的区域末尾位置
        int i = p - 1;
        // 扫描未处理区域和挨个和参照数进行比较
        for (int j = p; j < r; j++) 
            // 比参照数小的放到[p,i]区域,[p,i]区域就开始扩大了
            if (arr[j] < arr[r]) 
                swap(arr, i + 1, j);
                i++;
            
        
        // 再把参照数放到比它小的区域的后一个下标位置,这样 参照数左侧就是全部小于参照数的数,右侧就是大于参照数的数,可以继续往下拆分左右2侧递归了
        swap(arr, i + 1, r);
        return i + 1;
    

    private static void swap(int[] arr, int i, int j) 
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    

    private static void print(int[] sort) 
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < sort.length; i++) 
            builder.append(sort[i]).append(",");
        
        System.out.println(builder);
    


5.Activity启动模式

①标准模式——standard

这个启动模式是最常见的,Activity 默认就是此启动模式。每启动一次 Activity,就会创建一个新 Activity 实例并置于栈顶。谁启动了这个 Activity,那么这个 Activity 就运行在启动它的那个 Activity 所在的栈中。

其实后面这句话挺重要,之前学习的时候并不是太理解这句话,但也是在不久前遇到了一个问题让我重新理解了:Android 中有多窗口模式,这个问题是在多窗口模式下发现的,我们应用中有个地方会调用设置中的页面来选择声音,在正常模式下是没有问题的,但是多窗口模式下就会重新启动一个任务栈,但我们系统中限制多窗口模式下只能有一个应用在前台,结果我们自己的应用被干掉了。。。大家一定引以为戒,知识点的每一句话都有可能有用!

下面咱们来测试下标准模式,先一步一步来,先从第一个页面跳转到第二个页面,看下 log:

E/MainActivity: onCreate: 
E/MainActivity: onStart: 
E/MainActivity: onResume: 
E/MainActivity: onPause: 
E/TwoActivity: onCreate: 
E/TwoActivity: onStart: 
E/TwoActivity: onResume: 
E/MainActivity: onStop:

没什么问题,和预想的一致,然后回到桌面再打开应用,看下 log:

E/TwoActivity: onPause: 
E/TwoActivity: onStop: 
E/TwoActivity: onStart: 
E/TwoActivity: onResume:

嗯,没问题,现在任务栈里有两个 Activity,点击返回键依次退出再来看下 log:

E/TwoActivity: onPause: 
E/MainActivity: onStart: 
E/MainActivity: onResume: 
E/TwoActivity: onStop: 
E/TwoActivity: onDestroy: 
E/MainActivity: onPause: 
E/MainActivity: onStop:

从第二个 Activity 回到第一个 Activity 可以理解,但是大家有没有发现第一个 Activity 并没有走 onDestroy ,这里引用下一个厉害的大哥文章中的描述吧:

Android 12 以前,当我们处于 Root Activity 时,点击返回键时,应用返回桌面, Activity 执行 onDestroy,程序结束。Android 12 起同样场景下 Activity 只会 onStop,不再执行 onDestroy。

到这里标准模式就差不多了,因为这是默认的启动模式,大家使用也最频繁,也就不再啰嗦。

栈顶模式——singleTop

栈顶模式其实很好理解,如果栈顶存在该activity的实例,则复用,不存在新建放入栈顶,它的表现几乎和 上面刚说的标准模式一模一样,栈顶模式的 Activity 实例可以无限多,唯一的区别是如果在栈顶已经有一个相同类型的 Activity 实例,那么 Intent 则不会再去创建一个 Activity,而是通过 onNewIntent() 发送到现有的Activity。

比如应用现在在一个详情页面,而且这个页面启动模式为栈顶模式,这个时候来了一个通知,点击通知正好要跳转到详情页面,那么这个时候任务栈就不会为这个 Activity 再创建一个实例而用已经在栈顶的之前创建好的 Activity 实例。

③栈内复用——singleTask

这个模式之前真的没有理解透彻,之前我理解的就是如果栈内存在该 Activity 的实例则进行复用,如果不存在则创建。

接下来将刚才的 Demo 中的主 Activity 的启动模式改为栈内复用,先来看下启动应用后点击跳转到第二个 Activity 的 log:

E/MainActivity: onCreate: 
E/MainActivity: onStart: 
E/MainActivity: onResume: 
E/MainActivity: onPause: 
E/TwoActivity: onCreate: 
E/TwoActivity: onStart: 
E/TwoActivity: onResume: 
E/MainActivity: onStop:

目前来看还是比较正常的,接下来直接回到桌面,再来看下 log:

E/TwoActivity: onPause: 
E/TwoActivity: onStop:

也还对着呢,然后再次打开应用再看 log:

E/TwoActivity: onDestroy: 
E/MainActivity: onStart: 
E/MainActivity: onResume:

是不是不对了,我本来想让应用回到第二个 Activity,但为什么第二个 Activity 直接销毁了?

其实栈内复用中还有一点要注意,也正是我忽略的重要一点:栈内复用模式会将该实例上边的 Activity 全部出栈,将该实例置于栈顶,这也就是出现文章开头我说的那个问题的根本原因。

④单例模式——singleInstance

单例模式,顾名思义,就是新开一个任务栈,该栈内只存放当前实例。比如说项目中语音通话功能,来电显示页面采用的就可以采用单例模式进行处理。

当然还有别的方法来新开任务栈,比如说启动 Activity 的时候加上 FLAG_ACTIVITY_NEW_TASK ,也会开启一个新的任务栈。

这里需要注意,即使将 Activity 的启动模式设置为单例模式或者添加了 flag,也不会出现像上面某信那种效果,因为 Activity 的 taskAffinity 是一样的,但如果将 Activity 的 taskAffinity 修改下,就可以出现类似于上面某信的效果,如下图所示:

6.Activity四大启动方式生命周期

①Standard 标准模式启动

android:launchMode=“standard”

归纳:直接在栈顶新建一个Activity进行启动

生命周期:

===>开启一个Activity

I/LifecycleActivity_A: onCreate:
I/LifecycleActivity_A: onStart:
I/LifecycleActivity_A: onResume:

===>退出

I/LifecycleActivity_A: onPause:
I/LifecycleActivity_A: onStop:
I/LifecycleActivity_A: onDestroy:

②SingleTop 栈顶启动

manifest:android:launchMode=“singleTop”
FLAG:FLAG_ACTIVITY_SINGLE_TOP

归纳:当启动的Activity在栈顶时,则进行复用,如果栈顶没有时使用默认的Standard模式

生命周期:

===> 启动A:

A: onCreate:
A: onStart:
A: onResume:

===> 栈顶启动:

A: onPause:
A: onNewIntent:
A: onResume:

FLAG_ACTIVITY_CLEAR_TOP启动

栈顶复用不走newIntent(), 将栈内的需要启动的activity移到最顶处启动,并将上面的activity全部出栈销毁。

===> 启动A FLAG_ACTIVITY_NEW_TASK

I/LifecycleActivity_A: onCreate:
I/LifecycleActivity_A: onStart:
I/LifecycleActivity_A: onResume:
I/LifecycleActivity_A: onPause:

===> 启动B FLAG_ACTIVITY_NEW_TASK

I/LifecycleActivity_A: onPause:
I/LifecycleActivity_B: onCreate:
I/LifecycleActivity_B: onStart:
I/LifecycleActivity_B: onResume:
I/LifecycleActivity_A: onStop:

===> 启动A FLAG_ACTIVITY_CLEAR_TOP

I/LifecycleActivity_B: onPause:
I/LifecycleActivity_A: onDestroy:
I/LifecycleActivity_A: onCreate:
I/LifecycleActivity_A: onStart:
I/LifecycleActivity_A: onResume:
I/LifecycleActivity_B: onStop:
I/LifecycleActivity_B: onDestroy:

===>A退出

I/LifecycleActivity_A: onPause:
I/LifecycleActivity_A: onStop:
I/LifecycleActivity_A: onDestroy:

③SingleTask 栈内复用模式

android:launchMode=“singleTask”

归纳:当栈内有需要启动的Activity时,则将该Activity移动到栈顶,并在栈中这个activity顶部的activity全部出栈销毁。

生命周期:

===> 启动A:

A: onCreate:
A: onStart:
A: onResume:

===> 启动B:

A: onPause:
B: onCreate:
B: onStart:
B: onResume:
A: onStop:

===> 栈内启动A:

B: onPause:
A: onNewIntent:
A: onRestart:
A: onStart:
A: onResume:
B: onStop:
B: onDestroy:

④FLAG_ACTIVITY_REORDER_TO_FRONT启动

===>启动A FLAG_ACTIVITY_NEW_TASK

I/LifecycleActivity_A: onCreate:
I/LifecycleActivity_A: onStart:
I/LifecycleActivity_A: onResume:

===> 启动B FLAG_ACTIVITY_NEW_TASK

I/LifecycleActivity_A: onPause:
I/LifecycleActivity_B: onCreate:
I/LifecycleActivity_B: onStart:
I/LifecycleActivity_B: onResume:
I/LifecycleActivity_A: onStop:

===> 启动A FLAG_ACTIVITY_REORDER_TO_FRONT

I/LifecycleActivity_B: onPause:
I/LifecycleActivity_A: onNewIntent:
I/LifecycleActivity_A: onRestart:
I/LifecycleActivity_A: onStart:
I/LifecycleActivity_A: onResume:
I/LifecycleActivity_B: onStop:

===>A退出

I/LifecycleActivity_A: onPause:
I/LifecycleActivity_B: onRestart:
I/LifecycleActivity_B: onStart:
I/LifecycleActivity_B: onResume:
I/LifecycleActivity_A: onStop:
I/LifecycleActivity_A: onDestroy:

===>B退出

I/LifecycleActivity_B: onPause:
I/LifecycleActivity_B: onStop:
I/LifecycleActivity_B: onDestroy:

⑤SingleInstance 单例启动模式

android:launchMode=“singleinstance”

归纳:单独创建一个栈控件启动这个Activity,常用于启动壁纸,电话,闹钟等界面实现。

7.有序广播实例

有序广播:按照接收者的优先级接收,只有一个广播接收者能接收信息,在此广播接收者的逻辑执行完毕后,才会继续传递。

abortBroadcast();终止广播

有序广播功能概述:

广播类型:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btn"
        android:text="发送有序广播"
      />
 
 
</LinearLayout>

MainActivity.java

package com.example.myapplication;
 
import androidx.appcompat.app.AppCompatActivity;
 
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
 
public class MainActivity extends AppCompatActivity 
 MyReceiverOne one ;
 MyReceiverTow two;
 MyReceiverThree three;
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        registerReceiver();
 
        Button button =findViewById(R.id.btn);
        button.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View view) 
                Intent intent=new Intent();
                intent.setAction("Intercept_Stitch");
                sendOrderedBroadcast(intent,null);
            
        );
    
 
    private void registerReceiver() 
        one=new MyReceiverOne();
        IntentFilter filter1=new IntentFilter();
        filter1.setPriority(1000);
        filter1.addAction("Intercept_Stitch");
        registerReceiver(one,filter1);
 
        two=new MyReceiverTow();
        IntentFilter filter2=new IntentFilter();
        filter2.setPriority(900);
        filter2.addAction("Intercept_Stitch");
        registerReceiver(two,filter2);
 
        three=new MyReceiverThree();
        IntentFilter filter3=new IntentFilter();
        filter3.setPriority(600);
        filter3.addAction("Intercept_Stitch");
        registerReceiver(three,filter3);
    
 
 
 

myReceiverone

package com.example.myapplication;
 
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
 
class MyReceiverOne extends BroadcastReceiver 
 
    @Override
    public void onReceive(Context context, Intent intent) 
        Log.i("test","自定义的广播接收者One,接受到了广播事件");
    

MyRiverTwo

package com.example.myapplication;
 
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
 
class MyReceiverTow extends BroadcastReceiver 
    @Override
    public void onReceive(Context context, Intent intent) 
        Log.i("test","自定义的广播接收者Two,接受到了广播事件");
    

MyReceiverThree

package com.example.myapplication;
 
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
 
class MyReceiverThree extends BroadcastReceiver 
    @Override
    public void onReceive(Context context, Intent intent) 
        Log.i("test","自定义的广播接收者Three,接受到了广播事件");
    

8.SharedPreferences的详解

①SharedPreferences 首选项 介绍

存储软件的配置信息
存储的信息:很小,简单的数据;比如:自动登录,记住密码,小说app(返回后再次进入还是 原来看的页数),按钮的状态。
特点:当程序运行首选项里面的数据会全部加载进内容。

②SharedPreferences的简单使用

1.布局文件中,写入两个按钮,保存到SP,和从SP中获取数据
布局代码的文件就不再写了。

2.看MainActivity的代码,里面有注释详解

package com.example.spdemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity 

    private Button btn_save;
    private Button btn_obtain;

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

        btn_save=findViewById(R.id.btn_save);
        btn_obtain=findViewById(R.id.btn_obtain);

        //保存到SP
        btn_save.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                SharedPreferences sp = getSharedPreferences("onClick", MODE_PRIVATE);
                sp.edit().putString("SoundCode","测点代码").apply();//apply才会写入到xml配置文件里面
            
        );

        //获取到SP中的数据
        btn_obtain.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                SharedPreferences sp1 = getSharedPreferences("onClick", MODE_PRIVATE);
                //如果SoundCode,获取的值是空的,则会弹出后面的默认值
                String obtain = sp1.getString("SoundCode", "默认值");
                Toast.makeText(MainActivity.this, obtain, Toast.LENGTH_SHORT).show();
            
        );
    
    /**
     *
     *参数1:SP的名字
     *参数2:SP保存时,用的模式,MODE_PRIVATE常规(每次保存都会更新),MODE_APPEND(每次保存都会追加到后面)
     * @Override
     *     public SharedPreferences getSharedPreferences(String name, int mode) 
     *         return mBase.getSharedPreferences(name, mode);
     *     
     *
     */

③SharedPreferences的实战,实现记录密码和自动登录功能

效果如下:

1.XML代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:text="用户名:"
            android:textSize="25sp"
            android:gravity="right"
            />
        <EditText
            android:id="@+id/et_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="4"
            />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:text="密码:"
            android:textSize="25sp"
            android:gravity="right"
            />
        <EditText
            android:id="@+id/et_password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="4"
            android:inputType="textPassword"
            />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <CheckBox
            android:id="@+id/ck_password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="记住密码"
            android:textSize="20sp"
            />
        <CheckBox
            android:id="@+id/ck_login"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="自动登录"
            android:textSize="20sp"
            />

    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        >
        <Button
            android:id="@+id/btn_register"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="注册"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="5dp"
            />
        <Button
            android:id="@+id/btn_login"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="登录"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="5dp"
            />

    </LinearLayout>
</LinearLayout>

MainActivity代码如下:详解和注释都已经写好

package com.example.shareddemo;

import androidx.appcompat.app.AppCompatActivity;

import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity 

    private EditText et_name;
    private EditText et_password;
    private CheckBox ck_password;
    private CheckBox ck_login;
    private Button btn_register;
    private Button btn_login;
    private SharedPreferences sp;

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

        et_name=findViewById(R.id.et_name);
        et_password=findViewById(R.id.et_password);
        ck_password=findViewById(R.id.ck_password);
        ck_login=findViewById(R.id.ck_login);
        btn_register=findViewById(R.id.btn_register);
        btn_login=findViewById(R.id.btn_login);
         sp = getSharedPreferences("Personal", MODE_PRIVATE);
        //登录方法
        LoginMethod();
        //程序再次进入获取SharedPreferences中的数据
        AgainInto();
    

    private void AgainInto() 
        //如果获取为空就返回默认值
        boolean ck1 = sp.getBoolean("ck_password", false);
        boolean ck2 = sp.getBoolean("ck_login", false);

        //如果是记住密码
        if (ck1)
            String name=sp.getString("name","");
            String password=sp.getString("password","");
            et_name.setText(name);
            et_password.setText(password);
            //记住密码打上√
            ck_password.setChecked(true);
        
        //如果是自动登录
        if (ck2)
            ck_login.setChecked(true);
            Toast.makeText(this, "我是自动登录!", Toast.LENGTH_SHORT).show();
        
    

    private void LoginMethod() 
        btn_login.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                String name=et_name.getText().toString().trim();
                String password=et_password.getText().toString().trim();
                //判断用户名和密码是否为空
                if (TextUtils.isEmpty(name)||TextUtils.isEmpty(password))
                    Toast.makeText(MainActivity.this, "用户名和密码不能为空", Toast.LENGTH_SHORT).show();
                else 
                    //如果记录密码是勾选的
                    if (ck_password.isChecked())
                        //把用户名和密码保存在SharedPreferences中
                        sp.edit().putString("name",name).apply();
                        sp.edit().putString("password",password).apply();
                        sp.edit().putBoolean("ck_password",true).apply();
                    else //没有勾选,保存空值
                        sp.edit().putString("name","").apply();
                        sp.edit().putString("password","").apply();
                        sp.edit().putBoolean("ck_password",false).apply();
                    
                    //如果自动登录是勾选的
                    if (ck_login.isChecked())
                        sp.edit().putBoolean("ck_login",true).apply();
                    else 
                        sp.edit().putBoolean("ck_login",false).apply();
                    
                
            
        );

    

9.xml解析方式

①xml解析方式

解析:操作xml文档,将文档中的数据读取到内存中

操作xml文档

1.解析(读取):将文档中的数据读取到内存中

2.写入:将内存中的数据保存到xml文档中。持久化的存储

解析xml的方式:

1.DOM:将标记语言文档一次性加载进内存,在内存中形成一棵树dom树

优点:操作方便,可以对文档进行CRUD的所有操作

缺点:占内存

2.SAX:逐行读取,基于事件驱动的。

优点:不占内存

缺点:只能读取,不能增删改

②xml常见的解析器

  • JAXP:sun公司提供的解析器,支持dom和sax两种思想

  • DOM4J:一款非常优秀的解析器

  • Jsoup:jsoup 是一款Java 的html解析器,可直接解析某个URL地址、HTML文本内容。

它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据

  • PULL:Android操作系统内置的解析器,sax方式的。

10.json与xml的区别,json为什么比xml更好

① JSON相比XML的不同之处

  • 没有结束标签
  • 更短
  • 读写的速度更快
  • 能够使用内建的 javascript eval() 方法进行解析
  • 使用数组
  • 不使用保留字

总之: JSON 比 XML 更小、更快,更易解析。

②XML和JSON的区别:

XML的主要组成成分:

XML是element、attribute和element content。

JSON的主要组成成分

JSON是object、array、string、number、boolean(true/false)和null。

11.Android view绘制流程

(第一章第2题)

12.surfaceView的显示与隐藏

在安卓中如果有用最灵活的播放,則就是surfaceView实现的。

那么有这样一一个应用场景,就是我想要通过一个按钮比如叫做“选择一个视频”,改按钮会调用出一个dialog去从一个视频列表选择一个视频后,在当前界面载入。

像是如下这样

但是呢,一般的做法就是在当前界面先隐藏一个surfaceView,用setVisibility(View.GONE)或者直接在佈局上面写。

但是你会发现,当你视频文件选择之后,让其重新显示的时候就不能正常显示出来了。

这里要采用如:

sv_with_ds.getChildAt(0).setVisibility(View.VISIBLE);
sv_with_ds.setVisibility(View.VISIBLE);

这里我的surfaceview虽然没有隐藏,但是其父级隐藏了,之前我直接让父级显示,但是surfaceView仍旧不显示,对于这种情况下,要先让surfaceView先显示,虽然它本身没有设置隐藏。

当然纯粹的直接在surfaceView中进行隐藏和显示,我没有试过,那样应该是可以的

那么对于surfaceView来説比较原生一些,经常很容易由于自己理解的不够透彻而导致各种问题的发生。

最常见的就是其播放视频必须要在getHolder所获得的对象中添加一个回调,这个回调必须是实现了SurfaceView

13.关于移动端适配机型

14.ANR 什么时候出现,如何排查

①ANR产生的场景:

  • activity内对事件 5秒无法完成处理
  • BroadcastReceiver 内对事件10秒无法完成处理
  • Service 的各个生命周期函数在特定时间(20秒)内无法完成处理
  • 应用程序UI线程存在耗时操作,例如在UI线程中进行网络请求,数据库操作或者文件操作等,可能会导致UI线程无法及时处理用户输入等。
  • 应用程序UI线程等待子线程释放某个锁,从而无法处理用户的请求的输入
  • 耗时操作的动画需要大量的计算工作,可能导致CPU负载过重

② ANR产生的原因

主线程(UI线程)如果在规定时间内没有处理完相应的工作,就会出现ANR, 超时产生的原因一般有:

  • 主线程在做一些耗时任务
  • 主线程被其他线程锁
  • cpu被其他进程占用,该进程没被分配到足够的cpu资源
  • 自身服务阻塞
  • 系统阻塞
  • 内存紧张

③ANR产生原因定位分析

通过ANR 日志定位问题

当ANR发生时,我们往往通过Logcat和traces文件(目录/data/anr/)的相关信息输出去定位问题。主要包含以下几方面:

  • 基本信息,包括进程名、进程号、包名、系统build号、ANR 类型等等;
  • CPU使用信息,包括活跃进程的CPU 平均占用率、IO情况等等;
  • 线程堆栈信息,所属进程包括发生ANR的进程、其父进程、最近有活动的3个进程等等。

测试过程发现ANR的现状

  • 在平常测试中,ANR有基本测试不到,因为ANR基本发生在垃圾设备中,弱网络,频繁操作。
  • 问题不必现,即使看到了问题,定位麻烦:要去data/anr.txt 文件里面查找。必须root,没有对应关系,分析复杂,导出文件就必须依赖手机零距离。

引入ANR检测工具

由于anr问题不必现,因此引入以下ANR检测工具,当anr问题出现时,自动dump手机中的日志信息如trace文件、堆栈信息等,基本原理如下:

检测到UI主线程卡顿时间超过设定的时间,如4s,即dump trace文件以及堆栈信息,同时抛出异常,收集信息,根据这些文件信息即可定位到发生anr的原因 。

通过traces.txt的文件分析

在APP触发ANR时, 系统默认会生成一个traces.txt的文件,并放在/data/anr/下(我们可以借助第三方的MT管理器或者adb命令进行对traces.txt进行操作)。我们就可以结合traces.txt文件进行分析排查定位出是有app中的哪个类触发的ANR。

15.Android的几种动画定义与使用

Android动画的分类与使用

学习Android必不可少的就是动画的使用了,在Android版本迭代的过程中,出现了很多动画框架,这里做一个总结。

Android动画类型分类

逐帧动画【Frame Animation】,即顺序播放事先准备的图片

补间动画【Tween Animation】,View的动画效果可以实现简单的平移、缩放、旋转。

属性动画【Property Animation】,补间动画增强版,支持对对象执行动画。

过渡动画【Transition Animation】,实现Activity或View过渡动画效果。包括5.0之后的MD过渡动画等。

动画的分类与版本

Android动画实现方式分类都可以分为xml定义和java定义。

Android 3.0之前版本,逐帧动画,补间动画 Android 3.0之后版本,属性动画 Android 4.4中,过渡动画 Android 5.0以上 MD的动画效果

下面一起看看简单的实现吧。

①逐帧动画

推荐使用一些小图片,它的性能不是很好,如果使用大图的帧动画,会出现性能问题导致卡顿。

比较常用的方式,在res/drawable目录下新建动画XML文件:

设置或清除动画代码:

  //开始动画
    mIvRefreshIcon.setImageResource(R.drawable.anim_loading);
    mAnimationDrawable = (AnimationDrawable) mIvRefreshIcon.getDrawable();
    mAnimationDrawable.start();
​
    //停止动画
    mIvRefreshIcon.clearAnimation();
    if (mAnimationDrawable != null)
        mAnimationDrawable.stop();
    

设置Background和设置ImageResource是一样的效果:

 ImageView voiceIcon = new ImageView(CommUtils.getContext());
    voiceIcon.setBackgroundResource(message.isSelf() ? R.drawable.right_voice : R.drawable.left_voice);
    final AnimationDrawable frameAnim = (AnimationDrawable) voiceIcon.getBackground();
​
    frameAnimatio.start();
​
    MediaUtil.getInstance().setEventListener(new MediaUtil.EventListener() 
         @Override
         public void onStop() 
            frameAnimatio.stop();
            frameAnimatio.selectDrawable(0);
        
    );

②补间动画

一句话说明补间动画:只能给View加,不能给对象加,并且不会改变对象的真实属性。

无需关注每一帧,只需要定义动画开始与结束两个关键帧,并指定动画变化的时间与方式等 。主要有四种基本的效果

  • 透明度变化
  • 大小缩放变化
  • 位移变化
  • 旋转变化

可以在xml中定义,也可以在代码中定义!

透明度的定义:

<?xml version="1.0" encoding="utf-8"?> 
<set xmlns:android="http://schemas.android.com/apk/res/android" > 
    <alpha 
        android:duration="1000" 
        android:fromAlpha="0.0" 
        android:toAlpha="1.0" /> 
</set>

缩放的定义:

<?xml version="1.0" encoding="utf-8"?> 
<set xmlns:android="http://schemas.android.com/apk/res/android" > 
    <scale 
        android:duration="1000" 
        android:fillAfter="false" 
        android:fromXScale="0.0" 
        android:fromYScale="0.0" 
        android:interpolator="@android:anim/accelerate_decelerate_interpolator" 
        android:pivotX="50%" 
        android:pivotY="50%" 
        android:toXScale="1.4" 
        android:toYScale="1.4" /> 
</set>

平移的定义:

<?xml version="1.0" encoding="utf-8"?> 
<set xmlns:android="http://schemas.android.com/apk/res/android" > 
    <translate 
        android:duration="2000" 
        android:fromXDelta="30" 
        android:fromYDelta="30" 
        android:toXDelta="-80" 
        android:toYDelta="300" /> 
</set>

旋转的定义:

<?xml version="1.0" encoding="utf-8"?> 
<set xmlns:android="http://schemas.android.com/apk/res/android" > 
    <rotate 
        android:duration="3000" 
        android:fromDegrees="0" 
        android:interpolator="@android:anim/accelerate_decelerate_interpolator" 
        android:pivotX="50%" 
        android:pivotY="50%" 
        android:toDegrees="+350" /> 
</set>

Java代码中使用补间动画(推荐):

透明度定义:

AlphaAnimation alpha = new AlphaAnimation(0, 1); 
alpha.setDuration(500);          //设置持续时间 
alpha.setFillAfter(true);                   //动画结束后保留结束状态 
alpha.setInterpolator(new AccelerateInterpolator());        //添加差值器 
ivImage.setAnimation(alpha);

缩放定义:

ScaleAnimation scale = new ScaleAnimation(1.0f, scaleXY, 1.0f, scaleXY, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); 
scale.setDuration(durationMillis); 
scale.setFillAfter(true); 
ivImage.setAnimation(scale);

平移定义

TranslateAnimation translate = new TranslateAnimation(fromXDelta, toXDelta, fromYDelta, toYDelta); 
translate.setDuration(durationMillis); 
translate.setFillAfter(true); 
ivImage.setAnimation(translate);

旋转定义:

RotateAnimation rotate = new RotateAnimation(fromDegrees, toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); 
rotate.setDuration(durationMillis); 
rotate.setFillAfter(true); 
ivImage.setAnimation(rotate);

组合Set的定义:

 RelativeLayout rlRoot = (RelativeLayout) findViewById(R.id.rl_root);

	// 旋转动画
	RotateAnimation animRotate = new RotateAnimation(0, 360,
				Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
				0.5f);
	animRotate.setDuration(1000);// 动画时间
	animRotate.setFillAfter(true);// 保持动画结束状态


	// 缩放动画
	ScaleAnimation animScale = new ScaleAnimation(0, 1, 0, 1,
				Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,0.5f);
	animScale.setDuration(1000);
	animScale.setFillAfter(true);// 保持动画结束状态


	// 渐变动画
	AlphaAnimation animAlpha = new AlphaAnimation(0, 1);
	animAlpha.setDuration(2000);// 动画时间
	animAlpha.setFillAfter(true);// 保持动画结束状态


	// 动画集合
	AnimationSet set = new AnimationSet(true);
	set.addAnimation(animRotate);
	set.addAnimation(animScale);
	set.addAnimation(animAlpha);

	// 启动动画
	rlRoot.startAnimation(set);

	set.setAnimationListener(new AnimationListener() 

		@Override
		public void onAnimationStart(Animation animation) 
		

		@Override
		public void onAnimationRepeat(Animation animation) 
		

		@Override
		public void onAnimationEnd(Animation animation) 
			// 动画结束,跳转页面
			// 如果是第一次进入, 跳新手引导
			// 否则跳主页面
			boolean isFirstEnter = PrefUtils.getBoolean(
						SplashActivity.this, "is_first_enter", true);

			Intent intent;
			if (isFirstEnter) 
				// 新手引导
				intent = new Intent(getApplicationContext(),
						GuideActivity.class);
			 else 
				// 主页面
				intent = new Intent(getApplicationContext(),MainActivity.class);
			

			startActivity(intent);

			finish();
			
	);

③属性动画

补间动画增强版本。补充补间动画的一些缺点

作用对象:任意 Java 对象,不再局限于 视图View对象

实现的动画效果:可自定义各种动画效果,不再局限于4种基本变换:平移、旋转、缩放 & 透明度

分为ObjectAnimator和ValueAnimator。

3.1 一个简单的属性动画

先用xml的方式实现

<?xml version="1.0" encoding="utf-8"?> 
<set xmlns:android="http://schemas.android.com/apk/res/android"> 
    <animator 
        android:valueFrom="0" 
        android:valueTo="100" 
        android:valueType="intType" 
        android:duration="3000" 
        android:startOffset ="1000" 
        android:fillBefore = "true" 
        android:fillAfter = "false" 
        android:fillEnabled= "true" 
        android:repeatMode= "restart" 
        android:repeatCount = "0" 
        android:interpolator="@android:anim/accelerate_interpolator"/> 
</set> 

使用:

Button b3 = (Button) findViewById(R.id.b3); 
Animator mAnim = AnimatorInflater.loadAnimator(this, R.animator.animator_1_0); 
mAnim.setTarget(b3); 
mAnim.start();

当然我们可以直接使用Java代码实现:

public static ObjectAnimator setObjectAnimator(View view , String type , int start , int end , long time) 
    ObjectAnimator mAnimator = ObjectAnimator.ofFloat(view, type, start, end); 

    // 设置动画重复播放次数 = 重放次数+1 
    // 动画播放次数 = infinite时,动画无限重复 
    mAnimator.setRepeatCount(ValueAnimator.INFINITE); 
    // 设置动画运行的时长 
    mAnimator.setDuration(time); 
    // 设置动画延迟播放时间 
    mAnimator.setStartDelay(0); 
    // 设置重复播放动画模式 
    mAnimator.setRepeatMode(ValueAnimator.RESTART); 
    // ValueAnimator.RESTART(默认):正序重放 
    // ValueAnimator.REVERSE:倒序回放 
    //设置差值器 
    mAnimator.setInterpolator(new LinearInterpolator()); 
    return mAnimator; 

3.2 ValueAnimator与ObjectAnimator区别:

  • ValueAnimator 类是先改变值,然后手动赋值 给对象的属性从而实现动画;是间接对对象属性进行操作;
  • ObjectAnimator 类是先改变值,然后自动赋值 给对象的属性从而实现动画;是直接对对象属性进行操作;
   //不同的定义方式
        ValueAnimator animator = null;

        if (isOpen) 
            //要关闭
            if (longHeight > shortHeight) 
                isOpen = false;
                animator = ValueAnimator.ofInt(longHeight, shortHeight);
            
         else 
            //要打开
            if (longHeight > shortHeight) 
                isOpen = true;
                animator = ValueAnimator.ofInt(shortHeight, longHeight);
            
        

        animator.start();

       
       //不同的定义方式
       ObjectAnimator animatorX = ObjectAnimator.ofFloat(mSplashImage, "scaleX", 1f, 2f);  
       animatorX.start();

3.3 监听动画的方式:

mAnim2.addListener(new AnimatorListenerAdapter()  
    // 向addListener()方法中传入适配器对象AnimatorListenerAdapter() 
    // 由于AnimatorListenerAdapter中已经实现好每个接口 
    // 所以这里不实现全部方法也不会报错 
    @Override 
    public void onAnimationCancel(Animator animation)  
        super.onAnimationCancel(animation); 
        ToastUtils.showShort("动画结束了"); 
     
);

3.4 组合动画AnimatorSet:

xml的组合

<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" 
    android:ordering="sequentially" > 
    <!--表示Set集合内的动画按顺序进行--> 
    <!--ordering的属性值:sequentially & together--> 
    <!--sequentially:表示set中的动画,按照先后顺序逐步进行(a 完成之后进行 b )--> 
    <!--together:表示set中的动画,在同一时间同时进行,为默认值--> 

    <set android:ordering="together" > 
        <!--下面的动画同时进行--> 
        <objectAnimator 
            android:duration="2000" 
            android:propertyName="translationX" 
            android:valueFrom="0" 
            android:valueTo="300" 
            android:valueType="floatType" > 
        </objectAnimator> 

        <objectAnimator 
            android:duration="3000" 
            android:propertyName="rotation" 
            android:valueFrom="0" 
            android:valueTo="360" 
            android:valueType="floatType" > 
        </objectAnimator> 
    </set> 

    <set android:ordering="sequentially" > 
        <!--下面的动画按序进行--> 
        <objectAnimator 
            android:duration="1500" 
            android:propertyName="alpha" 
            android:valueFrom="1" 
            android:valueTo="0" 
            android:valueType="floatType" > 
        </objectAnimator> 
        <objectAnimator 
            android:duration="1500" 
            android:propertyName="alpha" 
            android:valueFrom="0" 
            android:valueTo="1" 
            android:valueType="floatType" > 
        </objectAnimator> 
    </set>
</set> 

Java方式的组合

ObjectAnimator translation = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);  // 平移动画 
ObjectAnimator rotate = ObjectAnimator.ofFloat(mButton,

以上是关于撒花,2022Android最新面试专题:完结篇的主要内容,如果未能解决你的问题,请参考以下文章

含泪刷128道面试题,50万字2022最新Android11位大厂面试专题

12W字;2022最新Android11位大厂面试专题

12W字;2022最新Android11位大厂面试专题阿里篇

12W字;2022最新Android11位大厂面试专题腾讯篇

面霸养成记;50万字2022最新Android11位大厂面试专题

完结撒花常见算法——动态规划