前言
这是一种很神奇的操作,在截图时隐去特定的内容。这里的隐去并不是指像 FLAG_SECURE
这样无聊的禁止截图(在 Android S+ 表现为对应区域显示为黑色),而是彻底的假装内容不存在。想象一下:你的屏幕上有一个不透明的悬浮窗,但是一截图,那个窗口却和被关闭了一样,直接露出了后面的东西。
之前有听说过类似的需求,比如:打游戏开挂时开直播,不想让外挂界面被看到。
但终究这不是什么好东西,当时也完全没有兴趣。
但是最近突然又有正经的类似需求了,于是研究了一下相关机制,稍做记录。
以下内容基于 Android 12L。
Android 截图操作调用链
先来记录一下 电源 + 音量下 进行截图的调用过程大概是怎样的。
当 java 层第一次有机会拦截按键事件时(interceptKeyBeforeQueueing
),组合键事件会被 KeyCombinationManager
拦截,并按照预先配置好的 TwoKeysCombinationRule
,调用 interceptScreenshotChord()
,从而开始截图之旅。
之后呢,会在对应的 Handler
上执行 ScreenshotRunnable
,调用 DisplayPolicy
的 takeScreenshot()
方法,并在其中再调用 ScreenshotHelper
的 takeScreenshot()
。
ScreenshotHelper
的 takeScreenshot()
中,截图请求以及相关信息会被封装一个 ScreenshotRequest
,并通过连接远程服务的方式,使用 Messenger
传给 frameworks-res
中写死的 config_screenshotServiceComponent
对应的服务,这个服务的默认为 com.android.systemui/com.android.systemui.screenshot.TakeScreenshotService
。
到此为止,运行在 WindowManagerService
的部分结束了,接下来的内容运行在 SystemUI
的上下文。
服务收到请求,对请求进行归类,并拆包取得请求中包含的信息,然后递交给 SystemUI
中的 ScreenshotController
来进一步执行操作。
ScreenshotController
会调用 SurfaceControl
的 captureDisplay()
方法,并最终调用 nativeCaptureDisplay()
走进 native 层中,java 部分到此结束。
ScreenshotController
除了负责转交截图请求外,还负责了截图动画显示、截图完提示窗显示、保存截图之类的操作,我想这是截图操作会通过服务连接的方式交给 SystemUI
完成的核心原因。
离开 java 层后,会进入 android_view_SurfaceControl.cpp
中的 nativeCaptureDisplay()
方法,然后调用 ScreenshotClient
的 captureDisplay()
方法,并最终以 binder ipc 的方式向 SurfaceComposerService
发起调用。
status_t ScreenshotClient::captureDisplay(uint64_t displayOrLayerStack,
const sp<IScreenCaptureListener>& captureListener) {
sp<ISurfaceComposer> s(ComposerService::getComposerService());
if (s == nullptr) return NO_INIT;
return s->captureDisplay(displayOrLayerStack, captureListener);
}
于是,这又是一次进程间通信,运行在 SystemUI
上下文的部分结束了。
那么,SurfaceComposer
的 Bn 端在哪里呢?
诶嘿,就是大名鼎鼎的 SurfaceFlinger
啊。
class SurfaceFlinger : public BnSurfaceComposer,
public PriorityDumper,
private IBinder::DeathRecipient,
private HWC2::ComposerCallback,
private ISchedulerCallback
于是,我们进入了系统服务 SurfaceFlinger
的上下文。
接下来的执行路径,便是 SurfaceFlinger
的 captureDisplay()
-> captureScreenCommon()
-> captureScreenCommon()
-> renderScreenImplLocked()
,最后 getRenderEngine().drawLayers()
把截图渲染出来。
好了,既然调用路径已经清晰了,那么就可以看看,有没有什么地方能够隐去截图中的内容了吧。
屏蔽某些 Layer 的渲染
此处不解释 SurfaceFlinger
的工作过程,我们只需要知道 java 层的 Window
反应到 SurfaceFlinger
中就是一个个的 Layer
。Layer
们根据 z-order 相互叠加和遮盖,渲染后就形成了我们看到的屏幕效果。
也就是说,只需要屏蔽某些 Layer
在截图中的渲染,就可以轻松的做到我们想要的效果。
那么,如何下手呢?
先来看看需要被渲染的 Layer
保存在哪里吧。
status_t SurfaceFlinger::renderScreenImplLocked(
const RenderArea& renderArea, TraverseLayersFunction traverseLayers,
const std::shared_ptr<renderengine::ExternalTexture>& buffer,
bool canCaptureBlackoutContent, bool regionSampling, bool grayscale,
ScreenCaptureResults& captureResults) {
......
// 要渲染的层信息列表
std::vector<compositionengine::LayerFE::LayerSettings> clientCompositionLayers;
......
// 描述背景层 即这个区域如果没有任何 layer 覆盖的话会显示为黑色
compositionengine::LayerFE::LayerSettings fillLayer;
fillLayer.source.buffer.buffer = nullptr;
fillLayer.source.solidColor = half3(0.0, 0.0, 0.0);
fillLayer.geometry.boundaries =
FloatRect(sourceCrop.left, sourceCrop.top, sourceCrop.right, sourceCrop.bottom);
fillLayer.alpha = half(alpha);
// 把背景层加入渲染列表中
clientCompositionLayers.push_back(fillLayer);
......
// 遍历所有可用 layer
traverseLayers([&](Layer* layer) {
......
compositionengine::LayerFE::ClientCompositionTargetSettings targetSettings{
clip,
layer->needsFilteringForScreenshots(display.get(), transform) ||
renderArea.needsFiltering(),
renderArea.isSecure(),
useProtected,
clearRegion,
layerStackSpaceRect,
clientCompositionDisplay.outputDataspace,
true, /* realContentIsVisible */
false, /* clearContent */
disableBlurs ? compositionengine::LayerFE::ClientCompositionTargetSettings::
BlurSetting::Disabled
: compositionengine::LayerFE::ClientCompositionTargetSettings::
BlurSetting::Enabled,
};
std::vector<compositionengine::LayerFE::LayerSettings> results =
layer->prepareClientCompositionList(targetSettings);
if (results.size() > 0) {
......
// 把这个层放进渲染列表里
clientCompositionLayers.insert(clientCompositionLayers.end(),
std::make_move_iterator(results.begin()),
std::make_move_iterator(results.end()));
......
}
});
// clientCompositionLayerPointers 来自于对 clientCompositionLayers 的指针萃取
// 也就是说要渲染的层应该被放进 clientCompositionLayers 里
std::vector<const renderengine::LayerSettings*> clientCompositionLayerPointers(
clientCompositionLayers.size());
std::transform(clientCompositionLayers.begin(), clientCompositionLayers.end(),
clientCompositionLayerPointers.begin(),
std::pointer_traits<renderengine::LayerSettings*>::pointer_to);
......
// 可以看到,最终描述绘制内容的是 clientCompositionLayerPointers
getRenderEngine().drawLayers(clientCompositionDisplay, clientCompositionLayerPointers, buffer,
kUseFramebufferCache, std::move(bufferFence), &drawFence);
......
}
分析代码的时候是从下往上看的,从 drawLayers()
开始,找出要渲染的东西来自哪里。当然这里为了方便就把分析结果作为注释直接打上去了,省略了分析过程。
这里牵扯到了 traverseLayers()
这个神奇的东西,一眼可能并不能看懂它是个啥,但仿佛有一种 Linux 内核里 for_each_xxx()
宏的感觉,不过还是不太一样的。那就来分析一下这个东西顺便看看 modern cpp 的阴间写法。
首先从参数列表里可以看到 TraverseLayersFunction traverseLayers
,这个东西是个 TraverseLayersFunction
类型的玩意儿。而在 SurfaceFlinger.h
中我们可以找到对这个类型的定义。
using TraverseLayersFunction = std::function<void(const LayerVector::Visitor&)>;
嗯,本质上是个函数指针嘛。
而关于它的参数的类型定义,可以在 LayerVector.h
中找到。
using Visitor = std::function<void(Layer*)>;
诶,也就是说,其实这个 traverseLayers
是一个参数为 void(Layer*)
的高阶函数。
那再来顺着找找这个变量是在哪里被初始化赋值的。
auto traverseLayers = [this, args, layerStack](const LayerVector::Visitor& visitor) {
traverseLayersInLayerStack(layerStack, args.uid, visitor);
};
可以看到,对它的赋值采用了 lambda 表达式的形式,并在其中调用了 traverseLayersInLayerStack()
。
void SurfaceFlinger::traverseLayersInLayerStack(ui::LayerStack layerStack, const int32_t uid,
const LayerVector::Visitor& visitor) {
// We loop through the first level of layers without traversing,
// as we need to determine which layers belong to the requested display.
for (const auto& layer : mDrawingState.layersSortedByZ) {
if (!layer->belongsToDisplay(layerStack)) {
continue;
}
// relative layers are traversed in Layer::traverseInZOrder
layer->traverseInZOrder(LayerVector::StateSet::Drawing, [&](Layer* layer) {
if (layer->getPrimaryDisplayOnly()) {
return;
}
if (!layer->isVisible()) {
return;
}
if (uid != CaptureArgs::UNSET_UID && layer->getOwnerUid() != uid) {
return;
}
visitor(layer);
});
}
}
循环出现了!mDrawingState.layersSortedByZ
是一个 SortedVector
,可以理解为按照纵向高度进行排序的 Layer
数组。
这里所作的事情是遍历它,并对所有符合标准的 Layer
执行传入的回调函数。
(这里其实是套了两层,但实际上表达的逻辑应该没有错)
那么事情已经很明确了, traverseLayers()
的作用就是对符合标准的 Layer
们每一个执行一次传入的函数。
而我们上面看到的
// 遍历所有可用 layer
traverseLayers([&](Layer* layer) {
......
compositionengine::LayerFE::ClientCompositionTargetSettings targetSettings{
clip,
layer->needsFilteringForScreenshots(display.get(), transform) ||
renderArea.needsFiltering(),
renderArea.isSecure(),
useProtected,
clearRegion,
layerStackSpaceRect,
clientCompositionDisplay.outputDataspace,
true, /* realContentIsVisible */
false, /* clearContent */
disableBlurs ? compositionengine::LayerFE::ClientCompositionTargetSettings::
BlurSetting::Disabled
: compositionengine::LayerFE::ClientCompositionTargetSettings::
BlurSetting::Enabled,
};
std::vector<compositionengine::LayerFE::LayerSettings> results =
layer->prepareClientCompositionList(targetSettings);
if (results.size() > 0) {
......
// 把这个层放进渲染列表里
clientCompositionLayers.insert(clientCompositionLayers.end(),
std::make_move_iterator(results.begin()),
std::make_move_iterator(results.end()));
......
}
});
所做的事情便是:为每一个符合标准的 Layer
构建一个 ClientCompositionTargetSettings
结构体,然后执行该 Layer
的 prepareClientCompositionList()
方法来进行一个预处理,并根据返回结果决定是否要把该 Layer
加入到渲染列表中。
预处理的过程就不分析了(因为不是很确定这里会不会牵扯到运行时多态,没时间细看了)。
所以,我们该如何屏蔽一个层呢?
很简单,在传入的函数中一开始就跳过这个层即可。
当然,也可以采用如修改 targetSettings
中的 realContentIsVisible
之类的方法,不过本质上的原理应该是一样的,都是不把这个层放入到 clientCompositionLayers
列表中。
Android 自带的更标准实现
没错,其实系统已经在默默的使用类似的操作了,只不过我一直没有发现。。
在 traverseLayersInLayerStack()
有一个判断
void SurfaceFlinger::traverseLayersInLayerStack(ui::LayerStack layerStack, const int32_t uid,
const LayerVector::Visitor& visitor) {
......
for (const auto& layer : mDrawingState.layersSortedByZ) {
......
layer->traverseInZOrder(LayerVector::StateSet::Drawing, [&](Layer* layer) {
if (layer->getPrimaryDisplayOnly()) {
return;
}
......
visitor(layer);
});
}
}
bool Layer::getPrimaryDisplayOnly() const {
const State& s(mDrawingState);
if (s.flags & layer_state_t::eLayerSkipScreenshot) {
return true;
}
sp<Layer> parent = mDrawingParent.promote();
return parent == nullptr ? false : parent->getPrimaryDisplayOnly();
}
eLayerSkipScreenshot
草。
嗯,在 Java 中也有对这个属性的赋值操作,至于属性从 Java 向 Native 层的传递这里就不分析了。
不过可以提一嘴的是,WindowManager LayoutParams 并不会原封不动的传进 Native 里,所以想要借助它传递额外信息还是算了吧。
// SurfaceControl.java
/**
* Adds or removes the flag SKIP_SCREENSHOT of the surface. Setting the flag is equivalent
* to creating the Surface with the {@link #SKIP_SCREENSHOT} flag.
*
* @hide
*/
public Transaction setSkipScreenshot(SurfaceControl sc, boolean skipScrenshot) {
checkPreconditions(sc);
if (skipScrenshot) {
nativeSetFlags(mNativeObject, sc.mNativeObject, SKIP_SCREENSHOT, SKIP_SCREENSHOT);
} else {
nativeSetFlags(mNativeObject, sc.mNativeObject, 0, SKIP_SCREENSHOT);
}
return this;
}
我们只需要操作这个属性,就可以轻松的获得我们想要达到的效果,甚至还有一些额外的 buff ,比如录屏也录不到,外加不会显示非主显示器的任何其它屏幕上(外接显示器时)(投屏应该也投不出来)(简直就是强大极了)。
默认情况下,只有一个东西在使用这个神奇的属性。
// WindowStateAnimator.java
if ((mWin.mAttrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0) {
flags |= SurfaceControl.SKIP_SCREENSHOT;
}
那就是 “屏幕圆角叠加层” 是一个用来使显示和屏幕边框更加契合的东西。
所以我们只需要
mWindowLayoutParams.privateFlags |=
WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
就可以快速获得 “隐身于截图” 的待遇。
当然,这个 Flag 是有鉴权的,应用想要滥用应该是没那么简单的。
总结
这么看了一大圈,还是挺有意思的,牵扯到的东西也挺多。
当然,最后的结论非常简单,什么修改 SurfaceFlinger 都是不需要的。
想要让你的 Window 隐身于截图,就在其 WindowManager.LayoutParams
的 privateFlags
加上 PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY
即可,不仅隐身于截图,还附带隐身于录屏的 buff 。
不过,需要系统级 app 的权限,可能还有隐藏 api 的限制?
请问常规App有可行方案么
没有,常规 app 最多做到 FLAG_SECURE 或 DRM 加密,这种东西在截图中会显示为黑色区域(或者干脆整个页面无法截图),无法完全隐藏穿透。
普通app可以用,直接反射调用SurfaceControl.setSkipScreenshot
已经解决12 以上的问题,但是低于12的连 SKIP_SCREENSHOT 这个东西都没有,不知道突破口在哪里
Color14有没有方法重现该方法 利用类似于悬浮球和侧边栏自由翻译的 截图不显现于图片上 C13.0.0.3F太老了
手上没有 C14 的设备了,不过思路都已经写在文章里了,有时间可以自己研究下
栓q