安卓开发常见面试问题总结

自己就之前的面试经历,以及其他比较常见的安卓开发面试的问题做的一些总结

安卓开发常见面试问题总结

自我介绍

Java

Java基础

基础知识

1. Java语言的特性
  • 平台无关性:运行在JVM上;
  • 面向对象:继承,多态,封装;
  • 自动内存管理:垃圾回收机制;
  • 解释型语言;
  • 支持并发编程;
  • 健壮性:强类型机制,异常处理
2. Java的基本数据类型

  byte short int long float double char boolean
  分别占1、2、4、8 、4、8、2、1字节,位数为其八倍。

3. ==和equals的区别。
  • 首先,从属性来说,==是操作符,equals是方法。这就意味着,后者只能用于比较对象类型,而前者可以用于基本类型。
  • 其次,从方法作用上来看,前者用于比较基本类型的值是否相等,对象类型的引用是否相等,后者用于比较对象类型的引用是否相等。当然,也存在例外情况,如包装器类型的变量就可以使用前者进行比较值是否相等,但这其中其实隐藏着自动拆包。不过,由于后者是方法,所以我们可以对其进行重写,如String类型就将其重写为比较String类型的值是否相等。
4. 重写equals有哪些要点?

  要满足自反性,传递性,对称性,非空性,一致性。

5. 什么是自动装箱、自动拆箱

  自动装箱就是将基本类型转换为包装器类,自动拆箱则相反,是将包装器类转换为基本类型。两者都是由编译器完成的。

6. 抽象类和接口有什么区别?什么情况下用抽象类,什么时候用接口?

  抽象类和其子类是 是(is) 的关系。
  接口和实现其的类是 有(has) 的关系。
  抽象类就相当于一个模板,模板中有子类可以公用的部分,也有需要子类自行实现的部分,是为模板式设计;
  而接口是对行为的抽象,它只定义一组行为规范,每个实现类都要实现所有规范,是辐射式设计;
  一个类有父类的时候只能选择接口,因为Java不支持多重继承。

7. Java的注解分为哪几类?

  标准注解:@Override,@SuppressWarnings,@Deprecated
  元注解
   @Target : 说明了注解修饰的对象范围
   @Retention :定义了该注解被保留的级别
   @Documented:表明该注解应该被javadoc工具记录
   @Inherited :允许子类继承父类中的注解
  自定义注解:由元注解编写的其他注解

8. 异常分为哪几类,各举出例子。

  异常公有父类为Throwable,分为错误(Error)和异常(Exception)两类。
  Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误,如OOMError,StackOverflowError。
  Exception分为两种:运行时异常(RuntimeException)和其他异常。前者属于由编程错误导致的异常,如:数组越界异常(ArrayOutOfIndexException),空指针异常(NullPointerException),强制类型转换异常(CasrClassException)。后者属于程序本身没有问题,但由于像IO错误这类问题导致的异常属于其他异常(IOException),如:文件不存在异常(FileNotFoundException)。

9. 哪些是非检查型异常?哪些是检查型异常?

  根据编译器是否能检测出异常,分为检查型异常和非检查型异常。
  Error和RuntimeException又被称为非检查型异常,所有其他的异常(IOException)被称为检查型异常。

10. Java中有几种引用类型?分别是什么。

  Java中的引用类型有强引用软引用弱引用虚引用
  强引用:在把一个对象赋给一个引用变量时,这个引用变量就是一个强引用。有强引用的对象一定是可达性状态,不会被垃圾回收。因此,强引用是内存泄漏的主要原因。
  软引用:用来描述一些有用但并不是必需的对象,内存不足时该对象会被回收。如果之后还不足,则抛出OOM。
  弱引用:也用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
  虚引用:和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来和引用队列搭配进行跟踪对象的垃圾回收状态。

11. 什么是泛型?

泛型即广泛的类型,指类型不确定的类型。

12. 类型擦除机制是什么?为什么要使用类型擦除?

无论何时定义一个泛型类型,都会自动提供个相应的原始类型(类型变量会被擦除(erased), 并替换为其限定类型(或者,对于无限定的变量则替换为Object)。
为了兼容以前的代码,Java将原有不支持泛型的类型扩展为支持泛型。如非泛型的写法,编译成的虚拟机汇编码块是A,而之后的泛型写法,只是在A的前面,后面“插入”了其它的汇编码,而并不会破坏A这个整体。这才算是既把非泛型“扩展为泛型”,又兼容了非泛型。

13. 面向对象编程和面向过程编程各自是什么,有什么区别?

  面向对象编程是以面向对象的思维编程,它将问题抽象为对象与对象之间的问题并予以解决,而面向过程编程没有对象的概念,将目标功能的实现分为多个步骤。程序依据步骤的过程一步步执行,最终实现程序功能。

String类相关

1. String类为什么不可以改变?

  首先,String类不可修改指的是其值不可修改,其引用是可以改变的。而其值设为不可修改是因为value数组是私有字段,并且没有提供更改器方法。

2. 为什么将String类设计成不可变的?

  为了使相同内容的字符串可以共享字符串常量池中已有的字符串。如果可变,那一个修改后,其他指向它的字符串变量就全都改变了。不安全。

3. 如果想要修改String变量,怎么修改?

  如果是改变其引用,直接赋以其他对象即可,如果是改变对象的值,即value数组,需要使用反射机制(运行时获取类型信息的机制),得到String对象中的value数组后,设为Accessible,进行修改。

public static void main(String[] args) throws Exception {
        // TODO Auto-generated method stub
        String str = "不可变的字符串";

        System.out.println(str.hashCode()+":"+str);         //改变前的hash值

        Field f = str.getClass().getDeclaredField("value"); //获取value属性
        f.setAccessible(true);                              //设置其可以被访问(private)
        f.set(str, new char[] { '改', '变', '后', '的', '值' }); //改变其值

        System.out.println(str.hashCode()+":"+str);         //改变后的hash值
    }
————————————————
版权声明:本文为CSDN博主「片刻清夏」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zjq_1314520/article/details/73430885
4. StringBuilder和StringBuffer有什么区别?

StringBuilder性能较高,但不具备线程安全性,StringBuffer用sychronized具备了线程安全,支持并发操作,但性能较低。

5. String 类型在 JVM 中是如何存储的?

  String常见的创建方式有两种, String s1 = “Java” 和 String s2 = new String(“Java”)的方式,两者在JVM的存储区域截然不同,在JDK 1.8中,s1会先去字符串常量池中找字符串"Java” ,如果有相同的字符则直接返回常量句柄,如果没有此字符串则会先在常量池中创建此字符串,然后再返回常量句柄;而变量s2是直接在堆上创建一个变量 ,如果调用intern方法才会把此字符串保存到常量池中
如下代码所示:

String s1 = new String("南街");
    String s2 = s1.intern();
    String s3 = "南街";
    System.out.println(s1 == s2); // false
    System.out.println(s2 == s3); // true

在这里插入图片描述

Hashcode相关

1. Hashcode是什么?有什么用?

  Hashcode是由hash函数计算得出的值,它代表着该对象在Hash表中的位置,其的存在主要是为了查找的快捷性,即HashCode是用来在散列存储结构中确定对象的存储地址的。

2. 为什么修改equals()的时候常常需要修改hashcode()?

  由于相同的对象在哈希表中的位置也应该是相同的,所以equals和hashcode必须相容,即如果两对象经equals方法后返回为true,它们的hashcode也应该是相同的。而Object下定义的Hashcode方法是根据对象的存储位置生成hashcode的,如果不进行修改,两对象的存储位置不同,两个对象的hashcode就不相同,这就违反了前面提到的equals和hashcode必须相容的原则。

集合相关

1. Java中集合类都有哪些数据结构?

  分为Collection(集合)和Map(映射)两种,前者包括:List,Queue,Set,后者包括Map

2. Vector如何保证线程安全?

  Vector在多线程环境下,通过频繁加锁和释放锁的操作,保证线程安全,这也导致了Vector的读写效率整体上比ArrayList低。

3. List和Map是如何实现扩容的?

  List默认初始容量为10,在数组列表满了的情况下继续添加元素时扩容,扩容为原来的1.5倍。
  Map默认初始容量为16,在实际容量超过阀值(最大容量×负载因子)时扩容,扩容为原来的2倍。

4. 现在我有一个很大的数组需要拷贝,原数组大小是 5k,请问如何快速拷贝?

  申请容量为5k的数组,如果使用默认容量会反复扩容,带来性能损耗。

4. ArrayList 数组,我们通过增强 for 循环进行删除,可以么?

  不可以,获得迭代器后不能对原集合进行不是由迭代器发起的结构性修改,否则会导致expectedModCount变量与modCount不一致,抛出ConcurrentModificationException异常。

5. Hashmap的底层是什么?如何解决碰撞冲突?

  底层是数组链表。
  解决碰撞冲突的方法有两种,一种是开放寻址法,包括线性探测法,二次探测法,双重哈希法等。另一种是链表法。Hashmap采用的就是这种,直接将对象缀在链表末尾。

6. 为什么扩容因子是0.75?

  这是均衡了时间和空间损耗算出来的值,因为当扩容因子设置比较大的时候,相当于扩容的门槛就变高了,发生扩容的频率变低了,但此时发生Hash冲突的几率就会提升,当冲突的元素过多的时候,无论是链表还是链表变成的红黑树都会增加查找成本(hash 冲突增加,链表长度变长)。而扩容因子过小的时候,会频繁触发扩容,占用的空间变大,比如重新计算Hash等,使得性能变差。

7. 为什么扩容会消耗性能?

  因为扩容的本质不是简单的增加容量,而是申请一块新的存储空间,将原来的数据复制过去。

8.为什么扩容后为原来的两倍?

  因为在使用是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。这是为了实现均匀分布。
深入解释可以参考这篇文章:HashMap中hash(Object key)原理,为什么(hashcode >>> 16)

9.HashMap做了哪些优化?

链表长度大于8时转为红黑树
头插改尾插

10. HashSet如何去重?

  先判断hash值,后通过equals方法。

并发编程

线程相关

1. 线程和进程之间是什么关系?

  进程是系统分配资源的最小单位,而线程是程序执行的最小单位。

2. 线程的创建方式?

  继承Thread类:新建一个类,实现run方法,创建实例,调用start方法
实现Runnable接口:实现Runnable接口,构造线程实例时将实现了Runnable接口的实例传入,调用start方法
通过ExecutorService和Callable< Class>实现有返回值的线程:用于收集各个线程的执行返回结果并将结果汇总起来
基于线程池:创建线程池,调用execute方法,传入实现Runnable接口的实例

2. 线程三大特性?

  原子性:指一个操作是不可中断的,要么全部执行成功要么全部执行失败
  有序性:即程序执行的顺序按照代码的先后顺序执行
  可见性:指线程对一个变量的修改对于其他线程而言是可见的

3. 线程的生命周期?

  新建状态:线程经new创建后,处于新建状态,此时为线程分配内存,并初始化其成员变量的值。
  就绪状态:调用线程的start方法后,线程处于就绪状态,此时JVM完成了方法调用栈和程序计数器的创建,等待该线程的调度和运行。
  运行状态:就绪状态的线程在得到时间片后,执行run方法 的线程执行体时,处于运行状态。
  阻塞状态:运行中的线程主动或被动放弃时间片暂停运行时,线程转入阻塞状态。阻塞状态有三种,等待阻塞,同步阻塞和其他阻塞。
  等待阻塞:调用wait方法后,JVM将线程放入等待队列,线程转入等待阻塞。
  同步阻塞:运行状态的线程获取对象锁失败时,JVM会将其放入锁池中,此时线程转为阻塞状态。
  其他阻塞:运行状态的线程在执行sleep、join方法或发出IO请求时,JVM会将该线程转完其他阻塞状态。
  死亡状态:线程有三种方式进入死亡状态,正常结束,异常退出,被手动结束(stop方法)

4. 线程的状态转化流程

  ① 调用new方法新建一个线程(此时处于新建状态)
  ② 调用start方法启动一个线程(此时处于就绪状态)
  ③ 处于就绪状态的线程等待线程获取时间片,获取后转入运行状态执行run方法
  ④ 正在运行的线程在调用了yield方法或失去CPU时,会再次进入就绪状态
  ⑤ 正在运行的线程在执行了sleep方法,发生IO阻塞、等待同步锁、等待通知时,会挂起,进入阻塞状态,进入锁池
  ⑥ 阻塞状态的线程由于出现sleep时间已到、IO方法返回、获得同步锁、收到通知等情况,就会进入就绪状态,等待时间片,获得时间片后转入运行状态
  ⑦ 处于运行状态的线程,在调用run方法完成或发生异常导致退出时,进入死亡状态

5. 什么是上下文切换?

上下文切换指的是线程的状态保存及再加载。

6. interrupt(),interrupted() 和 isInterrupted() 有什么区别?

  interrupt():将调用该方法的对象所表示的线程标记一个停止标记,并不是真的停止该线程。
  interrupted():获取当前线程的中断状态,并且会清除线程的状态标记。是一个是静态方法。
  isInterrupted():获取调用该方法的对象所表示的线程的中断状态,不会清除线程的状态标记。是一个实例方法。

7. 如何中断一个线程?

  方法一:调用interrupt方法,通知线程应该中断了;

这有两种情况:
A.如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出了一个InterruptedException异常。
B.如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将正常运行,不受影响。

  方法二:使用volatile boolean类型变量控制;

8. sleep()和wait()有什么区别?
  • wait方法在Object类下,sleep方法在Thread类下
  • 调用wait方法会释放所占有的对象锁,而调用sleep方法的对象不会释放所占有的对象锁
  • 调用wait方法的线程会进入等待阻塞状态(WAITING),只有等到其他线程通知或被中断后才能返回。而调用sleep方法需要传入时间参数,使线程进入阻塞状态(TIMED WAITING)一定的时间
9. sleep(0)方法有什么意义?和yield方法有什么区别?

  二者都是使操作系统立刻重新进行一次CPU竞争。区别在于调用sleep方法后线程进入的是阻塞状态,而调用yield方法进入的是就绪状态。

10. 如果在a线程中调用了b的sleep方法,会发生什么?

a线程进入阻塞状态。因为sleep方法是静态方法。在哪里出现,哪里就sleep

锁相关

1. 如何理解对象锁这个概念?

  对象锁是独占排他锁,一个线程获得后,其他需要获得该对象锁的线程只能等待该线程释放对象锁。

2. 什么是死锁?如何解决?什么情况下容易发生?

  死锁:线程间由于互相拥有对方需要的请求的资源,导致所有线程被阻塞。
  前提:互斥,请求与保持,不可剥夺,环路等待。
  解决方法:改变请求资源的顺序。
  当在一个同步块中需要获得另一个对象锁时容易发生死锁。

3. 显式锁和内置锁是什么?

显式锁是lock,内置锁是sychronized。

4. 公平锁和非公平锁有什么区别?

  公平锁指的是线程排队获取的锁,先来先得;而非公平锁是允许“插队”的,当一个线程请求锁时恰好另一个线程释放了这个锁,它将跳过前面已经在排队等待的线程,直接获取这个锁,如果没有的话则进入队列中等待。
  公平锁由于挂起和恢复存在一定的开销,因此性能不如非公平锁,所以 ReentrantLock 和 synchronized 默认都是非公平锁的实现方式。
(在一个锁释放之后,其他的线程会需要重新来获取锁。其中经历了持有锁的线程释放锁,其他线程从挂起恢复到RUNNABLE状态,其他线程请求锁,获得锁,线程执行,这一系列步骤。如果这个时候,存在一个线程直接请求锁,可能就避开挂起到恢复RUNNABLE状态的这段消耗,所以性能更优化)

5. 重量级锁和轻量级锁有什么区别?

  获取不到锁就马上进入阻塞状态的锁,我们称之为重量级锁。
  反之则为轻量级锁。轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况。

6. 什么是悲观锁和乐观锁?

  悲观锁:总是假设最坏的情况,悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能变动,一个事务拿到悲观锁后(可以理解为一个用户),其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。

传统的关系型数据库里就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的体现。

  乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

7. 悲观锁和乐观锁怎么实现?

  悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)=
  乐观锁一般会使用版本号机制或CAS算法实现。
  ;版本号机制
一般是在数据表中加一个数据版本号 version 字段,表示数据被修改的次数,当数据被修改时,version 值会加1。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。
  CAS 算法
CAS 指令是硬件支持的操作: Compare And Swap(比较与交换),是一种著名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS 算法涉及到三个操作数
    需要读写的内存地址 V
    进行比较的值 A
    拟写入的新值 B
  当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

8. CAS算法的三个问题是什么?
  1. ABA问题:

  因为CAS在进行操作的时候,总是需要比较新的操作数和旧的操作数,如果相同则更新。但是如果新的操作数经过两次修改之后仍为原来的值,那么就出现了ABA问题(该操作数经历了A→B→A)。解决问题的方法就是增加一个版本号,不仅仅通过检查值的变化来确定是否更新。

对于基本类型的值来说,这种把数字改变了在改回原来的值是没有太大影响的,但如果是对于引用类型的话,就会产生很大的影响了。

  1. 循环时间开销大
  2. 只能保证一个共享变量的原子操作

  解决的方法:把多个共享变量合并成一个共享变量。AtomicReference类来保证引用对象之间的原子性。

9. 共享锁和独占锁是什么?

  共享锁又称为读锁,简称 S 锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。如ReentranReadWriteLock中的读锁。
  独占锁又称为写锁,简称 X 锁。排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务可以对数据行读取和修。如ReentranLock。

10. 什么是自旋锁和自适应自旋锁?

  自旋锁就是,如果此时拿不到锁,它不马上进入阻塞状态,而是等待一段时间,看看这段时间有没其他人把这锁给释放了。类似于线程做空循环,如果循环一定的次数还拿不到锁,它才会进入阻塞的状态。
  而能够根据线程最近获得锁的状态来调整循环次数的自旋锁,我们称之为自适应自旋锁。

11. 自旋锁的优缺点?什么时候应该用自旋锁?

  优点是可以减少上下文切换,尤其是当占用锁的时间很短或锁竞争不激烈时,性能能够得到很大提升,因为此时自旋的CPU耗时明显短于线程阻塞、挂起、再唤醒时的两次上下文切换所用的时间。
  缺点是如果持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,造成CPU的浪费。
  因此,当占用锁的时间很短或锁竞争不激烈时,适合用自旋锁。

12. 什么是可重入锁?什么是不可重入锁?

  一个锁根据是否允许线程可以重复获得已获得的锁,分为不可重入锁和可重入锁。
  可重入锁有一个持有技术来跟踪对lock方法的嵌套调用。线程每使用一次lock后都要调用unlock来释放锁。由于这个特性,被一个锁保护的代码可以调用另一个使用相同锁的方法。
  它的原理是有一个引用计数,0表示未被线程获取,每次lock后+1,unlock-1,当重新为0时才被完全释放。
Java中ReentrantLock和sychronized都是可重入锁。

13. 分段锁是什么?

分段锁不是一种锁,而是一种思想。它将数据分段,并在每个分段上单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrentHashMap就是使用分段锁的思想实现的。

14. Java中锁有哪四种状态?
  • 无锁状态(unlocked):锁标志位为 01
  • 偏向锁状态(biasble):锁标志位为 01(锁标志位后有 1bit 的空间用来指示是否是偏向锁,是的话该 1bit 为 1,反之则为 0)
  • 轻量级锁状态(lightweight locked):锁标志位为 00
  • 重量级锁状态(inflated):锁标志位为 10

  这几种锁的级别从低到高依次是:无锁、偏向锁、轻量级锁、重量级锁。这几个状态会随着竞争情况逐渐升级,但要注意的是除了偏向锁可以恢复到无锁状态以外,只允许锁升级不允许降级,比如由偏向锁升级成轻量级锁之后,不能再降级为偏向锁。

15. Synchronized 和 ReentrantLock 是如何实现的?它们有什么区别?

  synchronized是独占式悲观锁,是通过JVM 层面实现的,synchronized 只允许同一时刻只有一个线程操作资源。在Java中每个对象都隐式包含一个monitor (监视器)对象,加锁的过程其实就是竞争monitor的过程,当线程进入字节码monitorenter指令之后,线程将持有monitor对象,执行monitorexit时释放monitor对象,当其他线程没有拿到monitor对象时,则需要阻塞等待获取该对象。
  ReentrantLock是Lock的默认实现方式之一,它是基于AQS (Abstract Queued Synchronizer,队列同步器)实现的,它默认是通过非公平锁实现的,在它的内部有一个state的状态字段用于表示锁是否被占用,如果是0则表示锁未被占用,此时线程就可以把state改为1,并成功获得锁,其他未获得锁的线程只能去排队等待获取锁资源。
  synchronized和ReentrantLock都具备互斥性和不可见性。但在JDK 1.6以前synchronized的性能低于ReentrantLock, JDK 1.6之后synchronized(锁膨胀)的性能略低于ReentrantLock,它的区别如下:

  • synchronized 是 JVM 层面实现的,而 ReentrantLock 是 基于Java 语言实现的 API。
  • ReentrantLock 可设为公平锁,而 synchronized 却不行。
  • ReentrantLock 只能修饰代码块,而synchronized 可以用于修饰方法、修饰代码块等。
  • ReentrantLock需要手动加锁和释放锁,如果忘记释放锁,则会造成资源被永久占用,而 synchronized 无需手动释放锁。
  • ReentrantLock 可以知道是否成功获得了锁,而 synchronized 却不行。 ReentrantLock 可以被中断,所以也被称为可中断锁。
16. synchronized的锁升级

  锁升级就是从偏向锁到轻量级锁再到重量级锁升级的过程,也称之为锁膨胀。

  • 偏向锁是指在无竞争的情况下设置的一种锁状态。偏向锁的意思是它会偏向于第一个获取它的线程,也即当没有竞争出现时,默认会使用偏向锁。JVM会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,这种情况下,如果是持有偏向锁的线程每次在进入的话,不再进行任何同步操作。
  • 当另一个线程尝试获取此锁的时候,偏向锁模式会结束,然后切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁,否则,则说明此锁已经被其他线程占用了。
  • 当两个以上的线程争抢此锁时,轻量级锁就膨胀为重量级锁,这就是锁升级的过程。
    深入理解Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
17. 同样是使用sychronized修饰,在修饰类和对象时,有什么区别?

  synchronized(类名.class)是类锁,是用来锁类的,类锁的作用就是使持有者可以同步地调用静态方法。当synchronized指定修饰静态方法或者class对象的时候,拿到的就是类锁,类锁是所有对象共同争抢一把。
  synchronized(this)对象锁,是用来锁对象的,synchronized修饰非静态方法或者this的时候拿到的就是对象锁,对象锁是每个对象各有一把的,即同一个类如果有两个对象,锁住一个对象的方法后还可以调用另一个的该方法。

JMM相关

1. JMM读取数据的过程?

在这里插入图片描述

  在线程执行时,首先会从主存中read变量值,再load到工作内存中的副本中,然后再传给处理器执行,执行完毕后再给工作内存中的副本赋值,随后工作内存再把值传回给主存,主存中的值才更新。

2. volatile可以保证什么性?

可见性与有序性。

3 .指令重排是什么?

  为了性能优化,编译器和处理器会进行指令重排序;如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的(这就有可能发生问题)。

4. volatile的使用时要注意?

volatile只能用于修饰成员变量和静态变量,且需要放在数据类型关键字之前。
volatile 和 final不能同时修饰一个变量。volatile 是保证变量被写时其结果其他线程可见,而 final 已经让该变量不能被再次写了

5. volatile如何实现可见性?

  Java就是利用volatile来提供可见性的。当一个变量被volatile修饰时,那么对它的写操作会立刻刷新到主存,强制缓存和主存同步,当其它线程需要读取该变量时,会发现缓存失效,然后去主存中读取新的值,由此保证了变量的可见性。
  此外,通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。

6. volatile如何实现有序性?

  在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序的结果不会影响到单线程的执行,但不能保证多线程并发执行时不受影响。
  而volatile可以禁止指令重排序,所以说其是可以保证有序性的。

7. volatile和sychronized的区别

synchronized主要特性:可见性;原子性;有序性;
修饰范围:可以是变量,可以方法。静态和非静态在锁的范围上会有区别。
volatile主要特性:有序性,可见性
修饰范围:只能是变量;

线程池

1. 线程池是什么?使用线程池有哪些好处?

  线程池是为了避免线程频繁的创建和销毁带来的性能消耗,而建立的一种池化技术,它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。

  通过线程池复用线程有以下几点优点:
   减少资源创建 => 减少内存开销,创建线程占用内存
   降低系统开销 => 创建线程需要时间,会延迟处理的请求
   提高稳定性 => 避免无限创建线程引起的OOM

2. 线程池的原理是什么?

  JVM先根据用用户的参数创建一定数量的可运行的线程任务,并将其放入队列中,在线程创建后启动这些任务,如果线程数量超过了最大线程数量,则超出数量的线程排队等候,在有任务执行完毕后,线程池调度器会发现有可用的线程,进而从阻塞队列中取出任务并执行。

3. 线程池的工作流程?
  • 线程池刚被创建时,向系统申请一个用于执行工作队列和管理线程池的线程资源。
  • 在调用execute添加一个任务时,如果此时正在运行的线程数量少于corePoolSize,线程池就会立刻创建线程并执行该任务。
  • 如果正在运行的线程数量大于等于corePoolSize,则会将线程添加至工作队列中。
  • 如果工作队列已满且正在运行的线程数量小于maximumPoolSize,则创建非核心线程立刻执行该线程任务。
  • 如果工作队列已满且正在运行的线程数量大于maximumPoolSize,则执行线程池的拒绝策略。
  • 线程任务执行完毕后,该任务将被从线程池队列中移除,线程池将从阻塞队列中取出一个线程任务继续执行。
  • 当线程处于空闲状态的时间超过keepAliveTime时,如果正在运行的线程超过了corePoolSize,该线程将被认定为空闲线程而停止。
  • 因此线程池中所有线程任务都执行完毕之后,线程池会收缩到corePoolSize大小。
4. 说出5种常用的线程池?
线程池名称说明用途
newCachedThreadPool缓存线程池,创建新线程时如果有可重用的线程,则重用之,否则重新创建一个新线程并将其添加到线程池中适合执行时间较短,或者大量时间都在阻塞的任务
newFixedThreadPool固定数量线程池,活动状态的线程数量大于等于核心线程池的数量时,则将新提交的任务加入阻塞队列,直至有可用的线程资源为了得到最优的运行速度,并发线程数等于处理器内核数
newScheduledThreadPool定时调度线程池,可设置在给定的延迟时间后或定期执行某线程任务
newSingleThreadExecutor单一线程池保证永远只有一个可用的线程,该线程停止或发生异常时,启动一个新的线程来代替该线程继续执行任务用于性能分析,单线程池替换其他线程池,就能测量不使用并发的情况下应用的运行速度会慢多少
newWorkingStealingPool工作密取线程池,执行是无序的,哪个线程抢到任务,就由它执行
5. 线程池为什么不允许使用Executors去创建,而是通过ThreadPoolExecutor?

  其实Executors在底层还是通过ThreadPoolExecutor创建的线程池,不同点在于Executors通过传给ThreadPoolExecutor其设置的默认参数创建线程池,而直接使用ThreadPoolExecutor创建,我们可以传入我们实际需要的参数。

6. ThreadPoolExecutor有哪些参数?
  • corePoolSize 表示线程池的核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程;如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值。对于执行比较慢、数量不大的 IO 任务,或许要考虑更多的线程数,而不需要太大的队列。而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是 CPU 核数或核数 *2(理由是,线程一定调度到某个 CPU进行执行,如果任务本身是 CPU 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲。
  • maximumPoolSize 表示线程池最大线程数。官方规定此值必须大于 0,也必须大于等于corePoolSize,此值只有在任务队列满时,才会用到,一个线程池最大承载量等于 maximumPoolSize +workQueue的容量。
  • keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于corePoolSize 为止,如果 maximumPoolSize 等于corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。
  • unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的。
  • workQueue 表示线程池阻塞队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。
  • threadFactory 表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程。
  • handler 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列 workQueue中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。
7. 线程池都有哪几种工作(阻塞)队列?或者也叫workQueue 都有哪几种工作队列?
  • ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
  • LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
  • DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;
  • SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
8. 线程池有哪几种拒绝策略?
  • AbortPolicy(直接抛出异常,也是默认的)
  • DiscardPolicy(抛弃处理不了的任务,允许任务丢失)
  • DiscardOldestPolicy(抛弃队列中等待的最久的任务)
  • CallerRunsPolicy(将处理不了的回退给调用者,也可以理解为交给线程池调用所在的线程进行处理)
  • 自定义拒绝策略(新建RejectedExecutionHandler 对象,然后重写它的 rejectedExecution() 方法)
9. execute() 和 submit()有什么区别?

  execute和submit都属于线程池的方法,
  execute只能提交Runnable类型的任务,而submit既能提交Runnable类型任务也能提交Callable类型任务。
  execute会直接抛出任务执行时的异常,submit会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出。
  execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
execute和submit的区别与联系

10. 怎么终止线程池?

shutdown()和shutdownNow()

11. shutdown与shutdownNow有什么区别?
  • shutdown会把线程池的状态改为SHUTDOWN,而shutdownNow把当前线程池状态改为STOP
  • shutdown只会中断所有空闲的线程,而shutdownNow会中断所有的线程。
  • shutdown返回方法为空,会将当前任务队列中的所有任务执行完毕;而
    shutdownNow把任务队列中的所有任务都取出来返回。

JVM

JVM内存模型

在这里插入图片描述

1. JVM内存是怎么划分的?分别用来存储什么数据?

  JVM的内存区域分为线程私有区和线程共享区。
  线程私有区包括程序计数器,虚拟机栈,本地方法栈,线程共享区包括堆和方法区。

名称描述异常状态线程私/公有
程序计数器当前线程所执行的字节码的行号指示器,用来保证上下文切换正常。它是唯一没有OOM的区域线程私有
虚拟机栈Java方法执行的线程内存模型,每个方法执行的时候虚拟机栈都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息线程请求栈深度超过虚拟机允许的深度是抛出StackOverflowError;当栈无法申请到足够内存时抛出OOM线程私有
本地方法栈与虚拟机栈类似,区别在于虚拟机栈为Java方法服务,本地方法栈为本地方法服务与虚拟机栈相同线程私有
存放对象实例堆中没有内存完成实例分配且无法扩展时抛出OOM线程公有
方法区存储被虚拟机加载的类型信息,常量,静态变量,即时编译器后的代码缓存等数据。方法区无法满足新的内存分配需求时抛出OOM线程公有
直接内存(堆外内存)它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。当无法满足内存需要时同样会抛出OOM----
2. 常量池存放在哪?

  Java6和6之前,常量池是存放在方法区(永久代)中的。
  Java7,将常量池是存放到了堆中。
  Java8之后,取消了整个永久代区域,取而代之的是元空间。运行时常量池和静态常量池存放在元空间中,而字符串常量池依然存放在堆中。

3. Java堆中是怎么分代的?

  分为新生代和老年代。
  新生代存放新生成的对象,特点是对象数量多但生命周期短,默认占1/3的堆空间。
  老年代存放大对象和生命周期长的对象,默认占2/3的堆空间。

垃圾回收机制

1. 如何确定垃圾?

  引用计数器和可达性分析。

2. 可达性分析到不了的就一定是垃圾吗?

  不一定。可达性分析算法中判定为不可达的对象暂时处于“缓刑”阶段,要真正宣告一个对象的死亡至少要经历两次标记过程:

3. 哪两次标记过程?

  如果可达性分析判定为不可达,将会被第一次标记。
  之后根据对象是否有必要执行finalize方法进行一次筛选,假如没有finalize方法或finalize方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。有必要执行finalize方法的对象会被放置在一个F-Queue中,并在稍后由一条虚拟机自动建立的、低调度优先级的Finalizer线程去执行(虚拟机承诺触发,但不保证等待其运行结束)它们的finalize方法。如果对象在finalize方法中成功与引用链上任何一个对象建立关联,即可避免被回收的命运,否则将被第二次标记,随后被回收。

4. 为什么虚拟机承诺触发,但不保证等待其运行结束?

  如果某对象的finalize方法执行缓慢甚至发生死循环,很可能导致F-Queue阻塞,甚至使整个内存回收子系统崩溃。

5. 垃圾回收算法有哪些?

  根据如何判断对象消亡的角度分为直接垃圾收集和间接垃圾收集。(JVM中并未涉及使用引用技术式的直接垃圾收集)

算法名称实现方法特点
标记清除算法标记出所有需要回收的对象,并在清除阶段清除标记的对象并释放其内存会引起内存碎片化的问题
标记复制算法将内存分为两块大小相等的内存区域1和区域2,新生成的对象都在区域1中,对区域1进行标记清除,之后将仍然存活的对象复制到区域2中,最后直接清理区域1并释放内存内存清理效率高且易于实现,但存在大量内存浪费,同时面对大量长时间存活的对象时来回复制会影响系统的运行效率
标记整理算法标记阶段于标记清除算法相同,标记完成后将存活的对象移到内存的另一端,然后清除该端的对象并释放内存“stop the world”
分代收集算法对以上算法的综合,JVM根据对象的不同类型将内存分为了新生代和老年代,对新生代使用标记复制算法,对老年代使用标记清除算法根据对象类型使用不同的算法算法
6. 新生代是怎么进行垃圾回收的?

  JVM将新生代进一步分为Eden区(8/10)和两块Servivor区(各1/10)。
  JVM内存中的对象主要被分配到新生代的Eden区和ServivorFrom区,对于大对象将被直接分配到老年代。如果没有足够内存,会发生发生MinorGC。
  MinorGC发生前,先扫描老年代最大可用的连续存储空间是否大于新生代所有对象的总空间,如果大于,则发生MinoGC。将在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区(如果ServivorTo区内存不足,会发生分配担保,将其放入老年代,大对象和年龄达到要求的对象也将被移入老年代),同时将这些对象的年龄加一,然后清空Eden区和ServivorFrom区的所有对象,之后再将ServivorTo区和ServivorFrom区互换,即原来的ServivorFrom区成为下一次MinorGC的ServivorTo区。

7. 为什么MinorGC发生前,先扫描老年代最大可用的连续存储空间是否大于新生代所有对象的总空间?

  为了满足分配担保。

8. 永久代和元空间有什么异同?

  二者都不会发生GC,不同在于元空间没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此元空间大小不受JVM内存限制,只和操作系统的内存有关。

类加载机制

JVM的类加载机制的五个阶段
  1. 加载:读取class文件,并根据class文件描述创建java.lang.Class对象的过程
  2. 验证:确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全
  3. 准备:在方法区中为类变量分配内存并设置类中变量的初始值
  4. 解析:将常量池中的符号引用替换为直接引用
  5. 初始化:执行类构造器的< client>方法为类进行初始化
类加载器有哪几种?

启动类加载器:负责加载Java_HOME/lib目录中的类库
扩展类加载器:负责加载Java_HOME/lib/ext目录中的类库
应用程序类加载器:负责加载用户路径(classpath)上的类库
此外,我们还可以通过继承java.lang.ClassLoader实现自定义加载器

双亲委派机制

深入理解双亲委派机制及作用

OOM

1. 发生OOM时首先应该干什么?

  分清是发生的内存泄漏还是内存溢出。

2. 内存泄漏和内存溢出有什么区别?

  内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
  内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
  memory leak会最终会导致out of memory!

3. 安卓中常见的内存泄漏情况及解决方法?
情况具体情况解决方法
单例和工具类造成的内存泄漏单例对象持有Activity的context时,activity销毁时候本该被内存回收,却无法回收,这就造成了内存泄漏将持有的context对象改为全局的ApplicationContext引用
内部类造成的内存泄漏非静态内部类会隐式持有外部类的引用。如果Activity该销毁了而handler里面还有任务未执行完毕,就会造成内存泄漏将其改为Handler持有activity的弱引用
数据库,文件流等使用完未及时关闭对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的代码,如果在Activity销毁时未关闭或者注销,这些资源将不会被回收,造成内存泄漏在Activity销毁时及时关闭或者注销
监听器没有注销造成的内存泄漏在Android程序里面存在很多需要register与unregister的监听器,如果未unregister则引起内存泄漏。我们需要确保及时unregister监听器

Android内存泄漏的几种案例

安卓

四大组件

安卓的四大组件都有哪些?

  Activity、Service、BroadcastReceiver、ContentProvider

Activity

1. Activity的生命周期,四种启动模式。

  onCreate、onStart、onResume、onPause、onStop、onDestroy、onRestart
  standard、singleTop、singleTask、singleInstance

2. 活动A跳转到活动B,A、B各自经历了哪些状态?如果又在B界面按了返回键呢?

  A:onPause,B:onCreate、onStart、onResume,A:onStop
  B:onPause,A:onRestart、onStart、onResume,B:onStop、onDestroy

3.活动A跳转到活动B,再回到A,如何恢复活动A中的数据?

  数据持久化:保存在文件中,SF中,数据库中
  保存在Bundle中(Intent的底层就是Bundle)onSaveInstanceState(outState: Bundle) outState.put…并在onCreate中判断saveinstance是否为空,非空则提取数据并恢复
  A中使用startActivityForResult跳转B,B中用Intent返回数据给A

4.如果跳转到活动B后,活动A被回收,此时再返回活动A,活动A会经历那些阶段?

  onCreate、onStart、onResume,

5.什么情况下会进行上面提到的回收?

  系统内存不足时

6. Activity的构造

  Activity包括一个PhoneWindow实现的Window,PhoneWindow中有一个DecorView,DecorView又由TitleView和ContentView组成。

7.活动栈和进程的关系

一个Task中的Activity可以来自不同的进程,同一个进程的Activity也可能不在一个Task中

Service

服务的两种启动方法

  安卓开发中开启服务的方式有两种,一种是onStartCommand直接开启服务,这种服务开启之后如果不stopservice关闭服务的话,它会在后台一直运行,影响性能,消耗内存。还有一种就是通过bindservice的方法开启服务,这种方法就是绑定服务,绑定之后会随着activity的关闭而销毁。

在绑定服务的时候可以写一个内部类继承binder,然后再调用的时候可以写一个内部类实现serviceconnection接口,在onServiceConnected的方法中会返回一个binder的代理人对象,这个代理人对象和绑定服务的binder对象是同一个对象

同时使用了start和bind两种方法的service怎么停止?

  stop&unbind

service内中有looper吗?是谁创建的?

  有,系统创建的。
  如果需要自己的looper需要开启子线程创建

BroadcastReceiver

广播注册的两种方法

  代码中动态注册,注册文件中静态注册

广播有哪些分类?

  标准广播和有序广播

系统是如何实现有序广播的?

Android中广播的基本原理,具体实现流程要点粗略概括如下:

  1. 广播接收者BroadcastReceiver通过Binder机制向AMS(Activity Manager Service)进行注册;
  2. .广播发送者通过binder机制向AMS发送广播
  3. AMS查找符合相应条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到BroadcastReceiver(一般情况下是Activity)相应的消息循环队列中;
  4. 消息循环执行拿到此广播,回调BroadcastReceiver中的onReceive()方法。 有序广播中的在放入循环队列的时候按优先级放入,从队列中拿出来的时候一个一个顺序执行onReceive。
广播接收器中的onReceive()方法中可以进行读取文件等IO操作吗?为什么?会发生什么?

  不可以,有可能会发生ANR。

应用卡死,也就是ANR所产生的原因?
  1. 系统可能由于上个事件未处理结束而没有处理该事件(5秒钟之内没有响应输入的事件,比如按键、屏幕触摸等)

  2. 该事件在一定时间内未处理完毕(广播接收器在10秒内没有执行完毕)
    而如果在主线程中进行耗时操作,就有可能触发ANR。

为什么不能在BroadcastReceiver中开启子线程?

不可靠,Receiver只在onReceive方法执行时是激活状态,只要onReceive一返回,Receiver就不再是激活状态了。由于activity可能会被用户退出,Broadcast Receiver的生命周期本身就很短,可能出现的情况是: 在子线程还没有结束的情况下,Activity已经被用户退出了,或者BroadcastReceiver已经结束了。在Activity已经退出、BroadcastReceiver已经结束的情况下,此时它们所在的进程就变成了空进程(没有任何活动组件的进程),系统需要内存时可能会优先终止该进程。如果宿主进程被终止,那么该进程内的所有子线程也会被中止,这样就可能导致子线程无法执行完成.。

如何在BroadcastReceiver中开始一个耗时操作?

开启一个Service

如何得到线程执行结果?

  通过ExecutorService和Callable< Class>实现有返回值的线程:用于收集各个线程的执行返回结果并将结果汇总起来

ContentProvider

内容提供器是什么,有什么用?

  ContentProvider,即内容提供器,主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整机制,允许一个程序访问另一个程序的数据,同时还能保证被访问数据的安全性。

安全性如何保证?

  不同于文件存储和SharedPerferences存储中的两种全局可读写模式,ContentProvider通过选择哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。

内容提供器中增删改查的方法中如何操控数据?

  依靠内容URI进行增删改查操作:
  内容URI可清楚表达我们想要访问哪个程序中的哪张表的数据,其由两部分组成,authority和path。前者用于区分不同程序,一般用应用包名命名,后者则区分同一程序中不同的表。
其标准格式如下:

 content://authoriy/path content://com.example.app.provider/table1
访问com.example.app.provider下名为table1的表中的数据
 content://com.example.app.provider/table1/1
 访问com.example.app.provider下名为table1的表中id为1的数据
创建自己的ContentProvider时怎么选择提供哪些数据?

  URIMatcher解析匹配本地数据

SharedPerferences

安卓中数据持久化都有哪些方式?

  文件存储,SharedPerferences,SQLite

什么样的数据适合使用文件存储,什么样的数据适合使用SharedPreferences,什么样的数据适合使用SQLlite?

  文件存储不对存储内容进行任何格式化处理,所有数据都是原封不动地保存到文件中的,因而适合一些文本数据或二进制数据;
  SharedPreferences使用键值对的方式存储数据,适用于保存一些简单的数据和键值对,通常用来存储一些简单的配置信息;
  SQlite适合存储大量复杂的关系型数据

SQLlite都有哪些特点?

  SQlite是一款轻量级的关系型数据库,运算速度非常快,占用资源很少,因而特别适合在移动设备上使用。SQLite不仅支持标准的SQL语法,还遵循了数据库的ACID事务,比一般数据库要简单,无需设置用户名密码即可使用

Android消息处理机制

介绍一下Android消息处理机制

  Android消息处理机制由Looper MessageQueue Handler组成
  Looper的loop方法将MessageQueue中的Message取出,交由Handler处理。
子线程默认是没有Looper的,如果需要使用Handler就必须为线程创建Looper

Handler的作用是什么?

Handler的主要作用是将一个任务切换到某个指定的线程中去执行,如切换到主线程中更新UI。

Handler工作原理?

Handler创建时会采用当前线程的Looper来构建内部的消息循环系统,如果当前线程没有Looper则会报错。Handler创建完毕后,这个时候其内部的Looper以及MessageQueue就可以和Handler一起协同工作了,然后通过Handler的post方法将一个Runnable投递到Handler内部的Looper去处理,也可以通过Handler的send方法发送一个消息,这个消息同样会在Looper中去处理。当Handler的send方法被调用时,它会调用MessageQueue的enqueueMessage方法将这个消息放入消息队列中,然后Looper发现有新消息到来时,就会处理这个消息,最终消息中的Runnable和Handler中的handlerMessage方法就会被调用。
注意,Looper是运行在创建Handler所在的线程中的,这样一来Handler的业务逻辑就被切换到创建Handler所在的线程中去执行了。

Handler处理消息的过程?

首先,检查Message的callback是否为Null,部位null就通过handleCallback处理消息。其次检查mCallback是否为null,不为null就调用mCallback的handleMessage方法来处理消息。最后,调用Handler的handleMessage方法来处理消息。

MessageQueue的内部存储结构是什么?

  尽管叫消息队列,但它内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。

Looper的工作原理?

通过Looper.prepare()即可为当前线程创建一个Looper,接着通过Looper.loop()开启消息循环。
通过quit和quitSafely两种方法退出,区别在于前者会直接退出Looper,而后者是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。
在子线程中,如果为其手动创建了looper,那么在所有事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待状态,而如果退出Looper以后,这个线程就会立刻终止。
loop方法是一个死循环,唯一退出循环的方式是MessageQueue的next方法返回了null(消息队列被标记为退出状态时,next方法就会返回null)。也就是说,Looper必须退出,否则loop方法就会无限循环下去。

为什么loop()不会触发ANR?

  ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的应用也就退出了。
  而因为Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,如果它停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。
https://www.zhihu.com/question/34652589

为什么loop一直在运行,但系统负担不大?

  主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。

ThreadLocal是什么?

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储之后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据

什么场景下可以使用ThreadLocal?

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

ThreadLocal是如何存储数据的?

ThreadLocal是一个泛型类,其数据存储在ThreadLocal.Value中。localvalues内部有一个数组table,其值就存放在这个table数组中。

Window

Window是什么?具体如何实现?

Window是一个抽象类,表示一个窗口的概念,具体实现是PhoneWindow

Window有哪几种类型?

应用Window:对应一个Activity,层级范围为1-99
子Window:不能单独存在,需要附属在特定的父Window之中,比如常见的一些Dialog就是一个子Window,层级范围为1000-1999
系统Window:需要声明权限才能创建的Window,比如Toast和系统状态栏,层级范围为2000-2999

Window实际存在吗?

Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,因此Window并不是实际存在的,它是以View的形式存在。

View&Layout

View是怎么绘制到屏幕上的?

  View的工作流程主要是指measure,layout,draw这三大流程,,即测量、布局、绘制。其中measure确定View的测量宽和高,layout确定View的最终宽高和四个顶点的位置,draw则将View绘制到屏幕上。
  

MeasureSpec是什么?

MeasureSpec参与measure过程,与父容器的MeasureSpec共同决定View的尺寸规格。
在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个measurespec来测量出View的测量宽高。
MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize。

测量模式有哪几种?

UNSPECIFIED:父容器对View没有任何限制,要多大给多大,常用于系统内部表示一种测量的状态。
EXACTLY:父容器已经检测出View所需要的精确大小,此时View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
AT_MOST:父容器指定了一个可用大小,View的大小不能超过这个最大值。它对应于LayoutParams中的wrap_content。

View的滑动冲突场景及解决方法?

外部内部滑动方向不一致、外部内部滑动方向不一致、嵌套
外部拦截,内部拦截

View的点击事件分发过程?
  • Activity接收到一个事件以后,调用dispatchTouchEvent()交给Phonewindow
  • Phonewindow再调用自己的dispatchTouchEvent()交给整个窗口的根View——DecorView
  • 之后再由DecorView将事件交给根ViewGroup
  • ViewGroup调用自己的dispatchTouchEvent()进而调用onInterceptTouchEvent()判断是否拦截此事件
  • 如果返回为true,则表示拦截该事件,进而调用onTouchEvent()方法,处理此事件;如果返回为false,则表示不拦截,交给给子view的dispatchTouchEvent()处理
  • 如果分发到最底层的View时,如果其onTouchEvent()返回为true,则事件由底层View消耗并处理;如果返回为false,则表示该View不做处理,则传递给父VIew的onTouchEvent()处理;
  • 如果父View的onTouchEvent()仍旧返回false,则继续传递给父View的父View,如此反复下去
说几个布局名称,以及它们是怎么布局的

  LinearLayout:线性布局,在某一方向上一次排列内部视图
  RelativeLayout:相对布局,默认是FrameLayout,可以取一个控件作为参考控件,以此安排该控件的位置
  FrameLayout:帧布局,默认叠放在左上角
  ConstraintLayout:约束布局,利用可视化操作进行布局
  MaterialCardView:卡片式布局,在帧布局的基础上额外提供了圆角和阴影等效果
  DrawerLayout:抽屉式布局,即滑动菜单,内含两个控件,第一个为主界面,第二个为菜单界面
  CoordinatorLayout:协调器布局,加强版帧布局,普通情况下与帧布局效果相同,可以监听其索引子控件的各种事件,并自动帮助我们做出最为合理的相应。

什么情况下用线性布局,什么情况适合相对布局?

  线性布局的局限性在于只能针对一个方向上布局视图,所以适用于所有控件width或height属性为match_parent的情况,此时不需要考虑另一个方向上的布局情况。
  而相对布局就弥补了线性布局的这个短板,它通过相对定位可以让内部视图出现在任意位置,适用于比较复杂的布局情况。

简述布局中的merge标签

用来与include标签搭配进行布局嵌套。

  1. merge标签中的子集是直接加到Activity的FrameLayout根节点下,(Activity视图的根节点都是frameLayout).如果你所创建的Layout并不是用FrameLayout作为根节点(而是应用LinearLayout等定义root标签),就不能通过merge来优化UI结构.
  2. 当应用Include或者ViewStub标签从外部导入xml结构时,可以将被导入的xml用merge作为根节点表示,这样当被嵌入父级结构中后可以很好的将它所包含的子集融合到父级结构中,而不会出现冗余的节点.

另外需要注意的是:

  1. < merge />只可以作为xml FrameLayout的根节点.

  2. 当需要扩充的xml layout本身是由merge作为根节点的话,需要将被导入的xml layout置于 viewGroup中,同时需要设置attachToRoot为True.

总之,标签在UI的结构优化中起着非常重要的作用,它可以删减多余的层级,优化UI。多用于替换FrameLayout或者当一个布局包含另一个时,标签消除视图层次结构中多余的视图组。例如你的主布局文件是垂直布局,引入了一个垂直布局的include,这是如果include布局使用的LinearLayout就没意义了,使用的话反而减慢你的UI表现。这时可以使用标签优化。
xml中Merge标签使用
Android布局优化之merge标签

RecyclerView

RecyclerView和ListVIew有什么区别?

参照这两篇博客
RecyclerView和ListView的区别、RecyclerView优化
RecyclerView 和 ListView 性能和效果区别

RemoteViews

什么是RemoteViews?

RemoteViews提供了一组基础操作,支持跨进程更新它的界面。

在Android中的使用场景?

主要用在通知栏和桌面小部件

为什么Android不允许在子线程中访问UI?

因为Android的UI控件不是线程安全的,如果多线程并发访问可能会导致UI控件处于不可预期的状态。

为什么不对UI控件通过上锁等机制保证线程安全呢?

  1. 加上锁机制会让UI访问逻辑变得复杂
  2. 锁机制还会降低UI访问的效率

动画

Android中有哪几种动画?
  1. Frame Animation
    帧动画,通过顺序播放一系列图像从而产生动画效果,图片过多时容易造成OOM(Out Of Memory内存用完)异常。
  2. Tween Animation
    view动画,是通过对场景里的对象不断做图像变换(透明度、缩放、平移、旋转)从而产生动画效果,是一种渐进式动画,并且View动画支持自定义。
  3. Accribute Animation
    属性动画,这也是在android3.0之后引进的动画,在手机的版本上是android4.0就可以使用这个动画,通过动态的改变对象的属性从而达到动画效果。
它们的工作原理分别是什么?

  View动画:通过渐进对对象做图像变换,从而产生动画效果。
  帧动画:通过顺序播放一系列图像从而产生动画效果。
  属性动画:根据传递的属性的初始值(如果没有提供初始值则需要提供get方法)和最终值,通过多次调用属性的set方法,从而产生动画效果。

同为动态改变对象,View动画和属性动画有什么区别?

  补间动画只是改变了View的显示效果而已,并不会真正的改变View的属性。而属性动画可以改变View的显示效果和属性。举个例子:例如屏幕左上角有一个Button按钮,使用补间动画将其移动到右下角,此刻你去点击右下角的Button,它是绝对不会响应点击事件的,因此其作用区域依然还在左上角。只不过是补间动画将其绘制在右下角而已,而属性动画则不会。

插值器和估值器的作用是什么?

插值器的作用是根据时间的流逝百分比,计算当前属性值变化的百分比。
估值器的作用是根据当前属性改变的百分比来计算改变后的属性值。

Android三种动画实现原理及使用

Android动画总结

IPC

安卓中如何进行进程间通信?

  Bundle、文件共享、Messenger、AIDL、ContentProvider、Socket

Binder机制是什么?

  Binder机制是​ Android系统中进程间通讯(IPC)的一种方式,Android中ContentProvider、Intent、aidl都是基于Binder。

如何使用Binder?

  (1)获得ServiceManager的对象引用
  (2)向duServiceManager注册新的Service
  (3)在Client中通过ServiceManager获得Service对象引用
  (3)在Client中发送请求,由Service返回结果。

Binder机制的好处:

  1、只需要进行一次数据拷贝,性能上仅次于共享内存
  2、基于C/S架构,职责明确,架构清晰,稳定性较好
  3、为每个App分配UID,UID可用来识别进程身份,安全性较好

线程和线程池

Android中线程分主线程和子线程,主线程即ActivityThread,主要处理和界面相关的事情,而子线程则往往执行耗时操作。

AsyncTask

什么是AsyncTask?

AsyncTask是一种轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度和最终结果传递给主线程并在主线程中更新UI

AsyncTask的四个核心方法是什么?
  1. onPreExecute(),在主线程中执行,在异步任务执行之前,此方法会被调用。
  2. doInBackground(Params … params),在线程池中进行,此方法用于执行异步任务。在该方法中可以通过publishProgress方法更新任务进度。
  3. onProgressUpdate(Progress … value),在主线程中执行,当后天任务的执行进度发生改变时此方法会被调用。
  4. onPostExecute(Result result),在主线程中执行,在异步任务执行之后,该方法会被调用。

HandlerThread

什么是HandlerThread?

HandlerThread继承了Thread,它是一种可以使用Handler的Thread。

Thread和HandlerThread有什么区别?

普通Thread主要用于在run方法中执行一个耗时任务,而HandlerThread在内部创建了消息队列,外界需要通过Handler的消息方式来通知HandlerThread执行一个具体的任务。

IntentService

什么是IntentService?

IntentService是一种特殊的Service,它继承了Service并且它是一个抽象类,因此必须创建它的子类才能使用IntentService。IntentService可用于执行后台耗时的任务,当任务执行后它会自动停止,同时由于IntentService是服务的原因,导致其优先级比单纯的线程高很多,所以IntentService比较适合执行一些高优先级的后台任务,因为其优先级高,不容易被系统杀死。

权限

Android中权限分为哪两类 ?有什么区别?

  危险权限和普通权限,普通权限只需要在注册文件中声明即可,危险权限不仅需要在注册文件中声明,还需要向用户申请权限许可。

Gradle

implement、api 和compile的区别?

api 指令
完全等同于compile指令

implement指令
这个指令的特点就是,对于使用了该命令编译的依赖,对该项目有依赖的项目将无法访问到使用该命令编译的依赖中的任何程序,也就是将该依赖隐藏在内部,而不对外部公开

用api指令编译,Glide依赖对app Module 是可见的
用implement指令编译依赖对app Module 是不可见的
android gradle tools 3.X 中依赖,implement、api 指令

热门技术

如何缩小APK的大小?

如何缩小APK包的尺寸

热修复、插件化、组件化的区别?

热修复、插件化、组件化的区别

版本

Android各个主要版本都增加了哪些东西?

  ①Android 5.0:使用一种新的Material Design设计风格
  ②Android 6.0:引入了运行时权限
  ③Android 7.0:引入了多窗口模式
  ④Android 8.0:引入了通知渠道,画中画模式
  ⑤Android 9.0:适配全面屏,引入全面屏手势
  ⑥Android 10.0:引入了黑暗模式
  ⑦Android 11.0:引入了一次性权限,屏幕录制工具。

数据结构

1. 数组和链表有什么区别?
  • 数组的大小是固定的,一旦申请就无法扩展,而链表大小是不受限制的。
  • 数组支持随机访问,链表只能顺序访问。
  • 数组插入元素需要挪动其他元素,链表只需改变几个指针。
2. 树的遍历方法
  • 先序遍历:根节点,左子树,右子树
  • 中序遍历:左子树,根节点,右子树
  • 后序遍历:左子树,右子树,根节点
  • 层序遍历:按层遍历
3. 层序遍历具体怎么实现?

借助Queue实现

4.说出你知道排序算法,以及他们的时间复杂度

在这里插入图片描述

设计模式

说出几个常用的设计模式

模式名称描述
观察者模式让对象能够在状态改变时被通知,如LiveData
单例模式确保有且只有一个对象
装饰模式包装一个对象,以提供新的行为
适配器模式封装对象,并提供不同的接口
状态模式封装了基于状态的行为,并使用委托在行为直接切换
迭代器模式在对象的集合之中游走,而不暴露集合的实现
外观模式简化一群类的接口
策略模式封装可以互换的行为,并使用委托来决定要使用哪一个
代理模式包装对象,以控制对此对象的访问
工厂方法模式由子类决定要创建的具体类是哪一个
抽象工厂模式允许客户创建对象的家族,而无需指定他们的具体类
模板方法模式由子类决定如何实现一个算法中的步骤
组合模式客户用一致的方式处理单个对象和对象集合
命令模式封装请求成为对象

单例模式的三种实现方法?

  懒汉模式:需要时才会去创建

public Class Singleton{
  private static Singleton instance = null;
  private  Singleton(){}
  //通过sychronized关键字保证线程安全
  public static synchronized Singleton getInstance(){
    if( instance == null ){instance = new Singleton(); }
    return instance;
  }
}

  饿汉模式:类加载时就创建了实例

public Class Singleton{
	//通过在静态初始化器中创建单件保证线程安全
  private static Singleton instance = new Singleton();
  private  Singleton(){}
  public static Singleton getInstance(){
    return instance;
  }
}

  双重校验锁:首先检查实例是否已经创建,如果尚未创建,“才”进行同步。

public class Singleton{
	//通过vilatiel关键字保证可见性
	private volatile static Singleton uniqueInstance;
	private Singleton(){}
  public static synchronized Singleton getInstance(){
  	//检查实例,如果不存在则进入同步块
  	if( instance == null ){
  		synchronized(Singleton.class{
       	//再检查一次,如果仍为null,才创建实例		
	       if(instance == null){
	         instance = new Singleton();
             }  
	     }
     }
    return uniqueInstance;
  }
}

三种模式的比较:

懒汉模式饿汉模式双重检验锁
优点实现简单,能够避免内存浪费实现起来也比较简单可大大减少getInstance方法的时间耗费
缺点由于使用了sychronized关键字性能代价较高只适用于总是创建并使用单件实例或创建运行负担不重的情况实现较为复杂,且仅适用于java5+

什么是动态代理?和静态代理有什么区别?

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。

静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
动态代理类:程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。

静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。

Retrofit框架就使用了动态代理。

计算机网络

1. TCP/IP协议下,有哪些层?

应用层,传输层,网络层,网络接口层

2. 列举一下这些层上的协议

应用层:FTP,SNMP,DNS
传输层:TCP, UDP
网络层:IP ,ARP
网络接口层:FDDI,ATM

2. TCP和UDP有什么区别?
  1. TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
  2. TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
  3. UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
  4. .每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  5. TCP对系统资源要求较多,UDP对系统资源要求较少。
4. TCP如何保证可靠性?

  Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

5. 三次握手四次挥手

三次握手的本质是确认通信双方收发数据的能力

  • 第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1),序列号seq=100。
  • 第二次握手:服务端收到客户端发过来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。
  • 第三次握手:客户端收到服务端的回复后发现ACK=1并且ack=101,于是知道服务端已经收到了序列号为100的那段报文;同时发现SYN=1,知道了服务端同意了这次连接,于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)、ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次seq就从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时seq还是101)。当服务端收到报文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。

四次挥手的目的是关闭一个连接

  • 第一次挥手:客户端向服务端发出释放连接报文,释放连接报文包含FIN标志位(FIN=1)、序列号seq=1101(100+1+1000,其中的1是建立连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。
  • 第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号1101+1)、序列号seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。
  • 第三次挥手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1,ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50)。
  • 第四次挥手:客户端收到服务端发的FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=2351、序列号seq=1102。
  • 注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。
6. 为什么四次挥手最后一次挥手时客户端不立即中断连接?

  要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。

7. http和https有什么区别?

  HTTP是以明文方式传输的报文,不安全,而HTTPS在HTTP的基础上加入了SSL协议,它采用对称加密方式,而在发送其公共密钥时采用的则是公钥加密方式(即非对称加密)。

8. 对称加密和非对称加密有什么区别?

  对称加密过程和解密过程使用的同一个密钥,加密过程相当于用原文+密钥可以传输出密文,同时解密过程用密文-密钥可以推导出原文。
  而非对称加密采用了两个密钥,一般使用公钥进行加密,使用私钥进行解密。

编程题

反转链表

镜像二叉树

用两个队列实现一个栈

输出输入数值二进制下1的个数

编程题1.写一个函数,传入一个字符串,返回该字符串是否是合法的IPv4地址

编程题2.写一个函数,该函数传入一个无序数组,返回该无序数组中最长的连续数长度与其数组

如输入:15, 7,12, 6,14, 13,9, 11
输出:5
11, 12, 13, 14,15

public static int longest(int[] nums){
        if (nums==null) return 0;
        if (nums.length==0 || nums.length==1) return nums.length;
        Arrays.sort(nums);
        int left = 0;
        int right = 0;
        int max = 0;
        int start = 0;
        int end = 0;
        for (int i = 1; i <nums.length ; i++) {
            if (nums[i] == nums[i-1]+1){
                right++;
            }else {
                if (right-left > max){
                    max = right-left;
                    start = left;
                    end = right;
                }
                left = i;
                right = i;
            }
        }
        if (right-left > max){
            max = right-left;
            start = left;
            end = right;
        }

        int[] res = Arrays.copyOfRange(nums,start,end+1);
        System.out.println(Arrays.toString(res));
        return max+1;
    }
  • 21
    点赞
  • 148
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值