Android 截屏——系统截屏方法分析

最近需要通过Android应用截取当前屏幕内容,但是由于使用了百度的地图SDK,地图view位置采用网络提供的

View.getDrawingCache()方法截取的总是黑色,百度SDK里面针对地图提供了截屏回调,但是从外部开始调用到函数执行需要4s以上的时间,显然超出了体验标准。但是对比通过电源键+音量下按键截取的内容就比较正常,而且速度较快。所以最近三天都在研究截图相关的方法,看了很多帖子,基本上大同小异,但是我的问题却没有解决。只能先将他人的智慧结晶消化一下先。


如下文章是讲解Android手机系统自带的截屏业务逻辑的,含金量不错。

=====================START====================

Android手机一般都自带有手机屏幕截图的功能:在手机任何界面(当然手机要是开机点亮状态),通过按组合键,屏幕闪一下,然后咔嚓一声,截图的照片会保存到当前手机的图库中,真是一个不错的功能!

 

以我手头的测试手机为例,是同时按电源键+音量下键来实现截屏,苹果手机则是电源键 +HOME键,小米手机是菜单键+音量下键,而HTC一般是按住电源键再按左下角的“主页”键。那么Android源码中使用组合键是如何实现屏幕截图功能呢?前段时间由于工作的原因仔细看了一下,这两天不忙,便把相关的知识点串联起来整理一下,分下面两部分简单分析下实现流程:


Android源码中对组合键的捕获。


Android源码中对按键的捕获位于文件PhoneWindowManager.java(alps\frameworks\base\policy\src\com\android\internal\policy\impl)中,这个类处理所有的键盘输入事件,其中函数interceptKeyBeforeQueueing()会对常用的按键做特殊处理。以我手头的测试机为例,是同时按电源键和音量下键来截屏,那么在这个函数中我们会看到这么两段代码:


.......
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_MUTE: {
<span style="white-space:pre">	</span>if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
<span style="white-space:pre">		</span>if (down) {
<span style="white-space:pre">			</span>if (isScreenOn && !mVolumeDownKeyTriggered
<span style="white-space:pre">					</span>&& (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
<span style="white-space:pre">				</span>mVolumeDownKeyTriggered = true;
<span style="white-space:pre">				</span>mVolumeDownKeyTime = event.getDownTime();
<span style="white-space:pre">				</span>mVolumeDownKeyConsumedByScreenshotChord = false;
<span style="white-space:pre">				</span>cancelPendingPowerKeyAction();
<span style="white-space:pre">				</span>interceptScreenshotChord();
<span style="white-space:pre">			</span>}
<span style="white-space:pre">		</span>} else {
<span style="white-space:pre">			</span>mVolumeDownKeyTriggered = false;
<span style="white-space:pre">			</span>cancelPendingScreenshotChordAction();
<span style="white-space:pre">		</span>}
......

case KeyEvent.KEYCODE_POWER: {
<span style="white-space:pre">	</span>result &= ~ACTION_PASS_TO_USER;
<span style="white-space:pre">		</span>if (down) {
<span style="white-space:pre">			</span>if (isScreenOn && !mPowerKeyTriggered
                            && (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
<span style="white-space:pre">				</span>mPowerKeyTriggered = true;
<span style="white-space:pre">				</span>mPowerKeyTime = event.getDownTime();
<span style="white-space:pre">				</span>interceptScreenshotChord();
<span style="white-space:pre">			</span>}
......

可以看到正是在这里(响应Down事件)捕获是否按了音量下键和电源键的,而且两个地方都会进入函数interceptScreenshotChord()中,那么接下来看看这个函数干了什么工作:


private void interceptScreenshotChord() {
	if (mVolumeDownKeyTriggered && mPowerKeyTriggered
			&& !mVolumeUpKeyTriggered) {
		final long now = SystemClock.uptimeMillis();
		if (now <= mVolumeDownKeyTime
				+ SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS
				&& now <= mPowerKeyTime
						+ SCREENSHOT_CHORD_DEBOUNCE_DELAY_MILLIS) {
			mVolumeDownKeyConsumedByScreenshotChord = true;
			cancelPendingPowerKeyAction();
			mHandler.postDelayed(mScreenshotChordLongPress,
					ViewConfiguration.getGlobalActionKeyTimeout());
		}
	}
}


在这个函数中,用两个布尔变量判断是否同时按了音量下键和电源键后,再计算两个按键响应Down事件之间的时间差不超过150毫秒,也就认为是同时按了这两个键后,算是真正的捕获到屏幕截屏的组合键。

附言:文件PhoneWindowManager.java类是拦截键盘消息的处理类,在此类中还有对home键、返回键等好多按键的处理。


Android源码中调用屏幕截图的接口


捕获到组合键后,我们再看看android源码中是如何调用屏幕截图的函数接口。在上面的函数interceptScreenshotChord中我们看到用handler判断长按组合键500毫秒之后,会进入如下函数:


private final Runnable mScreenshotChordLongPress = new Runnable() {
	public void run() {
		takeScreenshot();
	}
};

在这里启动了一个线程来完成截屏的功能,接着看函数takeScreenshot():

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)) {
			mScreenshotConnection = conn;
			mHandler.postDelayed(mScreenshotTimeout, 10000);
		}
	}
}

可以看到这个函数使用AIDL绑定了service服务到"com.android.systemui.screenshot.TakeScreenshotService",注意在service连接成功时,对message的msg.arg1和msg.arg2两个参数的赋值。其中在mScreenshotTimeout中对服务service做了超时处理。接着我们找到实现这个服务service的类TakeScreenshotService,看看其实现的流程:


public class TakeScreenshotService extends Service {
	private static final String TAG = "TakeScreenshotService";

	private static GlobalScreenshot mScreenshot;

	private Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case 1:
				final Messenger callback = msg.replyTo;
				if (mScreenshot == null) {
					mScreenshot = new GlobalScreenshot(
							TakeScreenshotService.this);
				}
				mScreenshot.takeScreenshot(new Runnable() {
					@Override
					public void run() {
						Message reply = Message.obtain(null, 1);
						try {
							callback.send(reply);
						} catch (RemoteException e) {
						}
					}
				}, msg.arg1 > 0, msg.arg2 > 0);
			}
		}
	};

	@Override
	public IBinder onBind(Intent intent) {
		return new Messenger(mHandler).getBinder();
	}
}

在这个类中,我们主要看调用接口,用到了mScreenshot.takeScreenshot()传递了三个参数,第一个是个runnable,第二和第三个是之前message传递的两个参数msg.arg1和msg.arg2。最后我们看看这个函数takeScreenshot(),位于文件GlobalScreenshot.java中(跟之前的函数重名但是文件路径不一样):


/**
 * Takes a screenshot of the current display and shows an animation.
 */
void takeScreenshot(Runnable finisher, boolean statusBarVisible,
		boolean navBarVisible) {
	// We need to orient the screenshot correctly (and the Surface api seems
	// to take screenshots
	// only in the natural orientation of the device :!)
	mDisplay.getRealMetrics(mDisplayMetrics);
	float[] dims = { mDisplayMetrics.widthPixels,
			mDisplayMetrics.heightPixels };
	float degrees = getDegreesForRotation(mDisplay.getRotation());
	boolean requiresRotation = (degrees > 0);
	if (requiresRotation) {
		// Get the dimensions of the device in its native orientation
		mDisplayMatrix.reset();
		mDisplayMatrix.preRotate(-degrees);
		mDisplayMatrix.mapPoints(dims);
		dims[0] = Math.abs(dims[0]);
		dims[1] = Math.abs(dims[1]);
	}

	// Take the screenshot
	mScreenBitmap = Surface.screenshot((int) dims[0], (int) dims[1]);
	if (mScreenBitmap == null) {
		notifyScreenshotError(mContext, mNotificationManager);
		finisher.run();
		return;
	}

	if (requiresRotation) {
		// Rotate the screenshot to the current orientation
		Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels,
				mDisplayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
		Canvas c = new Canvas(ss);
		c.translate(ss.getWidth() / 2, ss.getHeight() / 2);
		c.rotate(degrees);
		c.translate(-dims[0] / 2, -dims[1] / 2);
		c.drawBitmap(mScreenBitmap, 0, 0, null);
		c.setBitmap(null);
		mScreenBitmap = ss;
	}

	// Optimizations
	mScreenBitmap.setHasAlpha(false);
	mScreenBitmap.prepareToDraw();

	// Start the post-screenshot animation
	startAnimation(finisher, mDisplayMetrics.widthPixels,
			mDisplayMetrics.heightPixels, statusBarVisible, navBarVisible);
}

这段代码的注释比较详细,其实看到这里,我们算是真正看到截屏的操作了,具体的工作包括对屏幕大小、旋转角度的获取,然后调用Surface类的screenshot方法截屏保存到bitmap中,之后把这部分位图填充到一个画布上,最后再启动一个延迟的拍照动画效果。如果再往下探究screenshot方法,发现已经是一个native方法了:


/**
 * Like {@link #screenshot(int, int, int, int)} but includes all Surfaces in
 * the screenshot.
 * 
 * @hide
 */
public static native Bitmap screenshot(int width, int height);

使用JNI技术调用底层的代码,如果再往下走,会发现映射这这个jni函数在文件android_view_Surface.cpp中,这个真的已经是底层c++语言了,统一调用的底层函数是:


static jobject doScreenshot(JNIEnv* env, jobject clazz, jint width, jint height,
        jint minLayer, jint maxLayer, bool allLayers)
{
    ScreenshotPixelRef* pixels = new ScreenshotPixelRef(NULL);
    if (pixels->update(width, height, minLayer, maxLayer, allLayers) != NO_ERROR) {
        delete pixels;
        return 0;
    }

    uint32_t w = pixels->getWidth();
    uint32_t h = pixels->getHeight();
    uint32_t s = pixels->getStride();
    uint32_t f = pixels->getFormat();
    ssize_t bpr = s * android::bytesPerPixel(f);

    SkBitmap* bitmap = new SkBitmap();
    bitmap->setConfig(convertPixelFormat(f), w, h, bpr);
    if (f == PIXEL_FORMAT_RGBX_8888) {
        bitmap->setIsOpaque(true);
    }

    if (w > 0 && h > 0) {
        bitmap->setPixelRef(pixels)->unref();
        bitmap->lockPixels();
    } else {
        // be safe with an empty bitmap.
        delete pixels;
        bitmap->setPixels(NULL);
    }


    return GraphicsJNI::createBitmap(env, bitmap, false, NULL);
}

由于对C++不熟,我这里就不敢多言了。其实到这里,算是对手机android源码中通过组合键屏幕截图的整个流程有个大体了解了,一般我们在改动中熟悉按键的捕获原理,并且清楚调用的截屏函数接口即可,如果有兴趣的,可以继续探究更深的底层是如何实现的。

====================END===============================




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值