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

语言: CN / TW / HK

theme: mk-cute

前言

面试专题前面的百度篇,腾讯篇,阿里篇,京东篇,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》,让学习更贴近未来实战。

总共囊括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进行多次测量

举个例子 ```

<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宽度为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 次

image.png

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

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

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

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

image.png

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

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

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

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

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

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

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

image.png 总结成一句话就是,在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: 每段的"中间"下标

最好情况下 :

image.png

最坏情况下

image.png

时间复杂度: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);
}

} ```

image.png

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 修改下,就可以出现类似于上面某信的效果,如下图所示:

image.png

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();终止广播

有序广播功能概述:

image.png

广播类型:

image.png

activity_main.xml

```

<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="发送有序广播"
  />

```

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,接受到了广播事件"); } } ```

image.png

image.png

image.png

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代码如下:

```

</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>

```

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的主要组成成分:

text XML是element、attribute和element content。

JSON的主要组成成分

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

11.Android view绘制流程

(第一章第2题)

12.surfaceView的显示与隐藏

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

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

像是如下这样

image.png

但是呢,一般的做法就是在当前界面先隐藏一个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.关于移动端适配机型

image.png

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文件、堆栈信息等,基本原理如下:

image.png

检测到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文件:

image.png

设置或清除动画代码:

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

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

image.png

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中定义,也可以在代码中定义!

透明度的定义:

```

```

缩放的定义:

```

```

平移的定义:

```

```

旋转的定义:

```

```

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的方式实现

```

```

使用:

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的组合

```

<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>

```

Java方式的组合

ObjectAnimator translation = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX); // 平移动画 ObjectAnimator rotate = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f); // 旋转动画 ObjectAnimator alpha = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f); // 透明度动画 // 创建组合动画的对象 AnimatorSet animSet = new AnimatorSet(); // 根据需求组合动画 animSet.play(translation).with(rotate).before(alpha); animSet.setDuration(5000); //启动动画 animSet.start();

常用的组合方法

  • AnimatorSet.play(Animator anim) :播放当前动画
  • AnimatorSet.after(long delay) :将现有动画延迟x毫秒后执行
  • AnimatorSet.with(Animator anim) :将现有动画和传入的动画同时执行
  • AnimatorSet.after(Animator anim) :将现有动画插入到传入的动画之后执行
  • AnimatorSet.before(Animator anim) : 将现有动画插入到传入的动画之前执行

3.5 Evaluator估值器

表示计算某个时间点,动画需要更新 view 的值。

Evaluator.evaluate(float fraction, T startValue, T endValue) 是核心方法。其中,fraction 表示一个百分比。startValue 和 endValue 表示动画的起始值和结束值。通过 fraction、startValue、endValue 计算 view 对应的属性位置。

常用的就那么几个:

ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(animationView, "X", 0, 500); objectAnimator.setInterpolator(new LinearInterpolator()); objectAnimator.setEvaluator(new FloatEvaluator()); objectAnimator.setDuration(5 * 1000); objectAnimator.start();

3.6 简单Demo,

实现开始隐藏在屏幕顶部,已动画的形式慢慢返回:

``` text.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onGlobalLayout() { text.getViewTreeObserver().removeOnGlobalLayoutListener(this); textHeight = text.getHeight(); Log.e("tag", "textHeight: "+textHeight);

        //一开始需要先让text往上移动它自身的高度
        ViewHelper.setTranslationY(text, -textHeight);
        Log.e("tag", "top:"+text.getTop());
            //再以动画的形式慢慢滚动下拉
        text.animate(text).translationYBy(textHeight)
            .setDuration(500)
            .setStartDelay(1000)
            .start();

```

属性动画设置控件的高度,实现动画关闭和打开的效果:

``` private boolean isOpen = false;

/**
 * 状态的开关。上下关闭的属性动画
 */
private void toggle() {
    ValueAnimator animator = null;
    if (isOpen) {
        isOpen = false;
        //开启属性动画
        animator = ValueAnimator.ofInt(mDesHeight, 0);
    } else {
        isOpen = true;
        animator = ValueAnimator.ofInt(0, mDesHeight);
    }


    //动画的过程监听
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            Integer height = (Integer) valueAnimator.getAnimatedValue();
            mParams.height = height;
            llDesRoot.setLayoutParams(mParams);
        }
    });
    //设置动画的状态监听。给小箭头设置状态
    animator.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animator) {
        }


        @Override
        public void onAnimationEnd(Animator animator) {
             //结束的时候,更换小箭头的图片
            if (isOpen){
                ivArrow.setImageResource(R.drawable.arrow_up);
            }else {
                ivArrow.setImageResource(R.drawable.arrow_down);
            }
        }


        @Override
        public void onAnimationCancel(Animator animator) {
        }


        @Override
        public void onAnimationRepeat(Animator animator) {


        }
    });

    animator.setDuration(200);  //动画时间
    animator.start();           //启动
}

```

属性动画讲的好乱,太多了,比较复杂。后面会有更详细的代码!

④过渡动画

4.1 Android5.0以前的过渡动画

同样可以在xml中定义 ,也可以使用java代码控制

我们在style文件夹中定义

```

<style name="My_AnimationActivity" mce_bogus="1" parent="@android:style/Animation.Activity">
    <item name="android:activityOpenEnterAnimation">@anim/open_enter</item>
    <item name="android:activityCloseExitAnimation">@anim/close_exit</item>
</style>

<!--上下进出场的activity动画-->
<style name="up_down_activity_anim" mce_bogus="1" parent="@android:style/Animation.Activity">
    <item name="android:activityOpenEnterAnimation">@anim/open_up</item>
    <item name="android:activityCloseExitAnimation">@anim/close_down</item>
</style>

```

定义的文件如下,补间动画的方式:

```

<translate
    android:duration="270"
    android:fromXDelta="100%p"
    android:toXDelta="0%p" />

<translate
    android:duration="270"
    android:fromXDelta="0%p"
    android:toXDelta="-100%p" />

```

对应的Activity实现指定的样式即可实现。

在Java文件中同样可以通过 overridePendingTransition 来实现。

大致实现如下:

``` startActivity(intent); overridePendingTransition(R.anim.bottom_top_anim, R.anim.alpha_hide);

finish(); overridePendingTransition(R.anim.alpha_show, R.anim.top_bottom_anim); ```

4.2 Android5.0以后的过渡动画

5.0之后,Android就自带几种动画特效。 3种转场动画 ,1种共享元素。

三种转场动画如下:

``` @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void explode(View view) { intent = new Intent(this, TransitionActivity.class);

    intent.putExtra("flag", 0);

    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());


}


@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void slide(View view) {
    intent = new Intent(this, TransitionActivity.class);

    intent.putExtra("flag", 1);

    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());


}


@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public void fade(View view) {
    intent = new Intent(this, TransitionActivity.class);

    intent.putExtra("flag", 2);

    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle());

}

```

通过对面的页面来指定实现的方式:

``` @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);

     getWindow().requestFeature(Window.FEATURE_CONTENT_TRANSITIONS);


    int flag = getIntent().getExtras().getInt("flag");


    switch (flag) {
        case 0:
            //分解效果 上面的上面消失  下面的下面消失  分解掉了
            getWindow().setEnterTransition(new Explode());

            break;
        case 1:
            //滑动效果 默认上下滑动
            getWindow().setEnterTransition(new Slide());

            break;
        case 2:
            //淡出效果  透明度
            getWindow().setEnterTransition(new Fade());
            getWindow().setExitTransition(new Fade());

            break;
        case 3:
            break;
    }

    setContentView(R.layout.activity_transition);

}

```

5.0的Share共享动画:

跳转的方法:

``` @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void share(View view) { View fab = findViewById(R.id.fab_button); intent = new Intent(this, TransitionActivity.class);

    intent.putExtra("flag", 3);

    //创建单个共享

// startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, view, "share") // .toBundle());

    //创建多个共享
    startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this, Pair.create
            (view, "share"),
            Pair.create(fab,"fab"))
            .toBundle());

}

```

share的方式,不需要对方页面接收设置过渡动画,而是需要在xml中配置transitionName属性:

<View android:background="?android:colorPrimary" android:id="@+id/holder_view" android:transitionName="share" android:layout_width="match_parent" android:layout_height="300dp"/>

那边是一个button 共享名字叫“share” 那边是拿到的view 不是button 转过来定义的是view

那边共享的是button 共享名字叫tab 共享过来也定义的button。

如果Share动画 想Share一个ViewGroup怎么办?比如一个Item跳转到Detail页面 可以直接使用这种过渡效果。

``` private void toActivity(View sharedElement) { Intent intent = new Intent(getContext(), TimeTableAcivity.class); ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(getActivity(), sharedElement, "shared_element_end_root"); startActivity(intent, options.toBundle()); } @Override protected void onCreate(Bundle savedInstanceState) { getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS); findViewById(android.R.id.content).setTransitionName("shared_element_end_root"); setEnterSharedElementCallback(new MaterialContainerTransformSharedElementCallback()); getWindow().setSharedElementEnterTransition(buildContainerTransform(true)); getWindow().setSharedElementReturnTransition(buildContainerTransform(false)); super.onCreate(savedInstanceState); }

private MaterialContainerTransform buildContainerTransform(boolean entering) {
    MaterialContainerTransform transform = new MaterialContainerTransform(this, entering);

    transform.setAllContainerColors(
            MaterialColors.getColor(findViewById(android.R.id.content), R.attr.colorSurface));
    transform.addTarget(android.R.id.content);
    //设置动画持续时间(毫秒)
    transform.setDuration(666);
    return transform;
}

```

5.0之后在MD中还有其他的动画,比如揭露动画,不知道算不算转场动画的一种。因为一般也是用于转场的时候使用,但是这个动画我们使用的很少很少。

简单的使用如下:

``` View myView = findView(R.id.awesome_card);

int cx = (myView.getLeft() + myView.getRight()) / 2;
int cy = (myView.getTop() + myView.getBottom()) / 2;


int dx = Math.max(cx, myView.getWidth() - cx);
int dy = Math.max(cy, myView.getHeight() - cy);
float finalRadius = (float) Math.hypot(dx, dy);

Animator animator =
        ViewAnimationUtils.createCircularReveal(myView, cx, cy, 0, finalRadius);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(1500);
animator.start();

```

这些动画虽然牛皮,但是记得5.0以上才生效的哦,同时我们也不能看着什么动画炫酷都想上,转场动画也是在主线程执行的,如果定义不当也会造成卡顿的。

⑤异步动画

在子线程中执行动画?我懂了,看我操作!

``` Thread { val animatorscaleX = ObjectAnimator.ofFloat(mBinding.ivAnim, "scaleX", 2f) val animatorscaleY = ObjectAnimator.ofFloat(mBinding.ivAnim, "scaleY", 2f) val animatortranslationX = ObjectAnimator.ofFloat(mBinding.ivAnim, "translationX", 200f) val animatortranslationY = ObjectAnimator.ofFloat(mBinding.ivAnim, "translationY", 200f)

    val set = AnimatorSet()
    set.setDuration(1000).play(animatorscaleX).with(animatorscaleY).with(animatortranslationX).with(animatortranslationY)
    set.start()
    }.start()

```

开个线程,执行属性动画。 so easy! 等等,怎么写个属性动画这么多代码,修改一下,优雅一点,同样的效果一行代码解决。

Thread { mBinding.ivAnim.animate().scaleX(2f).scaleY(2f).translationX(200f).translationY(200f).setDuration(1000).start() }.start()

运行!

image.png

居然报错?不能运行在没有looper的子线程?哦...我懂了,子线程不能更新UI来着。

到此就引出一个经典面试题,子线程真的不能更新UI吗?当然可以更新UI了。看我操作!

``` public class MyLooperThread extends Thread {

// 子线程的looper
private Looper myLooper;
// 子线程的handler
private Handler mHandler;

// 用于测试的textview
private TextView testView;

private Activity activity;

public Looper getLooper() {
    return myLooper;
}

public Handler getHandler() {
    return mHandler;
}

public MyLooperThread(Context context, TextView view) {
    this.activity = (Activity) context;
    testView = view;
}

@Override
public void run() {
    super.run();
    // 调用了此方法后,当前线程拥有了一个looper对象
    Looper.prepare();
    YYLogUtils.w("消息循环开始");

    if (myLooper == null) {
        while (myLooper == null) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 调用此方法获取当前线程的looper对象
            myLooper = Looper.myLooper();
        }
    }

    // 当前handler与当前线程的looper关联
    mHandler = new Handler(myLooper) {
        @Override
        public void handleMessage(Message msg) {
            YYLogUtils.w("处理消息:" + msg.obj);

            //此线程,此Looper创建的ui可以随便修改
            addTextViewInChildThread().setText(String.valueOf(msg.obj));

            //发现跟ui创建的位置有关。如果ui是在main线程创建的,则在子线程中不可以更改此ui;
            // 如果在含有looper的子线程中创建的ui,则可以任意修改
            // 这里传进来的是主线程的ui,不能修改!低版本可能可以修改
            //CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

// try { // if (testView != null) { // testView.setText(String.valueOf(msg.obj)); // } // } catch (Exception e) { // e.printStackTrace(); // // } } }; Looper.loop(); YYLogUtils.w("looper消息循环结束,线程终止"); }

/**
 * 创建TextView
 */
private TextView addTextViewInChildThread() {
    TextView textView = new TextView(activity);

    textView.setBackgroundColor(Color.GRAY);  //背景灰色
    textView.setGravity(Gravity.CENTER);  //居中展示
    textView.setTextSize(20);

    WindowManager windowManager = activity.getWindowManager();
    WindowManager.LayoutParams params = new WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            0, 0,
            WindowManager.LayoutParams.FIRST_SUB_WINDOW,
            WindowManager.LayoutParams.TYPE_TOAST,
            PixelFormat.TRANSPARENT);
    windowManager.addView(textView, params);

    return textView;
}

} ```

我们需要定义线程,然后准备Looper,并创建内部的Handler处理数据。我们内部线程创建TextView,我们发送handle消息创建textview并赋值。

``` val looperThread = MyLooperThread(this, mBinding.tvRMsg) looperThread.start()

    mBinding.ivAnim.click {

        looperThread.handler.obtainMessage(200, "test set tv'msg").sendToTarget()

    }

```

效果:

image.png

正常显示子线程创建的textview,但是我们传入线程对象的tvRMsg是不能在子线程赋值的,会报错:

CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

结论:如果ui是在main线程创建的,则在子线程中不可以更改此ui; 如果在含有looper的子线程中创建的ui,则可以任意修改。

既然子线程都可以更新UI了,那么子线程执行动画行不行? 当然行!

我们直接修改代码:

``` val looperThread = MyLooperThread(this, mBinding.tvRMsg) looperThread.start()

    mBinding.ivAnim.click {

        //试试子线程执行动画看看
        looperThread.handler.post {
            mBinding.ivAnim.animate().scaleX(2f).scaleY(2f).translationX(200f).translationY(200f).setDuration(1000).start()
        }

    }

```

完美运行!

其实官方早有说明,RenderThread 中运行动画。其实我们上面的Thread类就是仿 HandlerThread 来写的。我们可以使用 HandlerThread 很方便的实现子线程动画。具体的使用方式和我们自定义的 Thread 类似。

我们可以基于系统类 HandlerThread 封装一个异步动画工具类:

class AsynAnimUtil private constructor() : LifecycleObserver { ​ private var mHandlerThread: HandlerThread? = HandlerThread("anim_run_in_thread") ​ private var mHandler: Handler? = mHandlerThread?.run { start() Handler(this.looper) } ​ private var mOwner: LifecycleOwner? = null private var mAnim: ViewPropertyAnimator? = null ​ companion object { val instance: AsynAnimUtil by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { AsynAnimUtil() } } ​ //启动动画 fun startAnim(owner: LifecycleOwner?, animator: ViewPropertyAnimator) { try { if (mOwner != owner) { mOwner = owner addLoopLifecycleObserver() } ​ if (mHandlerThread?.isAlive != true) { YYLogUtils.w("handlerThread restart") mHandlerThread = HandlerThread("anim_run_in_thread") mHandler = mHandlerThread?.run { start() Handler(this.looper) } } ​ mHandler?.post { mAnim = animator.setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) destory() } ​ override fun onAnimationCancel(animation: Animator?) { super.onAnimationCancel(animation) destory() } ​ override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) { super.onAnimationEnd(animation, isReverse) destory() } }) mAnim?.start() } ​ } catch (e: Exception) { e.printStackTrace() } ​ } ​ // 绑定当前页面生命周期 private fun addLoopLifecycleObserver() { mOwner?.lifecycle?.addObserver(this) } ​ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { YYLogUtils.i("AsynAnimUtil Lifecycle -> onDestroy") mAnim?.cancel() destory() } ​ private fun destory() { YYLogUtils.w("handlerThread quit") ​ try { mHandlerThread?.quitSafely() ​ mAnim = null mOwner = null mHandler = null mHandlerThread = null } catch (e: Exception) { e.printStackTrace() } } ​ }

使用的时候就可以直接拿工具类来进行异步动画

mBinding.ivAnim.click { ​ //试试HandlerThread执行动画 val anim = mBinding.ivAnim.animate() .scaleX(2f) .scaleY(2f) .translationXBy(200f) .translationYBy(200f) .setDuration(2000) ​ AsynAnimUtil.instance.startAnim(this, anim) ​ }

Ok,完美运行。这里注意需要传入LifecycleOwner 为了在当前页面关闭的时候及时的停止动画释放资源。

总结与其他动画效果

网上也有很多开源的第三方的动画框架,如gif动画 lottie动画 mp4动画 Leonids粒子动画 SVGA动画 SurfaceView线程动画 Motion动画 VAP动画 等等太多了。这里就不做过多的展开。如果大家有兴趣可以自行搜索哦!

16.startService与bindService的区别

区别:

  1. bindService绑定Activity可以和Service进行交互,而startService不可以
  2. startService启动的服务只可以通过stopService进行停止,bindService启动的无法用stopService停止
  3. bindService启动的服务只能通过unBindService方法进行停止

17.Service保活方式

最近开发了个内部即时通信的app,可以说是真的蛋疼了,我几乎把整个保活的文章全部看了一遍,可以说android界真的是特别的鱼龙混杂。很多文章都写得很片面,容易形成很大的误导。我先说一个最近研究得出来的结论,在7.0或之后的版本,包括三星和国内的这些原生rom,如果不通过用户或厂家设置,至少service是绝对没有任何办法保活的,绝对,除非你还能找到未知的漏洞。虽然我也很头疼,但我真的很赞同谷歌这样的做法,不然天天收推送通知真的是恶心得不行,IOS也得手动去关。现在android可以说是将一切统统杀掉,然后把仅有的一丝权限,给予用户去设置,甚至app中连弹出授权的api都不会给你。最后也提供下目前消息实时push的解决方案。

  • jni保活,在5.0以前,android系统本身是不管理jni层的,所以用linux那套fork机制,可以让进程和app分开,就算关闭app也不会影响到。所以那时很多人说android非常的卡,幸运的是我那段时间用的ios,这些进程连用户都没法关掉,真的特别恶心

  • jobservice和alarmmanager,在5.0之后,连native层也会受到系统限制,比如之前可用的jni包活方式,也就是说无论是杀掉还是冻结,都只和你启动的第一个进程有关,后面不再以父子的关系去看待,而是以历史同组的关系去看待。这个历史同组真的是一个很关键的改变,其实不止killgroup的作用性,无论怎样写代码,都脱离不了系统对你app的控制,比如让你何时休眠之类的。说了这么多,那么jobservice和alarmmanager到底有什么用呢,其实在整个体系下,就可以看做是一个智能省电的定时器,其他没有任何特殊功能,和线程一样会受到运行管控

  • doze,doze是在android6.0的时候出现的,作用在锁屏之后,对app就行一系列的管理,可以说doze是一种底层机制。感觉doze还是很友好的,比如说提供白名单api、延迟执行的操作等等。就是说你每个app都会给你机会去发通知之类的,并且如果在app中授权ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,doze就不会再去管你,也就是如果即时通信的话,加doze白名单后,如果没有monitor,就可以一直在后台刷数据

  • monitor,monitor可以看成是对doze的一种增强实现,也叫做厂商白名单,并且是对手机系统管理的一种实现,在绝大部分7.0的rom中都会有这东西,国产那几乎就是100%有了,可以看做是一个root权限的app。很多app之前一进去就弹窗进doze白名单,这样后台运行的app多起来又卡得不行,android又得背上这大锅。monitor就默认只允许白名单中的app后台运行和自启动,就算在doze白名单中也不行,并且不提供任何弹窗api授权,必须由用户在设置中手动加入白名单,或者找厂商给你加进去,比如QQ、微信、支付宝、搜狗这些

最后总结一下,那么从android7.0之后,到现在的android12,如何实现后台消息实时push呢?

  • 引导用户操作,同时加入doze白名单和monitor白名单,必须两个都加入,app将一直在后台运行,那么就可以保持tcp的连接,就可以实时获取到服务器数据。切记发心跳,需要使用智能省电的定时器~~

  • 和厂商对接,用钱解决一切,让厂商给你加doze和monitor白名单

  • 通过厂商的api,对接推送,这种和ios思路一样,比如目前的华为、小米都有开发实时推送api,比ios恶心的就是需要对接多套。

这是目前实践过能用的方案,其他什么乱七八糟,利用漏洞和系统抢实间的这些就不说了,基本都被堵死了

18.泛型

①为什么需要泛型?

  • 拥有不同参数类型却有相同的执行流程的方法,需要使用泛型;

  • 指定数据类型,可以在编译期间发现类型错误,也不需要进行强制类型转换;

②泛型类和泛型方法、泛型接口的定义

泛型类

public class A<T>{private T data; public A(T d){} public T getData(){return data;}……}

泛型接口:

``` public interface impl{public T method();}//定义1

class impls implements impl{}//调用1,也是泛型

class impls implements impl{public String method();}//调用2,指定了具体类型 ```

泛型方法: (完全独立,不一定要声明在泛型类和泛型接口中)

``` public T method(T,……) {} 泛型方法的标志

class.method();//调用1

class.method();//调用2 ```

③泛型方法辨析

正确判断泛型方法: 开头

④限定类型

extends :指定泛型类派生于哪个类/接口

public class

public T method(T a,V b);

类和接口混用,类要放在开头,有且只能有1个类

⑤泛型中的约束和局限性

  • 不能实例化类型变量:new T()// 不行

  • 静态域和静态方法里不能引用类型变量 private statc T instance;// 不行,因为在对象创建的时候,才知道T的具体类型

  • 静态方法本身可以是泛型方法

  • 泛型只能是类,不能用基础类型,可以用包装类

  • 泛型不支持instanceof

  • 泛型的原生类型,类型不会因为T的改变而改变:Test<T> 的对象 t(String)t(Float) 的类型是一样的

  • 泛型可以声明数组,却不能new:

    Test

    Test[] arrays;//可以

    Test[] arrays = new Test[10];//不可以

  • 泛型类不能够extends ExceptionThrowable,try……catch不能够捕获泛型类对象,但可以捕获Throwable

    public void doWork(T x)

    {

        try{}catch(T x){}//不行
    
        try{} catch(Throwable e) throw T{throw t;}//可以
    

    }

⑥泛型类型的继承规则

⑦通配符类型

解决继承规则中C和C没有任何关系的局限。

``` class A;

  method(A<? extends/super B>)

  ? extends B:主要用于安全的访问数据,访问类型B

```

限定了泛型类型的上限;必须派生B的派生类;调用时增加

get一定是B;set不能用。

限定了泛型类型的下限;必须是B的超类;

设置只能设置本身和子类,返回只能返回Object,主要用于安全的写入数据

⑧虚拟机如何实现泛型

类型擦除 T 擦除成Object,T extends A,擦除成A(第一个)。实现接口时,在适当的位置加强制类型转化

重载时,泛型参数的类型不通过。

19.重写equals方法需要重写hashCode方法吗

需要。

①我们为什么需要重写hashCode()方法和equals()方法?(Why)

有时在我们的业务系统中判断对象时有时候需要的不是一种严格意义上的相等,而是一种业务上的对象相等。在这种情况下,原生的equals方法就不能满足我们的需求了.

我们所知道的JavaBean的超类(父类)是Object类,JavaBean中的equals方法是继承自Object中的方法.Object类中定义的equals()方法是用来比较两个引用所指向的对象的内存地址是否一致.并不是比较两个对象的属性值是否一致,所以这时我们需要重写equals()方法.

Object类中equals()方法的源码

``` public boolean equals(Object obj) {

   return (this == obj);

}

public class Demo { public static void main(String[] args) { Student stu1 = new Student("awu",22); Student stu2 = new Student("awu",22); System.out.println(stu1.equals(stu2)); /因为Student这个JavaBean没有重写关于属性值相等的equals()方法 ,所以默认比较的是地址值,从而输出结果为false/
} } ```

那么为什么在重写equals方法的时候需要重写hashCode方法呢?

主要是Object.hashCode的通用约定:

  • 在java应用程序运行时,无论何时多次调用同一个对象时的hsahCode()方法,这个对象的hashCode()方法的返回值必须是相同的一个int值.
  • 如果两个对象equals()返回值为true,则他们的hashCode()也必须返回相同的int值.
  • 如果两个对象equals()返回值为false,则他们的hashCode()返回值也必须不同.

以HashSet来说明为什么要这么约定:HashSet存放元素时,根据元素的hashCode值快速找到要存储的位置,如果这个位置有元素,两个对象通过equals()比较,如果返回值为true,则不放入;如果返回值为false,则这个时候会以链表的形式在同一个位置上存放两个元素,这会使得HashSet的性能降低,因为不能快速定位了。

还有一种情况就是两个对象的hashCode()返回值不同,但是equals()返回true,这个时候HashSet会把这两个对象都存进去,这就和Set集合不重复的规则相悖了;所以,我们重写了equals()方法时,要按照b,c规则重写hashCode()方法!(其实就是如果只重写了 equals 方法,两个对象 equals 返回了true,但是如果没有重写 hashCode 方法,集合还是会插入元素。这样集合中就出现了重复元素了。)

②在什么情况下需要重写hashCode()方法和equals()方法? (When)

当我们自定义的一个类,想要把它的实例保存在以Hash散列查找的集合中时,我们就需要重写这两个方法;

```

public class Student { private String name;

private Integer age;

public Student(){

}

public Student(String name,Integer age){ this.name = name; this.age = age; }

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public Integer getAge() { return age; }

public void setAge(Integer age) { this.age = age; }

@Override
public int hashCode(){
final int prime = 31;
int result = 17;
result = prime * result + name.hashCode();
result = prime * result + age;
return result;
}

@Override  
public boolean equals(Object obj){  
    if(this == obj)  
        return true;  
    if(obj == null)  
        return false;  
    if(getClass() != obj.getClass())  
        return false;  
    final Student other = (Student)obj;  
    if(name.equals(other.name)){  
        return false;  
    }  
    if(age.equals(other.age)){  
        return false;  
    }  
    return true;  
}

}

public class Demo { public static void main(String[] args) { Student stu1 = new Student("awu",22); Student stu3 = new Student("awu",33); Student stu2 = new Student("awu",22);

Set set = new HashSet(); set.add(stu1); set.add(stu2); set.add(stu3);

System.out.println(set.size()); /输出结果为2/

} } ```

如果不是以Hash散列查找的集合,即使重写HashCode也没多大实际用处.比如如下栗子:

``` public class Demo { public static void main(String[] args) { Student stu1 = new Student("awu",22); Student stu3 = new Student("awu",33); Student stu2 = new Student("awu",22);

ArrayList list = new ArrayList(); list.add(stu1); list.add(stu2); list.add(stu3);

System.out.println(list .size()); /输出结果为3/

} } ```

③如何重写这两个方法?

``` public class Student { private String name;

private Integer age;

public Student(){

}

public Student(String name,Integer age){ this.name = name; this.age = age; }

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public Integer getAge() { return age; }

public void setAge(Integer age) { this.age = age; }

 @Override  
    public int hashCode(){  
        final int prime = 31;  
        int result = 17;  
        result = prime * result + name.hashCode();  
        result = prime * result + age;  
        return result;  
    }

    @Override  
    public boolean equals(Object obj){  
        if(this == obj)  
            return true;  
        if(obj == null)  
            return false;  
        if(getClass() != obj.getClass())  
            return false;  
            final Student other = (Student)obj;  
        if(name.equals(other.name)){  
            return false;  
        }  
        if(age.equals(other.age)){  
            return false;  
        }  
        return true;  
}

} ```

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

关注公众号:初一十五a

提前解锁 《整套50W字Android体系PDF》,让学习更贴近未来实战。

总共囊括1.腾讯Android开发笔记(33W字)

2.2022最新Android十一位大厂面试专题(12W字)

3.音视频经典面试题(6W字)

4.Jetpack全家桶

5.Android 性能监控框架Matrix

6.JVM

7.车载应用开发

《2022Android十一位大厂面试真题》 结合之前的 《腾讯Android开发笔记》 也算是双管齐下了!😃