7月17日喜提oppo提前批offer,oppo确实如他们企业核心价值观所说的那样:本分。给提前批的福利是可以在9月30日之前解除协议无需违约金,一面、部长面和HR面都蛮顺利(一千个人可能要了一百左右,但没有感受到竞争的激烈,说白了提高自己的核心竞争力才是硬道理,自己强了面试也会更顺利)。

oppo一面:

1.你要面的是Android开发工程师,那么Android开发主要是做什么的?

         Android大致分为4层系统架构,分别是:Linux内核层、系统运行库层、应用框架层和应用层。

     (1)Linux内核层:

       Android是基于Linux内核的,这一层为Android设备的各种硬件提供了底层的驱动,譬如蓝牙驱动、音频驱动、Wifi驱动、键盘驱动、照相机驱动等。

     (2)系统运行库层:

       分为Android运行时层和系统库,运行时层包括了核心库和DVM虚拟机,核心库允许开发者使用Java来编写Android应用,DVM虚拟机在5.0以前是DVM,5.0之后是ART。核心库包括了SQLite数据库、OpenGL3D绘图支持、WebKit库浏览器内核支持等。

      (3)应用框架层:

       主要是系统构建时所需要的若干API,譬如Activity、ContentProvider、View、Notification、Location等等。可以使用这些API来进行处理。

      (4)应用层:

        所有手机上的App都是属于这一层的,譬如系统自带的联系人、短信、小游戏、自己开发小程序等等。

2.你的问题是什么?

      (1)Kotlin是不是大势所趋?Flutter的Dart语言公司在用么?Fuchsia会替代Android么?

      (2)您觉得我在哪些方面需要加强学习?

3.Java中的难点?

        集合机制(List、Set和Map)、同步机制(加锁Synchorized、Lock和unLock、wait和notify、notifyall)。

4.Java当中遇到的锁?

        主要分为:(1)乐观锁/悲观锁;(2)独享锁/共享锁;(3)互斥锁/读写锁;(4)可重入锁;(5)公平锁/非公平锁;(6)自旋锁;(7)分段锁。

(1)乐观锁/悲观锁

       乐观锁与悲观锁并不是特指某两种类型的锁,是人们定义出来的概念或思想,主要是指看待并发同步的角度。

       乐观锁:每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。一般使用version方式和CAS操作方式。eg:CAS(比较并交换)。

       悲观锁:每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。在Java中,synchronized的思想也是悲观锁。

       乐观锁适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

       悲观锁使用场景:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

(2)独享锁/共享锁

        独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock(互斥锁)而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock(读写锁),其读锁是共享锁,其写锁是独享锁。

       读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的,对于Synchronized而言,当然是独享锁。

(3)互斥锁/读写锁

       互斥锁/读写锁就是独享锁/共享锁具体的实现。互斥锁在Java中的具体实现就是ReentrantLock。读写锁在Java中的具体实现就是ReadWriteLock。

(4)可重入锁

  可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

    对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是ReentrantLock 重新进入锁。

       对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

       不可重入锁:只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单;

       可重入锁:不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。

       设计了加锁次数,以在解锁的时候,可以确保所有加锁的过程都解锁了,其他线程才能访问。不然没有加锁的参考值,也就不知道什么时候解锁?解锁多少次?才能保证本线程已经访问完临界资源了可以唤醒其他线程访问了。实现相对复杂。)

(5)公平锁/非公平锁

   公平锁是指多个线程按照申请锁的顺序来获取锁。

        非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。ReetrantLock和Synchronized都是非公平锁;

(5)分段锁

        分段锁是一种设计,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

分段锁称为Segment,它即类似于HashMap(JDK7和JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock。

(6)自旋锁

       在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

     具体来说:
     Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入的重量级锁,以下两个锁都在JUC包下,是API层面上的实现
     ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
     ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

5.线程的了解?线程的创建?线程的切换?AsyncTask什么时候是在工作线程?什么时候是在主线程?

        (1)线程创建的三种方法:(1)继承Thread实现run方法;(2)上一中耦合性较高,实现Runnable接口并重写run方法;(3)使用匿名内部类实现并重写run方法。

        (2)Android中线程的切换:Handler、AsyncTask、HandlerThread。

2.1.AsyncTask底层是线程池,其余两个直接使用了线程。

     onPreExecute(界面的初始化操作):在主线程执行,在异步任务执行之前被调用,一般用于一些准备工作

     doInBackground(处理具体耗时任务):在线程池中执行,此方法用于执行异步任务,params参数表示异步任务的输入参数,在此方法中可以通过publishProgress方法来更新任务的进度,publishProgress会调用onProgressUpdate方法,此外此方法需要返回计算结果给onPostExecute

      onProgressUpdate(进行UI操作):在主线程执行,当后台任务执行进度发生改变时,返回结果

      onPostExecute(任务收尾工作):在主线程中执行,在异步任务结束后,此方法被调用,其中result参数是后台任务的返回值。

       AsyncTask中有两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler),其中线程池SerialExecutor用于任务的排队,而线程池THREAD_POOL_EXECUTOR用于真正的执行任务,IntentalHandler用于将执行环境从线程池切换到主线程。

2.2.HandlerThread

    使用步骤如下:

   (1)创建HandlerThread对象,可以重写onLooperPrepared()方法做一些初始化操作;

   (2)调用handlerThread.start()方法;

   (3)创建Handler,在构造中传入handlerThread.getLooper()方法创建的Looper对象,重写handleMessage(Message msg)方法,在子线程处理消息;

   (4)使用Handler对象在相关场景发送处理消息;

   (5)适时退出异步操作quit方法

     源码:

    (1)HandlerThread 继承了Thread,是一种可以使用Handler的Thread,在run方法中通过Looper.prepare()来创建消息队列,并通过loop()来开启消息循环,可以通过quit方法或者quitSafely方法来终止线程进行,允许在HandlerThread中创建Handler了。

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

      整个HandlerThread做的工作本质是在Thread中封装了Looper对象,以此在创建的Handler中能够进行异步的消息处理。

6.Violate关键字和变量不可见性。

    (1)保持内存可见性:所有线程都能看到共享内存的最新状态。每次读取前必须先从主内存刷新最新的值。每次写入后必须立即同步回主内存当中。(2)禁止指令重排序。通过“内存屏障”来防止指令被重排序。保证了共享变量的“可见性”,可见性的意思是一个线程修改一个共享变量时,另一个线程可以读到这个修改的值,它不会引起线程的上下文切换和调度。

     可见性、同步性;修饰对象;阻塞与否;编译器优化与否。

     (1)volatile关键字解决的是变量在多个线程之间的可见性(一个线程修改的状态对另一个线程是可见的。);而sychronized关键字解决的是多个线程之间访问共享资源的同步性。 

     (2)volatile只能用于修饰变量,而synchronized可以修饰变量,方法,以及代码块。

     (3)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

     (4)volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

补充:synchronized和Lock的区别

     (1)synchronized不会引起死锁,而Lock发生异常时,如果没有unLock解锁,可能会引起死锁现象。

     (2)Lock是接口,Sychronsized是关键字;

     (3)当竞争资源激烈时,Lock性能高于Sychronized。

     (4)Lock可以让等待的锁响应中断,但sychronsized不可以。一直等待下去。

7.Android SDK是个啥?如何开发?SDK如何使用?

https://www.jianshu.com/p/124110136f2b

       SDK是software development kit的缩写,顾名思义,是一套软件开发工具集。SDK最大的好处就是可以对基础的代码进行封装,将很多配置、平台兼容、固定用法的部分打包起来,只专注于提供逻辑方法。这样,顶层应用开发人员就不需要过多地去关注底层的实现,而只需要专注于逻辑的开发,功能的实现。然后,整个软件开发过程就可以严格地流程化,底层开发与应用功能开发互不影响,实现并行开发,这样就大大提高了软件开发的合作效率。

       类似于Jar包,直接add as a library即可。

8.项目上的问题?

        视频展示这块。性能方面:从D2C和DexVMP两方面去说。Dex文件的加载过程(DexClassLoader)和Dex文件的格式,多重虚拟化的实现,根据Smali指令类型进行指令解析转换;Android的项目超声波和蓝牙防丢器。协处理器有了解么?图网络的背景。

       超声波:

       用Matlab同样实现扬声器发送17.5KHZ高频超声波、麦克风以44100采样率收集原始数据,对原始信号的频谱进行相干解调,滤波处理之后对静态向量进行去除,通过相位变化计算总距离变化

        发射模块(扬声器17.5KHZ高频超声波)、接收模块(麦克风以44100采样率收集原始数据)、存储模块(pcm格式)、处理模块(处理模块分为调频模块(将原始信号(数组)下变频成为基带信号a+bi,将载波去掉)、CIC滤波模块(进行低通滤波,去掉高频部分的信号)、高通滤波模块(去掉振幅较低静态物体反射信号、去除静态向量)、计算相位模块(高通滤波之后利用相关库及自定义函数计算相位))以及显示模块(子线程更新UI)。

       华为荣耀3C不能发送高频声波,三星盖世S5、小米5可以。

       蓝牙防丢器:

       界面采用ViewPager+Fragment,地图服务采用了LBS SDK,聊天机器人采用了图灵机器人。

       首先,我们要判断手机是否支持BLE,并且获得各种权限,才能让我们之后的程序能正常运行。 

       然后,我们去搜索BLE设备,得到它的MAC地址。 

       其次,我们通过这个MAC地址去连接,连接成功后,去遍历得到Characteristic的uuid。 

       在我们需要发送数据的时候,通过这个uuid找到Characteristic,去设置其值,最后通过writeCharacteristic(characteristic)方法发送数据。 

       如果我们想知道手机与BLE设备的距离,则可以通过readRemoteRssi()去得到rssi值,通过这个信号强度,就可以换算得到距离。 只要我们连接上,我们就可以用BluetoothGatt的各种方法进行数据的读取等操作。


Oppo二面(部长面):

       部长面其实答得不好,有些问题不是很清楚。或许这是白菜价的原因吧,静下心来复习才是硬道理。

1.项目介绍:
2.Android虚拟机允许的最大内存

​https://chenfeng0104.iteye.com/blog/1164989​​​

       Max Heap Size,是堆内存的上限值,Android的缺省值是16M(某些机型是24M)。一般是动态分配的。

       堆(HEAP)是DVM中占用内存最多的部分,通常是动态分配的。堆的大小不是一成不变的,通常有一个分配机制来控制它的大小。比如初始的HEAP是4M大,当4M的空间被占用超过75%的时候,重新分配堆为8M大;当8M被占用超过75%,分配堆为16M大。倒过来,当16M的堆利用不足30%的时候,缩减它的大小为8M大。重新设置堆的大小,尤其是压缩,一般会涉及到内存的拷贝,所以变更堆的大小对效率有不良影响。

3.职业规划

      第一点:自己是一个什么样的人?第二点:自己对应聘岗位是否了解?第三点自己的规划。

      自己是一个比较踏实肯干(考研、导师评价)、目标明确、有一定抗压能力(项目中的经历)的人。

      应聘岗位的了解:应用层、系统框架层、Android运行时层、Linux Kernel层。

      Linux Kernel层:Android是基于Linux内核的,这一层为Android设备的各种硬件提供了底层的驱动,主要是写一些驱动,譬如蓝牙驱动、音频驱动等。

      Android运行时层:分为Android运行时层和系统库,运行时层包括了核心库和DVM虚拟机,核心库允许开发者使用Java来编写Android应用,DVM虚拟机在5.0以前是DVM,5.0之后是ART。核心库包括了SQLite数据库、OpenGL3D绘图支持、WebKit库浏览器内核支持等。DexVMP属于Android运行时层。

      系统框架层:主要是系统构建时所需要的若干API,譬如Activity、ContentProvider、View、Notification、Location等等。可以使用这些API来进行处理。

      应用层:所有手机上的App都是属于这一层的,譬如系统自带的联系人、短信、小游戏、自己开发小程序等等

      自己的职业规划(以自己研究生的经历为例)

      第一阶段,我希望从现在开始,1-2年之内能够在我目前申请的这个职位上沉淀下来,积累最起码的工作经验,把基础打牢;

      第二阶段,我希望利用3-5年的时间,成为一个在自己的专业方面能够独当一面的人,能够独自承担责任,发现问题,解决问题,不让上司操心;

      第三阶段,成为该领域的一名专业化人士,在工作中能有创新与发展,能为公司带来更大的价值。

4.Activity和Service之间的交互

      (1)BindService;(2)基于BindService的Handler;(2)BroadCastReceiver;(3)通过共享文件;(4)Messenger;(5)AIDL。

      (1)使用BindService进行数据的传递,Activity向Service传递数据靠的是Intent的Bundle和BindService;Service向Activity传递数据靠的是继承自Binder类的binder对象。最后在Acitivity的onServiceConnected中获取。也可以通过BindSerrvice

         基于BindService的Handler。(1)在Service中的onCreate方法中创建Service端的Handler和Messenger对象,并且在handleMessage方法中获取Activity端的Messenger;    

     (2)在Service的onBind方法中返回Messenger的Binder对象;(3)在Activity中创建一个Handler对象,用来处理消;(4)在Activity中创建一个ServiceConnection对象,并且在onServiceConnected方法中,获取Service端的Messenger对象;(5)在Activity创建Messenger,并封装在Message中传递给Service端。

                                      oppo提前批Android开发岗面经(附问题答案)_java         

     (3)通过共享文件,譬如SharedPreference可以实现Activity和Service的交互。

     (4)Messenger:通过它可以在不同进程间传递Meesage对象,Messenger是一种轻量级的IPC方案,底层是用AIDL实现的;

     (5)AIDL:AIDL属于Android的IPC机制,常用于跨进程通信,主要实现原理基于底层Binder机制;

     (6)通过广播进行交互:通过广播实现Activity和Service的交互简单容易实现,缺点是发送不广播受系统制约,系统会优先发送系统级的广播,自定义的广播接收器可能会有延迟,在广播里也不能有耗时操作,否则会导致程序无响应。

5.Bundle的最大限制,Bundle有什么要求?

​https://www.jianshu.com/p/32c641d62ae3​​​

      Buddle的最大限制为1Mb。大于1Mb会崩溃掉。

      Bundle的场景大多数为小数据量,ArrayMap内部是使用两个数组进行数据存储,一个数组记录key的hash值,另一个数组记录value值,内部使用二分法对key进行排序,并使用二分法进行添加、删除、查找数据,因此它只适合于小数据量操作,在数据量较大的情况下它的性能将会退化。而HashMap内部则是数组+链表的结构,在数据量较少的情况下,HashMap的Entry Array比ArrayMap占用更多的内存。

      由于使用Bundle的场景大多数为小数据量,所以相比之下,使用ArrayMap保存数据在操作速度和内存占用上都具有优势,因此使用Bundle来传递数据,可以保证更快的速度和更少的内存占用。

​https://www.jianshu.com/p/7424456c6eda​​​

      Bundle用于携带数据,类似于 Map集合,用来存放Key-Value键值对,但是它相对于Map,提供了常用类型的 putXxx()和getXxx()方法,比如putString()/getString()、putInt()/getInt(),putXxx()用于往Bundle中放入对象,getXxx()用于从Bundle中取出数据。

      Bundle可传递的数据类型:(1)基本类型的数据,如int、String、Float等等;(2)使用Serializable 和 Parceable 传递对象(Serializable:代码少,效率低;Parcelable:代码多,效率高)

6.OOM及其解决方案

       OOM是内存溢出,指程序在申请内存时,没有足够的内存空间供其使用,比如你需要100Mb,但系统只有90Mb,这样的话肯定会引起OOM。

      (1)合理加载资源:合理加载资源,既如果展示图片的ImageView只有128*96的像素大小,这时候把一张1024*768的图片完全加载到内存中,很明显是错误的行为。这个时候,就需要把要加载的图片进行压缩加载,就是合理地加载资源。

下面来进行图片的压缩讲解,设置BitmapFactory.Options中inSampleSize的值就可以实现等比例压缩。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用26M的内存,压缩后就只需要占用1.5M了。

      (2)合理回收资源:合理回收资源,既对加载在内存中的图片资源进行合理的回收,避免因为不再使用的图片资源还留存在内存中的情况出现。而要实现合理回收资源,最核心的一个类就是:LruCache,这个类非常适用于保存图片内存,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。(比如LruCache、DiskLruCache、对象重复并且频繁调用可以考虑对象池)

     (3)内存泄漏引起的内存溢出:静态变量导致的内存泄漏;Handler引起的内存泄漏;单例模式引起的内存泄漏;资源对象未关闭引起的内存泄漏;注册/反注册引起的内存泄漏等。

7.BroadCastReceiver有几种?开机启动的广播是什么广播?

       广播类型:标准广播(异步执行,同时收到、无法截断)和有序广播(同步执行、先后顺序,可截断)。开机启动的广播是静态注册的系统广播:action的名字为android.intent.action.BOOT_COMPLETED。

7.1.接收系统广播:

       代码中注册称为动态注册;在AndroidManifest.xml中注册称为静态注册。

       动态注册监听网络变化;步骤一:新建类继承自BroadCastReceiver,重写父类的onReceiver方法;具体处理逻辑放在其中;步骤二:onCreate方法中创建IntentFilter实例,系统发出android.net.conn.CONNECTIVITY_CHANGE的广播,在IntentFilter添加该Action;步骤三:在onCreate方法中使用registerReceiver进行注册;在onDestroy中使用unregisterReceiver进行取消注册。步骤四:添加权限:android.permission.ACCESS_NETWORK_STATE。

      静态注册实现开机启动:动态广播虽然灵活,但只有在程序启动之后才能接收到广播,静态注册可以在程序未启动的情况下接收到广播。步骤一:右键快速新建BroadCastReceiver,在XML中注册,在onReceiver中写一个简单的Toast;步骤二:系统启动完成之后会发出一条android.intent.action.BOOT_COMPLETED的广播,需要在<intent-filter>中添加相应Action;步骤三:声明权限:android.permission.RECEIVE_BOOT_COMPLETED。

7.2.发送自定义广播

      发送标准广播:步骤一:快速新建广播接收器,复写onReceiver方法,简单Toast;

      发送有序广播;将sendBroadcast(intent);改成 sendOrderedBroadcast(intent,null)。

7.3.使用本地广播

      系统全局广播可被任何程序接收到,不安全,Android引入一套本地广播机制,使得发出广播只能在应用程序内部传递。而且广播接收器只能接收到来自本应用程序发出的广播,安全性问题得以解决。优势安全高效。LocalBroadcastManager的getInstance得到它的一个实例,注册、注销都一样,发出一条com.example.hzk.LOCAL_BROADCAST的广播。

8.干过什么学生工作么?

      班级团支书、义务维修站站长。

9.Java虚拟机的最大内存是多少?

      JVM虚拟机的默认内存使用大小为64MB,也就是你不更改的话,他的JVM内存使用大小就是64MB,如果超出这个内存使用限度,就会报java head space错误。可以使用-Xmx900m进行更改堆大小避免溢出。

10.对导师专利一作是怎么看的?

       感恩感谢吧。

11.可以问我一个问题?

    需要提高的地方有哪些?

Oppo三面(HR面):

    就是唠家常,职业规划是必问的,尽可能实事求是,HR还是挺客气的。不过听说HR面还有挂人,有点可怕。