文章目录
算法题:给你一个数组,判断该数组是不是二叉搜索树的后序遍历序列。(剑指offer30题)
https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-hou-xu-bian-li-xu-lie-lcof/
由于工作了,好久没看树方面的知识,二叉搜索树的定义有点忘了,面试后搜索定义如下:
二叉查找树(Binary Search Tree),(又:二叉搜索树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
解法一
后序遍历的最后一个数字一定是根节点,我们可以在数组中确定根节点的位置。
上面这棵树的后序遍历:
[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的属性,比如位置,宽高等。而属性动画则会改变对象的属性值。
- 补间动画只能实现位移,缩放,旋转,透明度四种动画操作,而属性动画可以实现更多的效果。
属性动画是如何驱动的
详细的属性动画的驱动原理可以看下面的博客,博主介绍的很详细。
看了博客之后,可以将属性动画的运行原理大体概括为下面的流程。
- 调用start()之后,回去注册屏幕刷新信号,当接收到屏幕刷新信号后,会遍历待执行的属性动画列表,计算待执行的属性动画的动画行为。
- 每个动画在处理当前帧的刷新逻辑时,先会根据当前时间和动画和动画开始时间,以及动画持续时长来计算得到当前帧时动画所处的进度,然后将这个进度等价转换到0-1区间之内。
- 然后插值器会根据这个初步计算之后的进度值,得到实际的动画进度,取值也是在0-1区间内。
- 计算得到当前帧处于哪两个关键帧之间,然后根据估值器的规则将进度映射到实际的值。
- 然后将计算得到的值回掉到onAnimationUpdate接口中。
参考:https://www.jianshu.com/p/46f48f1b98a9
提升动画性能
Android应用性能优化最佳实践 2.7节动画性能中,对属性动画和补件动画的性能进行了对比,对比显示属性动画的性能更高。同时动画过程如果只涉及scale,alpha等非布局全量刷新的场景时,可以将硬件加速打开,提升动画流畅性,同时可以使用setLayerType设置layer的类型,比如设置为HARD_WARE_LAYER类型或者SOFTWARE_LAYER。
android中导致oom的主要原因
- 加载大图
Android中Bitmap会占用很多内存,在加载BItmap的时候可以先使用inJustDecodeBounds去解析图片的大小,预估图片占用的内存,或者对图片进行采样和缩放,避免占用过多内存。 - 内存泄漏
单例,非静态内部类持有外部类的引用(AsyncTask,Handler),context被引用导致内存泄漏。 - 应用程序自己的虚拟机内存上限太小。
- 堆溢出或者永久代溢出
堆溢出是由于分配大对象,或者内存泄漏引起的。
永久代中存放了被虚拟机加载的类信息,常量,静态变量等,方法区被撑爆也会造成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
设计模式基本原则
- 单一职责原则:
通俗地说,即一个类只负责一项职责。因为如果一个类负责多项职责的话,对其它功能的修改可能影响已有的功能。
优点:降低了类的复杂度,一个类仅负责一个职责。其逻辑肯定要比负责多项职责简单。
缺点:代码改动可能比较大 - 开放关闭原则
软件实体(类,模块,函数)是可以被扩展的,但是不可以被修改。
优点:代码可以具有很好的可扩展性,可维护性。扩展同类型产品的时候可以不修改旧的代码。工厂,抽象工厂 - 里氏替换原则:
里氏替换原则告诉我们,当使用继承的时候,类B继承类A时,除添加新的方法完成新增功能,尽量不要修改父类方法的预期行为。
里氏替换的重点在不影响原有功能,可以扩展父类的功能。 - 依赖倒转原则
定义:高层模块不应该依赖低层模块,抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒转原则的核心思想就是面向接口编程,子类可以根据抽象出来的接口完成自己的功能。这样上层只关注抽象出来的接口的功能,不用关注具体的实现。 - 接口隔离原则
接口分离原则,客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。避免一个实体类中有不需要的接口 - 迪米特法则
最小知道原则,一个对象应该对其他对象保持最少的了解,避免将过多的接口能力暴漏出去。 - 组合/聚合复用原则
组合/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分; 新的对象通过已有对象的接口达到复用已有功能的目的。
在面向对象的设计中,如果直接继承基类,会破坏封装,因为将基类的实现细节暴漏给了子类。基类的实现发生了改变,则子类的实现也不得不改变。
聚合和组合
- 聚合:整体与部分之前的弱关系,部分不会随着整体生命周期的结束而结束。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();
}
}