Android基礎-面試寶典

Android各個佈局異同

LinearLayout按照垂直和水平順序依次排列子元素,儘量少用layout_weight(子元素佔用剩餘空間的大小比例),會導致二次測量;FrameLayout所有子元素都疊放在左上角;RelativeLayout按照各個子元素之間相對位置完成佈局;TableLayout爲表格佈局,適用於N行N列的佈局格式;AbsoluteLayout是絕對位置佈局,在此佈局中的子元素的android:layout_x和android:layout_y屬性將生效,用於描述該子元素的座標位置

RelativeLayout和ConstrainLayout比較:ConstrainLayout功能更強大,通過各種約束,可以實現更加扁平化的頁面佈局

android四大組件、生命週期

Service生命週期:當我們既想綁定一個Service(實現Activity和Service交互)又想在 Activity停止時,Service不會停止,我們可以先StartService,然後再BindService(),這樣當Activity退出的時候,Service的onUnbind()方法就會被調用,但Sercvice並不會停止,然後我們可以再進入Activity重新綁定該Service,這個時候Service就會調用onRebind()方法。但是onRebind()方法被調用還有個前提是先前的onUnbind()方法返回值爲true,但是如果使用默認的 super.onUnbind(intent)是不行的,這時候我們要手動的使其返回true,再次綁定時onRebind()就會執行了

Activity生命週期(一定要提到activity棧),(onStart可見性、onResume可用性、onStop->onRestart->onStart),可見性和可用性在很多地方都有體現,dialog遮擋Activity時,Activity切換時

Activity異常生命週期:onSaveInstanceState在activity有可能被系統回收時調用,總結一下,調用onSaveInstanceState情況如下:當用戶按下HOME鍵、關閉屏幕、切換到新Activity、橫豎屏切換,前四種情況下生命週期如下:onPause->onSaveInstanceState->onStop->onRestart->onStart->onResume,第五種情況下生命週期如下:onPause->onSaveInstanceState->onStop->onDestory->onCreate->onStart->onRestoreInstanceState->onResume,另外,我們在onCreate中讀取bundle時需要判空,在onRestoreInstanceState中不需要判空

Activity之間通訊:Intent(Bundle)、類靜態變量,如果activity之間需要傳遞的數據量過大(1M - 8K),則應該使用靜態變量

Activity向Fragment傳遞數據:通過setArguments傳遞數據。在橫豎屏切換時,系統會調用默認構造方法來創建Fragment,但會恢復Arguments

public class FramentTest2Activity extends ActionBarActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout. activity_main);
    if (savedInstanceState == null) {
      getSupportFragmentManager().beginTransaction().
                  add(R.id. container, TestFragment.newInstance("param")).commit();
    }
  }

  public static class TestFragment extends Fragment {
    private static final String ARG = "arg";
    public TestFragment() {
      Log. i("INFO", "TestFragment non-parameter constructor" );
    }
    public static Fragment newInstance(String arg){
      TestFragment fragment = new TestFragment();
      Bundle bundle = new Bundle();
      bundle.putString( ARG, arg);
      fragment.setArguments(bundle);
      return fragment;
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
      View rootView = inflater.inflate(R.layout. fragment_main, container, false);
      TextView tv = (TextView) rootView.findViewById(R.id. tv);
      tv.setText(getArguments().getString( ARG));
      return rootView;
    }
  }
}

Fragment向Activity傳遞數據:Activity實現回調接口,在onAttach中保存Activity實例、onDetatch中置空

Activity與Service通訊:Service綁定、Intent(Bundle)

Handler:在Android異步消息機制中,最常用的就是Handler,它有四大功能類,分別是Handler、Looper、MessageQueue、Message,其中,Message用於承載線程之間傳遞的消息;MessageQueue是一個用鏈表實現的先進先出隊列,用來存儲Message;Handler用於發送Message到MessageQueue,並實現消息處理接口(handleMessage),其中,加入隊列的過程最終會調用到sendMessageAtTime,插入過程從隊列頭開始,判斷時間大小進行插入;Looper主要是爲每個線程開啓單獨的消息循環,原則就是隻有創建Looper的線程才能處理消息(通過ThreadLocal(線程副本)實現,prepare、myLooper、loop),其中Looper內部採用循環機制,每循環一次,從其內部的MessageQueue中取出一個消息,然後通過Message的target屬性獲取到Handler實例,進而調用到dispatchMessage函數,在該函數中,如果Message存在callback(手動設置,或者通過post(runnable)自動設置),會執行handleCallback方法,如果Handler的mCallback變量不爲空,則執行mCallback.handleMessage方法,否則執行Handler的handleMessage方法(實踐中我們經常重寫這個方法),Looper處理完成一個消息後則繼續循環。若消息隊列爲空,線程則會阻塞等待

ThreadLocal:每個線程中保存着ThreadLocalMap對象,調用ThreadLocal的get和set方法時,先通過當前Thread拿到ThreadLocalMap對象,Map的key爲ThreadLocal,value爲要保存的值,現在想爲什麼Map不用Thread作爲key值呢,因爲這樣就無法在一個線程中保存多個值了,關鍵點是get和set會拿到當前線程的ThreadLocalMap的概念,以及TheadLocal是key值

handler.sendEmptyMessage(1);
handler.sendEmptyMessageDelayed(2, 2);
handler.sendEmptyMessage(3);
handler.sendEmptyMessage(6);
handler.sendEmptyMessageDelayed(4, 12);
handler.sendEmptyMessageDelayed(5, 1);

//執行順序 1 3 6 5 2 4

Looper主要有三個方法,Looper#prepare、Looper#getLooper、Looper#loop,Looper#prepare會創建當前線程對應的Looper實例(ThreadLocal#put),Looper#getLooper會返回當前線程對應的Looper實例,Looper#loop會通過Looper#getLooper獲取當前線程對應的Looper,並執行消息隊列循環(msg.target.dispatchMessage)

public final class Looper {
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    final MessageQueue mQueue;

    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();
    }

    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            msg.target.dispatchMessage(msg);
        }
    }

    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }
}


public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
}


Handler內存泄漏:Runnable延時消息、消息過多,原因:Activity <— Handler <— Message <— MessageQueue <— Looper <— Thread(ThreadLocalMap),消息被放到主線程的Looper中,主線程的Looper是一直存在的,解決辦法(靜態內部類+弱引用、removeCallbacks、removemessages)

Android異步處理:線程、線程池(線程重用、可以對線程進行簡單管理)、HandlerThread(實現了Handler功能的Thread)、AsyncTask(異步執行的任務,擅長UI交互)、IntentService(異步執行的任務,由於是Service,優先級較高,適用於操作比較固定,順序執行的任務)

Service和Thread的區別:Service是系統組件,具有生命週期,生命週期函數運行在主線程;Thread是程序運行的最小單元,不由系統管理

//HandlerThread主要是在線程中創建Looper
public class HandlerThread extends Thread {
    Looper mLooper;
    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    @Override
    public void run() {
        Looper.prepare();
        Synchronized(this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Looper.loop();
    }
    public Looper getLooper() {
        Synchronized(this) {
            while(mLooper == null) {
                try {
                    wait();
                } catch (Exception e) {
                }
            }
        }
        return mLooper;
    }
}

IntentService在onStart中把Intent參數post到Handler中處理,Handler通過HandlerThread實現(創建HandlerThread實例,拿到Looper對象,繼而創建的Handler),Handler相當於維護一個隊列,在Service沒銷燬的情況下,每次startService都會向Handler中post一個消息,在消息處理結束後調用stopSelf(int)來關閉當前Service,如果當前Handler中還有別的消息要處理,自然無法關閉service,因爲每次調用startService時Service都會傳入一個startId值,從1開始,如果IntentService服務沒有被關閉,那這個值會順序增加,只有傳入的startId和當前Service的id相等的時候才能成功關閉Service,也就是說當前處理的Intent是最後一個

public abstract class IntentService extends Service {
    private volatile Looper mServiceLooper;
    private volatile ServiceHandler mServiceHandler;

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

    public IntentService(String name) {
        super();
        mName = name;
    }

    @Override
    public void onCreate() {
        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);
    }

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

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    protected abstract void onHandleIntent(@Nullable Intent intent);
}

AsyncTask封裝了Handler和線程池(避免不必要線程創建和銷燬的開銷)、回調方法有onPreExecute、doInBackground(內部調用publishProgress)、onProgressUpdate 、onPostExecute

AsyncTask要點:實例必須在主線程中創建、execute方法必須在主線程中調用、實例只能執行一次execute方法

AsyncTask原理:AsyncTask實例只能執行一次execute方法,在execute方法中會判斷AsyncTask實例的狀態,如果不是Pending狀態,則拋出異常,然後調用onPreExecute方法,最後AsyncTask通過SerialExecutor來執行任務,SerialExecutor實現了executor接口,SerialExecutor內部通過隊列保存FutureTask(Runnable任務,在構造方法中初始化,其中,FutureTask封裝了WorkerRunnable,WorkerRunnable實現了Callable接口),並順序傳入線程池,因此AsyncTask模式是串型執行任務的。在FutureTask中的Callable任務執行結束後,會調用done回調方法,AsyncTask在創建FutureTask時實現了done方法,在該方法中會給Handler發送MESSAGE_POST_RESULT消息,從而執行onPostExecute。另外,FutureTask實現了操縱Runnable所在線程的api,包括通過get方法獲取執行結果,cancel方法取消執行線程,如果調用cancel,AsyncTask則不調用onPostExecute回調方法,而是調用onCancelled回調方法

public abstract class AsyncTask<Params, Progress, Result> {

    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();

    public AsyncTask() {
        this((Looper) null);
    }

    public AsyncTask(@Nullable Handler handler) {
        this(handler != null ? handler.getLooper() : null);
    }

    public AsyncTask(@Nullable Looper callbackLooper) {
        mHandler = callbackLooper == null || 
            callbackLooper == Looper.getMainLooper() ? getMainHandler() : new Handler(callbackLooper);
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                Result result = doInBackground(mParams);
                return result;
            }
        };
        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                postResultIfNotInvoked(get());
            }
        };
    }

    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:the task has already been
                                    executed a task can be executed only once");
            }
        }
        mStatus = Status.RUNNING;
        onPreExecute();
        mWorker.mParams = params;
        exec.execute(mFuture);
        return this;
    }

    private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;
        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }
        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }
}


public class FutureTask<V> implements RunnableFuture<V> {
    public void run() {
        if (state != NEW) return;
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            result = c.call();
            set(result);
            finishCompletion();
        }
    }
    public V get(long timeout, TimeUnit unit) throws TimeoutException {
        int s = state;
        if (s <= COMPLETING && (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
            throw new TimeoutException();
        return report(s);
    }
    public boolean cancel(boolean mayInterruptIfRunning) {
        try {
            Thread t = runner;
            if (t != null) {
                t.interrupt();
            }
        } finally {
            finishCompletion();
        }
        return true;
    }

    private void finishCompletion() {
        done();
    }

    @WorkerThread
    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }
}

//new UpdateTextTask(MainActivity.this, "1").execute();
//new UpdateTextTask(MainActivity.this, "2").execute();
//new UpdateTextTask2(MainActivity.this, "3").execute();
//驗證結果,三個都是順序執行的

如果執行AsyncTask的execute方法,默認是採用DefaultExecutor(同步線程池),各個任務順序執行,我們也可以調用executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR),該執行器爲AsyncTask提供的異步線程池,其中,核心線程數等於cpu數量+1,最小值是2個,最大值是4個,最大線程數量等於cpu數量*2+1,另外我們也可以自己實現Executor

Android主線程主要用來繪製界面、響應事件

異步消息:Handler、AsyncTask

Android中主要的工作線程:AsyncTask、IntentService、HandlerThread

五種不同類型的進程:前臺進程(與用戶正在交互的Activity或者Activity用到的Service)、可見進程(處於暫停狀態的Activity或者綁定在其上的Service)、服務進程(其中運行着使用startService方法啓動的Service,例如音樂播放程序)、後臺進程(運行着執行onStop方法而停止的程序)、空進程(不包含任何組件的進程)

靜態廣播和動態廣播存活時間不同,動態廣播需要在onResume中註冊,在onPause中反註冊,LocalBroadCastManager只在同一進程內有效,因此效率較高,BroadcastReceiver是全局廣播,需要使用系統API,效率較低,另外LocalBroadCastManager相對安全,因爲廣播數據僅在進程範圍內傳播,不用擔心隱私數據泄露的問題(傳統廣播也可以在發送廣播時設置Permission來規避,或通過Intent.setPackage設置廣播僅對某個程序有效), 另外,不用擔心別的應用僞造廣播,造成安全隱患(可以在動態註冊廣播接收器時設置Permission,靜態廣播可以設置android:exported="false"

廣播類型:普通廣播、系統廣播、有序廣播(通過設置Receiver的優先級來控制接收順序,sendOrderedBroadcast、abortBroadcast,短信通知廣播)、Local廣播

事件分發:事件是從上向下傳遞,從下往上消費,最開始是從Activity的dispatchTouchEvent開始,傳遞給ViewGroup的dispatchTouchEvent,相比於Activity和View,ViewGroup多了一個攔截功能,如果ViewGroup攔截該事件,那麼直接從當前層開始消費事件,如果當前層不攔截,則最終會傳遞給View,從而進行事件消費,如果View不消費,則依次向上傳遞。如果事件被某一層消費,那麼後續事件都不會再向該層之下傳遞。對於攔截功能,如果子類調用了disallow方法,則父類不能攔截,down事件除外,因爲每個事件序列都會進行重置

TouchDelegate在onTouch之後,在onTouchEvent之前,TouchDelegate還是沒有破壞事件傳遞機制,事件還是會直接傳遞給父類,然後在由父類繼續dispatch

父View在消費事件時會判斷是否設置了mTouchDelegate,如果設置了mTouchDelegate,會調用mTouchDelegate的onTouchEvent方法
public boolean onTouchEvent(MotionEvent event) {
    // ...
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    // ...
}

public class TouchDelegate {
    // 該方法會判斷點擊事件是否在擴展的範圍內:如果在範圍內,則修改點擊事件位置,
    // 確保事件一定在View範圍內,調用對應View的dispatch方法;如果不在範圍內,
    // 也會修改事件位置,確保點擊事件一定不在View範圍內,調用對應View的dispatch方法,
    // 最終返回disPatch結果。題外話,只要當前層消費了DOWN事件,其餘事件是否消費都會傳遞給該層
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();
        boolean sendToDelegate = false;
        boolean hit = true;
        boolean handled = false;

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Rect bounds = mBounds;

            if (bounds.contains(x, y)) {
                mDelegateTargeted = true;
                sendToDelegate = true;
            }
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_MOVE:
            sendToDelegate = mDelegateTargeted;
            if (sendToDelegate) {
                Rect slopBounds = mSlopBounds;
                if (!slopBounds.contains(x, y)) {
                    hit = false;
                }
            }
            break;
        case MotionEvent.ACTION_CANCEL:
            sendToDelegate = mDelegateTargeted;
            mDelegateTargeted = false;
            break;
        }
        if (sendToDelegate) {
            final View delegateView = mDelegateView;

            if (hit) {
                // Offset event coordinates to be inside the target view
                event.setLocation(delegateView.getWidth() / 2, delegateView.getHeight() / 2);
            } else {
                // Offset event coordinates to be outside the target view (in case it does
                // something like tracking pressed state)
                int slop = mSlop;
                event.setLocation(-(slop * 2), -(slop * 2));
            }
            handled = delegateView.dispatchTouchEvent(event);
        }
        return handled;
    }
}

//com.lede.train.commondemo E/offer: CustomRelativeLayout dispatchTouchEvent 0
//com.lede.train.commondemo E/offer: CustomButton dispatchTouchEvent 0
//com.lede.train.commondemo E/offer: CustomButton onTouchEvent 0
//com.lede.train.commondemo E/offer: CustomRelativeLayout dispatchTouchEvent 1
//com.lede.train.commondemo E/offer: CustomButton dispatchTouchEvent 1
//com.lede.train.commondemo E/offer: CustomButton onTouchEvent 1

//com.lede.train.commondemo E/offer: CustomRelativeLayout dispatchTouchEvent 0
//com.lede.train.commondemo E/offer: CustomRelativeLayout onTouchEvent 0
//com.lede.train.commondemo E/offer: CustomButton dispatchTouchEvent 0
//com.lede.train.commondemo E/offer: CustomButton onTouchEvent 0
//com.lede.train.commondemo E/offer: CustomRelativeLayout dispatchTouchEvent 1
//com.lede.train.commondemo E/offer: CustomRelativeLayout onTouchEvent 1
//com.lede.train.commondemo E/offer: CustomButton dispatchTouchEvent 1
//com.lede.train.commondemo E/offer: CustomButton onTouchEvent 1

Activity啓動流程:系統啓動時通過android.intent.action.MAIN和android.intent.category.HOME啓動Launcher App,Launcher App通過PackageManager遍歷已安裝應用,拿到所有入口Activity(android.intent.action.MAIN和android.intent.category.LAUNCHER),用戶點擊圖標時通過Activity#startActivity啓動應用,Activity#startActivity最終會調用Activity#startActivityForResult,對應於RequestCode小於零。在啓動Activity時,用戶進程最終會調用Instrumentation#execStartActivity方法,該方法會通過ActivityManagerNative與AMS通訊,AMS保存請求參數後,通過ApplicationThread通知當前棧頂Activity執行Instrumentation#callActivityOnPause方法,進而執行Activity#onPause方法,客戶端執行完onPause方法後,會通過ActivityManagerNative#activityPaused方法通知系統,這時繼續啓動Activity邏輯。首先判斷是否創建了應用進程,如果進程沒創建,AMS會通過Socket和Zygote通訊,讓Zygote進程fork出一個應用進程,並執行ActivityThread#Main方法,該方法會初始化消息隊列等操作,接下來應用進程會和AMS通訊,最終執行Instrumentation#newAcvitity方法來創建Activity,執行Instrumentation#callActivityOnCreate、Instrumentation#callActivityOnStart、Instrumentation#callActivityOnResume方法,最終執行Activity的onCreate、onStart、onResume方法,ActivityThread在執行完onResume方法之後,會通過willActivityBeVisible方法告知AMS,這時系統會通知棧頂Activity執行onStop方法

public final class ActivityThread {
    private class ApplicationThread extends ApplicationThreadNative {

    }
}

public abstract class ApplicationThreadNative extends Binder implements IApplicationThread {

}


public abstract class ActivityManagerNative extends Binder implements IActivityManager {

}

package name和application id:package name聲明在manifest中,application id聲明在build.gradle中,應用最終的包名是由application id決定的,package name主要用來替換類路徑

透明度0是完全透明,256是不透明

java繼承關係:BootStrapClassLoader(啓動類) --> ExtClassLoader(擴展類) --> AppClassLoader(系統類) --> CustomClassLoader

android繼承關係:BaseDexClassLoader --> DexClassLoader --> PathClassLoader

視圖狀態:Enable表示當前視圖是否可用調用setEnable()方法可以改變視圖的可用狀態,傳入true表示可用,傳入false表示不可用。它們之間最大的區別在於,不可用的視圖無法響應onTouch事件和onClick事件,無法獲取焦點,enable屬性不會影響onTouchEvent的返回值,只要View是clickable或者long clickable的,onTouchEvent就會返回true;Focus用來表示當前視圖是否獲取焦點,需要注意touch模式下的FOCUS_IN_TOUCH_MODE;還有selected和pressed狀態等

Context:Android程序不像Java程序一樣,隨便寫個main()方法就能跑了,而是要有一個完整的工程環境,也就是我們這裏討論的Context,在絕大多數場景下,Activity、Service和Application這三種類型的Context都是繼承ContextWrapper,是可以通用的不過有幾種場景比較特殊,比如啓動Activity,還有彈出Dialog出於安全原因的考慮,Android是不允許Activity或Dialog憑空出現的,一個Activity的啓動必須要建立在另一個Activity的基礎之上,也就是以此形成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert類型的Dialog),因此在這種場景下,我們只能使用Activity類型的Context,否則將會出錯。getApplication()方法是用來獲取Application實例的,但是這個方法只有在Activity和Service中才能調用的到,如果我們也想在BroadcastReceiver中獲得Application的實例,這時就可以藉助getApplicationContext()方法。getBaseContext()方法得到的是一個ContextImpl對象,ContextImpl正是上下文功能的實現類。Application、Activity這樣的類其實並不會去具體實現Context的功能,而僅僅是做了一層接口封裝而已,Context的具體功能都是由ContextImpl類去完成的,Application中在onCreate()方法裏去初始化各種全局的變量數據是一種比較推薦的做法,但是如果你想把初始化的時間點提前到極致,也可以去重寫attachBaseContext()方法

Listview與ScrollView都可以做滾動效果,但ScrollView裏面只能放一個元素,當然這個元素能裏面可以擺放很多,當這個元素的高度超過ScrollView的高度的話就可以滾動了,當然ScrollView裏面的東西是初始化好就存在了,就已經在內存中了,ListView 是隻顯示listview面積內的東西,不再面積內的是不存在的,只有滑動到這個面積內纔會加載新的東西

View.post和Handler.post的區別:View.post方法想在非UI線程有效工作,必須保證該View已經被添加至窗口

ButterKnife:利用Java註解機制來實現輔助代碼生成(Annotation Processing)的框架,經典應用(@BindView)

gradle3.0及以上 gradle3.0之前 說明 作用
implementation compile compile目前是deprecated狀態 編譯依賴不存在傳遞性,運行時可見,提高編譯速度
api compile 同上 傳遞依賴,編譯時和運行時對其他module可見
compileOnly provided provided在3.0以後不可用 編譯時可用,不打包到apk中
runtimeOnly apk apk在3.0以後不可用 編譯時不可用,運行時添加到build的output中

SharedPreferences兩種提交方式的區別:

  • commit返回boolean值驗證是否提交成功 ;apply第二種沒有返回值
  • commit同步提交硬盤,容易造成線程堵塞;apply先提交到內存,然後異步提交到磁盤

SharedPreferences數據是常駐內存的,且數據寫入是全量數據寫入磁盤,因此不建議大數據寫到SharedPreferences,且建議SharedPreferences分類存放,另外SharedPreferences每次讀取都是從內存緩存中讀取,即使加了MULTI_PROCESS標記位之後,每次都從文件中讀取,但由於多進程訪問文件也是不安全的,因此SharedPreferences不適用於跨進程通訊

Android常用的數據加密方式:MD5(數據校驗)、RSA(非對稱加密)、AES(對稱加密)

Android數字簽名:計算MD5值,通過數字證書的私鑰加密,連同數字證書一起放到應用中發佈

如何判斷是否是同一個應用:數字證書是否相同

Android中的五種存儲方式:SharedPreferences、文件、Sqlite數據庫、ContentProvider、網絡

SharedPreferences是十分常用的數據持久化方式,開發人員應該避免使用null作爲Key,即便這樣做合法

Drawable代表圖片資源(可以是GIF(可以解析成多張位圖)、PNG、JPG、BMP等),Bitmap代表位圖,Canvas代表畫布,可以看成是一種處理過程,Paint是畫筆,可以設置字體、顏色等

GestureDetector gestureDetector = new GestureDetector(getContext(),
                                     new GestureDetector.OnGestureListener() {
    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2,
                                                float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2,
                                                float velocityX, float velocityY) {
        return false;
    }
});

嵌套滑動機制

NestedScrollingChild:如果一個View想要能夠產生嵌套滑動事件,這個View必須實現NestedScrollChild接口,從Android 5.0開始,View實現了這個接口,不需要我們手動實現

NestedScrollingParent:這個接口通常用來被ViewGroup來實現,表示能夠接收從子View發送過來的嵌套滑動事件

NestedScrollingChildHelper:這個類通常在實現NestedScrollChild接口的View裏面使用,他通常用來負責將子View產生的嵌套滑動事件報告給父View。也就是說,如果一個子View想要將產生的嵌套滑動事件交給父View,這個過程不需要我們來實現,而是交給NestedScrollingChildHelper來幫助我們處理

NestedScrollingParentHelper:這個類跟NestedScrollingChildHelper差不多,也是幫助來傳遞事件的,不過這個類通常用在實現NestedScrollingParent接口的View。如果一個父View不想處理一個事件,通過NestedScrollingParentHelper類幫助我們傳遞就行了

子View事件處理:整個事件傳遞過程中,首先能保證傳統的事件能夠到達子View,當一個事件序列開始時,首先會調用startNestedScroll方法來告訴父View,馬上就要開始一個滑動事件了,請問爸爸需要處理,如果處理的話,會返回true,不處理返回fasle。跟傳統的事件傳遞一樣,如果不處理的話,那麼該事件序列的其他事件都不會傳遞到父View裏面。然後就是調用dispatchNestedPreScroll方法,這個方法調用時,子View還未真正滑動,所以這個方法的作用是子View告訴它的爸爸,此時滑動的距離已經產生,爸爸你看看能消耗多少,然後父View會根據情況消耗自己所需的距離,如果此時距離還未消耗完,剩下的距離子View來消耗,子View滑動完畢之後,會調用dispatchNestedScroll方法來告訴父View,爸爸,我已經滑動完畢,你看看你有什麼要求沒?這個過程裏面可能有子View未消耗完的距離。其次就是fling事件產生,過程跟上面也是一樣,也是先調用dispatchNestedPreFling方法來詢問父View是否有所行動,然後調用dispatchNestedFling告訴父View,子View已經fling完畢。最後就是調用stopNestedScroll表示本次事件序列結束。整個過程中,我們會發現子View開始一個動作時,會詢問父View是否有所表示,結束一個動作時,也會告訴父View,自己的動作結束了,父View是否有所指示

父View事件處理:在系統中,沒有特定ViewGroup用來接收和消耗子View傳遞的事件。因此,只能自己動手了。在整個實現過程中,我們發現,我們只對onStartNestedScroll方法和onNestedPreScroll方法做了我們自己的實現,其他的要麼空着,要麼就是通過NestedScrollingParentHelper來幫助我們來實現

總結:

1、跟傳統的事件分發不同,嵌套滑動是由子View傳遞給父View,是從下到上的,傳統事件的分發是從上到下的
2、如果一個View想要傳遞嵌套滑動的事件,有兩個前提:實現NestedScrollingChild接口;setNestedScrollingEnabled方法設置爲true。如果一個ViewGroup想要接收和消耗子View傳遞過來的事件,必須實現NestedScrollingParent接口

ListView和RecyclerView對比:

編碼方式:

RecyclerView編碼更規範,支持LayoutManager、ItemDecoration、ItemAnimation

RecyclerView自動完成item複用,不需要setTag和getTag操作

佈局效果:

ListView佈局單一,只支持縱向佈局

RecyclerView佈局效果豐富,可以在LayoutManager中設置:縱向、橫向、線性、表格、瀑布流,另外,我們也可以自定義LayoutManager

數據處理:

ListView中存在setEmptyView(處理Adapter中數據爲空的情況)、addHeaderView(不會影響Adapter的編寫)、addFooterView(不會影響Adapter的編寫)

RecyclerView中需要在Adapter中自己處理(可以通過ViewHolder的Type類型)

列表刷新:

ListView通過notifyDataSetChanged刷新數據,但是這種刷新是全局的

RecyclerView可以通過notifyItemChanged實現局部刷新

動畫效果:

RecyclerView的notifyItemChanged、notifyItemInserted、notifyItemRemoved實現了動畫效果,我們可以也通過ItemAnimator實現自己的動畫效果

ListView並沒有實現動畫效果,但我們可以在Adapter自己實現item的動畫效果

點擊事件:

ListView中有onItemClickListener,但是如果有HeaderView和FooterView,這是postion會發生變化,可能造成數組越界問題,在工程實現時,我們要判斷是否存在HeaderView(getHeaderViewsCount),然後position動態減1

在RecyclerView中有onItemTouchListener,我們不需要考慮header和footer的問題,因爲所有item都是adapter中通過position生成的

嵌套滾動:

RecyclerView實現了NestedScrollingChild,可以實現嵌套滾動

ListView沒有實現嵌套滾動

滑動列表卡頓問題:解決辦法(滑動時停止圖片加載),在SCROLL_STATE_IDLE時加載圖片

Glide.with(getContext()).resumeRequests();

Glide.with(getContext()).pauseRequests();

minSdkVersion:應用支持的系統最低版本;

targetSdkVersion:應用已經兼容從minSdkVersion至targetSdkVersion之間的所有api變化,用於運行時判斷,該屬性是Android提供向前兼容的主要依據;

compileSdkVersion:編譯時兼容的api版本;

三者的關係:minSdkVersion <= targetSdkVersion <= compileSdkVersion,正常情況下targetSdkVersion和compileSdkVersion應該相等(既然編譯時已經適配了最新的api,那爲啥不改targetSdkVersion,理由是更改compileSdkVersion會提示那些api過時了,那我們儘量少用,但是有些問題我們通過版本判斷屏蔽了,所以還不能改targetSdkVersion)

git常用命令:init、status、diff、add、remove、commit、clone、branch、checkout

.9圖規則:左上黑邊爲拉伸區域,右下黑邊爲顯示區域

使用Gson過程中忽略字段

//忽略字段id
    private Gson getSkipIdGson() {
        Gson gson = new GsonBuilder().setExclusionStrategies(
                new ExclusionStrategy() {
                    @Override
                    public boolean shouldSkipField(FieldAttributes f) {
                        //過濾掉字段名包含"id","address"的字段
                        return f.getName().equals("id");
                    }
 
                    @Override
                    public boolean shouldSkipClass(Class<?> clazz) {
                        // 過濾掉 類名包含 Bean的類
                        return false;
                    }
                }).create();
        return gson;
    }
 
    private Gson getSkipIdAndGroupIdGson() {
        Gson gson = new GsonBuilder().setExclusionStrategies(
                new ExclusionStrategy() {
                    @Override
                    public boolean shouldSkipField(FieldAttributes f) {
                        //過濾掉字段名包含"id","address"的字段
                        return f.getName().equals("id") | f.getName().equals("groupGuid");
                    }
 
                    @Override
                    public boolean shouldSkipClass(Class<?> clazz) {
                        // 過濾掉 類名包含 Bean的類
                        return false;
                    }
                }).create();
        return gson;
    }

Android彈泡主要有兩種實現方式:PopupWindow、動態添加View。適用場景:PopupWindow適用於固定位置的提示、動態添加View適用於Feed流、ViewPager

PopupWindow可以在窗口的任意位置顯示,相對於其它窗口組件顯示的位置更靈活,Popupwindow不會給頁面的其它部分添加蒙層,可以自定義動畫,但想動態變更位置,只能手動調用update方法,容易出bug

showAtLocation與showAsDropDown:https://www.jianshu.com/p/12f53c931eda

下拉刷新(背景拉大+頂部轉球)實現:

        mAppBar.addOnOffsetChangedListener { _, offset ->
            var maxOffset = mHeaderView.height
            var maxDisplayOffset = mHeaderView.height
            mIsHeadFold = maxOffset + offset == 0
            if (!mIsHeadFold) checkCollapsingToolbarUpdate()
            if (mIsRefreshing) {
                maxOffset += mLoadingPullEndOffset
                maxDisplayOffset += mLoadingPullEndOffset
            }

            onAppBarCollapsing(offset, maxOffset, maxDisplayOffset)
        }
        mZoomLayout.setZoomReadyProxy(object : PullZoomExtendLayout.IZoomReadyProxy {
            override fun isReadyZoom(): Boolean {
                return mAppBarOffset >= 0 && mRefreshEnable && mAppBarMaxOffset > 0 && !isRefreshTagList
            }
        })
        mZoomLayout.addOnPullZoomListener(object : PullZoomBaseView.OnPullZoomListener {
            override fun onPullZooming(newScrollValue: Float) {
                val offset = Math.abs(newScrollValue).toInt()
                onPullZooming(true, if (mIsRefreshing) offset + mLoadingPullEndOffset else offset)
            }

            override fun onPullStart() {}
            override fun onPullZoomEnd(newScrollValue: Float) {
                val offset = Math.abs(newScrollValue).toInt()
                onPullZoomEnd(if (mIsRefreshing) offset + mLoadingPullEndOffset else offset)
            }
        })

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        if (!isZoomEnable){
            return false;
        }

        if (event.getAction() == MotionEvent.ACTION_MOVE && isPullStart){
            return true;
        }

        performInterceptAction(event);
        return isPullStart;
    }

    private void performInterceptAction(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                if (isReadyZoom()){
                    onZoomReadyActionDown(event);
                }
                break;

            case MotionEvent.ACTION_MOVE:
                if (isReadyZoom()) {
                    onZoomReadyActionMove(event);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // do nothing
                // the reset action will be done in the function onTouchEvent
                break;
            default:
                break;
        }
    }

    private void onZoomReadyActionDown(MotionEvent event) {
        mInitTouchY = mLastTouchY = event.getY();
        mInitTouchX = mLastTouchX = event.getX();
        isPullStart = false;
    }

    private void onZoomReadyActionMove(MotionEvent event) {
        float mCurrentX = event.getX();
        float mCurrentY = event.getY();

        float xDistance = mCurrentX - mLastTouchX;
        float yDistance = mCurrentY - mLastTouchY;

        if (mMode == ZOOM_HEADER && yDistance > mTouchSlop && yDistance > Math.abs(xDistance)
                || mMode == ZOOM_FOOTER && -yDistance > mTouchSlop && -yDistance > Math.abs(xDistance)){
            mLastTouchY = mCurrentY;
            mLastTouchX = mCurrentX;

            if (!mZoomListenerList.isEmpty()) {
                for (OnPullZoomListener listener : mZoomListenerList) {
                    listener.onPullStart();
                }
            }
            if (!isPullStart) {
                mInitTouchY = mCurrentY;
            }
            isPullStart = true;
        }
    }

下拉刷新(背景不動+頂部轉球)實現:開源控件(PullToRefresh),控件通過負padding和scroll實現UI效果,通過僞造TOUCH_DOWN事件來實現上下拖動(父視圖攔截事件後還可以讓子視圖重新獲取MOVE事件),該控件不會動態拉伸頭部背景,也是通過回調來判斷是否開始下拉刷新

class TabPullToRefreshView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
    : AppBarPullToRefreshLayout(context, attrs) {
    private lateinit var coordinatorLayout: CoordinatorLayout
    private lateinit var headerLoadingView: BollLoadingLayout

    override fun initLoadingLayout(): LoadingLayout? {
        headerLoadingView = BollLoadingLayout(context)
        return headerLoadingView
    }

    override fun getAppBarLayout(): AppBarLayout {
        return coordinatorLayout.findViewById(R.id.appbar_header)
    }

    override fun initRootChild(): CoordinatorLayout {
        coordinatorLayout = LayoutInflater.from(context).inflate(R.layout.peppa_tab_main_content_layout, null) as CoordinatorLayout
        return coordinatorLayout
    }

}


    override fun getPullToRefreshScrollDirection(): Orientation {
        return Orientation.VERTICAL
    }

    override fun createLoadingLayout(context: Context, mode: Mode?, attrs: TypedArray?): LoadingLayout {
        return initLoadingLayout() ?: super.createLoadingLayout(context, mode, attrs)
    }

    override fun createRefreshableView(p0: Context?, p1: AttributeSet?): CoordinatorLayout? {
        val childRoot = initRootChild()
        getAppBarLayout()?.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
            appBarOffset = verticalOffset
        }
        return childRoot
    }

    override fun isReadyForPullStart(): Boolean {
        val isNoOffset = appBarOffset >= 0L
        return isNoOffset && (pullReadyProxy?.isReadyForPullStart() ?: true)
    }

    override fun isReadyForPullEnd(): Boolean {
        return false
    }

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