Android面经整理-字节

文章目录

      • 算法题:给你一个数组,判断该数组是不是二叉搜索树的后序遍历序列。(剑指offer30题)
      • Android属性动画和补间动画的区别
      • 属性动画是如何驱动的
      • 提升动画性能
      • android中导致oom的主要原因
      • 多线程问题:线程A等待线程B的执行结果,线程B等待线程C的执行结果
      • IntentService了解过吗?
      • IntentService可以Bind吗?
      • 实现一个子线程的Handler
      • 设计模式
      • 设计模式基本原则
      • commit和apply的区别
      • 实现一个阻塞队列

算法题:给你一个数组,判断该数组是不是二叉搜索树的后序遍历序列。(剑指offer30题)

https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/
由于工作了,好久没看树方面的知识,二叉搜索树的定义有点忘了,面试后搜索定义如下:

二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。

解法一
后序遍历的最后一个数字一定是根节点,我们可以在数组中确定根节点的位置。
Android面经整理-字节_第1张图片
上面这棵树的后序遍历:
[3,5,4,10,12,9],
后续遍历的最后一个数字一定是根节点,所以数组中最后一个数字9就是根节点,我们从前往后找到第一个比9大的数字10,那么10后面的[10,12](除了9)都是9的右子节点,10前面的[3,5,4]都是9的左子节点,10以及10后面的需要判断一下,如果有小于9的,说明不是二叉搜索树,直接返回false。然后再以递归的方式判断左右子树。

代码:

    public boolean verifyPostorder(int[] postorder) {
        return helper(postorder, 0, postorder.length - 1);
    }

    boolean helper(int[] nums, int left, int right) {
        if (left >= right) {
            return true;
        }

        int leftIndex = left;

        // 数组中的最后一个数字是二叉搜索树的根节点
        int root = nums[right];
        while (nums[leftIndex] < root) {
            leftIndex++;
        }
        int rightIndex = leftIndex;
        while (rightIndex < right) {
            if (nums[rightIndex++] < root) {
                return false;
            }
        }
        return helper(nums, left, leftIndex - 1) && helper(nums, leftIndex, right - 1);
    }

Android属性动画和补间动画的区别

两者的主要区别:

  • 作用对象不同,补间动画只能作用于View上,而属性动画可以作用于所有对对应属性提供了set和get方法的对象上。
  • 补间动画只是改变动画的显示效果,不会真正改变View的属性,比如位置,宽高等。而属性动画则会改变对象的属性值。
  • 补间动画只能实现位移,缩放,旋转,透明度四种动画操作,而属性动画可以实现更多的效果。

属性动画是如何驱动的

详细的属性动画的驱动原理可以看下面的博客,博主介绍的很详细。
看了博客之后,可以将属性动画的运行原理大体概括为下面的流程。

  1. 调用start()之后,回去注册屏幕刷新信号,当接收到屏幕刷新信号后,会遍历待执行的属性动画列表,计算待执行的属性动画的动画行为。
  2. 每个动画在处理当前帧的刷新逻辑时,先会根据当前时间和动画和动画开始时间,以及动画持续时长来计算得到当前帧时动画所处的进度,然后将这个进度等价转换到0-1区间之内。
  3. 然后插值器会根据这个初步计算之后的进度值,得到实际的动画进度,取值也是在0-1区间内。
  4. 计算得到当前帧处于哪两个关键帧之间,然后根据估值器的规则将进度映射到实际的值。
  5. 然后将计算得到的值回掉到onAnimationUpdate接口中。

参考:https://www.jianshu.com/p/46f48f1b98a9

提升动画性能

Android应用性能优化最佳实践 2.7节动画性能中,对属性动画和补件动画的性能进行了对比,对比显示属性动画的性能更高。同时动画过程如果只涉及scale,alpha等非布局全量刷新的场景时,可以将硬件加速打开,提升动画流畅性,同时可以使用setLayerType设置layer的类型,比如设置为HARD_WARE_LAYER类型或者SOFTWARE_LAYER。

android中导致oom的主要原因

  1. 加载大图
    Android中Bitmap会占用很多内存,在加载BItmap的时候可以先使用inJustDecodeBounds去解析图片的大小,预估图片占用的内存,或者对图片进行采样和缩放,避免占用过多内存。
  2. 内存泄漏
    单例,非静态内部类持有外部类的引用(AsyncTask,Handler),context被引用导致内存泄漏。
  3. 应用程序自己的虚拟机内存上限太小。
  4. 堆溢出或者永久代溢出
    堆溢出是由于分配大对象,或者内存泄漏引起的。
    永久代中存放了被虚拟机加载的类信息,常量,静态变量等,方法区被撑爆也会造成oom。

多线程问题:线程A等待线程B的执行结果,线程B等待线程C的执行结果

使用CountDownLatch来实现,先简单介绍CountDownLatch的用法和作用。

  • CountDownLatch可以用来使一个线程等待其他线程执行完毕后再执行。调用一次countDown的话,计数器的值会减一,当计数器的值为0时,在CountDownLatch上面等待的线程会被唤醒,重新开始工作。

相关接口:

//参数count为计数值
public CountDownLatch(int count)

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException {};   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  

代码如下:


/**
 * 有三个线程,线程A等待线程B执行结果,
 * 线程B等待线程C的执行结果
 */
public class ThreadABC {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatchAB = new CountDownLatch(1);
        CountDownLatch countDownLatchBC = new CountDownLatch(1);

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatchAB.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(new java.util.Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }, "Thread-A");

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatchBC.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(new java.util.Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
                countDownLatchAB.countDown();
            }
        }, "Thread-B");

        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
                countDownLatchBC.countDown();
            }
        }, "Thread-C");

        threadA.start();
        threadB.start();
        threadC.start();
    }
}

下面的代码线程B会获取lock锁,然后进入await状态,然后线程d会等待1000ms后去尝试获取锁:


/**
 * 有三个线程,线程A等待线程B执行结果,
 * 线程B等待线程C的执行结果
 */
public class ThreadABC {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatchAB = new CountDownLatch(1);
        CountDownLatch countDownLatchBC = new CountDownLatch(2);
        Lock lock = new ReentrantLock();

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatchAB.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(new java.util.Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
            }
        }, "Thread-A");

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread b try lock");
                lock.lock();
                System.out.println("thread b lock");
                try {
                    countDownLatchBC.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                try {
                    Thread.sleep(new java.util.Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());
                countDownLatchAB.countDown();
                lock.unlock();
            }
        }, "Thread-B");

        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
                countDownLatchBC.countDown();
            }
        }, "Thread-C");

        threadA.start();
        threadB.start();
        threadC.start();

        Thread.sleep(1000);
        Thread threadD = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread d try lock");
                lock.lock();
                System.out.println(Thread.currentThread().getName());
                countDownLatchBC.countDown();
                lock.unlock();
            }
        }, "Thread-D");
        threadD.start();
    }
}
thread b try lock
thread b lock
Thread-C
thread d try lock

上面的代码执行结果日志如上,可以看到d县城尝试去lock并没有获取到锁,所以标名await()使线程进入到阻塞状态,但是并没有释放自己持有的锁。

IntentService了解过吗?

IntentService是继承于Service并异步处理请求的一个类,在IntentService内部有一个工作线程来处理耗时任务,当任务执行完时IntentService会自动停止。在执行任务的时候,每一个耗时任务会在onHandleIntent回调方法中执行,并且每次只会执行一个任务,第一个执行完了再执行第二个。
使用IntentService的优点:

  • 省去了在Service中手动开线程的麻烦,第二,当操作完成时,我们不用手动停止Service,第三,it’s so easy to use!

IntentService代码


public class DemoIntentService extends IntentService {
    private static final String TAG = "DemoIntentService";

    public DemoIntentService() {
        super("DemoIntentService");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate: ");
    }

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
        super.onStart(intent, startId);
        Log.i(TAG, "onStart: ");
    }

    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }


    @Override
    protected void onHandleIntent(@Nullable Intent intent) {
        Log.i(TAG, "onHandleIntent: " + intent.toString() + " current thread is " + Thread.currentThread());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: ");
    }
}

activity中启动IntentService的代码:

                //可以启动多次,每启动一次,就会新建一个work thread,但IntentService的实例始终只有一个
                //Operation 1
                Intent startServiceIntent = new Intent();
                startServiceIntent.setClassName("com.example.recyclerviewlearn", "com.example.recyclerviewlearn.DemoIntentService");
                Bundle bundle = new Bundle();
                bundle.putString("param", "oper1");
                startServiceIntent.putExtras(bundle);
                startService(startServiceIntent);

                //Operation 2
                Intent startServiceIntent2 = new Intent();
                startServiceIntent2.setClassName("com.example.recyclerviewlearn", "com.example.recyclerviewlearn.DemoIntentService");
                Bundle bundle2 = new Bundle();
                bundle2.putString("param", "oper2");
                startServiceIntent2.putExtras(bundle2);
                startService(startServiceIntent2);

两次启动IntentService运行的日志如下:

2021-05-18 20:53:01.244 21393-21393/com.example.recyclerviewlearn I/DemoIntentService: onCreate: 
2021-05-18 20:53:01.244 21393-21393/com.example.recyclerviewlearn I/DemoIntentService: onStartCommand: 
2021-05-18 20:53:01.244 21393-21393/com.example.recyclerviewlearn I/DemoIntentService: onStart: 
2021-05-18 20:53:01.245 21393-22982/com.example.recyclerviewlearn I/DemoIntentService: onHandleIntent: Intent { cmp=com.example.recyclerviewlearn/.DemoIntentService (has extras) } current thread is Thread[IntentService[DemoIntentService],5,main]
2021-05-18 20:53:01.245 21393-21393/com.example.recyclerviewlearn I/DemoIntentService: onStartCommand: 
2021-05-18 20:53:01.245 21393-21393/com.example.recyclerviewlearn I/DemoIntentService: onStart: 
2021-05-18 20:53:01.246 21393-22982/com.example.recyclerviewlearn I/DemoIntentService: onHandleIntent: Intent { cmp=com.example.recyclerviewlearn/.DemoIntentService (has extras) } current thread is Thread[IntentService[DemoIntentService],5,main]
2021-05-18 20:53:01.258 21393-21393/com.example.recyclerviewlearn I/DemoIntentService: onDestroy: 

可以看到onHandleIntent方法执行的时候,线程id变成了22982,任务再子线程中执行了。同时每次startService都会调用onStartCommand,onStart,最终调用onHandleIntent方法,任务执行结束后onDestory自动结束生命。
onCreate执行的时候会创建一个HandlerThread对象,并启动线程,紧接着创建ServiceHandler对象,ServiceHandler继承自Handler,用来处理消息。ServiceHandler将获取HandlerThread的Looper就可以开始正常工作了。

    @Override
    public void onCreate() {
        // TODO: It would be nice to have an option to hold a partial wakelock
        // during processing, and to have a static startService(Context, Intent)
        // method that would launch the service & hand off a wakelock.

        super.onCreate();
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();

        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

每一次启动onStart方法,会把数据和消息发送给mServiceHandler,然后发送给onHandleIntent方法。

IntentService可以Bind吗?

    public IBinder onBind(Intent intent) {
        return null;
    }

IntentService的onBind方法返回为null。不建议使用bind方法绑定,如果需要使用bind方法绑定的话,这个时候IntentService就变为了Service。

实现一个子线程的Handler

实现一个子线程的Handler我们可以使用下面的方式:

  • 创建当前线程的Looper

  • 创建当前线程的Handler

  • 调用当前线程Looper对象的loop方法
    代码如下:

public class ChildThreadHandlerActivity extends Activity {
    private MyThread childThread;

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

        childThread = new MyThread();
        childThread.start();

        Handler childHandler = new Handler(childThread.childLooper){//这样之后,childHandler和childLooper就关联起来了。
            public void handleMessage(Message msg) {
                
            };
        };
    }

    private class MyThread extends Thread{
        public Looper childLooper;

        @Override
        public void run() {
            Looper.prepare();//创建与当前线程相关的Looper
            childLooper = Looper.myLooper();//获取当前线程的Looper对象
            Looper.loop();//调用此方法,消息才会循环处理
        }
    }
}

但是上面的代码会存在空指针的问题,因为我们是在子线程的run方法中创建Looper的,有可能主线程中获取looper的时候子线程的run方法还没有得到执行,就会有空指针问题。
那如何解决上面的问题呢,我们可以使用HandlerThread创建得到一个子线程的Looper:

        HandlerThread handlerThread = new HandlerThread("HandlerThread");
        handlerThread.start();

使用HandlerThread是如何避免空指针的呢?查看HandlerThread的实现:

    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

getLooper的时候回去判断,当前的Looper如果为空的话,就会让线程wait(),那么唤醒的地方在哪里呢?

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

HandlerThread的run方法在执行的时候,先会去创建Looper,创建了Looper以后会去调用notifyAll(),这样就将等待的线程唤醒了,从而避免了空指针。

设计模式

工厂模式:
对创建对象的接口进行封装,只根据名称获取对应的具体实现类。
优点:获取产品的时候只用知道名称就可以,扩展的时候增加具体的实现类就可以了。
缺点:同一个接口中扩展产品困难。扩展产品只能增加工厂,增加了复杂度。
参考:https://www.runoob.com/design-pattern/factory-pattern.html

抽象工厂模式:
是围绕一个超级工厂创建其他工厂。该超级工厂又称为其它工厂的工厂。简单理解为有多个接口,根据需要选择实现对应的工厂去创建对应的产品。
优点:将不同产品的接口隔离开来,代码封装较好,
缺点:产品族扩展比较麻烦,得改多处代码。
参考:https://www.runoob.com/design-pattern/abstract-factory-pattern.html

设计模式基本原则

  1. 单一职责原则:
    通俗地说,即一个类只负责一项职责。因为如果一个类负责多项职责的话,对其它功能的修改可能影响已有的功能。
    优点:降低了类的复杂度,一个类仅负责一个职责。其逻辑肯定要比负责多项职责简单。
    缺点:代码改动可能比较大
  2. 开放关闭原则
    软件实体(类,模块,函数)是可以被扩展的,但是不可以被修改。
    优点:代码可以具有很好的可扩展性,可维护性。扩展同类型产品的时候可以不修改旧的代码。工厂,抽象工厂
  3. 里氏替换原则:
    里氏替换原则告诉我们,当使用继承的时候,类B继承类A时,除添加新的方法完成新增功能,尽量不要修改父类方法的预期行为。
    里氏替换的重点在不影响原有功能,可以扩展父类的功能。
  4. 依赖倒转原则
    定义:高层模块不应该依赖低层模块,抽象不应该依赖于细节,细节应该依赖于抽象。
    依赖倒转原则的核心思想就是面向接口编程,子类可以根据抽象出来的接口完成自己的功能。这样上层只关注抽象出来的接口的功能,不用关注具体的实现。
  5. 接口隔离原则
    接口分离原则,客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。避免一个实体类中有不需要的接口
  6. 迪米特法则
    最小知道原则,一个对象应该对其他对象保持最少的了解,避免将过多的接口能力暴漏出去。
  7. 组合/聚合复用原则
    组合/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分; 新的对象通过已有对象的接口达到复用已有功能的目的。
    在面向对象的设计中,如果直接继承基类,会破坏封装,因为将基类的实现细节暴漏给了子类。基类的实现发生了改变,则子类的实现也不得不改变。
    聚合和组合
  • 聚合:整体与部分之前的弱关系,部分不会随着整体生命周期的结束而结束。eg:电脑(整体)与 鼠标(部分)
  • 组合:整体与部分之间的强关系,部分随着整体生命周期的结束而结束(部分不能脱离整体单独存在)。 eg: 鸟(整体)与 翅膀(部分)
    设计模式七大基本原则:
    https://zhuanlan.zhihu.com/p/24614363

commit和apply的区别

相同点:

  • 两者都是提交Preference的修改数据
  • 两者都是原子的操作
    不同点
  • commit会有返回值,apply没有返回值
  • commit的执行是同步的,如果进行并发处理的时候会等待之前的commit执行后再进行操作,而Apply的执行是异步的,因此效率较高

当不需要返回值的时候优先使用apply,因为apply是异步的,当需要返回值的时候还是需要使用commit的。

实现一个阻塞队列

面试的时候被问到手动实现一个阻塞队列,回答得并不好,自己下来在网上搜了下,发现其实也没有那么难,其实就是对wait和notify机制需要掌握。
首先wait和notfy需要在同步块中调用,不然会抛出IllegalMonitorStateException异常,

在实现阻塞队列的时候我们主要实现两个接口,take和put,take用于从阻塞队列中获取元素,put用于向阻塞队列中放置元素,实现代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class BlockQueue {
    public static final int CAPASITY = 20;

    private final Object mLock = new Object();

    private static final List<Integer> mQueue = new ArrayList<>();

    public void add(int num) {
        synchronized (mLock) {

            // 这里使用while循环,防止当线程被唤醒的时候但是这个时候条件还是不满足的,
            // 就直接往队列中添加元素,比如一个线程添加元素后队列满,如果没有while,唤醒另一个生产者线程
            // 就会直接再次想list中添加元素
            while (mQueue.size() == CAPASITY) {
                System.out.println("Queue 满,生产者开始wait" + Thread.currentThread().getName());
                try {
                    mLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mQueue.add(mQueue.size(), num);
            System.out.println("Queue produce :[" + num + "] 当前线程为:" + Thread.currentThread().getName());

            // 唤醒在这个锁上面等待的所有线程,线程之间通过竞争获取锁
            mLock.notifyAll();
        }
    }

    public void take() {
        synchronized (mLock) {
        	// 当队列为空的时候停止消费者线程获取元素
            while (mQueue.size() == 0) {
                try {
                    System.out.println("队列为空,停止消费者");
                    mLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            int num = mQueue.remove(0);
            System.out.println("Queue Cosumer :[" + num + "] 当前线程为:" + Thread.currentThread().getName());
            // 唤醒这个锁上面等待的所有线程
            mLock.notifyAll();
        }
    }

    public static void main(String[] args) {
        BlockQueue blockQueue = new BlockQueue();

        new Thread(() -> {
            for (; ; ) {
                blockQueue.add(ThreadLocalRandom.current().nextInt());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "生产者线程").start();

        // 消费者sleep的时长大于生产者sleep的时长,所以队列会被生产满,
        new Thread(() -> {
            for (; ; ) {
                blockQueue.take();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者线程1").start();
        // 消费者sleep的时长大于生产者sleep的时长,所以队列会被生产满,
        new Thread(() -> {
            for (; ; ) {
                blockQueue.take();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者线程2").start();
    }
}

字节的面试对知识点扣的很细,因此面试的时候对基础要求很高,希望大家平常多准备,光靠面试的时候那几天准备是来不及的。

你可能感兴趣的:(面试,android)