Android面試寶典,附答案

android中的四大組件以及應用場景
  1. Activity:在Android應用中負責與用戶交互的組件。

  2. Service:常用於爲其他組件提供後臺服務或者監控其他組件的運行狀態。經常用來執行一些耗時操作。

  3. BroadcastReceiver:用於監聽應用程序中的其他組件。

  4. ContentProvider:Android應用程序之間實現實時數據交換



1、Activity的生命週期


生命週期:對象什麼時候生,什麼時候死,怎麼寫代碼,代碼往那裏寫。

注意:

  1. 當打開新的Activity,採用透明主題的時候,當前Activity不會回調onStop

  2. onCreate和onDestroy配對,onStart和onStop配對(是否可見),onResume和onPause配對(是否在前臺,可以與用戶交互)

  3. 打開新的Activity的時候,相關的Log爲:

    Main1Activity: onPause

    Main2Activity: onCreate

    Main2Activity: onStart

    Main2Activity: onResume

    MainA1ctivity: onStop

異常狀態下的生命週期:

資源相關的系統配置發生改變或者資源不足:例如屏幕旋轉,當前Activity會銷燬,並且在onStop之前回調onSaveInstanceState保存數據,在重新創建Activity的時候在onStart之後回調onRestoreInstanceState。其中Bundle數據會傳到onCreate(不一定有數據)和onRestoreInstanceState(一定有數據)。

   防止屏幕旋轉的時候重建,在清單文件中添加配置:

 android:configChanges="orientation"



2、Fragment的生命週期


正常啓動

   Activity: onCreate

   Fragment: onAttach

   Fragment: onCreate

   Fragment: onCreateView

   Fragment: onActivityCreated

   Activity: onStart

   Activity: onResume

正常退出

 Activity: onPause

   Activity: onStop

   Fragment: onDestroyView

   Fragment: onDestroy

   Fragment: onDetach

   Activity: onDestroy



3、Activity的啓動模式


  1. standard:每次激活Activity時(startActivity),都創建Activity實例,並放入任務棧;

  2. singleTop:如果某個Activity自己激活自己,即任務棧棧頂就是該Activity,則不需要創建,其餘情況都要創建Activity實例;

  3. singleTask:如果要激活的那個Activity在任務棧中存在該實例,則不需要創建,只需要把此Activity放入棧頂,即把該Activity以上的Activity實例都pop,並調用其onNewIntent;

  4. singleInstance:應用1的任務棧中創建了MainActivity實例,如果應用2也要激活MainActivity,則不需要創建,兩應用共享該Activity實例。


4、Activity與Fragment之間的傳值


  1. 通過findFragmentByTag或者getActivity獲得對方的引用(強轉)之後,再相互調用對方的public方法,但是這樣做一是引入了“強轉”的醜陋代碼,另外兩個類之間各自持有對方的強引用,耦合較大,容易造成內存泄漏。

  2. 通過Bundle的方法進行傳值,例如以下代碼:

    //Activity中對fragment設置一些參數

    fragment.setArguments(bundle);

    //fragment中通過getArguments獲得Activity中的方法

    Bundle arguments = getArguments()



  3. 利用eventbus進行通信,這種方法實時性高,而且Activity與Fragment之間可以完全解耦。

    //Activity中的代碼

    EventBus.getDefault().post("消息");


    //Fragment中的代碼

    EventBus.getDefault().register(this);

    @Subscribe

    public void test(String text) {

        tv_test.setText(text);

    }



5、Service


Service分爲兩種:
  1. 本地服務,屬於同一個應用程序,通過startService來啓動或者通過bindService來綁定並且獲取代理對象。如果只是想開個服務在後臺運行的話,直接startService即可,如果需要相互之間進行傳值或者操作的話,就應該通過bindService。

  2. 遠程服務(不同應用程序之間),通過bindService來綁定並且獲取代理對象。

對應的生命週期如下:

   

context.startService() ->onCreate()- >onStartCommand()->Service running--調用context.stopService() ->onDestroy()

   

   context.bindService()->onCreate()->onBind()->Service running--調用>onUnbind() -> onDestroy()


注意

Service默認是運行在main線程的,因此Service中如果需要執行耗時操作(大文件的操作,數據庫的拷貝,網絡請求,文件下載等)的話應該在子線程中完成。

!特殊情況是:Service在清單文件中指定了在其他進程中運行。


6、Android中的消息傳遞機制


爲什麼要使用Handler?

因爲屏幕的刷新頻率是60Hz,大概16毫秒會刷新一次,所以爲了保證UI的流暢性,耗時操作需要在子線程中處理,子線程不能直接對UI進行更新操作。因此需要Handler在子線程發消息給主線程來更新UI。

這裏再深入一點,Android中的UI控件不是線程安全的,因此在多線程併發訪問UI的時候會導致UI控件處於不可預期的狀態。Google不通過鎖的機制來處理這個問題是因爲:

  1. 引入鎖會導致UI的操作變得複雜

  2. 引入鎖會導致UI的運行效率降低

因此,Google的工程師最後是通過單線程的模型來操作UI,開發者只需要通過Handler在不同線程之間切花就可以了。

概述一下Android中的消息機制?

Android中的消息機制主要是指Handler的運行機制。Handler是進行線程切換的關鍵,在主線程和子線程之間切換隻是一種比較特殊的使用情景而已。其中消息傳遞機制需要了解的東西有Message、Handler、Looper、Looper裏面的MessageQueue對象。

如上圖所示,我們可以把整個消息機制看作是一條流水線。其中:

  1. MessageQueue是傳送帶,負責Message隊列的傳送與管理

  2. Looper是流水線的發動機,不斷地把消息從消息隊列裏面取出來,交給Handler來處理

  3. Message是每一件產品

  4. Handler就是工人。但是這麼比喻不太恰當,因爲發送以及最終處理Message的都是Handler

爲什麼在子線程中創建Handler會拋異常?

Handler的工作是依賴於Looper的,而Looper(與消息隊列)又是屬於某一個線程(ThreadLocal是線程內部的數據存儲類,通過它可以在指定線程中存儲數據,其他線程則無法獲取到),其他線程不能訪問。因此Handler就是間接跟線程是綁定在一起了。因此要使用Handler必須要保證Handler所創建的線程中有Looper對象並且啓動循環。因爲子線程中默認是沒有Looper的,所以會報錯。

正確的使用方法是:

 handler = null;

   new Thread(new Runnable() {

       private Looper mLooper;

       @Override

       public void run() {

           //必須調用Looper的prepare方法爲當前線程創建一個Looper對象,然後啓動循環

           //prepare方法中實質是給ThreadLocal對象創建了一個Looper對象

           //如果當前線程已經創建過Looper對象了,那麼會報錯

           Looper.prepare();

           handler = new Handler();

           //獲取Looper對象

           mLooper = Looper.myLooper();

           //啓動消息循環

           Looper.loop();

           //在適當的時候退出Looper的消息循環,防止內存泄漏

           mLooper.quit();

       }

   }).start();

主線程中默認是創建了Looper並且啓動了消息的循環的,因此不會報錯:
應用程序的入口是ActivityThread的main方法,在這個方法裏面會創建Looper,並且執行Looper的loop方法來啓動消息的循環,使得應用程序一直運行。

子線程中可以通過Handler發送消息給主線程嗎?

可以。有時候出於業務需要,主線程可以向子線程發送消息。子線程的Handler必須按照上述方法創建,並且關聯Looper。


7、事件傳遞機制以及自定義View相關


Android的視圖樹

Android中View的機制主要是Activity的顯示,每個Activity都有一個Window(具體在手機中的實現類是PhoneWindow),Window以下有DecorView,DecorView下面有TitleVie以及ContentView,而ContentView就是我們在Activity中通過setContentView指定的。


事件傳分發機制


ViewGroup有以下三個與事件分發的方法,而View只有dispatchTouchEvent和onTouchEvent。

 @Override

   public boolean dispatchTouchEvent(MotionEvent ev) {

       return super.dispatchTouchEvent(ev);

   }

   @Override

   public boolean onInterceptTouchEvent(MotionEvent ev) {

       return super.onInterceptTouchEvent(ev);

   }

   @Override

   public boolean onTouchEvent(MotionEvent event) {

       return super.onTouchEvent(event);

   }

事件總是從上往下進行分發,即先到達Activity,再到達ViewGroup,再到達子View,如果沒有任何視圖消耗事件的話,事件會順着路徑往回傳遞。其中:

  1. dispatchTouchEvent是事件的分發方法,如果事件能夠到達該視圖的話,就首先一定會調用,一般我們不會去修改這個方法。

  2. onInterceptTouchEvent是事件分發的核心方法,表示ViewGroup是否攔截事件,如果返回true表示攔截,在這之後ViewGroup的onTouchEvent會被調用,事件就不會往下傳遞。

  3. onTouchEvent是最低級的,在事件分發中最後被調用。

  4. 子View可以通過requestDisallowInterceptTouchEvent方法去請求父元素不要攔截。

注意
  1. 事件從Activity.dispatchTouchEvent()開始傳遞,只要沒有被停止或攔截,從最上層的View(ViewGroup)開始一直往下(子View)傳遞。子View 可以通過onTouchEvent()對事件進行處理。

  2. 事件由父View(ViewGroup)傳遞給子View,ViewGroup 可以通過onInterceptTouchEvent()對事件做攔截,停止其往下傳遞。

  3. 如果事件從上往下傳遞過程中一直沒有被停止,且最底層子View 沒有消費事件,事件會反向往上傳遞,這時父View(ViewGroup)可以進行消費,如果還是沒有被消費的話,最後會到Activity 的onTouchEvent()函數。

  4. 如果View 沒有對ACTION_DOWN 進行消費,之後的其他事件不會傳遞過來。

  5. OnTouchListener 優先於onTouchEvent()對事件進行消費。

自定義View的分類
  1. 對現有的View的子類進行擴展,例如複寫onDraw方法、擴展新功能等。

  2. 自定義組合控件,把常用一些控件組合起來以方便使用。

  3. 直接繼承View實現View的完全定製,需要完成View的測量以及繪製。

  4. 自定義ViewGroup,需要複寫onLayout完成子View位置的確定等工作

View的測量-onMeasure

View的測量最終是在onMeasure方法中通過setMeasuredDimension把代表寬高兩個MeasureSpec設置給View,因此需要掌握MeasureSpec。MeasureSpec包括大小信息以及模式信息。

MeasureSpec的三種模式:
  1. EXACTLY模式:精確模式,對應於用戶指定爲match_parent或者具體大小的時候(實際上指定爲match_parent實質上是指定大小爲父容器的大小)

  2. AT_MOST模式:對應於用戶指定爲wrap_content,此時控件尺寸只要不超過父控件允許的最大尺寸即可。

  3. UNSPECIFIED模式:不指定大小的測量模式,這種模式比較少用

下面給出模板代碼

   public class MeasureUtils {

       /**

        * 用於View的測量

        *

        * @param measureSpec

        * @param defaultSize

        * @return

        */

       public static int measureView(int measureSpec, int defaultSize) {

   

           int measureSize;

   

           //獲取用戶指定的大小以及模式

           int mode = View.MeasureSpec.getMode(measureSpec);

           int size = View.MeasureSpec.getSize(measureSpec);

   

           //根據模式去返回大小

           if (mode == View.MeasureSpec.EXACTLY) {

               //精確模式(指定大小以及match_parent)直接返回指定的大小

               measureSize = size;

           } else {

               //UNSPECIFIED模式、AT_MOST模式(wrap_content)的話需要提供默認的大小

               measureSize = defaultSize;

               if (mode == View.MeasureSpec.AT_MOST) {

                   //AT_MOST(wrap_content)模式下,需要取測量值與默認值的最小值

                   measureSize = Math.min(measureSize, defaultSize);

               }

           }

           return measureSize;

       }

   }


最後,複寫onMeasure方法,把super方法去掉:

   @Override

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

       setMeasuredDimension(MeasureUtils.measureView(widthMeasureSpec, 200),

               MeasureUtils.measureView(heightMeasureSpec, 200)

       );

   }


View的繪製-onDraw

View繪製,需要掌握Android中View的座標體系:


View的座標體系是以左上角爲座標原點,向右爲X軸正方向,向下爲Y軸正方向。

View繪製,主要是通過Android的2D繪圖機制來完成,時機是onDraw方法中,其中包括畫布Canvas,畫筆Paint。下面給出示例代碼。相關API不是介紹的重點,重點是Canvas的save和restore方法,通過save以後可以對畫布進行一些放大縮小旋轉傾斜等操作,這兩個方法一般配套使用,其中save的調用次數可以多於restore。

   @Override

   protected void onDraw(Canvas canvas) {

       super.onDraw(canvas);

       Bitmap bitmap = ImageUtils.drawable2Bitmap(mDrawable);

       canvas.drawBitmap(bitmap, getLeft(), getTop(), mPaint);

       canvas.save();

       //注意,這裏的旋轉是指畫布的旋轉

       canvas.rotate(90);

       mPaint.setColor(Color.parseColor("#FF4081"));

       mPaint.setTextSize(30);

       canvas.drawText("測試", 100, -100, mPaint);

       canvas.restore();

   }

View的位置-onLayout

與佈局位置相關的是onLayout方法的複寫,一般我們自定義View的時候,只需要完成測量,繪製即可。如果是自定義ViewGroup的話,需要做的就是在onLayout中測量自身以及控制子控件的佈局位置,onLayout是自定義ViewGroup必須實現的方法。


8、性能優化


佈局優化
  1. 使用include標籤,通過layout屬性複用相同的佈局。

     <include

        android:id="@+id/v_test"

        layout="@layout/include_view" /



  2. 使用merge標籤,去除同類的視圖

  3. 使用ViewStub來進行佈局的延遲加載一些不是馬上就用到的佈局。例如列表頁中,列表在沒有拿到數據之前不加載,這樣做可以使UI變得流暢。

    <ViewStub

        android:id="@+id/v_stub"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:layout="@layout/view_stub" />

    //需要手動調用inflate方法,佈局纔會顯示出來。

    stub.inflate();

    //其中setVisibility在底層也是會調用inflate方法

    //stub.setVisibility(View.VISIBLE);

    //之後,如果要使用ViewStub標籤裏面的View,只需要按照平常來即可。

    TextView tv_1 = (TextView) findViewById(R.id.tv_1);


  4. 儘量多使用RelativeLayout,因爲這樣可以大大減少視圖的層級。


內存優化


APP設計以及代碼編寫階段都應該考慮內存優化:
  1. 珍惜Service,儘量使得Service在使用的時候才處於運行狀態。儘量使用IntentService

    IntentService在內部其實是通過線程以及Handler實現的,當有新的Intent到來的時候,會創建線程並且處理這個Intent,處理完畢以後就自動銷燬自身。因此使用IntentService能夠節省系統資源。

  2. 內存緊張的時候釋放資源(例如UI隱藏的時候釋放資源等)。複寫Activity的回調方法。


    @Override

    public void onLowMemory() {

        super.onLowMemory();

    }

    @Override

    public void onTrimMemory(int level) {

        super.onTrimMemory(level);

        switch (level) {

            case TRIM_MEMORY_COMPLETE:

                //...

                break;

            case 其他:

        }

    }


  3. 通過Manifest中對Application配置更大的內存,但是一般不推薦

    android:largeHeap="true"

  4. 避免Bitmap的浪費,應該儘量去適配屏幕設備。儘量使用成熟的圖片加載框架,Picasso,Fresco,Glide等。

  5. 使用優化的容器,SparseArray等

  6. 其他建議:儘量少用枚舉變量,儘量少用抽象,儘量少增加類,避免使用依賴注入框架,謹慎使用library,使用代碼混淆,時當場合考慮使用多進程等。

  7. 避免內存泄漏(本來應該被回收的對象沒有被回收)。一旦APP的內存短時間內快速增長或者GC非常頻繁的時候,就應該考慮是否是內存泄漏導致的。

    分析方法

    1. 使用Android Studio提供的Android Monitors中Memory工具查看內存的使用以及沒使用的情況。

2. 使用DDMS提供的Heap工具查看內存使用情況,也可以手動觸發GC。

3. 使用性能分析的依賴庫,例如Square的LeakCanary,這個庫會在內存泄漏的前後通過Notification通知你。


什麼情況會導致內存泄漏


  1. 資源釋放問題:程序代碼的問題,長期保持某些資源,如Context、Cursor、IO 流的引用,資源得不到釋放造成內存泄露。

  2. 對象內存過大問題:保存了多個耗用內存過大的對象(如Bitmap、XML 文件),造成內存超出限制。

  3. static 關鍵字的使用問題:static 是Java 中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬於該類,而不是該類的實例。所以用static 修飾的變量,它的生命週期是很長的,如果用它來引用一些資源耗費過多的實例(Context 的情況最多),這時就要謹慎對待了。

    解決方案
    1. 應該儘量避免static 成員變量引用資源耗費過多的實例,比如Context。
    2. Context 儘量使用ApplicationContext,因爲Application 的Context 的生命週期比較長,引用它不會出現內存泄露的問題。
    3. 使用WeakReference 代替強引用。比如可以使用WeakReference<Context> mContextRef

  4. 線程導致內存溢出:線程產生內存泄露的主要原因在於線程生命週期的不可控。例如Activity中的Thread在run了,但是Activity由於某種原因重新創建了,但是Thread仍然會運行,因爲run方法不結束的話Thread是不會銷燬的。

    解決方案

    1. 將線程的內部類,改爲靜態內部類(因爲非靜態內部類擁有外部類對象的強引用,而靜態類則不擁有)。

2. 在線程內部採用弱引用保存Context 引用。

查看內存泄漏的方法、工具
  1. android官方提供的工具:Memory Monitor(當APP佔用的內存在短時間內快速增長或者GC變得頻繁的時候)、DDMS提供的Heap工具(手動觸發GC)

  2. Square提供的內存泄漏檢測工具,LeakCanary(能夠自動完成內存追蹤、檢測、輸出結果),進行演示,並且適當的解說。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章