高级Android开发面试汇总

Posted 唯夜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高级Android开发面试汇总相关的知识,希望对你有一定的参考价值。

高级 android 开发面试题汇总

一、 Android 基础

1. Service 的两种启动方式

简单的来说就是 直接启动绑定启动 两种方式。

// 直接启动
Context.startService()

// 直接启动后需要手动调用停止服务才会停止
Context.stopService()
Service.selfStop()
// 绑定启动
Context.bindService()

两种启动方式的生命周期不同,下面詳細说明下生命周期。

官方生命周期图如下:

从上图可以看出,无论是哪种启动方式,都会调用 onCreate 方法,服务第一次创建时会调用该方法,以后再调用 startService也不会调用 onCreate方法,这里面适合做初始化操作。当处于激活状态时,由 startService启动 Service 需要自己停止或者外部客户端停止服务;由 bindService启动的服务,当所连接的客户端都调用 unbindService后停止。

根据不同的使用场景选择合适的启动方式,例如音乐播放器需要后台播放以及 widget 控制播放这时候就要使用 startService启动服务,在用户退出应用的时候结束服务。再比如,某个 Service 的作用范围只在某个 activity 或几个 activity中时,使用bindService方式会比较合适。

题外话:Service 保活

  1. 在 onStartCommand 中返回 START_STICKY

    这种方式并不能保证 Service 不被杀死,只是提高 Service 被杀死后快速重启概率。

  2. 提高 Service 优先级

  3. 设置为前台服务

    在service中设置常驻通知栏,这样服务就可以在后台常驻,但是通知栏会显示一个通知。

  4. 双进程守护

    为后台常驻 Service 设置守护进程,相互监听对方的状态,当监测到对方被杀死后立即重启对方 Service 达到守护 service 的目的。

上述方法中通常情况下只能在原生 Android 中实现,国内修改过的系统为了让系统有更好的续航通常都会阻止应用自启动,因此上述方法基本失效。

关于上述方法失效问题很多人会换一种方式解决这个问题,通过第三方推送sdk启动应用,例如 友盟、极光等。具体操作方法就是在自定义推送中启动我们应用的后台服务从而启动应用。具体使用中发现也只能在部分系统中生效,flyme,miui杜绝了这种全家桶式的自启动方式。

终极解决办法,通过微信服务号向用户推送关键信息并通过浏览器打开应用。微信服务号的推送不能滥用,滥用微信会停止服务号的推送功能。

其他相关文章:

Android Service两种启动方式详解(总结版)

Service的两种启动方式

2. IntentService原理分析

普通 Service 在未指定进程的情况下和主线程运行在同一进程,并且也在主线程中,因此在这样的 Service 中做过多耗时操作也会阻塞UI线程。

这种情况下如果我们不想出现跨进程的情况,我们就不能指定 Service 在独立的进程中,我们可以在 Service 中创建 Thread 来处理这些耗时的操作,Android官方给我们提供了一种便利的方式,我们只需要继承IntentService即可,IntentService 会自动我们的操作创建 Thread 并执行。具体实现如下:

public abstract class IntentService extends Service 
    private volatile Looper mServiceLooper;
    private volatile ServiceHandler mServiceHandler;
    private String mName;
    private boolean mRedelivery;

    private final class ServiceHandler extends Handler 
        public ServiceHandler(Looper looper) 
            super(looper);
        

        @Override
        public void handleMessage(Message msg) 
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        
    

    /**
     * Creates an IntentService.  Invoked by your subclass's constructor.
     *
     * @param name Used to name the worker thread, important only for debugging.
     */
    public IntentService(String name) 
        super();
        mName = name;
    

    /**
     * Sets intent redelivery preferences.  Usually called from the constructor
     * with your preferred semantics.
     *
     * <p>If enabled is true,
     * @link #onStartCommand(Intent, int, int) will return
     * @link Service#START_REDELIVER_INTENT, so if this process dies before
     * @link #onHandleIntent(Intent) returns, the process will be restarted
     * and the intent redelivered.  If multiple Intents have been sent, only
     * the most recent one is guaranteed to be redelivered.
     *
     * <p>If enabled is false (the default),
     * @link #onStartCommand(Intent, int, int) will return
     * @link Service#START_NOT_STICKY, and if the process dies, the Intent
     * dies along with it.
     */
    public void setIntentRedelivery(boolean enabled) 
        mRedelivery = enabled;
    

    @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);
    

    @Override
    public void onStart(@Nullable Intent intent, int startId) 
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    

    /**
     * You should not override this method for your IntentService. Instead,
     * override @link #onHandleIntent, which the system calls when the IntentService
     * receives a start request.
     * @see android.app.Service#onStartCommand
     */
    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) 
        onStart(intent, startId);
        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
    

    @Override
    public void onDestroy() 
        mServiceLooper.quit();
    

    /**
     * Unless you provide binding for your service, you don't need to implement this
     * method, because the default implementation returns null.
     * @see android.app.Service#onBind
     */
    @Override
    @Nullable
    public IBinder onBind(Intent intent) 
        return null;
    

    /**
     * This method is invoked on the worker thread with a request to process.
     * Only one Intent is processed at a time, but the processing happens on a
     * worker thread that runs independently from other application logic.
     * So, if this code takes a long time, it will hold up other requests to
     * the same IntentService, but it will not hold up anything else.
     * When all requests have been handled, the IntentService stops itself,
     * so you should not call @link #stopSelf.
     *
     * @param intent The value passed to @link
     *               android.content.Context#startService(Intent).
     *               This may be null if the service is being restarted after
     *               its process has gone away; see
     *               @link android.app.Service#onStartCommand
     *               for details.
     */
    @WorkerThread
    protected abstract void onHandleIntent(@Nullable Intent intent);

从上面的源码中我们可以看出 IntentService 使用了 Handler 来处理发送过来的任务,调用 startService后首先会进入 onStartCommand接着我们看到 onStart方法中将 intent 封装成 Message 丢给 handler ,handler 中的 handleMessage 方法调用 onHandleIntent,我们只需要在 onHandleIntent 完成我们要做的操作即可。 我们看到 onHandleIntent 有一个自定义的注解 @WorkerThread, 添加了该注解的方法系统会自动为我们创建一个线程然后再执行 onHandleIntent 方法。

因此,IntentService 中的消息是依次执行的,如果有很多任务并发执行,这些任务都会放在消息队列中,等待前一个任务执行完成后才能执行下一个任务。

相关文章:

Android中IntentService实现原理详解

IntentService的原理和实例分析

3. 静态广播和动态广播的区别?

静态广播需要在 androidManifest.xml 文件中申明组件,否则无法接收广播;
动态广播需要在运行的时候动态注册,有很多系统广播只能用动态注册的方式使用。

  1. 动态注册的广播永远要快于静态注册的广播,不管静态注册的优先级设置的多高,不管动态注册的优先级有多低
  2. 生存期,静态广播的生存期可以比动态广播的长很多,因为静态广播很多都是用来对系统时间进行监听,比如我们可以监听手机开机。而动态广播会随着context的终止而终止
  3. 动态广播无需在AndroidManifest.xml中声明即可直接使用,也即动态;而静态广播则需要,有时候还要在AndroidManifest.xml中加上一些权限的声明

4. android 消息机制

图就不画了,博客上找了几张,上图:

上面这张图说明了Handler中的消息循环机制:

  1. Handler 消息处理器
  2. Looper 消息泵,在消息队列中循环读取消息
  3. MesageQueue 消息队列,存放消息
  4. Message 消息体

老生常谈了,Handler 通过 sendMessage 向消息队列中发送消息,Looper 循环从 MessageQueue 中取出消息,然后要求 Handler dispatchMessage ,最后回调到 handleMessage 中。从结构上来看简单的就是这么描述。

上图中很明确的说明了,Looper是属于某个一线程的,在android中我们知道要在某个线程中创建 Handler 首先需要初始化 Looper,否则就会报错。下面我们说几条结论,稍后通过源码证明:

  1. 一个线程只能有且仅有一个Looper
  2. 在线程中创建 Handler 前需要先初始化 Looper
// ActivityThread.java

public static void main(String[] args) 
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

    // CloseGuard defaults to true and can be quite spammy.  We
    // disable it here, but selectively enable it later (via
    // StrictMode) on debug builds, but using DropBox, not logs.
    CloseGuard.setEnabled(false);

    Environment.initForCurrentUser();

    // Set the reporter for event logging in libcore
    EventLogger.setReporter(new EventLoggingReporter());

    // Make sure TrustedCertificateStore looks in the right place for CA certificates
    final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
    TrustedCertificateStore.setDefaultUserDirectory(configDir);

    Process.setArgV0("<pre-initialized>");

    Looper.prepareMainLooper();

    // Find the value for @link #PROC_START_SEQ_IDENT if provided on the command line.
    // It will be in the format "seq=114"
    long startSeq = 0;
    if (args != null) 
        for (int i = args.length - 1; i >= 0; --i) 
            if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) 
                startSeq = Long.parseLong(
                        args[i].substring(PROC_START_SEQ_IDENT.length()));
            
        
    
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) 
        sMainThreadHandler = thread.getHandler();
    

    if (false) 
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");

主线程中我们未做 Looper 初始化是因为在 main 函数中系统已经帮我们做了初始化;

通常情况下我们是这样创建 Handler 的:

new Handler();

构造函数中没有参数,我们看下最终的构造函数如下:

// Handler.java

public Handler(Callback callback, boolean async) 
    if (FIND_POTENTIAL_LEAKS) 
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) 
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        
    

    mLooper = Looper.myLooper();
    if (mLooper == null) 
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;

可以看到 mLooper 是通过 Looper.myLooper(); 获得的,继续往下看:

// Looper.java

public final class Looper 

    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    public static void prepare() 
        prepare(true);
    

    private static void prepare(boolean quitAllowed) 
        if (sThreadLocal.get() != null) 
            throw new RuntimeException("Only one Looper may be created per thread");
        
        sThreadLocal.set(new Looper(quitAllowed));
    

    public static @Nullable Looper myLooper() 
        return sThreadLocal.get();
    

    /**
     * Run the message queue in this thread. Be sure to call
     * @link #quit() to end the loop.
     */
    public static void loop() 
        final Looper me = myLooper();
        ...
    

myLooper 返回的是 ThreadLocal 中保存的线程贡献变量。

**```ThreadLocal```是用于保存线程共享变量的类,对它进行 set 时候各个线程的变量不会相互影响,```Thread.ThreadLocalMap```会给每个线程保存一个线程共享变量。 ```ThreadLocal```是切换线程的关键,```ThreadLocal```中保存的是当前线程的 ```Looper```,```Looper```中包含一个线程的消息队列 ```MessageQueue```, ```MessageQueue```中的 ```Message```中又持有 Hanmdler 的引用,当 ```Looper```取到 ```Message```后,可通过 ```Message```关联的 ```Handler```直接调用相关函数,由于是 ```Looper```调用的 ```Handler```,这时候操作 ```Handler```的线程就是 ```Looper```所在的线程了,这样就做到了线程切换。**

我们再看 prepare()函数中做了判断,如果 sThreadLocal有值则直接抛出异常,这里限制一个线程只能有一个 Looper

下面是我纯 Java 环境下实现的模拟 Handler 简单原理:

模拟Handler实现

相关文章:

Handler是如何实现线程之间的切换的

5. 布局

常用布局:

FrameLayout``````LinearLayout``````RelativeLayout

什么情况下使用 RelativeLayout

Google 官方文档有这么一段描述:

A RelativeLayout is a very powerful utility for designing a user interface because it can eliminate nested 
view groups and keep your layout hierarchy flat, which improves performance. If you find yourself using 
several nested LinearLayout groups, you may be able to replace them with a single RelativeLayout.

也就是说为了提高布局性能,当我们过多的布局嵌套的时候我们尝试使用一个 RelativeLayout 来解决多级嵌套问题,从而提高布局性能。

提高性能布局:

ConstraintLayout

ConstraintLayout也是为了解决布局嵌套问题,它比 RelativeLayout更加灵活,使用也稍微复杂一些。

相关文章:

RelativeLayout和LinearLayout性能比较

实战篇ConstraintLayout的崛起之路

6. 数据库

数据库迁移

数据库升级时版本号最好是连续的,方便使用循环升级,下面我们来看一段代码:

public class DBHelper extends SQLiteOpenHelper 

    private static final int VERSION = 3;

//    private static final String CREATE_RECORDING = "CREATE TABLE IF NOT EXISTS recording(" +
//            "ringing_time,wait_time,call_time,myphone,othrephone,localfilepath,remotefilepath," +
//            "nickname,type,incoming,username,add_time,task_type,test);"; // version 1
//    private static final String CREATE_RECORDING = "CREATE TABLE IF NOT EXISTS recording(" +
//            "ringing_time,wait_time,call_time,myphone,othrephone,localfilepath,remotefilepath," +
//            "nickname,type,incoming,username,add_time,task_type,test,task_id);"; // version 2
    private static final String CREATE_RECORDING = "CREATE TABLE IF NOT EXISTS recording(" +
            "ringing_time,wait_time,call_time,myphone,othrephone,localfilepath,remotefilepath," +
            "nickname,type,incoming,username,add_time,task_type,test," +
            "task_id,close_type,is_connected,is_accepted);"; // version 3

    public DBHelper(Context context) 
        super(context, "mllrecordingwizard.db", null, VERSION);
    

    @Override
    public void onCreate(SQLiteDatabase db) 
        db.execSQL(CREATE_RECORDING);
    

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) 
        String sql;
        for (int i = oldVersion; i < newVersion; i++) 
            switch (i) 
                case 1:
                    sql = "ALTER TABLE recording ADD COLUMN task_id;";
                    db.execSQL(sql);
                    break;
                case 2:
                    sql = "ALTER TABLE recording ADD COLUMN close_type;";
                    db.execSQL(sql);
                    sql = "ALTER TABLE recording ADD COLUMN is_connected;";
                    db.execSQL(sql);
                    sql = "ALTER TABLE recording ADD COLUMN is_accepted;";
                    db.execSQL(sql);
                    break;
            
        
    

    public SQLiteDatabase getSQLiteDatabase() 
        return this.getWritableDatabase();
    


上例中数据库初始版本号是1,升级到最新版本号是3。为了能够有迹可循,最好保存每个版本数据库表的设计,如上面的注释。

  • 数据库不存在时,只会调用 onCreate方法我们在 onCreate中创建表即可;
  • 数据库已存在时,不会调用 onCreate只会调用 onUpgrade方法,我们需要在 onUpgrade方法中执行升级操作即可。

7. ANR出现的情况有几种? 怎么分析解决ANR问题?

ANR(Application Not responding)。Android中,主线程(UI线程)如果在规定时内没有处理完相应工作,就会出现ANR。具体来说,ANR会在以下几种情况中出现:

  1. 输入事件(按键和触摸事件)5s内没被处理
  2. BroadcastReceiver的事件(onRecieve方法)在规定时间内没处理完(前台广播为10s,后台广播为60s)
  3. service 前台20s后台200s未完成启动
  4. ContentProvider的publish在10s内没进行完

分析ANR问题,需要结合Log以及trace文件。具体分析流程,可参照以下两篇文章:

https://www.jianshu.com/p/fa962a5fd939

https://blog.csdn.net/droyon/article/details/51099826

8. 列表控件对比

ListView 与 RecyclerView 对比:

  1. ListView使用适配器时需要手动复用 view ,为非必要操作,数据多了以后很容易早晨内存溢出情况; RecyclerView通过代码的形式约束,控件自身实现 view 的复用,避免了问题的出现。
  2. RecyclerView支持布局管理器,布局方式灵活。
  3. RecyclerView两级缓存
  4. RecyclerViewitem 动画灵活
  5. RecyclerViewadapter 更新更加灵活,可单独更新某个item,不用更新整个列表。

RecyclerView 缓存分析

讲解 RecyclerView如何进行缓存的文章很多了,分析源码也很到位,图文并茂。这里不再重复造轮子,这里说一下为什么使用多级缓存:

设置多级缓存意义:

  • 一级缓存 scrap 容量最小,只保存最先出屏幕的 ViewHolder ,需要显示是也是首先从一级缓存中取出 ViewHodler

  • 二级缓存 cacheView 二级缓存缓存一级缓存放不下的 ViewHolder

  • 三级缓存 RecyclerViewPool 三级缓存,对象池缓存,多个 RecyclerView 共享缓存。

    **各级缓存容量依次增大,一级缓存使用最频繁不宜很大,一级缓存过大遍历时间长影响效率;二级缓存是一级缓存的补充,为了弥补一级缓存的容量不足;三级缓存是共享缓存,提高应用整体的复用性以及缓存的容量。缓存容量越小命中率越高,但是过于小的缓存并不能起到缓存的作用,所以三级缓存的容量依次增大。**

相关文章:

一级缓存、二级缓存和三级缓存有什么区别

RecyclerView 源码分析(三) - RecyclerView的缓存机制

RecyclerView缓存原理

9. Activity启动模式

  • Standard

    在同一个任务栈中可以有多个实例,每次调用 startActivity 都会新建一个实例添加到栈中

  • SingleTop

    栈顶复用,若不在栈顶创建一个新的 Activity

  • SingleTask

    栈内复用,若栈中已经存在这个 Activity ,那么将其上方的其他 Activity 弹出栈并将其 放到栈顶。相当于栈内单例模式。

  • SingleInstance

    进程单例模型,系统中唯一单例存在,单独一个任务栈。

10. Touch 事件分发

相关文章:

Android Touch事件传递机制全面解析(从WMS到View树)

剖析Activity、Window、ViewRootImpl和View之间的关系

11. TCP/TP

计算机网络-运输层

二、 开源框架

1. 网络框架

OkHttp

相关文章:

OkHttp源码解析

源码地址: https://github.com/square/okhttp

Retrofit

Retrofit 的核心就是 Java 的动态代理和动态注解,只要理解了这两个基本原来就是掌握了。

// Retrofit.java
/**
* Create an implementation of the API endpoints defined by the @code service interface.
* <p>
* The relative path for a given method is obtained from an annotation on the method describing
* the request type. The built-in methods are @link retrofit2.http.GET GET,
* @link retrofit2.http.PUT PUT, @link retrofit2.http.POST POST, @link retrofit2.http.PATCH
* PATCH, @link retrofit2.http.HEAD HEAD, @link retrofit2.http.DELETE DELETE and
* @link retrofit2.http.OPTIONS OPTIONS. You can use a custom HTTP method with
* @link HTTP @HTTP. For a dynamic URL, omit the path on the annotation and annotate the first
* parameter with @link Url @Url.
* <p>
* Method parameters can be used to replace parts of the URL by annotating them with
* @link retrofit2.http.Path @Path. Replacement sections are denoted by an identifier
* surrounded by curly braces (e.g., "foo"). To add items to the query string of a URL use
* @link retrofit2.http.Query @Query.
* <p>
* The body of a request is denoted by the @link retrofit2.http.Body @Body annotation. The
* object will be converted to request representation by one of the @link Converter.Factory
* instances. A @link RequestBody can also be used for a raw representation.
* <p>
* Alternative request body formats are supported by method annotations and corresponding
* parameter annotations:
* <ul>
* <li>@link retrofit2.http.FormUrlEncoded @FormUrlEncoded - Form-encoded data with key-value
* pairs specified by the @link retrofit2.http.Field @Field parameter annotation.
* <li>@link retrofit2.http.Multipart @Multipart - RFC 2388-compliant multipart data with
* parts specified by the @link retrofit2.http.Part @Part parameter annotation.
* </ul>
* <p>
* Additional static headers can be added for an endpoint using the
* @link retrofit2.http.Headers @Headers method annotation. For per-request control over a
* header annotate a parameter with @link Header @Header.
* <p>
* By default, methods return a @link Call which represents the HTTP request. The generic
* parameter of the call is the response body type and will be converted by one of the
* @link Converter.Factory instances. @link ResponseBody can also be used for a raw
* representation. @link Void can be used if you do not care about the body contents.
* <p>
* For example:
* <pre>
* public interface CategoryService 
*   &#64;POST("category/cat/")
*   Call&lt;List&lt;Item&gt;&gt; categoryList(@Path("cat") String a, @Query("page") int b);
* 
* </pre>
*/
@SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
public <T> T create(final Class<T> service) 
Utils.validateServiceInterface(service);
if (validateEagerly) 
  eagerlyValidateMethods(service);

return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]  service ,
    new InvocationHandler() 
      private final Platform platform = Platform.get();
      private final Object[] emptyArgs = new Object[0];

      @Override public @Nullable Object invoke(Object proxy, Method method,
          @Nullable Object[] args) throws Throwable 
        // If the method is a method from Object then defer to normal invocation.
        if (method以上是关于高级Android开发面试汇总的主要内容,如果未能解决你的问题,请参考以下文章

急着找Android开发岗位的快看看这篇《2023最新Android中高级面试真题汇总+解析》吧!

2019大厂Android高级面试题汇总

备战金三银四2022最新Android中高级大厂面试题汇总,高薪必备(文末巨量资料免费分享)

2022最新 Android 中高级面试题汇总(含答案解析)

2022最新 Android 中高级面试题汇总(含答案解析)

Android 大厂高级面试必问36题以及算法合集(附:2022年Android 中高级面试题汇总以及面试题解析)