Android 手机系统中的截屏功能是大家经常用到的一项功能,使用截屏功能能便捷且快速地把手机当前屏幕显示的信息,以图片的方式保存在手机中以便我们日后的查阅,这大大方便了需要我们在很短的时间记忆屏幕中的显示信息。
使用过 Android 手机系统截屏功能的人都知道,截屏操作一般有两种途径:
下拉手机屏幕上方状态栏点击快捷功能面板中的截屏(部分手机品牌中支持此功能)。
同时按住手机侧方的电源键+音量减键来实现截屏。
第一种途径的部分功能不是 Google 的 Android 系统自带功能,由各个手机品牌商主开发添加的功能项,而各个手机品牌对此项功能的实现源码,我们是无法获取的,故无法就此操作实现原理展开讲解,但可以肯定第一种途径与第二种途径的截屏实现最后都是调用了系统的同一个实现原理,只是他们的截屏触发不同而已,所以本文只针对第二种途径的实现以源码解析的方式进行讲解,使用的 Android 源码版本是 8.0.0。
该事件中涉及的 Android 知识点并不多,但为更清晰的了解事件完整过程,在此简要说明以下两个具有代表性的重要知识点。
Messenger 在 Service 中的双向通信过程可以简单分为下面三个步骤:
首先由客户端通过 bindService
方式启动服务端的 Service,服务端 Service
执行其自身生命周期并在 onBind
回调方法中获取 Service 自身 Messenger 所对应的 IBinder
,并再将其发送客户端。
IBinder 对象会传入客户端 ServiceConnection
对象的 onServiceConnected
回调方法中,然后用得到远程 Messenger 对象(服务端内的
Messenger)给服务端发送消息,因此远程 Messenger 对象内部有指向服务端的 handler, 所以服务端 handler
在得到消息后会在自身的 handleMessage
方法中处理消息,其中包括获取客户端的 Messenger
对象。
在服务端处理完事务后,会用客户端 Messenger 对象在客户端发送反馈消息。
Android 中多线程的实现有多种方式,截屏的实现过程中用到了其中 AsyncTask 与 Runnable 接口两种方法。
AsyncTask 是 Android 已封装好了一个轻量级异步类,它是对 Handler
与线程池的封装后的抽象类,在使用的时候必须实现它,且必须实现其中的 onPreExecute()
, doInBackground()
,onPostExecute()
这三个方法,AsyncTask 的用作主要是可以将耗时的工作移出应用的主线程(也叫 UI 线程),并将处理结果返回给 UI
线程更新界面
实现 Runnable 接口创建多线程是 Java 编程语言中常用的方法,即实现此接口并创建该实例的对象,在此不多解释。
Android 截屏流程先是按手机物理按键触发截屏功能请求,在 system_Server
进程中对物理按键的判断请求条件其是否满足截屏要求,如满足后则设置截屏方式是全屏或区域屏幕截屏,再调用 SystemUI
应用进一步判断屏截请求条件,并初始化一些前置准备工作,通过 JNI
获取截屏图片成功后,最后保存图片并用通知提示用户保存是否异常等工作。下面将按此事件的时间顺序分步讲解 Android 截屏事件。
手机截屏功能如何被触发,Android 系统又是如何实现截屏功能以及截屏图片的保存等,带着这些疑问开始我们 Android 源码的遨游。
在 Android 中一般会有 Back(返回)、Home(主键)、Menu(菜单)、Power(电源键)、-Volume(音量减)、+
Volume(音量加)等物理按键或虚拟按键,而 Android 系统中对各类物理与虚拟按键的处理都是在 Framework 层中的 PhoneWindowManager.java
内实现,如需对部分按键实现特定功能即可在
PhoneWindowManager.interceptKeyBeforeQueueing 方法内中客制功能。该 PhoneWindowManager
运行于 system_server
进程中属系统进程,而非某一个 APP 进程中。
1 2 3 4 5 6 7 8 9 10 11 12 | case KeyEvent.KEYCODE_POWER: {//匹配电源键 // Any activity on the power button stops the accessibility shortcut cancelPendingAccessibilityShortcutAction(); result &= ~ACTION_PASS_TO_USER; isWakeKey = false; // wake-up will be handled separately if (down) {//当电源键被按下状态时 interceptPowerKeyDown(event, interactive); } else { interceptPowerKeyUp(event, interactive, canceled); } break; } |
清单 1 代码为 Power 键被按下时触发,这里只看 down
部分代码,进入 interceptPowerKeyDown
方法中,将 Power 键的触发状态属性赋值 true
,然后获取 Power 触发的时间。
1 2 3 4 5 6 7 8 9 10 | private void interceptPowerKeyDown(KeyEvent event, boolean interactive) { //....省略部分代码 // Latch power key state to detect screenshot chord. if (interactive && !mScreenshotChordPowerKeyTriggered && (event.getFlags() &KeyEvent.FLAG_FALLBACK) == 0) { mScreenshotChordPowerKeyTriggered = true; //Power 键被触发的标示符 mScreenshotChordPowerKeyTime = event.getDownTime(); //获取 Power 键的触发时间 interceptScreenshotChord(); } } |
接着进入 interceptScreenshotChord ()
方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | private void interceptScreenshotChord() { if (mScreenshotChordEnabled //系统是否开启截屏功能 &&mScreenshotChordVolumeDownKeyTriggered //音量减键已被按下 &&mScreenshotChordPowerKeyTriggered //电源减键已被按下 && !mA11yShortcutChordVolumeUpKeyTriggered) { //音量减键未被按下 final long now = SystemClock.uptimeMillis(); //获取当前的时间 //当前时间要小于或等于音量减键被按下时间+150S if (now < = mScreenshotChordVolumeDownKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS // 当前时间要小于或等于电源键被按下时间+150S && now < = mScreenshotChordPowerKeyTime + SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) { mScreenshotChordVolumeDownKeyConsumed = true; cancelPendingPowerKeyAction(); mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN); mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay()); } } } |
在 interceptScreenshotChord
方法中有一个 mScreenshotChordVolumeDownKeyTriggered
变量,我们来看看此变量是在何处何时赋值为 true
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (down) {//VOLOME_DOWN 状态为按下状态 if (interactive & & !mScreenshotChordVolumeDownKeyTriggered & & (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { mScreenshotChordVolumeDownKeyTriggered = true;//音量减键被触发的标示符 mScreenshotChordVolumeDownKeyTime = event.getDownTime(); mScreenshotChordVolumeDownKeyConsumed = false; cancelPendingPowerKeyAction(); interceptScreenshotChord(); interceptAccessibilityShortcutChord(); } } else { mScreenshotChordVolumeDownKeyTriggered = false; cancelPendingScreenshotChordAction(); cancelPendingAccessibilityShortcutAction(); } |
对比清单 4 与清单 2 代码可以发现两处代码实现基本一致,都有获取操作时间,给状态变量赋值和执行 interceptScreenshotChord
方法,而在 interceptScreenshotChord
方法中判断属性 mScreenshotChordVolumeDownKeyTriggered
与 mScreenshotChordPowerKeyTriggered
都必须为 true
才满足执行条件,就是说两个按键状态必须已被触发,且音量加键的触发状态为 false
,接着是获取当前的时间并判断它是否小于或等于两键触发时间加系统规格的延时时间 150 秒,这些条件满足后将 mScreenshotChordVolumeDownKeyConsumed
赋值 false
,该属性为了防止截屏的时候音量下键生效出现调节音量的 dialog 状态值,再执行防止触发 Power
键长按功能,最后延时开启截屏线程 ScreenshotRunnable
,且设置截屏方式是全屏截取。
我们来看看 ScreenshotRunnable
线程,该线程较为简单,只是一个设置截屏方式的方法与 run
方法,截屏方式分全屏截屏与区域性截屏,系统默认为全屏截屏,如要区域性截屏则需要在构建该线程对象时指定截屏方式。
1 2 3 4 5 6 7 8 9 10 | private class ScreenshotRunnable implements Runnable { private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN;//默认截屏方式是全屏截取 public void setScreenshotType(int screenshotType) { mScreenshotType = screenshotType; } @Override public void run() { takeScreenshot(mScreenshotType);//执行 takeScreenshot 方法 } } |
在 ScreenshotRunnable
线程的 run
方法中就一条执行语句,从形参的字意大概可以看出此方法指定截屏方式。我们继续看 takeScreenshot
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | private void takeScreenshot(final int screenshotType) { synchronized (mScreenshotLock) { if (mScreenshotConnection != null) { return; } final ComponentNameserviceComponent = new ComponentName(SYSUI_PACKAGE, SYSUI_SCREENSHOT_SERVICE); final Intent serviceIntent = new Intent(); serviceIntent.setComponent(serviceComponent); ServiceConnection conn = new ServiceConnection() { //....省略部分代码 if (mContext.bindServiceAsUser(serviceIntent, conn, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, UserHandle.CURRENT)) { mScreenshotConnection = conn; mHandler.postDelayed(mScreenshotTimeout, 10000); } } |
先看 SYSUI_PACKAGE, SYSUI_SCREENSHOT_SERVICE
两属性的定义:
private static final String SYSUI_PACKAGE = "com.android.systemui";
private static final String SYSUI_SCREENSHOT_SERVICE = "com.android.systemui.screenshot.TakeScreenshotService";
后面又有执行 bindServiceAsUser
方法,它与 bindService
两者都在 ContextImpl
类中定义,且他们方法内部都是调用同样的 bindServiceCommon
方法,区别就是两方法的参数个数不同,前者是系统内部隐藏方法,非 SDK 开发 API
接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Override public booleanbindService(Intent service, ServiceConnection conn, int flags) { warnIfCallingFromSystemProcess(); return bindServiceCommon(service, conn, flags, mMainThread.getHandler(), getUser()); } /** @hide */ @Override public booleanbindServiceAsUser(Intent service, ServiceConnection conn, int flags, UserHandle user) { return bindServiceCommon(service, conn, flags, mMainThread.getHandler(), user); } |
您可能已经熟悉 bindService
的使用,bindService
是 Android 启动
Servicer 方式之一,使用 bindService
则表明调用者和绑定者绑在一起,调用者一旦退出也就终止服务了,且 bindService
能跨进程启动另一个 APP 应用中的 Service,在之前定义的 serviceComponent
中的 TakeScreenshotService
,该
Service 是定义在 SystemUI 模块(状态栏应用)中,运行在 SystemUI 进程内。而 PhoneWindowManager
是运行在 System_server
进程中的,两者是运行在不同的进程中.而在 Android 系统中其已为我们提供了多种跨进程通信的方式,其中 IPC
是较常用的一种跨进程通信方式,如我们常用的 AIDL 就是 IPC 通信方式,在清单 7 的实现中用到的 Messenger 也是 IPC
通信方式(参见前文"Messenger 与 Service 进程间的双向通信")。
1 2 3 4 5 6 7 8 9 10 | 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) { } |
启动 Service 连接成功后对 Message 的 replyTo
,arg1
,arg2
参数赋值,并通过
Messenger 实现跨进程的通信。
至此我们已进入了 TakeScreenshotService
类中,它是 Service 的子类,在此 Service
中通过 IPC 跨进程通信方式响应截屏请求。
1 2 3 4 | @Override public IBinderonBind(Intent intent) { return new Messenger(mHandler).getBinder(); //返回 IBinder 对象给客户端 } |
在返回给客户端的 Ibinger 对象内部有指向服务端的 handler, 所以服务端 handler 会接收来自客户端的消息,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { final Messenger callback = msg.replyTo;//获取客户端传过来的 Messenger 对象 Runnable finisher = new Runnable() { @Override public void run() { Message reply = Message.obtain(null, 1); try { //Messenger 双向通信,在服务端用远程客户端的 Messenger 对象给客户端发送信息 callback.send(reply); } catch (RemoteException e) { } } }; // If the storage for this user is locked, we have no place to store // the screenshot, so skip taking it instead of showing a misleading // animation and error notification. //判断用户是否已解锁设备。 if (!getSystemService(UserManager.class).isUserUnlocked()) { Log.w(TAG, "Skipping screenshot because storage is locked!"); post(finisher); return; } if (mScreenshot == null) { mScreenshot = new GlobalScreenshot(TakeScreenshotService.this); } //根据信息类型匹配执行不同的任务, switch (msg.what) { case WindowManager.TAKE_SCREENSHOT_FULLSCREEN://全屏截屏 mScreenshot.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0); break; case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION://屏幕区域性截屏 mScreenshot.takeScreenshotPartial(finisher, msg.arg1 > 0, msg.arg2 > 0); break; default: Log.d(TAG, "Invalid screenshot option: " + msg.what); } } } |
上面的代码中的 finisher
是截屏之后的回调,谁发起的截屏就在截屏完成之后运行此 Runnable
接口的匿名实现,通过 Messenger 向截屏发起者发送消息。
我们再进 GlobalScreenshot
类,在 GlobalScreenshot.java
文件中的可以看到该类有多个内部类,其中 SaveImageInBackgroundTask
为 AsyncTask
的继承子类,是为截屏新创建的一个异步处理线程,清单 11
开始处定义大量的动画常量属性值,不一一介绍。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private static final int SCREENSHOT_FLASH_TO_PEAK_DURATION = 130; private static final int SCREENSHOT_DROP_IN_DURATION = 430; private static final int SCREENSHOT_DROP_OUT_DELAY = 500; private static final int SCREENSHOT_DROP_OUT_DURATION = 430; private static final int SCREENSHOT_DROP_OUT_SCALE_DURATION = 370; private static final int SCREENSHOT_FAST_DROP_OUT_DURATION = 320; private static final float BACKGROUND_ALPHA = 0.5f; private static final float SCREENSHOT_SCALE = 1f; // 预览图的宽和高 private final int mPreviewWidth; private final int mPreviewHeight; // 异步保存截图的 AsyncTask private AsyncTask清单 12.GlobalScreenshot 的构造方法Void, Void, Void>mSaveInBgTask; // 截屏时发出模拟快门的声音 private MediaActionSoundmCameraSound; // 截屏的屏幕动画 private AnimatorSetmScreenshotAnimation; // 截图的 Bitmap private Bitmap mScreenBitmap; |
我们接着看构造方法中又实现了哪些功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //载加截屏布局文件 mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null); mScreenshotLayout.setOnTouchListener(new View.OnTouchListener() { @Override public booleanonTouch(View v, MotionEvent event) { //拦截和消耗掉所有触摸事件, return true; } }); //截屏后通知图标的大小 mNotificationIconSize = r.getDimensionPixelSize(android.R.dimen.notification_large_icon_height); // 截屏背景的边距 mBgPadding = (float) r.getDimensionPixelSize(R.dimen.global_screenshot_bg_padding); mBgPaddingScale = mBgPadding / mDisplayMetrics.widthPixels; // 加载截屏时发出的声音 mCameraSound = new MediaActionSound(); mCameraSound.load(MediaActionSound.SHUTTER_CLICK); |
前文我们一起查看了截屏动作前的条件初始化,接着我们来看一下系统如何截屏并保存截屏后的图片,这里以全屏截屏方式为讲解示例,我们先看如何获取截屏图片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // GlobalScreenshot::takeScreenshot //省略部分代码… //执行 SurfaceControl.screenshot 方法并返回截屏图片, mScreenBitmap = SurfaceControl.screenshot(crop, width, height, rot); if (mScreenBitmap == null) { notifyScreenshotError(mContext, mNotificationManager, R.string.screenshot_failed_to_capture_text); finisher.run(); return; } //接着再进入 SurfaceControl::screenshot 方法中 public static void screenshot(IBinder display, Surface consumer,int width, int height) { //执行 screenshot 方法 screenshot(display, consumer, new Rect(), width, height, 0, 0, true, false); } // SurfaceControl::screenshot 重截方法, @UnsupportedAppUsage private static void screenshot(IBinder display, Surface consumer, RectsourceCrop, int width, int height, int minLayer, int maxLayer, booleanallLayers, booleanuseIdentityTransform) { if (display == null) { throw new IllegalArgumentException("displayToken must not be null"); } if (consumer == null) { throw new IllegalArgumentException("consumer must not be null"); } nativeScreenshot(display, consumer, sourceCrop, width, height, minLayer, maxLayer, allLayers, useIdentityTransform); } // 一步步跟进执行方法,最后到 SurfaceControl 的本地方法 nativeScreenshot private static native void nativeScreenshot(IBinderdisplayToken, Surface consumer, RectsourceCrop, int width, int height, int minLayer, int maxLayer, booleanallLayers, booleanuseIdentityTransform); |
跟进 SurfaceControl.screenshot(..)
会发现此方法的返回是通过一个本地方法 nativeScreenshot
获取截屏图片,也就是说其截图的实现的通过 JNI 由底层来实现的,并不是在 Java
层实现的,至于 native 层具体的实现这里
再细究,您只需要知道 Java 层传了截屏的截屏区域、宽、高、旋转的度数。如果未成功获取图片 mScreentBitmap==null
则直接 notify 错误信息,再运行 Messenger
跨进程通信将截屏结果告诉截屏请求者;如果成功获取截屏图片,则其会开始截屏播放截屏声明与动画,在动画的结尾处调用了 saveScreenshotInWorkerThread(
来保存截图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //构建动画 ValueAnimatorscreenshotDropInAnim = createScreenshotDropInAnimation(); ValueAnimatorscreenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h, statusBarVisible, navBarVisible); mScreenshotAnimation = new AnimatorSet(); //整合动画 mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim); //添加截屏动画的监听事件 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { saveScreenshotInWorkerThread(finisher); //在异步线程中保存截屏图片 mScreenBitmap = null;//保存图片文件后清除释放截屏位图 //省略部分代码 } }); mScreenshotLayout.post(new Runnable() { @Override public void run() { // Play the shutter sound to notify that we've taken a screenshot mCameraSound.play(MediaActionSound.SHUTTER_CLICK);//播放截屏快门声音 mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null); mScreenshotView.buildLayer(); mScreenshotAnimation.start();//开始播放动画 } }); |
接着会进入异步线程来执行保存截图。在 saveScreenshotInWorkerThread
方法中首先获取 SaveImageInBackgroundData
对象,并对其属性变量赋值,从此类名可以看出它是截屏图片的参数信息类。
1 2 3 4 5 6 7 8 9 10 11 12 13 | private void saveScreenshotInWorkerThread(Runnable finisher) { SaveImageInBackgroundData data = new SaveImageInBackgroundData(); data.context = mContext; data.image = mScreenBitmap;//native 层返回的截屏位图 data.iconSize = mNotificationIconSize;//系统提示中 icon 尺寸 data.finisher = finisher; data.previewWidth = mPreviewWidth;//图片预览宽 data.previewheight = mPreviewHeight;//图片预览高 if (mSaveInBgTask != null) { mSaveInBgTask.cancel(false); } mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data, mNotificationManager).execute();//执行异步线程 } |
SaveImageInBackgroundTask
是 AsyncTask
的子类,用来处理耗时工作的异步任务类,先来看该类的构造方法功能实现。
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。