普通视图

发现新文章,点击刷新页面。
昨天以前mikaelzero

VirtualDisplay中APP的生命周期处理

作者 mikaelzero
2023年4月20日 08:00

当我们想将一个 Activity 或者是 APP 切换到后台时,可以使用 moveStack 的方法或者直接回到 Home,这样 Activity 就会从 resumed 状态变更为 stopped 状态。但是在虚拟屏上的逻辑可不是这样,当我们切换到 Home,只会将 Home 所在的 Display 的 Stack 进行切换,而 Home 类型的 ActivityStack 只会在默认屏幕上。因此,如果想在主屏幕上切换到 Home 时,同时也想把虚拟屏上的生命周期进行处理,就需要自己手动进行切换。

VirtualDisplay 中有一个方法setDisplayState,是用来设置 on/off 的状态,参数为 boolean

public void setDisplayState(boolean isOn) {
if (mToken != null) {
mGlobal.setVirtualDisplayState(mToken, isOn);
}
}

通过该方法可以切换 VirtualDisplay 的显示状态,从而来暂停或者恢复 VirtualDisplay 中的 Activity 的状态。接下来看看它的原理是怎样的。

该函数的调用链如下:

DisplayManagerGlobal::setVirtualDisplayState
DMS::setVirtualDisplayStateInternal
VirtualDisplayAdapter::setVirtualDisplayStateLocked
VirtualDisplayDevice::setDisplayState
DisplayAdapter::sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED)
DMS::handleDisplayDeviceChanged

handleDisplayDeviceChanged 中会发送一个EVENT_DISPLAY_CHANGED 消息,会回调所有的监听者,监听的方法为 onDisplayChanged。RootActivityContainer 注册了该 Linstener,在 RootActivityContainer 的 onDisplayChanged 中会循环调用 ActivityDisplay 的 onDisplayChanged 方法,如下:

void onDisplayChanged() {
// The window policy is responsible for stopping activities on the default display.
final int displayId = mDisplay.getDisplayId();
if (displayId != DEFAULT_DISPLAY) {
final int displayState = mDisplay.getState();
if (displayState == Display.STATE_OFF && mOffToken == null) {
mOffToken = mService.acquireSleepToken("Display-off", displayId);
} else if (displayState == Display.STATE_ON && mOffToken != null) {
mOffToken.release();
mOffToken = null;
}
}
......
}

如果 displayState == Display.STATE_OFF 成立,则会调用 ATMS(ActivityTaskManagerService) 的 acquireSleepToken,该方法中会创建需要 sleep 的所有 Token,之后在 updateSleepIfNeededLocked 中,调用 applySleepTokens

mRootActivityContainer.applySleepTokens(true /* applyToStacks */);

applySleepTokens 会根据上面生成的 token 列表中,来判断是否需要休眠,如果是的话,则会调用对应 ActivityStack 的 goToSleepIfPossible,该方法就会将 ActivityStack 下的所有 Activity 切换为 stopped

AMS拦截并启动虚拟屏到Unity的面板中

作者 mikaelzero
2023年4月11日 08:00

需求场景

  1. Unity 中存在一个面板,这个面板是用来显示 2D 应用的画面的
  2. Unity 中的面板是 Unity 创建的,它接收的是一个 TextureID,并创建 Shader 后根据纹理 ID 来渲染
  3. 在 android 系统层面,需要拦截所有的 2D 应用并将它们显示到 Unity 的面板中
  4. 且这些 2D 应用都是显示到虚拟屏幕中,也就是 VirtualDisplay

Unity 部分

Unity 部分比较简单,使用自带的接口根据 TextureId 来创建 2D 纹理并且创建对应的 Shader 即可

Shader "Custom/HwAndroid"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}

SubShader
{

Cull Off
Pass
{

GLSLPROGRAM

#ifdef VERTEX

varying vec2 uv;
void main()
{
uv = gl_MultiTexCoord0.xy;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

#endif


#ifdef FRAGMENT

#extension GL_OES_EGL_image_external_essl3 : require
uniform samplerExternalOES _MainTex;
varying vec2 uv;
void main()
{
gl_FragColor = texture2D(_MainTex, uv);
}

#endif

ENDGLSL
}
}

FallBack "Unlit/Texture"
}

Unity 的脚本部分:

textureId = nativeUnityHolder.Call<int>("createNativeTexture", 1920, 1080, pkg);
texture = Texture2D.CreateExternalTexture(1920, 1080, TextureFormat.RGBA32, false, false, (IntPtr)textureId);
texture.wrapMode = TextureWrapMode.Clamp;
texture.filterMode = FilterMode.Bilinear;
GetComponent<MeshRenderer>().material.shader = Shader.Find("Custom/HwAndroid");

createNativeTexture 是 native 的一个 SDK 中的方法,主要作用就是创建 Surface 以及 SurfaceTexture,大致如下:

private int createGLTexture(int width, int height) {
int[] textures = new int[1];
GLES30.glGenTextures(1, textures, 0);
int textureId = textures[0];
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);

GLES30.glGetError();
return textureId;
}

private void attachGLTexture(int textureId) {
mTextureId = textureId;
mSurfaceTexture = new SurfaceTexture(mTextureId);
mSurfaceTexture.setDefaultBufferSize(mSurfaceFrame.width(), mSurfaceFrame.height());
mSurfaceTexture.setOnFrameAvailableListener(this);

mSurface = new Surface(mSurfaceTexture);
}

SurfaceTexture

SurfaceTexture 和 Surface 以及 VirtualDisplay 三者之间的关系是:

VirtualDisplay 会接收一个 Surface 参数,会将虚拟屏上的画面画在这个 Surface 上,而 SurfaceTexture 是可以从 Surface 中,将画面转为纹理,而 Unity 则可以根据这个纹理 ID 来渲染。所以流程分以下三步:

  1. 在 Server 进程中,根据包名创建虚拟屏,并保存
  2. 创建完成后发送消息给 Unity 的 SDK,SDK 根据包名创建对应的 Surface 和 SurfaceTexture,并返回 textureID 给 Unity
  3. SDK 将 Surface 返回给 Server 进程,传递给 VirtualDisplay

VirtualDisplay

每个 2D 应用在 VR 系统中都是无法直接显示的,因为都会直接显示到主屏幕中,就无法根据 VR 眼镜的转动而渲染。

所以一个解决方案是将所有的 2D 应用显示到虚拟屏幕中,创建虚拟屏幕时有一些参数:

VirtualDisplay virtualDisplay = dm.createVirtualDisplay(packageName, width, height, 240, null,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC);

其中的 Surface,是我们可以操作的地方,我们可以在将 2D 启动到虚拟屏幕后,手动设置这个 Surface,这个 Surface 就是上面 Unity 部分创建的 Surface,这样 Unity 就能够获取到虚拟屏幕的画面。

拦截部分的代码是写在 ActivityStater 的 startActivity 中,大约 795 行后,在创建 ActivityRecord 之前,重新设置一遍 checkedOptions。

代码如下:

public ActivityOptions handleStartActivity(Context context, RootActivityContainer mRootActivityContainer, Intent intent, ActivityOptions options) {
String packageName = intent.getComponent().getPackageName();
//这里需要过滤一下包名,不仅仅是3D,其他的系统应用也要过滤
if (3DUtils.isVrApp(packageName) > 0) {
return options;
}
if (options == null) {
options = ActivityOptions.makeBasic();
}
int virtualDisplayId = VirtualServiceImpl.getInstance().createVirtualDisplay(packageName, 1920, 1080);
options.setLaunchDisplayId(virtualDisplayId);
return options;
}

修改后发现,在 handleStartActivity 方法中的 virtualDisplayId 为正确的值,也就是非 0,但是最终在 dumps activitys 时,2D 总是显示在主屏幕上,说明在拦截了之后,肯定还有个地方修改了 displayId。

当 Activity 在启动过程中,在 startActivityUnchecked 方法下,会调用 mSupervisor.getLaunchParamsController().calculate 然后转到 TaskLaunchParamsModifier 的 calculate,该方法的 getPreferredLaunchDisplay 会去校验一下 DisplayId 对应的 ActivityDisplay

private int getPreferredLaunchDisplay(... ActivityOptions options ...) {
......
int displayId = INVALID_DISPLAY;
final int optionLaunchId = options != null ? options.getLaunchDisplayId() : INVALID_DISPLAY;
if (optionLaunchId != INVALID_DISPLAY) {
displayId = optionLaunchId;
}
......
if (displayId != INVALID_DISPLAY
&& mSupervisor.mRootActivityContainer.getActivityDisplay(displayId) == null) {
displayId = currentParams.mPreferredDisplayId;
}
......
}

如果 mRootActivityContainer.getActivityDisplay 获取到的 ActivityDisplay 为 null,displayId 就变为 0 了,所以自然无法启动到指定的虚拟屏上了。

那么为什么明明启动了虚拟屏,却没有找到呢?说明肯定有个地方是异步的,AOSP 中到处都是 Handler 通信,所以创建完了虚拟屏后,肯定做了异步处理。

果不其然,当调用 createVirtualDisplay 创建虚拟屏后,一路走到 DMS 的 handleDisplayDeviceAddedLocked 方法,该方法会调用 addLogicalDisplayLocked,之后会发送一个 Handler 的 Message

sendDisplayEventLocked(displayId, DisplayManagerGlobal.EVENT_DISPLAY_ADDED);

该方法会通过 Handler 发送一条消息,最终在 RootActivityContainer 中会进行接收,回调 RootActivityContainer 的 onDisplayAdded 后,通过 getActivityDisplayOrCreate 创建 ActivityDisplay。

既然如此,那就必须等到 ActivityDisplay 创建完毕才能启动我们的 Activity。

一开始想到的是在自己的服务类中接收到 onDisplayAdded 后再处理 Activity 的启动,当时这个方案改动太大且影响了原有 Activity 启动逻辑,所以废弃。

既然最终是要在 RootActivityContainer 中通过 getActivityDisplayOrCreate 创建 ActivityDisplay,那我手动调用 getActivityDisplayOrCreate 不就行了?

所以这部分的代码改动如下:

public ActivityOptions handleStartActivity(Context context, RootActivityContainer mRootActivityContainer, Intent intent, ActivityOptions options) {
......
int virtualDisplayId = VirtualServiceImpl.getInstance().createVirtualDisplay(packageName, 1920, 1080);
//getActivityDisplayOrCreate 方法需要手动设置为 public
mRootActivityContainer.getActivityDisplayOrCreate(virtualDisplayId);
options.setLaunchDisplayId(virtualDisplayId);
return options;
}

总结

总结几个注意点:

  1. 自己写的服务需要在 Server 进程,这样才能和 Server 进程中的 AMS 通信,无需用 Binder 来处理
  2. 写完后记得 make update api
  3. Unity 进程传递 Surface 到 Server 进程需要用到

MPAndroidChart踩坑

作者 mikaelzero
2023年9月6日 08:00

打印 value 的值时出现 1.7E 这类文本

需要先将 value.toInt()之后再 toString

X 轴最后一个标签不显示

axisMaximum 需要设置正确,假设要显示 5 个 X 轴,那么 axisMinimum 为 0f,axisMaximum 就需要设置为 4f 而不是 5f,否则会出现 6 个点,0 到 5 是 6 个点

X 轴出现相同的 Label

当我请求接口之后,会更新数据,出现在于 Viewpager2 滑动后再滑动回来的情况下,出现了相同的 Label

发现在 AxisRenderer 中的 computeAxisValues 方法中,range 不对,最终发现是 setVisibleXRangeMaximum 方法导致的。

setNoDataText 无效

注意 setNoDataText 的调用时机,我的问题出在项目框架中 Base 中没有提供 initData 之前的回调,initData 是在 resume 调用的,导致在显示出来的一帧并没有用到 setNoDataText,需要将 setNoDataText 提前到 create 等生命周期中

setCircleColor 无效

在最新的 3.1.0 的源码中 LineChartRenderer 里,drawCircles 做了修改,增加了 DataSetImageCache,导致 boolean changeRequired = imageCache.init(dataSet);这段代码返回的是 false,就不会调用imageCache.fill(dataSet, drawCircleHole, drawTransparentCircleHole);,imageCache.fill 中会重新 drawCircle,同时也会更新 color

protected boolean init(ILineDataSet set) {

int size = set.getCircleColorCount();
boolean changeRequired = false;

if (circleBitmaps == null) {
circleBitmaps = new Bitmap[size];
changeRequired = true;
} else if (circleBitmaps.length != size) {
circleBitmaps = new Bitmap[size];
changeRequired = true;
}

return changeRequired;
}

在 init 中只判断了 circleBitmaps 是否为 null,但是如果更改了颜色的话,circleBitmaps 是不为 null 的,所以这里逻辑是有问题的,在原项目的 pull request 中也能找到解决方案 https://github.com/PhilJay/MPAndroidChart/pull/5231/files#diff-bce659457c538f8f1d5143d3694d99430338adc9ff68d7010e50562e1956fd41 ,或者直接把 changeRequired 这个判断给注释了

MarkView 固定显示在中心

源码中并不支持总是显示 Markview,如果在 Highlight 的值为空的情况下是不绘制 markview 的,所以需要通过继承 LineChart 来重写 drawMarkers 方法

class AlwaysShowMarkViewLineChart : LineChart {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle)

override fun drawMarkers(canvas: Canvas) {
// if there is no marker view or drawing marker is disabled
if (mMarker == null || !isDrawMarkersEnabled) {
if (mMarker != null) {
mMarker.draw(canvas, 0f, 0f)
}
return
}
if (!valuesToHighlight()) {
return
}
for (highlight in mIndicesToHighlight) {
val set: IDataSet<Entry?> = mData.getDataSetByIndex(highlight.dataSetIndex)
val e = mData.getEntryForHighlight(highlight)
val entryIndex = set.getEntryIndex(e)

// make sure entry not null
if (e == null || entryIndex > set.entryCount * mAnimator.phaseX) {
mMarker.draw(canvas, 0f, 0f)
continue
}
val pos = getMarkerPosition(highlight)

// check bounds
if (!mViewPortHandler.isInBounds(pos[0], pos[1])) {
mMarker.draw(canvas, 0f, 0f)
continue
}

// callbacks to update the content
mMarker.refreshContent(e, highlight)

// draw the marker
mMarker.draw(canvas, pos[0], pos[1])
}
}
}

并且,如果想要一直居中,还需要在自定义的 MarkerView 中做下判断

override fun draw(canvas: Canvas?, posX: Float, posY: Float) {
if (needOverrideDraw) {
super.draw(canvas, (chartView.width - viewPortHandler.contentLeft()) / 2f + viewPortHandler.contentLeft() , chartView.height.toFloat())
} else {
super.draw(canvas, posX, posY)
}
}

重新加载数据出现闪动

场景是,原本可能有 10 条数据,本身有较为明显的曲线效果,然后重新加载了新的数据后,定位到了指定的位置,这时候要调用 centerTo 等方法来定位到某个点,这就导致会从原先的曲线变换到另一个曲线,即使使用 centerViewToAnimated 的方式,也会慢慢从一个点到另一个点,如果直接使用 centerViewTo,那么就会闪动一下,会将最新的曲线显示出来,如果和原曲线差距过大,就有明显的闪动。

解决方案是,加一个动画 animateY,也就是 phaseY 这个属性,这样在一开始绘制的时候,新曲线会直接绘制在 0

2D应用大小在VR系统中的显示问题

作者 mikaelzero
2023年6月13日 08:00

场景

在 VR 系统中,显示一个 2D 应用的方案基本上都是通过 VirtualDisplay 的方式,其中最基本的功能就是需要保证能够正常显示。

在开发过程中出现了 2D 应用各种比例不对,View 显示不正常的现象,以此记录。

DPI

首先,VR 眼镜的分辨率一般都非常高,低的都有 4K,高的则 5K 甚至 8K,那么如果一个 app 显示到 5K 分辨率上的屏幕,即时自己的 app 能够进行适配达到正常显示,你无法规避那些其他第三方的 app 也适配各种分辨率,因此,显示 2D 的 virtualDisplay 需要保持一个正常的分辨率和 DPI。

一般而言,1920*1080 分辨率的屏幕,各类 app 显示都不会有太大的问题,因此分辨率设置为 1920*1080 或者更小的分辨率,当然设置大一点也行,但是会影响到给 Unity 的 texture 大小,所以尽量小一些,小一半也可以。

主要还是 DPI 需要设置正确,DPI 的大小和分辨率和屏幕大小相关,但是我们不能拿主屏幕的屏幕大小,因为太大了,所以选择一个常见的 4.95 英寸的屏幕大小是一个比较好的选择。

那么计算 DPI 的代码大致如下:

// 屏幕分辨率
int width = 1080;
int height = 1920;
// 屏幕大小(对角线长度)
double screen_size = 4.95; // 单位:英寸
// 计算屏幕对角线的像素数
double diagonal_pixels = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
// 计算DPI
double dpi = diagonal_pixels / screen_size;

屏幕信息获取问题

DPI 问题解决后,基本上大部分的 app 都能够正常显示,但是当有些 app 根据屏幕的宽度来绘制 View 大小的时候会出现一些问题。

假设在一个页面中要显示一个按钮,这个按钮是屏幕宽度的一般,如下图,正常情况下是左边正常显示,但是实际现象为右边。

如果我屏幕大小是 5K,VirtualDisplay 的大小是 1920*1080,而 app 根据屏幕宽度的一半来绘制一个 Button 的话,这个 Button 的大小就会是 2K 多的大小,就会远远超过 VirtualDisplay 的宽度,显示自然是不正常的。

因为在获取屏幕宽高的时候,大多数都是直接获取主屏幕的大小,比如下面一个工具类中获取屏幕高度的方式:

public static int getScreenHeight() {
WindowManager wm = (WindowManager) Utils.getApp().getSystemService(Context.WINDOW_SERVICE);
if (wm == null) return -1;
Point point = new Point();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
wm.getDefaultDisplay().getRealSize(point);
} else {
wm.getDefaultDisplay().getSize(point);
}
return point.y;
}

可以看到是直接 getDefaultDisplay()来获取大小,显然这是不正确的。严谨的说,获取的时候需要指定 DisplayId,只是大部分场景只有一个主屏幕。

首先我们无法拦截用户去获取主屏幕,所以只能在 getRealSize 等代码中做文章。

可以发现,在 getRealSize 或者类似的方法中,都会先调用 updateDisplayInfoLocked 方法来更新正确的 DisplayInfo,那么我们返回正确的 DisplayInfo 给用户不就行了?

所以当用户调用这类方法的时候,根据调用者的身份来判断包名,然后根据包名对应的 VirtualDisplay,返回正确的 DisplayInfo 给用户,代码如下:

DisplayInfo newInfo;
if(mDisplayId==0){
int tempDisplayId = mDisplayService.getTargetDisplayId();
newInfo = mGlobal.getDisplayInfo(tempDisplayId);
}else{
newInfo = mGlobal.getDisplayInfo(mDisplayId);
}

mDisplayService 是自己的一个 Binder 对象,因为 VirtualDisplay 都是在 services 包下面的,而这里的代码是在 base 的 client 端下,所以需要 Binder 通信。

大致的代码如下:

public int getTargetDisplayId() {
int uid = Binder.getCallingUid();
if (uid == 0 || uid == 1000) {
return 0;
}

getServiceLocked();
if (mService == null) {
return 0;
}

//如果是主屏幕的调用方则不进行binder处理
int[] ids = DisplayManagerGlobal.getInstance().getDisplayIds();
for (int id : ids) {
if (DisplayManagerGlobal.getInstance().isUidPresentOnDisplay(uid, id)) {
if (id == 0) {
return 0;
}
}
}
if (mService != null) {
try {
return mService.getTargetDisplayId(uid);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
return 0;
}

Density

另外还有一个获取 Density 的方法,比如 B 站在显示某些 View 的时候就是根据这个信息来设置大小,所以这部分也需要进行拦截,否则显示一样会异常。

public static float getScreenDensity() {
return Resources.getSystem().getDisplayMetrics().density;
}

正常而言,应该是根据 Context 的 Resource 来获取对应的信息,才会获取到正确的屏幕信息,而这个方法获取的是默认的屏幕信息,也就是主屏幕。

这部分的拦截需要再 ResourcesImpl 的 getDisplayMetrics 获取的时候进行拦截,

DisplayMetrics getDisplayMetrics() {
if(mPreloading){
return mMetrics;
}
int displayId = YourServiceManagerGlobal.getInstance().getTargetDisplayId();
if (displayId != 0) {
DisplayMetrics metrics = new DisplayMetrics();
DisplayManagerGlobal.getInstance().getRealDisplay(displayId).getMetrics(metrics);
return metrics;
}
return mMetrics;
}

注意一个地方,因为在 zygote 启动的时候也会去获取这个信息,具体逻辑在 ZygoteInit 的 preloadResources 方法中

private static void preloadResources() {
......
mResources = Resources.getSystem();
mResources.startPreloading();
......
}

这个时候 binder 是无法使用的,所以需要加一个 mPreloading

AMS拦截并启动虚拟屏到Unity的面板中

作者 mikaelzero
2023年4月11日 08:00

需求场景

  1. Unity 中存在一个面板,这个面板是用来显示 2D 应用的画面的
  2. Unity 中的面板是 Unity 创建的,它接收的是一个 TextureID,并创建 Shader 后根据纹理 ID 来渲染
  3. 在 android 系统层面,需要拦截所有的 2D 应用并将它们显示到 Unity 的面板中
  4. 且这些 2D 应用都是显示到虚拟屏幕中,也就是 VirtualDisplay

Unity 部分

Unity 部分比较简单,使用自带的接口根据 TextureId 来创建 2D 纹理并且创建对应的 Shader 即可

Shader "Custom/HwAndroid"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}

SubShader
{

Cull Off
Pass
{

GLSLPROGRAM

#ifdef VERTEX

varying vec2 uv;
void main()
{
uv = gl_MultiTexCoord0.xy;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

#endif


#ifdef FRAGMENT

#extension GL_OES_EGL_image_external_essl3 : require
uniform samplerExternalOES _MainTex;
varying vec2 uv;
void main()
{
gl_FragColor = texture2D(_MainTex, uv);
}

#endif

ENDGLSL
}
}

FallBack "Unlit/Texture"
}

Unity 的脚本部分:

textureId = nativeUnityHolder.Call<int>("createNativeTexture", 1920, 1080, pkg);
texture = Texture2D.CreateExternalTexture(1920, 1080, TextureFormat.RGBA32, false, false, (IntPtr)textureId);
texture.wrapMode = TextureWrapMode.Clamp;
texture.filterMode = FilterMode.Bilinear;
GetComponent<MeshRenderer>().material.shader = Shader.Find("Custom/HwAndroid");

createNativeTexture 是 native 的一个 SDK 中的方法,主要作用就是创建 Surface 以及 SurfaceTexture,大致如下:

private int createGLTexture(int width, int height) {
int[] textures = new int[1];
GLES30.glGenTextures(1, textures, 0);
int textureId = textures[0];
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);

GLES30.glGetError();
return textureId;
}

private void attachGLTexture(int textureId) {
mTextureId = textureId;
mSurfaceTexture = new SurfaceTexture(mTextureId);
mSurfaceTexture.setDefaultBufferSize(mSurfaceFrame.width(), mSurfaceFrame.height());
mSurfaceTexture.setOnFrameAvailableListener(this);

mSurface = new Surface(mSurfaceTexture);
}

SurfaceTexture

SurfaceTexture 和 Surface 以及 VirtualDisplay 三者之间的关系是:

VirtualDisplay 会接收一个 Surface 参数,会将虚拟屏上的画面画在这个 Surface 上,而 SurfaceTexture 是可以从 Surface 中,将画面转为纹理,而 Unity 则可以根据这个纹理 ID 来渲染。所以流程分以下三步:

  1. 在 Server 进程中,根据包名创建虚拟屏,并保存
  2. 创建完成后发送消息给 Unity 的 SDK,SDK 根据包名创建对应的 Surface 和 SurfaceTexture,并返回 textureID 给 Unity
  3. SDK 将 Surface 返回给 Server 进程,传递给 VirtualDisplay

VirtualDisplay

每个 2D 应用在 VR 系统中都是无法直接显示的,因为都会直接显示到主屏幕中,就无法根据 VR 眼镜的转动而渲染。

所以一个解决方案是将所有的 2D 应用显示到虚拟屏幕中,创建虚拟屏幕时有一些参数:

VirtualDisplay virtualDisplay = dm.createVirtualDisplay(packageName, width, height, 240, null,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC);

其中的 Surface,是我们可以操作的地方,我们可以在将 2D 启动到虚拟屏幕后,手动设置这个 Surface,这个 Surface 就是上面 Unity 部分创建的 Surface,这样 Unity 就能够获取到虚拟屏幕的画面。

拦截部分的代码是写在 ActivityStater 的 startActivity 中,大约 795 行后,在创建 ActivityRecord 之前,重新设置一遍 checkedOptions。

代码如下:

public ActivityOptions handleStartActivity(Context context, RootActivityContainer mRootActivityContainer, Intent intent, ActivityOptions options) {
String packageName = intent.getComponent().getPackageName();
//这里需要过滤一下包名,不仅仅是3D,其他的系统应用也要过滤
if (3DUtils.isVrApp(packageName) > 0) {
return options;
}
if (options == null) {
options = ActivityOptions.makeBasic();
}
int virtualDisplayId = VirtualServiceImpl.getInstance().createVirtualDisplay(packageName, 1920, 1080);
options.setLaunchDisplayId(virtualDisplayId);
return options;
}

修改后发现,在 handleStartActivity 方法中的 virtualDisplayId 为正确的值,也就是非 0,但是最终在 dumps activitys 时,2D 总是显示在主屏幕上,说明在拦截了之后,肯定还有个地方修改了 displayId。

当 Activity 在启动过程中,在 startActivityUnchecked 方法下,会调用 mSupervisor.getLaunchParamsController().calculate 然后转到 TaskLaunchParamsModifier 的 calculate,该方法的 getPreferredLaunchDisplay 会去校验一下 DisplayId 对应的 ActivityDisplay

private int getPreferredLaunchDisplay(... ActivityOptions options ...) {
......
int displayId = INVALID_DISPLAY;
final int optionLaunchId = options != null ? options.getLaunchDisplayId() : INVALID_DISPLAY;
if (optionLaunchId != INVALID_DISPLAY) {
displayId = optionLaunchId;
}
......
if (displayId != INVALID_DISPLAY
&& mSupervisor.mRootActivityContainer.getActivityDisplay(displayId) == null) {
displayId = currentParams.mPreferredDisplayId;
}
......
}

如果 mRootActivityContainer.getActivityDisplay 获取到的 ActivityDisplay 为 null,displayId 就变为 0 了,所以自然无法启动到指定的虚拟屏上了。

那么为什么明明启动了虚拟屏,却没有找到呢?说明肯定有个地方是异步的,AOSP 中到处都是 Handler 通信,所以创建完了虚拟屏后,肯定做了异步处理。

果不其然,当调用 createVirtualDisplay 创建虚拟屏后,一路走到 DMS 的 handleDisplayDeviceAddedLocked 方法,该方法会调用 addLogicalDisplayLocked,之后会发送一个 Handler 的 Message

sendDisplayEventLocked(displayId, DisplayManagerGlobal.EVENT_DISPLAY_ADDED);

该方法会通过 Handler 发送一条消息,最终在 RootActivityContainer 中会进行接收,回调 RootActivityContainer 的 onDisplayAdded 后,通过 getActivityDisplayOrCreate 创建 ActivityDisplay。

既然如此,那就必须等到 ActivityDisplay 创建完毕才能启动我们的 Activity。

一开始想到的是在自己的服务类中接收到 onDisplayAdded 后再处理 Activity 的启动,当时这个方案改动太大且影响了原有 Activity 启动逻辑,所以废弃。

既然最终是要在 RootActivityContainer 中通过 getActivityDisplayOrCreate 创建 ActivityDisplay,那我手动调用 getActivityDisplayOrCreate 不就行了?

所以这部分的代码改动如下:

public ActivityOptions handleStartActivity(Context context, RootActivityContainer mRootActivityContainer, Intent intent, ActivityOptions options) {
......
int virtualDisplayId = VirtualServiceImpl.getInstance().createVirtualDisplay(packageName, 1920, 1080);
//getActivityDisplayOrCreate 方法需要手动设置为 public
mRootActivityContainer.getActivityDisplayOrCreate(virtualDisplayId);
options.setLaunchDisplayId(virtualDisplayId);
return options;
}

总结

总结几个注意点:

  1. 自己写的服务需要在 Server 进程,这样才能和 Server 进程中的 AMS 通信,无需用 Binder 来处理
  2. 写完后记得 make update api
  3. Unity 进程传递 Surface 到 Server 进程需要用到

Android同时resume多个Activity

作者 mikaelzero
2023年3月28日 08:00

之前在 surfaceflinger 修改 layer 层级 文章中实现了底部的 Layer 能否显示在顶部,但是在休眠唤醒的时候会出现问题,因此这篇文章是 surfaceflinger 修改 layer 层级的后续完善版本。

场景

一个 Activity A 和 Activity B,A 做为 Home,也就是桌面,当启动 B 的时候,A 同样能够显示在 B 的上面。A 其实是一个半透明的页面。

有人想到的方案可能是多窗口,但其实这和多窗口不是一回事,因为我们的架构设计中,都是虚拟屏幕。

当然一开始最快能想到的方案自然就是直接再次把 A 拉起来,放在栈顶,但是这样就会导致 B 会 pause,由于 B 大多数都是 3D 应用,一旦 pause,大部分程序是直接不渲染的,也就是黑屏。

同样的,想解决这个黑屏问题也可以直接不 pause B,但是休眠唤醒的时候就会有问题,唤醒的时候只会唤醒栈顶的 Activity,也就是 A,所以还必须同时唤醒 B 才能达到效果。并且改动拉起部分代码不少,所以选择了另外一个方案,逻辑其实还是类似的。

首先 A 还是 Launcher,B 还是普通的 3D 应用,正常启动 A,然后启动 B,只是在 pause 的时候,需要让 A 不能够 pause 且 A 的画面能够在顶层,这个在 surfaceflinger 修改 layer 层级 一文中已经解释过,这个解决了第一个问题。

其次还是在休眠唤醒上,在 A 之上启动了一个 B 之后,栈关系是 B 在 A 之上,所以休眠唤醒时,还是只会唤醒 B,所以需要处理的是,同时唤醒 A。

resume 多个 Activity

第一个问题就是,在启动了 B 之后,A 如何还保持着 resume 状态。由于我们的 A 是一个 launcher,一个 Home 类型的 Activity,并且是一个 Unity 程序,所以在处理过 Layer 的前提之上,还需要保持 A 为 resume 状态,否则 Unity 程序就会结束 VR 模式。

在启动一个新的 Activity 时,会在对应的 ActivityStack 中调用resumeTopActivityInnerLocked,在resumeTopActivityInnerLocked流程中,会调用

boolean pausing = getDisplay().pauseBackStacks(userLeaving, next, false);

pauseBackStacks的作用就是 pause 掉当前正在 resume 的 Activity。在我们的场景下,启动 B 的时候,需要 pause 的就是 A。所以在这个地方,直接过滤掉我们的 A 即可,下面是修改代码:

boolean pauseBackStacks(boolean userLeaving, ActivityRecord resuming, boolean dontWait) {
boolean someActivityPaused = false;
for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
final ActivityStack stack = mStacks.get(stackNdx);
final ActivityRecord resumedActivity = stack.getResumedActivity();
if (resumedActivity != null
&& (stack.getVisibility(resuming) != STACK_VISIBILITY_VISIBLE
|| !stack.isFocusable())) {
//改动在这 判断是否为Launcher
if (resumedActivity.isActivityTypeHome()){
return false;
}
someActivityPaused |= stack.startPausingLocked(userLeaving, false, resuming,
dontWait);
}
}
return someActivityPaused;
}

这样就能让我们的 A 同时保持 resume 状态。

唤醒时 resume 多个 Activity

唤醒的时候,默认是 resume 在 top 的 Activity。比如我们现在的栈是 A -> B,那么休眠唤醒时,默认就是 resume B,所以我们需要同时 resume A。

在休眠唤醒的时候,流程如下:

唤醒的时候应用resume的流程
ActivityStackSupervisor.java
SleepTokenImpl.release
-->removeSleepTokenLocked
-->updateSleepIfNeededLocked
-->applySleepTokens
-->resumeFocusedStacksTopActivities
-->resumeTopActivityUncheckedLocked
-->resumeTopActivityInnerLocked

applySleepTokens流程中,会调用resumeFocusedStacksTopActivities来 resume 起在顶部的 Activity,那么我们需要在它之前,先 resume 起我们的 A,代码如下:

ActivityRecord tmpTop = stack.topRunningActivityLocked();
final ActivityRecord r = getActivityDisplay(DEFAULT_DISPLAY).getHomeActivity();
if (r != null && !r.finishing) {
boolean result = r.getActivityStack().resumeTopActivityUncheckedLocked(r, null);
}

resumeFocusedStacksTopActivities()

并且还需要在 resumeTopActivityUncheckedLocked 方法的 shouldBeVisible 条件中加入一个判断,因为如果不加判断,A 就会直接调用 completeResumeLocked 导致不走 resume

if (shouldBeVisible(next)||next.isActivityTypeHome()) {
notUpdated = !mRootActivityContainer.ensureVisibilityAndConfig(next, mDisplayId,
true /* markFrozenIfConfigChanged */, false /* deferResume */);
}

Layer 异常

上述操作改完后,发现唤醒时,A 无法显示,只能显示 B,且通过下面命令查看 Activity 的状态时,发现都是 resume 的状态。

adb shell dumpsys activity activities | grep -E 'Stack|TaskRecord|Hist|Display|state'

所以问题不会出现在生命周期上,于是排查下 Layer 是否有什么问题。这里的 Layer 就是指使用 adb shell dumps SurfaceFlinger 时的 Layer,也是指每一个 app 对应的 Layer。

正常情况下,一个 Unity 程序的 Layer 有两个,一个是 Unity 程序的壳,一个是使用 SurfaceView 渲染的 Layer,如下所示:

- Output Layer 0x7564882000 (Composition layer 0x7564786898) (com.mikaelzero.dock/UnityActivity#0)
Region visibleRegion (this=0x7564882028, count=1)
[ 0, 0, 5088, 2544]
forceClientComposition=false clearClientTarget=true displayFrame=[0 0 5088 2544] sourceCrop=[0.000000 0.000000 5088.000000 2544.000000] bufferTransform=0 (0) z-index=1
hwc: layer=0x0812 composition=DEVICE (2)
- Output Layer 0x7560edde00 (Composition layer 0x7571e40598) (SurfaceView - com.mikaelzero.dock/UnityActivity#0)
Region visibleRegion (this=0x7560edde28, count=1)
[ 0, 0, 5088, 2544]
forceClientComposition=false clearClientTarget=true displayFrame=[0 0 5088 2544] sourceCrop=[0.000000 0.000000 5088.000000 2544.000000] bufferTransform=0 (0) z-index=2
hwc: layer=0x0813 composition=DEVICE (2)

可以看到 Layer 的名字,一个是 com.mikaelzero.dock/UnityActivity,一个是 SurfaceView - com.mikaelzero.dock/UnityActivity,只有 SurfaceView - com.mikaelzero.dock/UnityActivity 这个 Layer 输出,Unity 程序才能正常显示。

Layer 输出的意思就是指:指定的 Layer 出现在使用 adb shell dumps SurfaceFlinger 时,Output Layer 的部分。Output Layer 出现的 Layer 就是最终显示在显示屏上的 Layer。

而当出现问题的时候,日志是这样的:

1 Layer  - Output Layer 0x6f6890d000 (Composition layer 0x6ffe117698) (com.mikaelzero.dock/UnityActivity#0)
Region visibleRegion (this=0x6f6890d028, count=1)
[ 0, 0, 5088, 2544]
forceClientComposition=false clearClientTarget=true displayFrame=[0 0 5088 2544] sourceCrop=[0.000000 0.000000 5088.000000 2544.000000] bufferTransform=0 (0) z-index=0
hwc: layer=0x087 composition=DEVICE (2)

那么,就是少了 SurfaceView - com.mikaelzero.dock/UnityActivity 这个 Layer。那么问题就应该出现在这。

在排查这个问题时,耗费了非常多的时间,因为问题给出的线索很少,一开始所有的状态都是正确的,唯独这个 Layer 不显示。排查了 WMS 处理窗口的部分时,也没发现特别之处,所以开始怀疑问题点并不在系统。

而且我对 Unity 程序是信心满满,一直没怀疑 Unity 那边有什么问题,虽然确实不是 Unity 的问题,但是也是从 Unity 那部分排查得出的结果。

后来在 Unity 程序的 Log 中,发现了一些端倪,也就是在唤醒的时候,Unity 的日志居然一点也不输出(在做这个需求的时候,自己在 adb 时都是完全过滤掉了 Unity 的日志,所以一开始都没发现)。

不输出日志,说明在唤醒时 Unity 就是处于不工作的状态,查看代码发现 OnApplicationPause 也不回调,按理来说,如果 Activity resume 了,那么就会回调 OnApplicationPause,且参数为 false,这是 Unity 自带的函数,所以应该也不会有什么问题。

直到我在 Unity 导出的 Android 工程的项目中 Acitvity 的生命周期打印一些日志,顺便在这个函数打印了一下:

@Override public void onWindowFocusChanged(boolean hasFocus)
{
super.onWindowFocusChanged(hasFocus);
mUnityPlayer.windowFocusChanged(hasFocus);
}

发现当启动 B 的时候,A 会回调 onWindowFocusChanged,且参数为 false,而唤醒时却没有回调。

在测试了正常流程下的 Unity 程序,只有在有 focus 的情况下,才会回调 OnApplicationPause。

onWindowFocusChanged 这个函数,是在焦点变换时调用的,而且是在 resume 之后调用,当我存在 A 时,且调用了 B,A 的焦点变成了 false,这是正常的,而且唤醒的时候,焦点应该也是只有栈顶的 Acitivty 才会获得,由于我们的 A 并不是栈顶,所以显然 A 并不会获得栈顶。既然如此,那就手动给 A 获取焦点,这个函数仅仅是一个回调而已,并不会影响系统的逻辑功能。

所以,最后一步,就是在 Activity 的 resume 中,如果是 Home 类型的 Acitivty,就手动调用 onWindowFocusChanged 方法。

public void performResume(...){
......
onWindowFocusChanged(true);

为什么VR和AR需要Micro OLED?

作者 mikaelzero
2023年3月25日 08:00

什么是 Micro OLED

Micro OLED 指微型 OLED(有机发光二极管)显示技术。Micro OLED 屏幕是一种通过在非常小的面积上排列数百万像素来生成高分辨率图像的显示技术。该技术使用有机化合物作为发光材料,这些有机化合物将受到电流的刺激,并通过对颜色的控制来呈现图像。Micro OLED 屏幕非常薄小,每个像素都由发光二极管和一些辅助电路组成。这种显示技术被广泛应用于 VR(虚拟现实)和 AR(增强现实)设备,因为它提供了更高的像素密度(PPI)、更高的对比度和响应速度,这有助于提高用户的体验和感受。

PPI

PPI 指每英寸像素数(Pixels Per Inch)的英文缩写。PPI 是用于描述显示屏分辨率的一种度量,它表示在每英寸区域内有多少像素点。PPI 值越高,像素越密集,显示屏的画面就越清晰,图像就越细腻自然。在不同的设备上,PPI 的标准也不同。例如,对于智能手机和平板电脑而言,PPI 值通常在 200 到 500 之间,而对于电脑和电视而言,PPI 值通常在 70 到 150 之间。PPI 是评估屏幕分辨率和显示质量的重要指标。

例如,一个分辨率为 1920×1080 的显示屏,在大小相同的情况下,其 PPI 值会随着显示屏的大小而有所不同。如果是一块 24 英寸显示屏,则该分辨率的显示屏 PPI 约为 91PPI。而在一个更大的 27 英寸显示屏上,同样的分辨率下,PPI 值则会降低至 82PPI。换言之,PPI 值越高,每个像素在屏幕上的面积越小,图像看起来也就越锐利、细腻,提供更出色的显示体验。

对于 VR 来说,一些常见的 VR 设备的 PPI 范围为 700 至 4000 左右,这已经比较接近人眼在正常距离下所能分辨的极限了。比如最新的 PICO4 的 PPI 达到 1200,arpara 的 5k 版达到了 3514。传言苹果 MR 硬件上配有 2 块 4K 分辨率的 Micro OLED,PPI 超过 3000。

为什么 VR 需要高 PPI

在 VR 设备中,用户可以将眼睛放在接触到设备屏幕的非常近的位置,以及 VR 设备需要显示具有逼真感的 3D 图像和视频,因此需要使用更高的 PPI 来确保在眼睛感受到的视觉范围内提供高分辨率的图像和提供更多的像素以显示更加详细和逼真的图像。另外,VR 设备还需要非常快的响应时间和更加稳定的帧速率。更高的 PPI 可以带来更好的图像质量和更加真实的视觉体验,同时也可以减少屏幕门效应和图像抖动,从而提高用户使用体验。因此,高 PPI 是 VR 设备的一个非常重要的指标。

谁在使用 Micro OLED

Micro LED、Mini LED 和 Micro OLED

LCD(液晶显示器)
液晶显示器(LCD)使用液晶面板和白色 LED 背光来显示图像。在 LCD 技术中,像素是由液晶分子的属性控制的,液晶分子通过电流来控制光的旋转程度,从而控制像素的透过程度,达到显示图像的目的。经过多年的改进,LCD 显示器已经成为了最常见的显示技术之一,一些最新型的 LCD 显示器配备 LED 背光,提高了显示质量并改善了色彩表现。

LED(发光二极管)显示器
LED 显示器和 LCD 显示器非常相似,但是 LED 显示器使用的是 OLED(有机发光二极管)而不是传统的白色 LED 背光,这意味着每个像素都会发光。这种技术可以提高显示质量和颜色表现,减少电力消耗,并减少屏幕厚度。它还可以提供更高的对比度,因为每个像素可以独立调节亮度和颜色。

OLED(有机发光二极管)显示器
OLED 显示器是一种具有自发光的有机材料的显示技术,它可以独立控制每个像素的亮度和颜色,这使得 OLED 显示器能够提供更高的对比度、更好的色彩饱和度和更快的响应时间,同时也具有更低的能耗和更薄的尺寸。OLED 显示器通常被用于高端手机、电子书和电视中。

Micro LED(微米级 LED)显示器
Micro LED 显示器使用微米级的 LED 作为发光器件,每个像素会独立发光,因此每个像素能够提供更高的亮度和更好的能源效率。其显示效果更加逼真,对比度非常高,并且具有更长的使用寿命。Micro LED 显示器被认为是下一代显示技术的趋势。

MiniLED(微型 LED)背光显示器
MiniLED 背光显示器使用数千个微型 LED 灯来照亮整个面板,这些灯可以独立控制,从而产生更高的对比度和更准确的色彩表现。MiniLED 背光显示器可以提供更高的亮度和更好的颜色表现,同时降低能源消耗,成为一种受欢迎的选择。

Micro OLED(微型 OLED)显示器
Micro OLED 显示器是一种采用有机发光二极管技术的微型显示器,它提供了更高的像素密度、更高的对比度和响应速度,这有助于提高用户的体验和感受。Micro OLED 显示器可以被广泛应用于 VR(虚拟现实)和 AR(增强现实)设备中,因为它能够提供更加真实、逼真的图像。

参考链接

https://www.aibangled.com/a/1903

Android显示原理

作者 mikaelzero
2023年1月6日 08:00

相关的类的定义和功能可以查看 Android 显示原理(手册)

显示流程, 表面意思就是在 android 系统中, 是如何将画面显示在手机上的整体流程.

android 系统的显示流水线,主要包含三个阶段, 绘制、合成和显示

绘制

绘制阶段包括了窗口的概念, 涉及到 AMS, WMS, DMS, 大致流程可以分为下面几部分

  1. 应用层创建 Activity, 通过 AMS 启动 Activity
  2. 创建 Activity 时, 通过我们设置的 setContentView 附属到 DectorView 中,通过 WMS 创建对应的 Window,这个过程会创建一系列的对象
    • Activity
    • ViewRootImpl
    • Window
    • Surface
    • Layer
  3. 窗口
  4. 绘制

绘制阶段是比较长的一个阶段, 涉及到的模块也很多, 大部分在 framework 的 JAVA 层中, 我们做应用开发时经常接触到的就是 Activity、Window 以及 View, 涉及到 framework 中的服务就是 AMS、WMS、DMS 等, 你可能经常听到窗口, 一般口头上我们说到窗口或者窗口管理的时候,都是指所有的窗口,比如 Activity、dialog 等, Activity 委托 View 负责显示, 三者的关系可以简单理解为, view 组成 Activity, Activity 在窗口模块中被抽象为 Window

View 主要有三个方法, Measure、Layout 和 Draw, Draw 的时候才会开始真正分配 buffer

在 View 中有一个比较重要的 DectorView, 它是 Activity 的根 View, 窗口的添加就是通过 WindowManager.addView()把该 mDecorView 添加到 WindowManager 中, 所以一个 Activity 对应一个窗口, 当然你也可以直接通过 WindowManager 添加一个单独的 view, 这样也是一个窗口

控制端

WindowManagerService (服务端, 与绘制无关)

  • 窗口的状态、属性, 如大小, 位置; (WindowState, 与上面的 DectorView 一一对应)
  • View 增加、删除、更新
  • 窗口顺序
  • Input Event 消息收集和处理等(与绘制无关, 可不用关心)

SurfaceFlinger(服务端, 与绘制无关)

  • Layer 的大小、位置(Layer 与上面的 WindowState 一一对应)
  • Layer 的增加、删除、更新;
  • Layer 的 zorder 顺序

内容端(绘制)

framework/base: Canvas

  • SoftwareCanvas (skia/CPU)
  • HardwareCanvas (hwui/GPU)

framework/base: Surface

  • 区别于 WMS 的 Surface 概念, 与 Canvas 一一对应, 内容生产者

framework/native:

  • Surface: 负责分配 buffer 与填充(由上面的 Surface 传下来)
  • SurfaceFlinger
    • Layer 数据已填充好, 与上面提到的 Surface 同样是一一对应
    • 可见 Layer 这个概念即是控制端又是内容端, SF 更重要的是合成

概念梳理

Window -> DecorView-> ViewRootImpl -> WindowState -> Surface -> Layer 是一一对应的
一般的 Activity 包括的多个 View 会组成 View hierachy 的树形结构, 只有最顶层的 DecorView, 也就是根结点视图, 才是对 WMS 可见的, 即有对应的 Window 和 WindowState.
一个应用程序窗口分别位于应用程序进程和 WMS 服务中的两个 Surface 对象有什么区别呢?
虽然它们都是用来操作位于 SurfaceFlinger 服务中的同一个 Layer 对象的, 不过, 它们的操作方式却不一样. 具体来说, 就是位于应用程序进程这一侧的 Surface 对象负责绘制应用程序窗口的 UI, 即往应用程序窗口的图形缓冲区填充 UI 数据, 而位于 WMS 服务这一侧的 Surface 对象负责设置应用程序窗口的属性, 例如位置、大小等属性.
这两种不同的操作方式分别是通过 C++层的 Surface 对象和 SurfaceControl 对象来完成的, 因此, 位于应用程序进程和 WMS 服务中的两个 Surface 对象的用法是有区别的. 之所以会有这样的区别, 是因为绘制应用程序窗口是独立的, 由应用程序进程来完即可, 而设置应用程序窗口的属性却需要全局考虑, 即需要由 WMS 服务来统筹安排.

软件绘制

在 Android 应用程序进程这一侧, 每一个窗口都关联有一个 Surface. 每当窗口需要绘制 UI 时, 就会调用其关联的 Surface 的成员函数 lock 获得一个 Canvas, 其本质上是向 SurfaceFlinger 服务 dequeue 一个 Graphic Buffer.
Canvas 封装了由 Skia 提供的 2D UI 绘制接口, 并且都是在前面获得的 Graphic Buffer 上面进行绘制的, 这个 Canvas 的底层是一个 bitmap, 也就是说, 绘制都发生在这个 Bitmap 上. 绘制完成之后, Android 应用程序进程再调用前面获得的 Canvas 的成员函数 unlockAndPost 请求显示在屏幕中, 其本质上是向 SurfaceFlinger 服务 queue 一个 Graphic Buffer, 以便 SurfaceFlinger 服务可以对 Graphic Buffer 的内容进行合成, 以及显示到屏幕上去.

surface.lockCanvas():

//android_view_Surface.cpp
static jlong nativeLockCanvas(JNIEnv* env, jclass clazz,
jlong nativeObject, jobject canvasObj, jobject dirtyRectObj) {
sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));

......

ANativeWindow_Buffer outBuffer;
//调用了Surface的dequeueBuffer, 从SurfaceFlinger中申请内存GraphicBuffer,这个buffer是用来传递绘制的元数据的
status_t err = surface->lock(&outBuffer, dirtyRectPtr);
if (err < 0) {
const char* const exception = (err == NO_MEMORY) ?
OutOfResourcesException :
"java/lang/IllegalArgumentException";
jniThrowException(env, exception, NULL);
return 0;
}

SkImageInfo info = SkImageInfo::Make(outBuffer.width, outBuffer.height,
convertPixelFormat(outBuffer.format),
outBuffer.format == PIXEL_FORMAT_RGBX_8888
? kOpaque_SkAlphaType : kPremul_SkAlphaType);
//新建了一个SkBitmap, 并进行了一系列设置
SkBitmap bitmap;
ssize_t bpr = outBuffer.stride * bytesPerPixel(outBuffer.format);
bitmap.setInfo(info, bpr);
if (outBuffer.width > 0 && outBuffer.height > 0) {
//bitmap对graphicBuffer进行关联
bitmap.setPixels(outBuffer.bits);
} else {
// be safe with an empty bitmap.
bitmap.setPixels(NULL);
}
//构造一个native的Canvas对象(SKCanvas), 再返回这个Canvas对象, java层的Canvas对象其实只是对SKCanvas对象的一个简单包装, 所有绘制方法都是转交给SKCanvas来做.
Canvas* nativeCanvas = GraphicsJNI::getNativeCanvas(env, canvasObj);
//bitmap对下关联了获取的内存buffer, 对上关联了Canvas,把这个bitmap放入Canvas中
nativeCanvas->setBitmap(bitmap);

......

sp<Surface> lockedSurface(surface);
lockedSurface->incStrong(&sRefBaseOwner);
return (jlong) lockedSurface.get();
}

canvas.drawXXX

Skia 深入分析

SkCanvas 是按照 SkBitmap 的方法去关联 GraphicBuffer

一、渲染层级 从渲染流程上分, Skia 可分为如下三个层级:

指令层:SkPicture、SkDeferredCanvas->SkCanvas
这一层决定需要执行哪些绘图操作, 绘图操作的预变换矩阵, 当前裁剪区域, 绘图操作产生在哪些 layer 上, Layer 的生成与合并.

解析层:SkBitmapDevice->SkDraw->SkScan、SkDraw1Glyph::Proc
这一层决定绘制方式, 完成坐标变换, 解析出需要绘制的形体(点/线/规整矩形)并做好抗锯齿处理, 进行相关资源解析并设置好 Shader.

渲染层:SkBlitter->SkBlitRow::Proc、SkShader::shadeSpan 等
这一层进行采样(如果需要), 产生实际的绘制效果, 完成颜色格式适配, 进行透明度混合和抖动处理(如果需要).

mSurface.unlockCanvasAndPost(canvas):

最后一个步骤会通知消费者的 onFrameAvailable 接口, 进一步调用 SurfaceFlinger 的 signalLayerUpdate 发起更新操作

HardwareRenderer 硬件绘制

具体参考:https://www.jianshu.com/p/40f660e17a73

CPU 更擅长复杂逻辑控制, 而 GPU 得益于大量 ALU 和并行结构设计, 更擅长数学运算.
页面由各种基础元素(DisplayList)构成, 渲染时需要进行大量浮点运算.
硬件加速条件下, CPU 用于控制复杂绘制逻辑, 构建或更新 DisplayList;GPU 用于完成图形计算, 渲染 DisplayList.
硬件加速条件下, 刷新界面尤其是播放动画时, CPU 只重建或更新必要的 DisplayList, 进一步提高渲染效率.

开启硬件绘制条件是在 ViewRootImpl 的 draw 中

private void draw(boolean fullRedrawNeeded) {
...
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
//关键点1 是否开启硬件加速
if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
...
dirty.setEmpty();
mBlockResizeBuffer = false;
//关键点2 硬件加速绘制
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
...
//关键点3 软件绘制
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
}
}

硬件加速渲染和软件渲染一样, 在开始渲染之前, 都是要先向 SurfaceFlinger 服务 dequeue 一个 Graphic Buffer. 不过对硬件加速渲染来说, 这个 Graphic Buffer 会被封装成一个 ANativeWindow, 并且传递给 OpenGL 进行硬件加速渲染环境初始化.
在 Android 系统中, ANativeWindow 和 Surface 可以是认为等价的, 只不过是 ANativeWindow 常用于 Native 层中, 而 Surface 常用于 Java 层中. OpenGL 获得了一个 ANativeWindow, 并且进行了硬件加速渲染环境初始化工作之后, Android 应用程序就可以调用 OpenGL 提供的 API 进行 UI 绘制了, 绘制出来内容就保存在前面获得的 Graphic Buffer 中.
当绘制完毕, Android 应用程序再调用 libegl 库(一般由第三方提供)的 eglSwapBuffer 接口请求将绘制好的 UI 显示到屏幕中, 其本质上与软件渲染过程是一样的.

硬件加速绘制包括两个阶段:构建阶段 + 绘制阶段, 所谓构建就是递归遍历所有视图, 将需要的操作缓存下来, 之后再交给单独的 Render 线程利用 OpenGL 渲染. 在 Android 硬件加速框架中, View 视图被抽象成 RenderNode 节点, View 中的绘制都会被抽象成一个个 DrawOp(DisplayListOp), 比如 View 中 drawLine, 构建中就会被抽象成一个 DrawLintOp, drawBitmap 操作会被抽象成 DrawBitmapOp, 每个子 View 的绘制被抽象成 DrawRenderNodeOp, 每个 DrawOp 有对应的 OpenGL 绘制命令, 同时内部也握着绘图所需要的数据. 如下所示:

如此以来, 每个 View 不仅仅握有自己 DrawOp List, 同时还拿着子 View 的绘制入口, 如此递归, 便能够统计到所有的绘制 Op, 很多分析都称为 Display List, 源码中也是这么来命名类的, 不过这里其实更像是一个树, 而不仅仅是 List, 示意如下:

硬件加速

构建完成后, 就可以将这个绘图 Op 树交给 Render 线程进行绘制, 这里是同软件绘制很不同的地方, 软件绘制时, View 一般都在主线程中完成绘制, 而硬件加速, 除非特殊要求, 一般都是在单独线程中完成绘制, 如此以来就分担了主线程很多压力, 提高了 UI 线程的响应速度.

硬件加速模型

引进 Display List 的概念有什么好处呢?主要是两个好处。
第一个好处是在下一帧绘制中,如果一个 View 的内容不需要更新,那么就不用重建它的 Display List,也就是不需要调用它的 onDraw 成员函数。
第二个好处是在下一帧中,如果一个 View 仅仅是一些简单的属性发生变化,例如位置和 Alpha 值发生变化,那么也无需要重建它的 Display List,只需要在上一次建立的 Display List 中修改一下对应的属性就可以了,这也意味着不需要调用它的 onDraw 成员函数。这两个好处使用在绘制应用程序窗口的一帧时,省去很多应用程序代码的执行,也就是大大地节省了 CPU

❌
❌