Android如何在應用層進行截屏及截屏源碼分析(上)

最近在看framework層代碼時發現其中有一個是測試截屏操作的專門的包,於是潛意識的驅使下就研究了這方面的知識,今天作個總結吧!以及我們在寫上層應用時如何做截屏操作的,那麼我們先來看看截屏的源碼分析,其實截屏操作就java這部分是放在了系統SystemUI那裏,用過android系統手機的同學應該都知道,一般的android手機按下音量減少鍵和電源按鍵就會觸發截屏事件(國內定製機做個修改的這裏就不做考慮了)

我們知道這裏的截屏事件是通過我們的按鍵操作觸發的,所以這裏就需要我們從android系統的按鍵觸發模塊開始看起,由於我們在不同的App頁面,操作音量減少鍵和電源鍵都會觸發系統的截屏處理,所以這裏的按鍵觸發邏輯應該是Android系統的全局按鍵處理邏輯。

在android系統中,由於我們的每一個Android界面都是一個Activity,而界面的顯示都是通過Window對象實現的,每個Window對象實際上都是PhoneWindow的實例,而每個PhoneWindow對象都一個PhoneWindowManager對象,當我們在Activity界面執行按鍵操作的時候,在將按鍵的處理操作分發到App之前,首先會回調PhoneWindowManager中的dispatchUnhandledKey方法,按鍵分發處理,見名知意該方法主要用於執行當前App處理按鍵之前的操作,PhoneWindowManager所在包如下圖所示:

這裏寫圖片描述

那麼在我們看該方法是怎麼實現時先來看看這個方法在哪被調用吧,

這裏寫圖片描述

這裏寫圖片描述

ctrl+Shift+G可以發現PhoneWindowManager的dispatchUnhandledKey方法在InputManagerService的dispatchUnhandledKey執行,而InputManagerService.dispatchUnhandledKey是一個Native callback.,學過NDK的人都知道這個方法看來是通過JNI回調了,即是硬件驅動屏檢測到按鍵輸入再包裝到庫層通過C++實現,再C++那邊調用了該方法傳回一些參數然後傳給我們上層操作。

在InputManagerService.dispatchUnhandledKey方法中通過一個mWindowManagerCallbacks實現,那麼我們再看看mWindowManagerCallbacks吧,該mWindowManagerCallbacks是InputManagerService的內部接口
這裏寫圖片描述

這裏寫圖片描述

該接口的實例是被作爲觀察者模式傳進來的,在SystemServer中傳入

這裏寫圖片描述

而InputManagerService又作爲Android的一個服務被添加到SystemServer中,如果對這方面不是很瞭解的同學,請移步至Android開發如何定製framework層服務 作個具體的瞭解。

看上圖可知mWindowManagerCallbacks最終是通過WindowManagerService.getInputMonitor()得到的,我們去WindowManagerService這個服務裏面看看這個getInputMonitor方法:
這裏寫圖片描述
該方法返回一個InputMonitor,我們再進去InputMonitor看看什麼情況:
這裏寫圖片描述
該InputMonitor實現了InputManagerService.WindowManagerCallbacks這個接口,dispatchUnhandledKey方法如下:
這裏寫圖片描述
看圖可知dispatchUnhandledKey該方法最後還是通過WindowManagerService這個Android服務來實現的,對Android而言所有的UI都是通過WindowManagerService這個服務去操作;

接下來我們再繼續看一下具體該方法的實現。

 /** {@inheritDoc} */
    @Override
    public KeyEvent dispatchUnhandledKey(WindowState win, KeyEvent event, int policyFlags) {
        // Note: This method is only called if the initial down was unhandled.
        if (DEBUG_INPUT) {
            Slog.d(TAG, "Unhandled key: win=" + win + ", action=" + event.getAction()
                    + ", flags=" + event.getFlags()
                    + ", keyCode=" + event.getKeyCode()
                    + ", scanCode=" + event.getScanCode()
                    + ", metaState=" + event.getMetaState()
                    + ", repeatCount=" + event.getRepeatCount()
                    + ", policyFlags=" + policyFlags);
        }

        KeyEvent fallbackEvent = null;
        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
            final KeyCharacterMap kcm = event.getKeyCharacterMap();
            final int keyCode = event.getKeyCode();
            final int metaState = event.getMetaState();
            final boolean initialDown = event.getAction() == KeyEvent.ACTION_DOWN
                    && event.getRepeatCount() == 0;

            // Check for fallback actions specified by the key character map.
            final FallbackAction fallbackAction;
            if (initialDown) {
                fallbackAction = kcm.getFallbackAction(keyCode, metaState);
            } else {
                fallbackAction = mFallbackActions.get(keyCode);
            }

            if (fallbackAction != null) {
                if (DEBUG_INPUT) {
                    Slog.d(TAG, "Fallback: keyCode=" + fallbackAction.keyCode
                            + " metaState=" + Integer.toHexString(fallbackAction.metaState));
                }

                final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
                fallbackEvent = KeyEvent.obtain(
                        event.getDownTime(), event.getEventTime(),
                        event.getAction(), fallbackAction.keyCode,

該方法主要是包裝了一下KeyEvent event,關鍵代碼如下:
這裏寫圖片描述
記住這個紅色框框部分,該部分再下面要說到,那麼這裏將KeyEvent 放到了interceptFallback這個方法中處理了,我們再進去這個interceptFallback裏面看看吧,如下:
這裏寫圖片描述

然後我們看到在interceptFallback方法中我們調用了interceptKeyBeforeQueueing方法,通過閱讀我們我們知道該方法主要實現了對截屏按鍵的處理流程,這樣我們繼續看一下interceptKeyBeforeWueueing方法的處理,該方法比較長:

  /** {@inheritDoc} */
    @Override
    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags, boolean isScreenOn) {
        if (!mSystemBooted) {
            // If we have not yet booted, don't let key events do anything.
            return 0;
        }

        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
        final boolean canceled = event.isCanceled();
        final int keyCode = event.getKeyCode();

        final boolean isInjected = (policyFlags & WindowManagerPolicy.FLAG_INJECTED) != 0;

        // If screen is off then we treat the case where the keyguard is open but hidden
        // the same as if it were open and in front.
        // This will prevent any keys other than the power button from waking the screen
        // when the keyguard is hidden by another activity.
        final boolean keyguardActive = (mKeyguardMediator == null ? false :
                                            (isScreenOn ?
                                                mKeyguardMediator.isShowingAndNotHidden() :
                                                mKeyguardMediator.isShowing()));

        if (keyCode == KeyEvent.KEYCODE_POWER) {
            policyFlags |= WindowManagerPolicy.FLAG_WAKE;
        }
        final boolean isWakeKey = (policyFlags & (WindowManagerPolicy.FLAG_WAKE
                | WindowManagerPolicy.FLAG_WAKE_DROPPED)) != 0;

        if (DEBUG_INPUT) {
            Log.d(TAG, "interceptKeyTq keycode=" + keyCode
                    + " screenIsOn=" + isScreenOn + " keyguardActive=" + keyguardActive
                    + " policyFlags=" + Integer.toHexString(policyFlags)
                    + " isWakeKey=" + isWakeKey);
        }

        if (down && (policyFlags & WindowManagerPolicy.FLAG_VIRTUAL) != 0
                && event.getRepeatCount() == 0) {
            performHapticFeedbackLw(null, HapticFeedbackConstants.VIRTUAL_KEY, false);
        }

        // Basic policy basedn screen state and keyguard.
        // FIXME: This policy isn't quite correct.  We shouldn't care whether the screen
        //        is on or off, really.  We should care about whether the device is in an
        //        interactive state or is in suspend pretending to be "off".
        //        The primary screen might be turned off due to proximity sensor or
        //        because we are presenting media on an auxiliary screen or remotely controlling
        //        the device some other way (which is why we have an exemption here for injected
        //        events).
        int result;
        if ((isScreenOn && !mHeadless) || (isInjected && !isWakeKey)) {
            // When the screen is on or if the key is injected pass the key to the application.
            result = ACTION_PASS_TO_USER;
        } else {
            // When the screen is off and the key is not injected, determine whether
            // to wake the device but don't pass the key to the application.
            result = 0;
            if (down && isWakeKey && isWakeKeyWhenScreenOff(keyCode)) {
                if (keyguardActive) {
                    // If the keyguard is showing, let it wake the device when ready.
                    mKeyguardMediator.onWakeKeyWhenKeyguardShowingTq(keyCode);
                } else {
                    // Otherwise, wake the device ourselves.
                    result |= ACTION_WAKE_UP;
                }
            }
        }

        // Handle special keys.    switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_DOWN:
            case KeyEvent.KEYCODE_VOLUME_UP:
            case KeyEvent.KEYCODE_VOLUME_MUTE: {
                if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
                    if (down) {
                        if (isScreenOn && !mVolumeDownKeyTriggered
                                && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
                            mVolumeDownKeyTriggered = true;
                            mVolumeDownKeyTime = event.getDownTime();
                            mVolumeDownKeyConsumedByScreenshotChord = false;
                            cancelPendingPowerKeyAction();
                            interceptScreenshotChord();
                        }
                    } else {
                        mVolumeDownKeyTriggered = false;
                        cancelPendingScreenshotChordAction();
                    }
                } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
                    if (down) {
                        if (isScreenOn && !mVolumeUpKeyTriggered
                                && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
                            mVolumeUpKeyTriggered = true;
                            cancelPendingPowerKeyAction();
                            cancelPendingScreenshotChordAction();
                        }
                    } else {
                        mVolumeUpKeyTriggered = false;
                        cancelPendingScreenshotChordAction();
                    }
                }
                if (down) {  ITelephony telephonyService = getTelephonyService();
                    if (telephonyService != null) {
                        try {
                            if (telephonyService.isRinging()) {
                                // If an incoming call is ringing, either VOLUME key means
                                // "silence ringer".  We handle these keys here, rather than
                                // in the InCallScreen, to make sure we'll respond to them
                                // even if the InCallScreen hasn't come to the foreground yet.
                                // Look for the DOWN event here, to agree with the "fallback"
                                // behavior in the InCallScreen.
                                Log.i(TAG, "interceptKeyBeforeQueueing:"
                                      + " VOLUME key-down while ringing: Silence ringer!");

                                // Silence the ringer.  (It's safe to call this
                                // even if the ringer has already been silenced.)
                                telephonyService.silenceRinger();

                                // And *don't* pass this key thru to the current activity
                                // (which is probably the InCallScreen.)
                                result &= ~ACTION_PASS_TO_USER;
                                break;
                            }
                            if (telephonyService.isOffhook()
                                    && (result & ACTION_PASS_TO_USER) == 0) {
                                // If we are in call but we decided not to pass the key to
                                // the application, handle the volume change here.
                                handleVolumeKey(AudioManager.STREAM_VOICE_CALL, keyCode);
                                break;
                            }
                        } catch (RemoteException ex) {

                            Log.w(TAG, "ITelephony threw RemoteException", ex);
                        }
                    }

                    if (isMusicActive() && (result & ACTION_PASS_TO_USER) == 0) {
                        // If music is playing but we decided not to pass the key to the
                        // application, handle the volume change here.
                        handleVolumeKey(AudioManager.STREAM_MUSIC, keyCode);
                        break;
                    }
                }
                break;
            }

            case KeyEvent.KEYCODE_ENDCALL: { result &= ~ACTION_PASS_TO_USER;
                if (down) {
                    ITelephony telephonyService = getTelephonyService();
                    boolean hungUp = false;
                    if (telephonyService != null) {
                        try {
                            hungUp = telephonyService.endCall();
                        } catch (RemoteException ex) {
                            Log.w(TAG, "ITelephony threw RemoteException", ex);
                        }
                    }
                    interceptPowerKeyDown(!isScreenOn || hungUp);
                } else {
                    if (interceptPowerKeyUp(canceled)) {
                        if ((mEndcallBehavior
                                & Settings.System.END_BUTTON_BEHAVIOR_HOME) != 0) {
                            if (goHome()) {
                                break;
                            }
                        }
                        if ((mEndcallBehavior
                                & Settings.System.END_BUTTON_BEHAVIOR_SLEEP) != 0) {
                            result = (result & ~ACTION_WAKE_UP) | ACTION_GO_TO_SLEEP;
                        }
                    }
                }
                break;
            }

            case KeyEvent.KEYCODE_POWER: { result &= ~ACTION_PASS_TO_USER;
                if (down) {
                    if (isScreenOn && !mPowerKeyTriggered
                            && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
                        mPowerKeyTriggered = true;
                        mPowerKeyTime = event.getDownTime();
                        interceptScreenshotChord();
                    }

                    ITelephony telephonyService = getTelephonyService();
                    boolean hungUp = false;
                    if (telephonyService != null) {
                        try {
                            if (telephonyService.isRinging()) {
                                // Pressing Power while there's a ringing incoming
                                // call should silence the ringer.
                                telephonyService.silenceRinger();
                            } else if ((mIncallPowerBehavior
                                    & Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP) != 0
                                    && telephonyService.isOffhook()) {
                                // Otherwise, if "Power button ends call" is enabled,
                                // the Power button will hang up any current active call.
                                hungUp = telephonyService.endCall();
                            }
                        } catch (RemoteException ex) {
                            Log.w(TAG, "ITelephony threw RemoteException", ex);
                        }
                    }
                    interceptPowerKeyDown(!isScreenOn || hungUp|| mVolumeDownKeyTriggered || mVolumeUpKeyTriggered);
                } else {
                    mPowerKeyTriggered = false;
                    cancelPendingScreenshotChordAction();
                    if (interceptPowerKeyUp(canceled || mPendingPowerKeyUpCanceled)) {
                        result = (result & ~ACTION_WAKE_UP) | ACTION_GO_TO_SLEEP;
                    }
                    mPendingPowerKeyUpCanceled = false;
                }
                break;
            }

            case KeyEvent.KEYCODE_MEDIA_PLAY:
            case KeyEvent.KEYCODE_MEDIA_PAUSE:
            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:if (down) {
                    ITelephony telephonyService = getTelephonyService();
                    if (telephonyService != null) {
                        try {
                            if (!telephonyService.isIdle()) {
                                // Suppress PLAY/PAUSE toggle when phone is ringing or in-call
                                // to avoid music playback.
                                break;
                            }
                        } catch (RemoteException ex) {
                            Log.w(TAG, "ITelephony threw RemoteException", ex);
                        }
                    }
                }
            case KeyEvent.KEYCODE_HEADSETHOOK:
            case KeyEvent.KEYCODE_MUTE:
            case KeyEvent.KEYCODE_MEDIA_STOP:
            case KeyEvent.KEYCODE_MEDIA_NEXT:
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
            case KeyEvent.KEYCODE_MEDIA_REWIND:
            case KeyEvent.KEYCODE_MEDIA_RECORD:
            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
                if ((result & ACTION_PASS_TO_USER) == 0) { // Only do this if we would otherwise not pass it to the user. In that
                    // case, the PhoneWindow class will do the same thing, except it will
                    // only do it if the showing app doesn't process the key on its own.
                    // Note that we need to make a copy of the key event here because the
                    // original key event will be recycled when we return.
                    mBroadcastWakeLock.acquire();
                    Message msg = mHandler.obtainMessage(MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK,
                            new KeyEvent(event));
                    msg.setAsynchronous(true);
                    msg.sendToTarget();
                }
                break;
            }

            case KeyEvent.KEYCODE_CALL: {
                if (down) {
                    ITelephony telephonyService = getTelephonyService();
                    if (telephonyService != null) {
                        try {
                            if (telephonyService.isRinging()) {
                                Log.i(TAG, "interceptKeyBeforeQueueing:"
                                      + " CALL key-down while ringing: Answer the call!");
                                telephonyService.answerRingingCall();

                                // And *don't* pass this key thru to the current activity
                                // (which is presumably the InCallScreen.)
                                result &= ~ACTION_PASS_TO_USER;
                            }
                        } catch (RemoteException ex) {
                            Log.w(TAG, "ITelephony threw RemoteException", ex);
                        }
                    }
                }
                break;
            }
        }
        return result;
    }

可以發現這裏首先判斷當前系統是否已經boot完畢,若尚未啓動完畢,則所有的按鍵操作都將失效,若啓動完成,則執行後續的操作,這裏我們只是關注音量減少按鍵和電源按鍵組合的處理事件。另外這裏多說一句像安卓系統的HOME按鍵事件,MENU按鍵事件,進程列表按鍵事件等等都是在這裏實現的
我們關注一下關鍵代碼部分就好了,看看按鍵捕獲部分,如下:

這裏寫圖片描述
當我用按下音量減少按鍵的時候回進入到:case KeyEvent.KEYCODE_VOLUME_MUTE分支並執行相應的邏輯,然後同時判斷用戶是否按下了電源鍵,若同時按下了電源鍵,則執行:

這裏寫圖片描述

見名知意可以發現這裏的interceptScreenshotChrod方法就是系統準備開始執行截屏操作的開始,我們繼續看一下interceptcreenshotChord方法的實現。

這裏寫圖片描述

在方法體中我們最終會執行發送一個延遲的異步消息,請求執行截屏的操作而這裏的延時時間,若當前輸入框是打開狀態,則延時時間爲輸入框關閉時間加上系統配置的按鍵超時時間,若當前輸入框沒有打開則直接是系統配置的按鍵超時處理時間,再看看mScreenshotChordLongPress這個Runnable的具體實現。

這裏寫圖片描述

方法體中並未執行其他操作,直接就是調用了takeScreenshot方法,這樣我們繼續看一下takeScreenshot方法的實現。

 // Assume this is called from the Handler thread.
    private void takeScreenshot() {
        synchronized (mScreenshotLock) {
            if (mScreenshotConnection != null) {
                return;
            }
            ComponentName cn = new ComponentName("com.android.systemui",
                    "com.android.systemui.screenshot.TakeScreenshotService");
            Intent intent = new Intent();
            intent.setComponent(cn);
            ServiceConnection conn = new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    synchronized (mScreenshotLock) {
                        if (mScreenshotConnection != this) {
                            return;
                        }
                        Messenger messenger = new Messenger(service);
                        Message msg = Message.obtain(null, 1);
                        final ServiceConnection myConn = this;
                        Handler h = new Handler(mHandler.getLooper()) {
                            @Override
                            public void handleMessage(Message msg) {
                                synchronized (mScreenshotLock) {
                                    if (mScreenshotConnection == myConn) {
                                        mContext.unbindService(mScreenshotConnection);
                                        mScreenshotConnection = null;
                                        mHandler.removeCallbacks(mScreenshotTimeout);
                                    }
                                }
                            }
                        };
                        msg.replyTo = new Messenger(h);  msg.arg1 = msg.arg2 = 0;
                        if (mStatusBar != null && mStatusBar.isVisibleLw())
                            msg.arg1 = 1;
                        if (mNavigationBar != null && mNavigationBar.isVisibleLw())
                            msg.arg2 = 1;
                        try {
                            messenger.send(msg);
                        } catch (RemoteException e) {
                        }
                    }
                }
                @Override
                public void onServiceDisconnected(ComponentName name) {}
            };
            if (mContext.bindService(
                    intent, conn, Context.BIND_AUTO_CREATE, UserHandle.USER_CURRENT)) {
                mScreenshotConnection = conn;
                mHandler.postDelayed(mScreenshotTimeout, 10000);
            }
        }
    }

那麼看代碼可知這裏是啓動了一個TakeScreenshotService,該service即是最上面的圖中systemUI下的,我們再看看TakeScreenshotService這個類裏面到底做了什麼事吧!

這裏寫圖片描述

該service在被成功綁定時候回有一個handler回調過來然後拿到一個GlobalScreenshot去執行takeScreenshot方法,好吧,繼續看一下takeScreentshot方法的執行邏輯。

這裏寫圖片描述

該方法後面有兩個參數:statusBarVisible,navBarVisible是否可見,而這兩個參數在我們PhoneWindowManager.takeScreenshot方法傳遞的,在我們啓動TakeScreenshotService時傳入:

這裏寫圖片描述

可見若果狀態條可見,則傳遞的statusBarVisible爲true,若導航條可見,則傳遞的navBarVisible爲true。然後我們在截屏的時候判斷nStatusBar是否可見,mNavigationBar是否可見,若可見的時候則截屏同樣將其截屏出來。

再來看看GlobalScreenshot.takeScreenshot方法中截屏最關鍵的代碼:
這裏寫圖片描述
看註釋可知,這裏就是執行截屏事件的具體操作了,然後我看一下SurfaceControl.screenshot方法的具體實現,另外這裏需要注意的是,截屏之後返回的是一個Bitmap對象,其實熟悉android繪製機制的朋友都應該要知道android中所有顯示能夠顯示的東西,在內存中表現都是Bitmap對象。
這裏寫圖片描述
如圖可知,那麼這個Surface.screenshot方法被@了一個hide,看來是被Google隱藏掉了,該方法最後是調用了本地方法nativeScreenshot函數,這個是在C++那邊操作的,具體的實現在JNI層,由於個人對C++不是很熟練那麼這邊不做過多的介紹。framework中間層和HAL庫函數打交道基本上都是這個模式。

另外在GlobalScreenshot.takeScreenshot這個方法中其它是做了一些動畫,通知等操作,比如我們截屏時保存的圖片會有一個動畫以及截屏成功之後會在通知欄收到一條通知等。

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