Android面试题整理

日常总结 专栏收录该内容
18 篇文章 0 订阅

前言

本文整理了简书 goeasyway 面试相关文章,并在文章中加以自己的理解以及看见的部分精彩评论,所有文章并非自己原创,如对Android面试有兴趣,可前往作者文章专栏传送门或者关注对方的微信公众号:Android面试启示录
这里写图片描述


startService只是启动Service,启动它的组件(如Activity)和Service并没有关联,只有当Service调用stopSelf或者其他组件调用stopService服务才会终止。
bindService方法启动Service,其他组件可以通过回调获取Service的代理对象和Service交互,而这两方也进行了绑定,当启动方销毁时,Service也会自动进行unBind操作,当发现所有绑定都进行了unBind时才会销毁Service。
Service生命周期函数
IntentService——可以看做是Service和HandlerThread的结合体,在完成了使命之后会自动停止,适合需要在工作线程处理UI无关任务的场景。

  • IntentService 是继承自 Service 并处理异步请求的一个类,在 IntentService内有一个工作线程来处理耗时操作。
  • 当任务执行完后,IntentService 会自动停止,不需要我们去手动结束。
  • 如果启动 IntentService 多次,那么每一个耗时操作会以工作队列的方式在 IntentService 的 onHandleIntent 回调方法中执行,依次去执行,使用串行的方式,执行完自动结束。

Normal broadcasts无序广播,会异步的发送给所有的Receiver,接收到广播的顺序是不确定的,有可能是同时。
Ordered broadcasts有序广播,广播会先发送给优先级高(android:priority)的Receiver,而且这个Receiver有权决定是继续发送到下一个Receiver或者是直接终止广播。
sendStickyBroadcast发送Sticky类型的广播。Sticky简单说就是,在发送广播时Reciever还没有被注册,但它注册后还是可以收到在它之前发送的那条广播。
LocalBroadcastManager发送广播时限定有权限(receiverPermission)的接收者才能收到。但是我们知道APK太容易被反编译,注册广播的权限也只是一个字符串,并不安全。然后可能使用Handler,没错,往主线程的消息池(Message Queue)发送消息,只有主线程的Handler可以分发处理它,广播发送的内容是一个Intent对象,我们可以直接用Message封装一下,留一个和sendBroadcast一样的接口。在handleMessage时把Intent对象传递给已注册的Receiver。
如果不是频繁地刷新,使用广播来做也是可以的。但对于较频繁地刷新动作,建议还是不要使用这种方式。广播的发送和接收是有一定的代价的,它的传输是通过Binder进程间通信机制来实现的(细心人会发现Intent是实现了Parcelable接口的),那么系统定会为了广播能顺利传递做一些进程间通信的准备。

如果一个Activity在用户可见时才处理某个广播,不可见时注销掉,那么应该在哪两个生命周期的回调方法去注册和注销BroadcastReceiver呢?
如果有一些数据在Activity跳转时(或者离开时)要保存到数据库,那么你认为是在onPause好还是在onStop执行这个操作好呢?
简单说一下Activity A启动Activity B时,两个Activity生命周期的变化。

来源于Android源码中的Call应用,AsyncTask中的onPostExecute片段

@Override
    protected void onPostExecute(Void result) {
        final Activity activity = progressDialog.getOwnerActivity();

        if (activity == null || activity.isDestroyed() || activity.isFinishing()) {
            return;
        }

        if (progressDialog != null && progressDialog.isShowing()) {
            progressDialog.dismiss();
        }
    }

参考TextView的源代码,BaseSavedState是View的一个内部静态类,从代码上我们也很容易看出是把控件的属性(如selStart)打包到Parcel容器,Activity的onSaveInstanceState、onRestoreInstanceState最终也会调用到控件的这两个同名方法。

/**
     * User interface state that is stored by TextView for implementing
     * {@link View#onSaveInstanceState}.
     */
    public static class SavedState extends BaseSavedState {
        int selStart;
        int selEnd;
        CharSequence text;
        boolean frozenWithFocus;
        CharSequence error;

        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(selStart);
            out.writeInt(selEnd);
            out.writeInt(frozenWithFocus ? 1 : 0);
        ......

View有唯一的ID;
View的初始化时要调用setSaveEnabled(true) ;
这里写图片描述

“在Java里面参数传递都是按值传递”
这句话的意思是:按值传递是传递的值的拷贝,按引用传递其实传递的是引用的地址值,所以统称按值传递。
简单的说,基本类型是按值传递的,方法的实参是一个原值的复本。类对象是按对象的引用地址(内存地址)传递地址的值,那么在方法内对这个对象进行修改是会直接反应在原对象上的(或者说这两个引用指向同一内存地址)。不过要注意String这个类型,如下代码:

public static void main(String[] args) {
    String x = new String("goeasyway");
    change(x);
    System.out.println(x);
}

public static void change(String x) {
    x = "even";
}
  • Message:消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息;
  • MessageQueue:消息队列的主要功能向消息池投递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next);
  • Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);
  • Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。
  • Serializalbe会使用反射,序列化和反序列化过程需要大量I/O操作,Parcelable自已实现封送和解封(marshalled
    &unmarshalled)操作不需要用反射,数据也存放在Native内存中,效率要快很多。

  • 两个Activity之前传递对象一定要注意对象的大小,Intent中的Bundle是在使用Binder机制进行数据传递的,能使用的Binder的缓冲区是有大小限制的(有些手机是2M),而一个进程默认有16个binder线程,所以一个线程能占用的缓冲区就更小了(以前做过测试,大约一个线程可以占用128KB)。所以当你看到“The Binder transaction failed because it was too large.”这类TransactionTooLargeException异常时,你应该知道怎么解决了。

Context类本身是一个纯abstract类,它有两个具体的实现子类:ContextImpl和ContextWrapper。其中ContextWrapper类,如其名所言,这只是一个包装而已,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。ContextThemeWrapper类,如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题就是指在AndroidManifest.xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service是不需要主题的,因为Service是没有界面的后台场景,所以Service直接继承于ContextWrapper,Application同理。而ContextImpl类则真正实现了Context中的所以函数,应用程序中所调用的各种Context类的方法,其实现均来自于该类。一句话总结:Context的两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。Activity,Application,Service虽都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。
一个应用程序有几个Context?
从上面的关系图我们已经可以得出答案了,在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有Activity,Service持有Context,那Broadcast
Receiver,Content Provider呢?Broadcast Receiver,Content
Provider并不是Context的子类,他们所持有的Context都是其他地方传过去的,所以并不计入Context总数。传送门

在一次显示ListView的界面时,getView会被执行几次?
每次getView执行时间应该控制在多少毫秒之内?
getView中设置listener要注意什么?
- 重用ConvertView;
- 使用View Holder模式;
- 使用异步线程加载图片(一般都是直接使用图片库加载,如Glide, Picasso);
- 在adapter的getView方法中尽可能的减少逻辑判断,特别是耗时的判断;
- 避免GC(可以从LOGCAT查看有无GC的LOG);
- 在快速滑动时不要加载图片;
- 尽可能减少List Item的Layout层次(如可以使用RelativeLayout替换LinearLayout,或使用自定的View代替组合嵌套使用的Layout);
- 将ListView的scrollingCache和animateCache这两个属性设置为false(默认是true);

这里写图片描述

实现的方式也很简单,直接调用Android开放的接口Resources.updateConfiguration:

public static void changeSystemLanguage(Context context, String language) {
        if (context == null || TextUtils.isEmpty(language)) {
            return;
        }

        Resources resources = context.getResources();
        Configuration config = resources.getConfiguration();
        if (Locale.SIMPLIFIED_CHINESE.getLanguage().equals(language)) {
            config.locale = Locale.SIMPLIFIED_CHINESE;
        } else {
            config.locale = new Locale(language);
        }
        resources.updateConfiguration(config, null);
    }

线程池可以同时执行多少个TASK?
Android 3.0之前(1.6之前的版本不再关注)规定线程池的核心线程数为5个(corePoolSize),线程池总大小为128(maximumPoolSize),还有一个缓冲队列(sWorkQueue,缓冲队列可以放10个任务),当我们尝试去添加第139个任务时,程序就会崩溃。当线程池中的数量大于corePoolSize,缓冲队列已满,并且线程池中的数量小于maximumPoolSize,将会创建新的线程来处理被添加的任务。如下图会出现第16个Task比第6-15个Task先执行的情况。
多个AsyncTask任务是串行还是并行?

从Android 1.6到2.3(Gingerbread)
AsyncTask是并行的,即上面我们提到的有5个核心线程的线程池(ThreadPoolExecutor)负责调度任务。从Android
3.0开始,Android团队又把AsyncTask改成了串行,默认的Executor被指定为SERIAL_EXECUTOR。
AsyncTask容易引发的Activity内存泄露
如果AsyncTask被声明为Activity的非静态的内部类,那么AsyncTask会保留一个对创建了AsyncTask的Activity的引用。如果Activity已经被销毁,AsyncTask的后台线程还在执行,它将继续在内存里保留这个引用,导致Activity无法被回收,引起内存泄露。

commit这种方式很常用,在比较早的SDK版本中就有了,这种提交修改的方式是同步的,会阻塞调用它的线程,并且这个方法会返回boolean值告知保存是否成功(如果不成功,可以做一些补救措施)。
而apply是异步的提交方式,目前Android Studio也会提示大家使用这种方式。
多进程操作和读取SharedPreferences的问题
在SDK 3.0及以上版本,可以通过Context.MODE_MULTI_PROCESS属性来实现SharedPreferences多进程共享。本来以为通过MODE_MULTI_PROCESS属性使用SharedPreferences就可以实现不同时程间共享数据,但是在真正使用中确发现有会有一定概率出现这个取值出错(变为初始值)问题。

最后发现在官网上Google也在SDK 6.0的版本将这个MODE_MULTI_PROCESS标识为deprecated(不赞成使用)。

封装。对数据进行封装,提供统一的接口,使用者完全不必关心这些数据是在DB,XML、Preferences或者网络请求来的。当项目需求要改变数据来源时,使用我们的地方完全不需要修改。
提供一种跨进程数据共享的方式。
ContentProvider接口方法运行在哪个线程中呢?
1. ContentProvider和调用者在同一个进程,ContentProvider的方法(query/insert/update/delete等)和调用者在同一线程中;
2. ContentProvider和调用者在不同的进程,ContentProvider的方法会运行在它自身所在进程的一个Binder线程中。

Object的wait和notify/notifyAll如何实现线程同步?

在Object.java中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()是唤醒所有的线程。

wait和yield(或sleep)的区别?

  1. wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,而yield()是让线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权。
  2. wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁。
try
    {
        sqLiteDatabase.beginTransaction();
        SQLiteStatement stat = sqLiteDatabase.compileStatement(insertSQL);

        // 插入10000次
        for (int i = 0; i < 10000; i++)
        {
            stat.bindLong(1, 123456);
            stat.bindString(2, "test");
            stat.executeInsert();
        }
        sqLiteDatabase.setTransactionSuccessful();
    }
    catch (SQLException e)
    {
        e.printStackTrace();
    }
    finally
    {
        // 结束
        sqLiteDatabase.endTransaction();
        sqLiteDatabase.close();
    }

线程问题

我们常常在多线程中只使用一个SQLiteDatabase引用,在用SQLiteDataBase.close()的时需要注意调是否还有别的线程在使用这个实例。如果一个线程操作完成后就直接close了,别一个正在使用这个数据库的线程就会异常。所以有些人会直接把SQLiteDatabase的实例放在Application中,让它们的生命周期一致。也有的做法是写一个计数器,当计数器为0时才真正关闭数据库。

为难原由:Activity的启动模式(launchMode)有哪些,有什么区别?
很多人在使用startActivityForResult启动一个Activity时,会发现还没有开始界面跳转本身的onActivityResult马上就被执行了,这是为什么呢?

如下面表格,左边第1列代表MainActivity的启动模式,第一行代表SecondActivity(即要startActivityForResult启动的Activity)的启动模式,打叉代表在这种组合下onActivityResult会被马上调用。

standsingleTopsingleTasksingleInstance
standxx
singleTopxx
singleTaskxx
singleInstancexxxx

Android在5.0及以后的版本修改了这个限制。也就是说上面x的地方全部变成了√。但是如在Intent中设置了FLAG_ACTIVITY_NEW_TASK再startActivityForResult,即使是标准的启动模式仍然会有这个问题。

最终我们发现只要是不和原来的Activity在同一个Task就会产生这种立即执行onActivityResult的情况

当前应用有两个Activity A和B,B的android:launchMode设置了singleTask模式,A是默认的standard,那么A startActivity启动B,B会新启一个Task吗?如果不会,那么startActivity的Intent加上FLAG_ACTIVITY_NEW_TASK这个参数会不会呢?

设置了”singleTask”启动模式的Activity,它在启动的时候,会先在系统中查找属性值affinity等于它的属性值taskAffinity的任务存在;如果存在这样的任务,它就会在这个任务中启动,否则就会在新任务中启动。

当Intent对象包含FLAG_ACTIVITY_NEW_TASK标记时,系统在查代时仍然按Activity的taskAffinity属性进行匹配,如果找到一个Task的taskAffinity与之相同,就将目标Activity压入此Task栈中,如果找不到则创建一个新的Task。

注意:设置了”singleTask”启动模式的Activity在已有的任务中已经存在相应的Activity实例,再启动它时会把这个Activity实例上面的Activity全部结束掉。

  1. 设置为singleTask的启动模式,当Activity的实例已经存在时,再启动它,那么它的回调函数是否会被执行?我们可以在哪个回调中处理新的Intent协带的参数?(通过startActivity(Intent)启动)
  2. 或者问设置为singleTop的启动模式,当Activity的实例已经存在于Task的栈顶,我们可以在哪个回调中处理新的Intent协带的参数?(在当前Activity中从通知栏点击再跳转到此Activity就是这种在栈顶的情况)

当您请求要为其提供备用资源的资源时,Android 会根据当前的设备配置选择要在运行时使用的备用资源。

为演示 Android 如何选择备用资源,假设以下可绘制对象目录分别包含相同图像的不同版本:

drawable/

drawable-en/

drawable-fr-rCA/

drawable-en-port/

drawable-en-notouch-12key/

drawable-port-ldpi/

drawable-port-notouch-12key/

同时,假设设备配置如下:

语言区域 = en-GB
屏幕方向 = port
屏幕像素密度 = hdpi
触摸屏类型 = notouch
主要文本输入法 = 12key

通过将设备配置与可用的备用资源进行比较,Android 从 drawable-en-port 中选择可绘制对象。

系统使用以下逻辑决定要使用的资源:

淘汰与设备配置冲突的资源文件。 drawable-fr-rCA/ 目录与 en-GB 语言区域冲突,因而被淘汰。

drawable/

drawable-en/

drawable-fr-rCA/

drawable-en-port/

drawable-en-notouch-12key/

drawable-port-ldpi/

drawable-port-notouch-12key/
例外:屏幕像素密度是唯一一个未因冲突而被淘汰的限定符。 尽管设备的屏幕密度为 hdpi,但是 drawable-port-ldpi/
未被淘汰,因为此时每个屏幕密度均视为匹配。如需了解详细信息,请参阅支持多种屏幕文档。

选择列表(表 2)中(下一个)优先级最高的限定符。(先从 MCC 开始,然后下移。) 是否有资源目录包括此限定符?

若无,请返回到第 2 步,看看下一个限定符。(在该示例中,除非达到语言限定符,否则答案始终为“否”。)

若有,请继续执行第 4 步。 淘汰不含此限定符的资源目录。在该示例中,系统会淘汰所有不含语言限定符的目录。

drawable/
drawable-en/
drawable-en-port/
drawable-en-notouch-12key/
drawable-port-ldpi/
drawable-port-notouch-12key/

例外:如果涉及的限定符是屏幕像素密度,则 Android 会选择最接近设备屏幕密度的选项。通常,Android 倾向于缩小大型原始图像,而不是放大小型原始图像。

返回并重复第 2 步、第 3 步和第 4步,直到只剩下一个目录为止。在此示例中,屏幕方向是下一个判断是否匹配的限定符。因此,未指定屏幕方向的资源被淘汰:

drawable-en/
drawable-en-port/
drawable-en-notouch-12key/
剩下的目录是 drawable-en-port。

尽管对所请求的每个资源均执行此程序,但是系统仍会对某些方面做进一步优化。 例如,系统一旦知道设备配置,即会淘汰可能永远无法匹配的备用资源。
比如说,如果配置语言是英语(“en”),则系统绝不会将语言限定符设置为非英语的任何资源目录包含在选中的资源池中(不过,仍会将不带语言限定符的资源目录包含在该池中)。

根据屏幕尺寸限定符选择资源时,如果没有更好的匹配资源,则系统将使用专为小于当前屏幕的屏幕而设计的资源(例如,如有必要,大尺寸屏幕将使用标准尺寸的屏幕资源)。
但是,如果唯一可用的资源大于当前屏幕,则系统不会使用这些资源,并且如果没有其他资源与设备配置匹配,应用将会崩溃(例如,如果所有布局资源均用
xlarge 限定符标记,但设备是标准尺寸的屏幕)。

注:限定符的优先顺序(表 2 中)比与设备完全匹配的限定符数量更加重要。例如,在上面的第 4 步中,列表剩下的最后选项包括三个与设备完全匹配的限定符(方向、触摸屏类型和输入法),而 drawable-en 只有一个匹配参数(语言)。但是,语言的优先顺序高于其他两个限定符,因此 drawable-port-notouch-12key 被淘汰。
查找最佳匹配资源的流程图
传送门

密度建议尺寸
mipmap-mdpi48 * 48
mipmap-hdpi72 * 72
mipmap-xhdpi96 * 96
mipmap-xxhdpi144 * 144
mipmap-xxxhdpi196 * 96

Android drawable微技巧,你所不知道的drawable的那些细节

  • px:pixel,像素,电子屏幕上组成一幅图画或照片的最基本单元
  • pt: point,点,印刷行业常用单位,等于1/72英寸
  • ppi:pixel per inch,每英寸像素数,该值越高,则屏幕越细腻
  • dpi: dot per inch,每英寸多少点,该值越高,则图片越细腻
  • dp: dip,Density-independent pixel,是安卓开发用的长度单位,1dp表示在屏幕像素点密度为160ppi时1px长度
  • sp: scale-independent pixel,安卓开发用的字体大小单位。

px、pt、ppi、dpi、dp、sp之间的关系

其实图片最终要显示在屏幕上,都会对应一个屏幕上的点,即对应一个颜色值。不同格式的图片,只是不同压缩编码和解压算法。也就是说,我们看到的.jpg、.png图片的文件大小只有几十KB,担把它们加载到内存中时,每张图片最终都按长X宽展开,计算其占用内存大小的就变成了(ARGB_8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte,假设是ARGB_8888):
内存占用=长 X 宽 X 4bytes

这种算法其这还忽略屏幕的Density,放在不同的drawable目录中的图片显示时会根据Denisty有一定的缩放。所以有时候图片占用的内存会比我们上面公式计算出来的还要大很多。

Bitmap和Drawable

Bitmap是Android系统中的图像处理的最重要类。可以简单地说,Bitmap代表的是图片资源在内存中的数据结构,如它的像素数据,长宽等属性都存放在Bitmap对象中。
Drawable官文文档说明为可绘制物件的一般抽象。也就是Drawable是一种抽像,最终实现的方式可以是绘制Bitmap的数据或者图形、Color数据等。理解了这些,你很容易明白为什么我们有时候需要进行两者之间的转换。

由于Fragment之间是没有任何依赖关系的,因此如果要进行Fragment之间的通信,建议通过Activity作为中介,不要Fragment之间直接通信。当然,也可以选择EventBus等框架实现通信。
关于Fragment的更多知识点可查阅Android 基础:Fragment,看这篇就够了 (上)Android 基础:Fragment,看这篇就够了 (下)

常有些开发不知道为什么自己的Application.onCreate中的代码执行了两次,如果你遇到这样的情况可以检查一下AndroidManifest.xml是否给某个组件配置了android:process属性。每个进程创建后,都会启动一个主线程(Looper接收消息),每个组件启动前都会先创建Application实例(一个进程只创建一个)。

优先级

  • 前台进程
  • 可见进程
  • 服务进程
  • 后台进程
  • 空进程

一般提高进程优先级的方法

  1. 进程要运行一些组件,不要成为空进程。
  2. 远行一个Service,并设置为前台运行方式(startForeground)。
  3. AndroidManifest.xml中配置persistent属性(persistent的App会被优先照顾,进程优先级设置为PERSISTENT_PROC_ADJ=-12)

关于第2点,摘抄一段代码给大家看:

private void keepAlive() {
        try {
            Notification notification = new Notification();
            notification.flags |= Notification.FLAG_NO_CLEAR;
            notification.flags |= Notification.FLAG_ONGOING_EVENT;
            startForeground(0, notification); // 设置为前台服务避免kill,Android4.3及以上需要设置id为0时通知栏才不显示该通知;
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

在Service的onCreate方法调用keepAlive()即可,其实就是是欺骗系统把自己当成一个一直在通知栏的Notification。不过这种方式,并不保证在所有的机型上都有效。

  • Android面试一天一题(Day 26:ScrollView嵌套ListView的事件冲突)——如何解决ScrollView嵌套中一个ListView的滑动冲突?

    1. ScrollView布局中嵌套Listview显示是不正常的,确切地说是只会显示ListView的第一个项。
    2. ScrollView和ListView都是上下滑动的,嵌套在一起后ScrollView中的ListView就没法上下滑动了,事件被ScrollView响应了。

问题1:为什么会只显示ListView的第一个Item,简单的说就是ListView在计算(比较正式的说法是:测量)自己的高度时对MeasureSpec.UNSPECIFIED这个模式在测量时只会返回一个List
Item的高度(当然还有一些padding这些的值我们可以先忽略),而ScrollView的重写了measureChildWithMargins方法导致它的子View的高度被强制设置成了MeasureSpec.UNSPECIFIED模式。

UNSPECIFIED

不限定,父View不限制子View的具体的大小,所以子View可以按自己需求设置宽高(前面说的ScrollView就给子View设置了这个模式,ListView就会自己确认自己高度)。

EXACTLY

父View决定子View的确切大小,子View被限定在给定的边界里,忽略本身想要的大小。

AT_MOST

最多的,子View最大可以达到的指定大小(当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少。

知道了这些我们解决这个问题,就不算难了,我们也可以重写ListView的onMeasure让它按我们的要求测量高度。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 固定高度(实际中这个值应该是根据手机屏幕计算出来的)
        int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(480, 
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, newHeightMeasureSpec);
    }

问题2:ScrollView和ListView的事件冲突问题,从ScrollView的源码可以看到它对Touch事件(ACTION_MOVE)进行了拦截,所以滑动的事件传递不到ListView。

引用块内容
所以我们解决这个问题,需要让在ListView区域的滑动事件ScrollView不要拦截。这样在ListView区域外的还是由ScrollView去处理事件,ListView外滑动的就是ScrollView。这里用到一个系统自带的API来实现这种方案:requestDisallowInterceptTouchEvent(我觉得可以从名字直接读出它的用途,不再解释),代码也不复杂:

public class MyListView extends ListView {

    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(480, // 固定高度(实际中这个值应该是根据手机屏幕计算出来的)
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, newHeightMeasureSpec);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

Dalvik虚拟机就是针对Android应用(.dex)的一个JVM,虽然他们实现和原理有很多不同,但我们可以简单这样理解。每个应用的进程中都会有一个Dalvik虚拟机实例,它们是相互独立的,一个应用的Dalvik虚拟机Crash了并不影响其他的。APK应用在运行时,就需要Dalvik虚拟机去加载类并转化成机器码执行,这个过程就是JIT(Just-In-Time),虽然dex经过优化会生成一个odex文件,但这个过程仍然是运行时让编译器去解释字节码,相当于多了一道转换工序,对性能的影响不小。
ART(Android runtime)和Dalvik不一样的地方,就是想法去掉了中间解释字节码的过程,Dalvik是JIT的机制,与JIT相对的是AOT(Ahead-Of-Time),它发生在程序运行之前。如我们用静态语言(例如C/C++)来开发应用程序的时候,编译器直接就把它们翻译成目标机器码。这种静态语言的编译方式也是AOT的一种。

问题1:使用MAT等内存分析工具监测,在使用工具时,您应积极地对自己的应用代码进行测试并尝试强制内存泄漏。在应用中引发内存泄漏的一种方式是,先让其运行一段时间,然后再检查堆。泄漏在堆中将逐渐汇聚到分配顶部。不过,泄漏越小,您越需要运行更长时间的应用才能看到泄漏。

您还可以通过以下方式之一触发内存泄漏:

  1. 将设备从纵向旋转为横向,然后在不同的活动状态下反复操作多次。旋转设备经常会使应用泄漏 Activity、Context 或 View
    对象,因为系统会重新创建 Activity,而如果您的应用在其他地方保持对这些对象其中一个的引用,系统将无法对其进行垃圾回收。

  2. 处于不同的活动状态时,在您的应用与另一个应用之间切换(导航到主屏幕,然后返回到您的应用)。

内存分析工具参数说明

问题2:优化建议

  1. 注意单例模式和静态变量是否会持有对Context的引用;
  2. 注意监听器的注销;(在Android程序里面存在很多需要register与unregister的监听
  3. 不要在Thread或AsyncTask中的引用Activity;

标准单例模式如下

public class Singleton{
    private volatile static Singleton instance;
    private Singleton() {};
    public static Singleton getInstance() {
        if (instance==null) {
            synchronized(Singleton.class) {
                if (instance==null)
                    instance=new Singleton();
            }
        }
        return instance;
    }
}
  1. invalidate和postInvalidate方法的区别?
  2. 自定义View的绘制流程?
  3. View的Touch事件分发流程?(Day 26:ScrollView嵌套ListView的事件冲突已经详细说明了这个问题)

答案:

问题1:
invalidate()是用来刷新View的,必须是在UI线程中进行工作。比如在修改某个view的显示时,调用invalidate()才能看到重新绘制的界面。invalidate()的调用是把之前的旧的view从主UI线程队列中pop掉。

对于屏幕刷新有以下几种情况可以考虑:

  • 不使用多线程和双缓冲

这种情况最简单了,一般只是希望在View发生改变时对UI进行重绘。你只需在Activity中显式地调用View对象中的invalidate()方法即可。系统会自动调用
View的onDraw()方法。

  • 使用多线程和不使用双缓冲

这种情况需要开启新的线程,新开的线程就不好访问View对象了。强行访问的话会报:

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

这时候你需要创建一个继承了android.os.Handler的子类,并重写handleMessage(Message
msg)方法。android.os.Handler是能发送和处理消息的,你需要在Activity中发出更新UI的消息,然后再你的Handler(可以使用匿名内部类)中处理消息(因为匿名内部类可以访问父类变量,
你可以直接调用View对象中的invalidate()方法
)。也就是说:在新线程创建并发送一个Message,然后再主线程中捕获、处理该消息。

  • 使用多线程和双缓冲

Android中SurfaceView是View的子类,她同时也实现了双缓冲。你可以定义一个她的子类并实现SurfaceHolder.Callback接口。由于实现SurfaceHolder.Callback接口,新线程就不需要android.os.Handler帮忙了。SurfaceHolder中lockCanvas()方法可以锁定画布,绘制玩新的图像后调用unlockCanvasAndPost(canvas)解锁(显示),还是比较方便得。

另一个是postInvalidate(),invalidate()是在UI线程自身中使用,而postInvalidate()在非UI线程中使用。

View 类中postInvalidate()方法源码如下,可见它也是用到了handler的:

public void postInvalidate() {
        postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        if (mAttachInfo != null) {
            Message msg = Message.obtain();
            msg.what = AttachInfo.INVALIDATE_MSG;
            msg.obj = this;
            mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds);
        }
    }

问题2:自定义View的绘制流程

  1. 自定义View的属性
  2. 在View的构造方法中获得我们自定义View的步骤
  3. 重写onMeasure(不必须)
  4. 重写onDraw
  5. 重写onTouchEvent事件(不必须)

除了太基础的题目(原则)之外,面试者答错题并不算是严重的问题,严重的是面试者思路让别人难以理解,或者说面试者没有很好的表达能力让别人更容易理解他的思路。

回答思路:

  1. 问题背景(让面试官听得懂你的问题产生自什么情景,以助于了解问题Why)
  2. 问题描述(问题的详细描述,让面试官听懂问题是怎么回事?Where、When、What)
  3. 解决思路(有哪些解决方案,其中有哪些坑?how)
  4. 最终效果(最后选择了什么方案,并怎么解决的坑?how mush)

可不可以在使用第三方开源库时,就是没有问题呢?当然可以,不过你可以聊一聊你为什么要选择这个库,其实就是从侧面来说明:你对这个库的优缺点的看法,以及你是否了解它的实现原理,因为你要对自己的项目负责(开源作者可不需要)。

引用块内容
进一步学习可研究浅谈Android中的MVP与动态代理的结合

一人个在成长中是会经历各个不同的阶段的,这些阶段从他常浏览的风站也能窥视一二。所以,问对方常浏览的网站这样的面试题,我一般会这样问:“你以前常上(喜欢)什么网站,现在常上什么网站,为什么你会有这个转变?”根据面试者的回答,你可以从侧面了解他目前处于一个什么样的阶段。如果一个开发没有常浏览的网站,其实你也很容易定义他为“不爱学习,对Android的热情、兴趣不大”,意外的情况应该很少。

  • 为什么要使用Binder?
  • Binder运行机制
  • Binder的线程管理
  • Binder对应用开发者的用处

一个进程的Binder线程数默认最大是16,超过的请求会被阻塞等待空闲的Binder线程。理解这一点的话,你做进程间通信时处理并发问题就会有一个底,比如使用ContentProvider时(又一个使用Binder机制的组件),你就很清楚它的CRUD(创建、检索、更新和删除)方法只能同时有16个线程在跑。

会使用AIDL进行进程间通信;
会手写AIDL的编码,加深对Binder机制的理解。

需求分析

  • 可以浏览用户已经上传到服务器的图片;
  • 可以查看某张图片的详情;
  • 选择和编辑要上传的图片;(从本地或者摄像头获取图片)
  • 工作线程上传一张图片到服务器;

架构设计

  • 框架的设计:UI和逻辑的分层设计,错误处理机制 (这点其实很重要)
  • 性能要求:列表要滑动顺畅,不会出现OOM
  • 应对需求变代的可扩展性:是否需要加入用户登录和注册?每张图片是否可以点赞或者添加评论?是否要加上保存和收藏图片功能?编辑图片时添加滤镜等图片处理效果;搜索、定位、个人偏好的算法等等。

相关的模块和涉及的技术点:

  • Retrofit做网络请求和解析(Gson);
  • Http文件上传的协义;
  • 图片浏览的性能优化或者遇到的问题:如图片库的加载和ListView的展示配合,RecyclerView瀑布流展示页面跳动等问题;
  • 框架的设计:UI和逻辑的分离问题;
  • 无网或网络异常时的处理;

吹牛回答方向

  • 和对方交流清楚,确定理解面试官关注和考察的重点;
  • 尽可能在谈设计思路时考虑到一些实际问题(边界、异常等),这方面的经历其实正是面试官想看到的;
  • 从用户的角度去展开和设计这个应用;

讨论结束

也许你不相信,接下来最重要的不是你的技术水平,而是我们称为编码之外的能力。而这些能力往往又有些主观,难于用一两道诸如算法题来测试你的能力值。而考察你的输出能力就是一个很好的验证你编码外能力的办法。
当你学会怎么理解别人和怎么让别人更好的理解你,这个编码之外的能力给你带来的效果可能会远远好于你的技术水平。

如果我们直接这样组合就认为是一个应用框架的话,那我认为你还没有真正认识框架,或者没有遇到稍大一点复杂一点的项目,所以你毫不费力就有了自己“高大上”的框架。

但是在你整合这些库时,你更应该学习一下他们是怎么能无缝地对接上的,这一点也是我认为可以问面试者的一个重要的点。如Retrofit的解耦方式:

  • 通过注解来配置请求参数;
  • 通过工厂来生成CallAdapter,Converter。

Kotlin的一个主要优点是它的简洁。你用很少的代码就可以实现原来Java写的功能,而你写的代码越少,你犯错误的概率就越小。光这个原因我就比较推荐大家尝试一下Kotlin来开发应用。

常用招式

  • merge标签
  • ViewStub标签
  • include标签
  • 用RelativeLayout代替LinearLayout
  • 使用ConstraintLayout减少布局嵌套

新招式
FlexBoxLayout

面向对象设计的SOLID原则:

  • S 单一功能原则:对象应该仅具有一种单一功能。
  • O 开闭原则:软件体应该是对于扩展开放的,但是对于修改封闭的。
  • L 里氏替换原则:程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的。
  • I 接口隔离原则:多个特定客户端接口要好于一个宽泛用途的接口。
  • D 依赖反转原则:依赖于抽象而不是一个实例,依赖注入是该原则的一种实现方式。

标准 参考答案
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。观察者模式完美的将观察者和被观察的对象分离开,一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新。
回调函数其实也算是一种观察者模式的实现方式,回调函数实现的观察者和被观察者往往是一对一的依赖关系。
所以最明显的区别是观察者模式是一种设计思路,而回调函数式一种具体的实现方式;另一明显区别是一对多还是多对多的依赖关系方面。

Java内存模型

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。程序中的变量存储在主内存中,每个线程拥有自己的工作内存并存放变量的拷贝,线程读写自己的工作内存,通过主内存进行变量的交互。JMM就是规定了工作内存和主内存之间变量访问的细节,通过保障原子性、有序性、可见性来实现线程的有效协同和数据的安全。

JVM如何判断一个对象实例是否应该被回收?

垃圾回收器会建立有向图的方式进行内存管理,通过GC Roots来往下遍历,当发现有对象处于不可达状态的时候,就会对其标记为不可达,以便于后续的GC回收。

说说JVM的垃圾回收策略。
JVM采用分代垃圾回收。在JVM的内存空间中把堆空间分为年老代和年轻代。将大量创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象。

一个content provider可以接受来自另外一个进程的数据请求。尽管ContentResolver与ContentProvider类隐藏了实现细节,但是ContentProvider所提供的query(),insert(),delete(),update()都是在ContentProvider进程的线程池中被调用执行的,而不是进程的主线程中。这个线程池是有Binder创建和维护的,其实使用的就是每个应用进程中的Binder线程池。


后记

整理这篇面试题花了我不少精力,把题刷一遍以后再将面试题抄一遍的做法看上去显得有一些笨拙。而为了看上去不是这么笨拙,我对作者提到的问题专门去思考了自己平时看见的问题以及查阅相关的资料,并将这些资料放在了相对应的面试题下面。整理本文的初衷是为了应付面试官,但是越看越觉得自己欠缺的越多,因此不管是作者分享的面试经验还是作者分享的技术细节对自己都是如此弥足珍贵!这个过程就好像当初我们上学一样,一开始为了应付老师和家长的要求,但是随着自己逐渐长大才发现这个过程自己的收获远远大于应付老师和家长的要求。
最后,努力将面试题中提到的短板补足,也希望各位看到这里的看官都能找到一份满意的工作或者在文章中找到了自己想要的东西!


  • 7
    点赞
  • 0
    评论
  • 43
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值