知识总结


  • Home

  • Tags

  • Categories

  • Archives

双屏异显异触流程

Posted on 2019-11-18 | In display

异显apk分析

在安卓的SDK中有线程的ApiDemo提供给我们去参考设计

目录为:development/samples/ApiDemos/src/com/example/android/apis/app/PresentationActivity.java

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
43
44
45
46
47
48
49
50
 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView == mShowAllDisplaysCheckbox) {
// Show all displays checkbox was toggled.
mDisplayListAdapter.updateContents();
} else {
// Display item checkbox was toggled.
final Display display = (Display)buttonView.getTag();
if (isChecked) {
DemoPresentationContents contents = new DemoPresentationContents(getNextPhoto());
showPresentation(display, contents);
} else {
hidePresentation(display);
}
mDisplayListAdapter.updateContents();
}
}
private final static class DemoPresentationSurfaceView extends Presentation {
private GLSurfaceView mSurfaceView;
private final String TAG = "DemoPresentationActivity";
public DemoPresentationSurfaceView(Context context, Display display) {
super(context, display);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
// Be sure to call the super class.
super.onCreate(savedInstanceState);

// Get the resources for the context of the presentation.
// Notice that we are getting the resources from the context of the presentation.
Resources r = getContext().getResources();

// Inflate the layout.
setContentView(R.layout.presentation_with_media_router_content);

// Set up the surface view for visual interest.
mSurfaceView = (GLSurfaceView)findViewById(R.id.surface_view);
mSurfaceView.setRenderer(new CubeRenderer(false));
final Button button = (Button)findViewById(R.id.textbutton);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Log.d(TAG, "There are currently zhuangnanjian");
}
});
}

public GLSurfaceView getSurfaceView() {
return mSurfaceView;
}
}

这是通过Button的ID去选择哪个显示设备;安卓还提供了另外两种可以获取Presentation的方法

一:通过Media router去获取首选的设备和显示presentation

1
2
3
4
5
mMediaRouter = (MediaRouter)getSystemService(Context.MEDIA_ROUTER_SERVICE);
MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute(MediaRouter.ROUTE_TYPE_LIVE_VIDEO);
Display presentation = route != null ? route.getPresentationDisplay() :null;
presentation.show();
presentation.setOnDismissListener(mOnDismissListener);

除了ROUTE_TYPE_LIVE_VIDEO,还有ROUTE_TYPE_REMOTE_DISPLAY,ROUTE_TYPE_USER

二:使用displayManager获取

1
2
3
4
5
6
Display[]presentationDisplays = mDisplayManager.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
Display display = presentationDisplays[0];
final int displayId = display.getDisplayId();
Display display = presentationDisplays[0];
Presentation presentation = new MyPresentation(context, presentationDisplay);
presentation.show();

支持的类型如下:

1571399167593

综上为三种创建Presentation对象方法,一:直接指定DisplayId,创建Presentation对象;二:通过MediaRoute获取Presentation对象 三:通过displayManager获取Display对象,获取DisplayId,然后创建Presentation对象;然后调用Presentation的show函数;

接下来我们就进入framework看下这个对象和相关的show函数

Presentation&&show

Presenation结构体

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
43
44
45
46
47
48
49
50
public Presentation(Context outerContext, Display display, int theme) {              
super(createPresentationContext(outerContext, display, theme), theme, false); //调用dialog的构造函数

mDisplay = display;
mDisplayManager = (DisplayManager)getContext().getSystemService(DISPLAY_SERVICE);

final Window w = getWindow();
final WindowManager.LayoutParams attr = w.getAttributes();
attr.token = mToken;
w.setAttributes(attr);
w.setGravity(Gravity.FILL);
w.setType(TYPE_PRESENTATION);
setCanceledOnTouchOutside(false);
}

private static Context createPresentationContext(
Context outerContext, Display display, int theme) {
if (outerContext == null) {
throw new IllegalArgumentException("outerContext must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}

Context displayContext = outerContext.createDisplayContext(display);
if (theme == 0) {
TypedValue outValue = new TypedValue();
displayContext.getTheme().resolveAttribute(
com.android.internal.R.attr.presentationTheme, outValue, true);
theme = outValue.resourceId;
}

// Derive the display's window manager from the outer window manager.
// We do this because the outer window manager have some extra information
// such as the parent window, which is important if the presentation uses
// an application window type.
final WindowManagerImpl outerWindowManager =
(WindowManagerImpl)outerContext.getSystemService(WINDOW_SERVICE);
final WindowManagerImpl displayWindowManager =
outerWindowManager.createPresentationWindowManager(displayContext);
return new ContextThemeWrapper(displayContext, theme) {
@Override
public Object getSystemService(String name) {
if (WINDOW_SERVICE.equals(name)) {
return displayWindowManager;
}
return super.getSystemService(name);
}
};
}

Presentation继承自Dialog,获取到Presentation要显示的设备后,就要将Activity的context对象和设备信息作为参数来创建Presentation对象;将设备记录在成员变量mDisplay中,将Presentation设置为不可在外部点击取消;

show流程

1571476950445

创建ViewRoot并将view添加到链表上

1
2
3
4
5
6
7
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);

setView的作用主要有两个:一:创建InputChannel来接受输入事件;二:将Window加入到WindowManager

输入事件的传递

输入设备类型

从InputReader.h我们可以看出来,安卓将输入设备分为以下几种类型:

开关:SwitchInputMapper

震动器,严格意义上是输出设备:VibratorInputMapper

鼠标:CursorInputMapper

键盘:KeyboardInputMapper

触摸设备 TouchInputMapper,SingleTouchInputMapper,MultiTouchInputMapper,

触控笔ExternalStylusInputMapper,

游戏杆:JoystickInputMapper

input设备不同初始化参数也不一样如触摸设备的话可以配置displayId,而鼠标则只能输出到主显;

touch.displayId的配置流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
services/inputflinger/InputReader.cpp
void InputReader::loopOnce() {
processEventsLocked(mEventBuffer, count);
addDeviceLocked(rawEvent->when, rawEvent->deviceId);
device->configure(when, &mConfig, 0);
configureParameters()


if (mParameters.orientationAware
|| mParameters.deviceType == Parameters::DEVICE_TYPE_TOUCH_SCREEN
|| mParameters.deviceType == Parameters::DEVICE_TYPE_POINTER) {
mParameters.hasAssociatedDisplay = true;
if (mParameters.deviceType == Parameters::DEVICE_TYPE_TOUCH_SCREEN) {
mParameters.associatedDisplayIsExternal = getDevice()->isExternal();
String8 uniqueDisplayId;
getDevice()->getConfiguration().tryGetProperty(String8("touch.displayId"),
uniqueDisplayId);
mParameters.uniqueDisplayId = uniqueDisplayId.c_str();
}
}

touch.displayId的传递流程

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
43
44
45
46
47
48
49
50
51
52
53
54
services/inputflinger/InputReader.cpp
void TouchInputMapper::dispatchMotion
const int32_t displayId = getAssociatedDisplay().value_or(ADISPLAY_ID_NONE);
pointerProperties[pointerCount].copyFrom(properties[index]);
pointerCoords[pointerCount].copyFrom(coords[index]);
std::optional<int32_t> TouchInputMapper::getAssociatedDisplay() {
if (mParameters.hasAssociatedDisplay) {
if (mDeviceMode == DEVICE_MODE_POINTER) {
return std::make_optional(mPointerController->getDisplayId());
} else {
return std::make_optional(mViewport.displayId);
}
}
return std::nullopt;
}
NotifyMotionArgs args(mContext->getNextSequenceNum(), when, deviceId,
source, displayId, policyFlags,
action, actionButton, flags, metaState, buttonState, MotionClassification::NONE,
edgeFlags, deviceTimestamp, pointerCount, pointerProperties, pointerCoords,
xPrecision, yPrecision, downTime, std::move(frames));
getListener()->notifyMotion(&args);

压入队列里面然后在read 的loopOnce中会进行通知
services/inputflinger/InputListener.cpp
void QueuedInputListener::notifyConfigurationChanged(
const NotifyConfigurationChangedArgs* args) {
mArgsQueue.push(new NotifyConfigurationChangedArgs(*args));
}

void InputReader::loopOnce() {
...
mQueuedListener->flush();
args->notify(mInnerListener);
...
}
在InputDispatcher中同样是用的一个线程找到对应的窗口进行分发
services/inputflinger/InputDispatcher.cpp
void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
...
needWake = enqueueInboundEventLocked(newEntry);
mPolicy->interceptMotionBeforeQueueing(args->eventTime, /*byref*/ policyFlags);
...
}
bool InputDispatcherThread::threadLoop() {
...
mDispatcher->dispatchOnce();
-->mLooper->pollOnce(timeoutMillis);
-->dispatchOnceInnerLocked(&nextWakeupTime);
-->done = dispatchMotionLocked(currentTime, typedEntry,
&dropReason, nextWakeupTime);
findTouchedWindowTargetsLocked
dispatchEventLocked(currentTime, entry, inputTargets);
...
}

从软件流程分析,touchScreen类型的话是通过配置device.internal来配置TP的输入设备输出到辅显,而Pointer类型的话则是通过displayId去设置输出的方向;

idc文件配置

idc(Input Device Configuration)为输入设备配置文件,包含设备具体的配置属性,这些属性影响输入设备的行为,常见的配置有;

1
2
3
4
5
6
device.internal 指定输入设备属于内置组件;还是外部链接(很可能拆卸)的外围设备;0表示外部,1表示内部
touch.deviceType touchScreen(与显示屏相关的触摸屏),touchPad(不与显示相关连的触摸板), touchNavigation,pointer(类似于鼠标),default(系统根据分类算法自动检测设备类型)
touch.orientationAware 等于1时表示触摸会随着显示屏方向更改,为0则表示不受显示屏方向更改的影响
touch.gestureMode single-touch multi-touch default
touch.wake tp是否需要唤醒系统,一般希望外部设备才有这种能力;如果是内部的设备需要唤醒系统,你也可以进行配置
...

从代码流程上看,在双屏异触的场景下,如果是两个TP,需要配置一个为内部设备,一个为外部设备;

由于安卓的默认鼠标设备的DisplayId只能是0,如代码所示:

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
void CursorInputMapper::sync(nsecs_t when) {
...
if (mSource == AINPUT_SOURCE_MOUSE) {
if (moved || scrolled || buttonsChanged) {
mPointerController->setPresentation(
PointerControllerInterface::PRESENTATION_POINTER);

if (moved) {
mPointerController->move(deltaX, deltaY);
}

if (buttonsChanged) {
mPointerController->setButtonState(currentButtonState);
}

mPointerController->unfade(PointerControllerInterface::TRANSITION_IMMEDIATE);
}

float x, y;
mPointerController->getPosition(&x, &y);
pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_X, x);
pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_Y, y);
pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X, deltaX);
pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y, deltaY);
displayId = ADISPLAY_ID_DEFAULT;
} else {
pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_X, deltaX);
pointerCoords.setAxisValue(AMOTION_EVENT_AXIS_Y, deltaY);
displayId = ADISPLAY_ID_NONE;
}
...
}

如果想要让鼠标设备支持输出到辅显也可以改变这个displayId,并且需要修改framework/base下对于鼠标控件sprite Layer的LayerStack属性的设置;笔者的话是通过鼠标在主显响应,触摸在辅屏响应来实现双屏异显异触的验证,那么这样其实可以配置;

1
2
3
system/usr/idc/gslX680.idc
device.internal = 0
touch.deviceType = touch

总结:作为应用,需要指定送显时候的displayId;作为输入设备也需要指定

参考链接

Android折叠屏适配攻略

聊聊安卓折叠屏给交互设计和开发带来的变化

Android开发-双屏异显(Presentation)实现

手把手带你深入浅出神秘的设计模式

Android7.1 Presentation双屏异显原理分析

输入输出设备配置文件

Android触摸事件传递

Android Input of Pointer Location

Android双屏分析

输入事件是怎么分发到目标窗口的

androidQ更改默认屏幕方向

Posted on 2019-09-30 | In display

屏幕参数方向引入

由于客户的选型,需要更改自身屏幕方向;在P之前的android SDK中,谷歌并不支持相关的功能;所以需要原厂自行定制;在P上 MTK为统一刷GSI和不刷GSI方向的统一,向谷歌mainline提供了对应的补丁;之后在Q上谷歌又进行了相关的修改;使得安卓支持改变默认显示方向的功能也日趋稳定;当然也是会存在bug,如在android Q上就出现了开机动画和应用启动动画异常问题(已解决);但为了统一后续android平台下的横屏竖用使用配置;全志平台androidQ横屏竖用也是依托这个补丁进行横屏竖用的实现;

有兴趣的可以访问这个网站查看更多的信息:GSI 支持横屏模式

本文目的

一:梳理安卓TP,gsensor,surfaceflinger中rotation的系数之间的关系,客户配置地方;

二:系统旋转监听以及应用旋转方向申请;

三:现有显示框架支持横屏竖用原理;

参数配置

dtbo中设置TP方向

为了分离linux环境(dragonboard)和android环境的中TP的方向,使其不相互影响,为了使用android的新朝向,我们将TP方向更改的配置放到dtbo中;在android source并lunch对应的版型后,cbd即可到对应的路径;本文将以全志A50 A7版型为例讲解配置;

1565697980955

1
2
3
4
5
6
7
8
9
10
11
12
13
&soc {
dtbo_version = <0x00000001>;
dtbo{
compatible = "allwinner,sunxi-dtbo";
dtbo_type = <1>;
};
+ ctp {
+ ctp_screen_max_x = <480>;
+ ctp_screen_max_y = <800>;
+ ctp_revert_y_flag = <1>;
+ ctp_exchange_x_y_flag = <0>;
+ };
};

参数解释:

1
2
3
4
5
ctp_screen_max_x 触摸板的x轴最大坐标
ctp_screen_max_y 触摸板的y轴最大坐标
ctp_revert_x_flag 是否需要翻转x坐标,需要则置1,反之置0
ctp_revert_y_flag 是否需要翻转y坐标,需要则置1,反之置0
ctp_exchange_x_y_flag 是否需要x轴y轴坐标对换

修改好需要重新编译和打包内核,在配置该参数的需注意检查驱动是否支持该方法;

非安全固件可以通过命令行修改参数进行验证,将default/env.cfg中将bootdelay改成3s,配合串口在uboot启动过程中按住enter键进入uboot命令行,用fdt 命令的修改验证;

步骤如下:

1569824097100

1
2
3
4
5
如当下需要修改ctp_screen_max_x,可按如下步骤进行修改
1. fdt set /soc/ctp ctp_screen_max_x <0x01>
2. fdt list /soc/ctp 确认是否修改到
3. fdt save //保存配置
4. boot

方案目录下配置显示和sensor方向

1565698555002

1
2
3
4
5
6
7
8
9
10
+	# change SurfaceFlinger Orientation(0, 90, 180, 270)
+ PRODUCT_PROPERTY_OVERRIDES += \
ro.surface_flinger.primary_display_orientation=ORIENTATION_90 \
+ #change minui Orientation
+ #ROTATION_NONE=0, ROTATION_RIGHT=90, ROTATION_DOWN=180, ROTATION_LEFT=270
+ PRODUCT_SYSTEM_DEFAULT_PROPERTIES += \
ro.minui.default_rotation=ROTATION_RIGHT
+ #change Gsensor rotation(0, 90, 180. 270)
+ PRODUCT_PROPERTY_OVERRIDES += \
ro.vendor.sf.rotation=270

camera方向配置

在有摄像头的方案下面还需调整自己的摄像头方向,提供的配置地方有:

configs/camera.cfg

1
camera_orientation //可设置0,90,180,270

hawkview/sensor_list_cfg.ini

1
2
sensor_hflip0  //水平反转
sensor_vflip0 //垂直反转

客户可以adb remount,选择busybox vi到对应的文件下;先调整camera的朝向,在调整镜像;

1
2
vendor/etc/camera.cfg
vendor/etc/hawkview/sensor_list_cfg.ini

Gsensor方向调整方法二

对于Gsensor还有一种改法与camera一样,在方案目录下面有相应的文件可以提供配置;方案商的话其实根据想要的屏幕朝向和sensor的摆放,配置这个文件会是一件更为简单和兼容性更好的操作;

目录为config/gsensor.cfg,

1
2
3
4
5
gsensor_name = sc7660
gsensor_direct_x = false //x轴是否反向
gsensor_direct_y = false //y轴是否反向
gsensor_direct_z = false //z轴是否反向
gsensor_xy_revert = true //是否反转x,y轴

gsensor坐标系:

gsensor方向

匹配规则:

安卓是用的右手坐标系,Z轴相对屏幕朝外,gsensor的数据是一个三维数组,values[0]对应的x轴的值,value[1]对应的y轴,value[2]对应的Z轴,将相对轴竖直朝上放置时候,以该轴为对应的值为正(约9.8左右),其他两轴对应为0为准;

总结

对于Q平台上需要更改默认屏幕方向,为兼容多个设备,且统一后续平台谷歌方案配置需要配置的选项如上述所述配置Android显示方向,recovery显示方向,TP方向,gsensor,camera,以及bootlogo图片方向;

至此,若是只需要知道怎么配置的可以不看以下的章节;

Input系统

linux input简介

软件框图

1565673944332

Tp驱动流程

借着梳理这个事件上报流程,我们也可以将主要的架构和流程涵盖;

以TP gslx680为例:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static int  gsl_ts_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
...
queue_work(gslX680_wq, &glsX680_init_work);
-->glsX680_init_events
ret = input_request_int(&(config_info.input_type), gsl_ts_irq,
CTP_IRQ_MODE, ts_init);

gsl_ts_init_ts
-->set_bit(ABS_MT_POSITION_X, input_device->absbit);
set_bit(ABS_MT_POSITION_Y, input_device->absbit);
set_bit(ABS_MT_TOUCH_MAJOR, input_device->absbit);
set_bit(ABS_MT_WIDTH_MAJOR, input_device->absbit);
input_set_abs_params(input_device, ABS_MT_POSITION_X, 0,
SCREEN_MAX_X, 0, 0);
input_set_abs_params(input_device, ABS_MT_POSITION_Y, 0,
SCREEN_MAX_Y, 0, 0);
input_set_abs_params(input_device, ABS_MT_TOUCH_MAJOR, 0,
PRESS_MAX, 0, 0);
input_set_abs_params(input_device, ABS_MT_WIDTH_MAJOR, 0, 200, 0, 0);
设置TP支持的设备属性和参数
--> input_device = input_allocate_device();
c = input_register_device(input_device);
跟常见的platform总线模型一样,会将input_device加到input_device_list
在register的时候通过match在input_handler_list中找到对应的handler
流程如下所示:
--> input_attach_handler(dev, handler);
id = input_match_device(handler, dev);
error = handler->connect(handler, dev, id);
这里就会调到:evdev.c中的event_connect
-->error = input_register_handle(&evdev->handle);
evdev基本上支持所有类型,所以这里TP将会与evdev通过handle进行连接
...
}

接下来就是中断处理流程,这里我们就能看出他的层级关系;
input driver -->input_dev
gsl_ts_irq{
queue_work(ts->wq, &ts->work);
gsl_ts_xy_worker
process_gslX680_data
input_report_abs(ts->input, ABS_MT_TRACKING_ID, id);
input_report_abs(ts->input, ABS_MT_TOUCH_MAJOR, pressure);
input_report_abs(ts->input, ABS_MT_POSITION_X, x);
input_report_abs(ts->input, ABS_MT_POSITION_Y, y);
input_report_abs(ts->input, ABS_MT_WIDTH_MAJOR, 1);
}
input core
input_event(dev, EV_ABS, code, value);
input_handle_event(dev, type, code, value);
input_pass_values(dev, dev->vals, dev->num_vals);
count = input_to_handler(handle, vals, count);
handler->event(handle, v->type, v->code, v->value);
evdev --> input_handler
evdev_event
evdev_events(handle, vals, 1);
evdev_pass_values(client, vals, count, ev_time);
__pass_event(client, &event);
-->kill_fasync(&client->fasync, SIGIO, POLL_IN);
//走到这里的时候epoll_wait dev/input/xxx的监听就能收到消息
wake_up_interruptible(&evdev->wait);

简而言之:input_dev在中断处理函数里面上报事件 -> input_core找到对应的handler -> input_handler:如edev通过___pass_event将事件上报给用户层;

android Input流程

安卓接受事件之后的处理流程

android处理input事件处理流程

流程简单介绍:

一:APP setView建立inputChannel和window的连接

当一个应用程序有Activity能接受用户输入,他就要将自己的Window(ViewRoot)通过setView()注册到Window Manager Service中;

frameworks/base/core/java/android/view/ViewRootImpl.java

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
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
requestLayout();//对控件树进行测量,布局,向WMS申请修改窗口属性以及重绘的所有工作
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
//服务端过程
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
--> mService.addWindow
-->openInputChannel
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
mInputChannel = inputChannels[0];
mClientChannel = inputChannels[1];
-->mClientChannel.transferTo(outInputChannel);//将socketPair的另外一个FD放在参数OutputChannel里,创建WindowInputEventRecevier用于接受InputDispatchor传来的事件;后者同样通过AddFd()将读端SocketFd加入到Looper中,这样一旦InputDispatchor发送Event,Looper就会立即醒来处理;
-->mWmService.mInputManager.registerInputChannel(mInputChannel, mClient.asBinder());//调用到InputDispacher的RegisterInputChannel()方法;InputDispacher会通过addFd 将channel的fd加入到loop中,这样,只要某个Window在Socket的另一端写入数据,Looper就会马上从睡眠中醒来,进行处理;

mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
...
}

建立Window和InputDispatch的联系;

二:framework层的收集和分发

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
framework/native/services/inputflinger/EventHub.cpp
EventHub::EventHub(void) : {
mEpollFd = epoll_create1(EPOLL_CLOEXEC);
mINotifyFd = inotify_init();
mInputWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
}
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
for(;;){
...
nRead = read(mWakeReadPipeFd, buffer, sizeof(buffer));
int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
...
}
}
总结:Eventhub的作用是监听,读取/dev/input目录下产生的新事件,如节点的增删以及事件的上报,并封装成RawEvent结构体共InputReader使用。
framework/native/services/inputflinger/InputReader.cpp
void InputReader::loopOnce() {
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
if (count) {
processEventsLocked(mEventBuffer, count);
-->device->process(rawEvents, count);
-->mapper->process(rawEvent); //将事件压入队列
}
mQueuedListener->flush();//将事件进行分发
}
void TouchInputMapper::process(const RawEvent* rawEvent) {
...
sync(rawEvent->when);
-->processRawTouches(false /*timeout*/)
-->cookAndDispatch(mCurrentRawState.when);
-->dispatchTouches(when, policyFlags);
--> dispatchMotion(when, policyFlags, mSource,
AMOTION_EVENT_ACTION_MOVE, 0, 0, metaState, buttonState,
AMOTION_EVENT_EDGE_FLAG_NONE,
mCurrentCookedState.deviceTimestamp,
mCurrentCookedState.cookedPointerData.pointerProperties,
mCurrentCookedState.cookedPointerData.pointerCoords,
mCurrentCookedState.cookedPointerData.idToIndex,
currentIdBits, -1,
mOrientedXPrecision, mOrientedYPrecision, mDownTime);
-->getListener()->notifyMotion(&args);
-->mArgsQueue.push_back(new NotifySwitchArgs(*args));
...
}
void QueuedInputListener::flush() {
args->notify(mInnerListener);
-->listener->notifyMotion(this);//这里的listen就是InputDispatcher了
mArgsQueue.clear();
}
总结:InputReader的就是循环的读取EventHub中的数据,然后通过InputDispatcher进行分发

framework/native/services/inputflinger/InputDispatcher.cpp
void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
MotionEntry* newEntry = new MotionEntry(args->sequenceNum, args->eventTime,
args->deviceId, args->source, args->displayId, policyFlags,
args->action, args->actionButton, args->flags,
args->metaState, args->buttonState, args->classification,
args->edgeFlags, args->xPrecision, args->yPrecision, args->downTime,
args->pointerCount, args->pointerProperties, args->pointerCoords, 0, 0);
needWake = enqueueInboundEventLocked(newEntry);

interceptMotionBeforeQueueingNonInteractive
-->InputManagerService.interceptMotionBeforeQueueingNonInteractive
InputMonitor.interceptMotionBeforeQueueingNonInteractive
PhoneWindowManager.interceptMotionBeforeQueueingNonInteractive
PhoneWindowManager.wakeUp
PowerManagerService$BinderService.wakeUp
}
bool InputDispatcherThread::threadLoop() {
mDispatcher->dispatchOnce();
-->dispatchOnceInnerLocked(&nextWakeupTime);
-->done = dispatchMotionLocked(currentTime, typedEntry,
&dropReason, nextWakeupTime);
-->injectionResult = findTouchedWindowTargetsLocked(currentTime,
entry, inputTargets, nextWakeupTime, &conflictingPointerActions);
-->dispatchEventLocked(currentTime, entry, inputTargets);
-->getConnectionIndexLocked(inputTarget.inputChannel);//这个inputChannel用于与window实例通信
-->prepareDispatchCycleLocked
-->enqueueDispatchEntriesLocked
-->startDispatchCycleLocked
-->publishMotionEvent
-->mChannel->sendMessage(&msg)
return true;
}
InputDispatcher的作用就是通过sendMessage进行跨进程通信,把msg发给对应的window
如果是按键类型的时间,那么就会通过notifyKey的流程,然后传递给PhoneWindowManager这个读者可以自行梳理

流程总结:内核将原始事件写入设备节点中,InputReader不断地通过Eventhub将原始事件取出来并翻译加工成android输入事件,然后交给InputDispacther.InputDispactcher根据WMS提供的窗口信息,将事件交给格式的窗口,窗口的ViewRootimpl对象再沿着控件树将事件派发给感兴趣的控件,控件对其接收到的事件做出响应,更新自己的画面,执行特定的动作;当事件处理完成后,会执行finishInputEvent()方法,在进一步调用InputConsumer:sendFinshedSignal告知InputDispatcher线程事件已经处理完成,InputDispatch完成后,最终会调用doDispatchCycleFinishedLockedInterrupttible方法将dispacthEntry从等待队列里面移除;

如Gityuan总结的图片所示

input

显示方向旋转

Sensor的框架分为三个层次,客户层,服务层,HAL层,服务端负责从HAL层读取数据,并将数据写到管道中,客户端通过管道读取服务端数据;具体细节可以看这两份文档

Android Sensor Framework概览

sensor框架Framework层解读

流程图:

1567068378839

自动旋转监听

系统从SystemUI或者setting中获取到是否开启自动旋转;最终设置到mUserRotationMode和mUserRotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
systemUI:	
./services/core/java/com/android/server/wm/DisplayRotation.java
void freezeRotation(int rotation) {
rotation = (rotation == -1) ? mDisplayContent.getRotation() : rotation;
setUserRotation(WindowManagerPolicy.USER_ROTATION_LOCKED, rotation);
}
void thawRotation() {
setUserRotation(WindowManagerPolicy.USER_ROTATION_FREE, mUserRotation);
}
settings:
updateSettings() {
final int userRotation = Settings.System.getIntForUser(resolver,
Settings.System.USER_ROTATION, Surface.ROTATION_0,
UserHandle.USER_CURRENT);
final int userRotationMode = Settings.System.getIntForUser(resolver,
Settings.System.ACCELEROMETER_ROTATION, 0, UserHandle.USER_CURRENT) != 0
? WindowManagerPolicy.USER_ROTATION_FREE
: WindowManagerPolicy.USER_ROTATION_LOCKED;
updateOrientationListenerLw()
-->mOrientationListener.enable(true /* clearCurrentRotation */);
}
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
services/core/java/com/android/server/policy/WindowOrientationListener.java
一:注册监听
public void enable(boolean clearCurrentRotation) {
if (mSensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mSensorManager.registerListener(
mOrientationJudge, mSensor, mRate, DEFAULT_BATCH_LATENCY, mHandler);
} else {
mSensorManager.registerListener(mOrientationJudge, mSensor, mRate, mHandler);
}
二:sensor有数据的时候就会发一个回调
public void onSensorChanged(SensorEvent event) {
这里就会计算出一个proposedRotation,分发给需要的模块
onProposedRotationChanged(proposedRotation);
}
services/core/java/com/android/server/wm/DisplayRotation.java
三:决策是否旋转和启动旋转动画流程
public void run() {
// Send interaction hint to improve redraw performance.
mService.mPowerManagerInternal.powerHint(PowerHint.LAUNCH, 1);
if (isRotationChoicePossible(mCurrentAppOrientation)) {
final boolean isValid = isValidRotationChoice(mRotation);
sendProposedRotationChangeToStatusBarInternal(mRotation, isValid);
} else {
mService.updateRotation(false /* alwaysSendConfiguration */,
false /* forceRelayout */);
}
}

rotation和orientation区别

rotation:旋转方向是指界面(不是手机)相对于默认情况情况顺时针旋转的角度,平板一般默认横屏,而小屏幕设备默认竖屏;

1
2
3
4
5
frameworks/base/core/java/android/view/Surface.java
public static final int ROTATION_0 = 0;
public static final int ROTATION_90 = 1;
public static final int ROTATION_180 = 2;
public static final int ROTATION_270 = 3;

orientation:分为两种情况,一个是ActivityInfo.java,另一个在Configuration.java.前者具体来说是ActivityInfo.screenOrientation,这个值用于记录App强制设定的方向或者旋转模式。具体代码如下:

rotation旋转的是物体坐标系,而orientation旋转的是世界坐标系,即观察视角;

应用申请旋转

对于应用开发者而言可以通过调用如下api去申请屏幕朝向

1
2
core/java/android/app/Activity.java
setRequestedOrientation

获取命令如下:

1
dumpsys window | grep mCurrentAppOrientation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
frameworks/base/core/java/android/content/pm/ActivityInfo.java
/**
* The preferred screen orientation this activity would like to run in.
* From the {@link android.R.attr#screenOrientation} attribute, one of
* {@link #SCREEN_ORIENTATION_UNSPECIFIED}, -1;//未指定,默认值,由安卓选择合适方向,关闭系统旋转屏幕,就不会转动屏幕
* {@link #SCREEN_ORIENTATION_LANDSCAPE}, 0//横屏
* {@link #SCREEN_ORIENTATION_PORTRAIT}, 1//竖屏
* {@link #SCREEN_ORIENTATION_USER}, 2//用户选择方向
* {@link #SCREEN_ORIENTATION_BEHIND}, 3//继承Activity堆栈中当前Activity下面的那个Activity的方向
* {@link #SCREEN_ORIENTATION_SENSOR}, 4//由物理感应器决定显示方向
* {@link #SCREEN_ORIENTATION_NOSENSOR}, 5//忽略物理感应器
* {@link #SCREEN_ORIENTATION_SENSOR_LANDSCAPE}, 6//锁定屏幕横屏,可以180旋转,相当于SCREEN_ORIENTATION_USER_LANDSCAPE并且打开自动旋转功能
* {@link #SCREEN_ORIENTATION_SENSOR_PORTRAIT}, 7//锁定屏幕竖屏,可以180旋转
* {@link #SCREEN_ORIENTATION_REVERSE_LANDSCAPE}, 8//强制横屏方向
* {@link #SCREEN_ORIENTATION_REVERSE_PORTRAIT}, 9//竖屏方向转换
* {@link #SCREEN_ORIENTATION_FULL_SENSOR}, 10//当手机底部朝上时,根据重力变换朝向,形成相对竖屏旋转180的效果
* {@link #SCREEN_ORIENTATION_USER_LANDSCAPE}, 11//横屏显示,当打开自动旋转功能时,如果用户在横屏方向上调整方位,屏幕也会跟着变动
* {@link #SCREEN_ORIENTATION_USER_PORTRAIT}, 12//竖屏显示
* {@link #SCREEN_ORIENTATION_FULL_USER}, 13//相当于默认打开自动旋转功能
* {@link #SCREEN_ORIENTATION_LOCKED},14 //锁定屏幕方向
*/

流程图如下所示:

setRequestOrientation

横屏竖用原理

参数设置流程介绍

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
hardware/interface
configstore/1.1/default/surfaceflinger.mk
LOCAL_CFLAGS += -DPRIMARY_DISPLAY_ORIENTATION=$(SF_PRIMARY_DISPLAY_ORIENTATION)
configstore/1.1/default/SurfaceFlingerConfigs.cpp
Return<void> SurfaceFlingerConfigs::primaryDisplayOrientation(
...
orientation = PRIMARY_DISPLAY_ORIENTATION;
...
}

services/surfaceflinger/SurfaceFlingerProperties.cpp
SurfaceFlingerProperties::primary_display_orientation_values primary_display_orientation(
SurfaceFlingerProperties::primary_display_orientation_values defaultValue) {
...
DisplayOrientation result =
getDisplayOrientation<V1_1::ISurfaceFlingerConfigs,
&V1_1::ISurfaceFlingerConfigs::primaryDisplayOrientation>(
configDefault);
return SurfaceFlingerProperties::...
...
}

services/surfaceflinger/SurfaceFlinger.cpp
SurfaceFlinger::primaryDisplayOrientation = DisplayState::...
creationArgs.displayInstallOrientation = isInternalDisplay ? primaryDisplayOrientation : DisplayState::eOrientationDefault;

应用欺骗

在SurfaceFlinger中初始化primaryDisplayOrientation,当应用获取显示配置的时候getDisplayConfigs,此时的应用就能够获取到对应的竖屏配置了;

1
2
3
4
5
6
7
8
status_t SurfaceFlinger::getDisplayConfigs(const sp<IBinder>& displayToken,
Vector<DisplayInfo>* configs) {
...
if (displayId == getInternalDisplayIdLocked() &&
primaryDisplayOrientation & DisplayState::eOrientationSwapMask) {
std::swap(info.w, info.h);
...
}

显示欺骗

在DisplayDevice中就会初始化mDisplayInstallOrientation,在设置矩阵的时候oritentationToTransform就会被篡改;

1
2
3
4
5
6
7
8
9
void DisplayDevice::setProjection(int orientation,
const Rect& newViewport, const Rect& newFrame) {
...
if (isPrimary()) {
DisplayDevice::orientationToTransfrom(
(orientation + mDisplayInstallOrientation) % (DisplayState::eOrientation270 + 1),
w, h, &R);
...
}

这样给到GPU的Transform就会被欺骗带有一个角度;

1567234573802

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
system/core/libsystem/include/system/graphics-base-v1.0.h
typedef enum {
HAL_TRANSFORM_FLIP_H = 1, // (1 << 0)
HAL_TRANSFORM_FLIP_V = 2, // (1 << 1)
HAL_TRANSFORM_ROT_90 = 4, // (1 << 2)
HAL_TRANSFORM_ROT_180 = 3, // (FLIP_H | FLIP_V)
HAL_TRANSFORM_ROT_270 = 7, // ((FLIP_H | FLIP_V) | ROT_90)
} android_transform_t;

include/gui/LayerState.h
struct DisplayState {
enum {
eOrientationDefault = 0,
eOrientation90 = 1,
eOrientation180 = 2,
eOrientation270 = 3,
eOrientationUnchanged = 4,
eOrientationSwapMask = 0x01
};

这样就实现了从应用到显示横屏竖用的欺骗流程

其他

常见命令

模拟输入事件

1
2
3
4
5
6
1. 模拟文本 input text "hello,word"
2. 模拟按键 input keyevent POWER
3. 模拟滑动 input swipe 0 20 300 500
4. 模拟屏幕轻触:input tap 100 400
5. 获取输入事件的总类:getevent -p
6. input keyevent --longpress POWER

旋转事件

1
2
3
settings put system accelerometer_rotation 0 //关闭自动旋转
settings put system user_rotation 0
settings put system user_rotation 3

安卓触点显示

1
2
settings put system show_touches 1
settings put system pointer_location 1

传感器

1
2
可以利用查看目前sensor上报的事件值
dumpsys sensorservice | grep wall

1566995553990

参考资料

一次触摸,Android到底干了啥

Android屏幕旋转源码探索及应用实践

Android P图形显示系统

图解Android-AndroidGUI 系统(5)-Android的Event Input System

Android硬件加速(二)-RenderThread与OpenGL GPU渲染

源码解读Android属性动画

动画详解

深入理解surface

应用绘制流程

screenOrientation参数解析

Android中的转屏流程

Input系统-事件处理全过程

alarm机制概览

Posted on 2019-07-18 | In standby

常用消息机制

安卓有很多消息传递机制,基于不用使用场景,我们会采用对应的消息传递机制,而无论是那种消息传递机制,都是按照消息发送者和消息接收者这个进行设计的,以下是我们在不同场景下会选择的消息传送机制;

同一app内部的同一组件内的消息通信(单个或多个线程之间) ;

常用方法:Message Handler/Message Loop

原理图:

1562654136209

MessageQueue流程:

  1. Looper 通过addFd 添加文件管道监听和事件回调;
  2. 当文件监听到消息后,handler通过dispatchMessage进行消息派发,Looper->sendMessage;
  3. 收到消息后走到Handler的handleMessgae,进行消息处理

同一app内部的不同组件之间的消息通信(单个进程)

常用方法:EventBus

原理图:

1562053083623

EventBus流程:

还没有接触使用过,暂时不讲,是安卓的一种轻量级的事件通知机制;

其他

  1. 同一app具有多个进程的不同组件之间的消息通信

  2. 不同app之间的组件之间消息通信

  3. Android 系统在特定情况下与App之间的消息通信

​ 常用方法:PendingIntent

​ 原理图:

** 1562056294011

​

BroadCast流程:

  1. 将Intentfliter跟receiver绑定,可通过静态方式:在AndroidManifest.xml 进行配置,也可以动态方式,registerReceiver;
  2. Brastcast先打包Intent,然后通过sendBrocast,进行广播;
  3. recevier接受到对应的广播在onReceive中走处理流程;

闹钟app也是依托这些广播的机制进行运作;以下我们会对闹钟设置与闹钟响应的流程进行简单的介绍;

DeskClock APP

代码路径:packages/apps/DeskClock

设置闹钟流程:

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
43
44
45
46
47
48
当用户点击选择相应的闹钟后:
TimePickerDialogFragment.java
public void onClick(DialogInterface dialog, int which) {
listener.onTimeSet(TimePickerDialogFragment.this,
timePicker.getCurrentHour(), timePicker.getCurrentMinute());
}
添加对应闹钟:
src/com/android/deskclock/alarms/AlarmTimeClickHandler.java
asyncAddAlarm(a)
// Create and add instance to db
setupAlarmInstance(newAlarm)
这里会做两件重要的事情:
一:Pending获取和闹钟注册,
private static class AlarmManagerStateChangeScheduler implements StateChangeScheduler {
@Override
public void scheduleInstanceStateChange(Context context, Calendar time,
AlarmInstance instance, int newState) {
final long timeInMillis = time.getTimeInMillis();
LogUtils.i("Scheduling state change %d to instance %d at %s (%d)", newState,
instance.mId, AlarmUtils.getFormattedTime(context, time), timeInMillis);
final Intent stateChangeIntent =
createStateChangeIntent(context, ALARM_MANAGER_TAG, instance, newState);
// Treat alarm state change as high priority, use foreground broadcasts
stateChangeIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
PendingIntent pendingIntent = PendingIntent.getService(context, instance.hashCode(),
stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);

final AlarmManager am = (AlarmManager) context.getSystemService(ALARM_SERVICE);
if (Utils.isMOrLater()) {
// Ensure the alarm fires even if the device is dozing.
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);//这里就能让闹钟跟pendingIntent进行绑定,闹钟来临时就会广播Intent中的Action
} else {
am.setExact(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);
}
}


当createStateChangeIntent的时候,就会将服务与Intent进行绑定
public static Intent createStateChangeIntent(Context context, String tag,
AlarmInstance instance, Integer state) {
...
Intent intent = AlarmInstance.createIntent(context, AlarmService.class, instance.mId);
intent.setAction(CHANGE_STATE_ACTION);
...
}


二:updateNextAlarm(context) 将用户时钟更新到数据库中,以便保存闹钟信息;这个时间会被保存到settings里面;这样的好处是重新启动后信息不会丢失;确保闹钟能收到闹钟信息;

pendingIntent:等待着的Intent获取pendingIntent有以下方法,getActivity:(用于启动一个Activity的pendingIntent); getBroadcast(方法从系统取得一个用于向BrocastReceiver的Intent广播的PendingIntent); getService(用于启动一个Service的pendingIntent)分别对应着3个行为,跳转到一个activity组件,打开一个广播组件和打开一个服务组件;

如上发起端DeskClock通过PendingIntent.getService会在到在AMS中得到PendingIntentRecord对象,当对应的闹钟事件到来时,AlarmManagerService会通过传递的PendingIntent对象中的send方法发起回调,从而做出执行对应的动作;

通过am的set方法设置后,当闹钟时间来临的时候;就会触发下面的响应流程;

闹钟响应流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

当闹钟来临的时候,就会调到AlarmService的onStartCommand
public int onStartCommand(Intent intent, int flags, int startId) {
...
case AlarmStateManager.CHANGE_STATE_ACTION:
AlarmStateManager.handleIntent(this, intent);
startAlarm(instance);
...
}

private void startAlarm(AlarmInstance instance) {
...
AlarmAlertWakeLock.acquireCpuWakeLock(this);
//当处于后台,此时就会出现我们常见的界面全屏的闹钟提示;
AlarmNotifications.showAlarmNotification(this, mCurrentAlarm);
sendBroadcast(new Intent(ALARM_ALERT_ACTION));
...
}

注意事项:

一:闹钟响应流程中睡眠

从DeskClock应用行为我们也可以看到在应用写闹钟处理函数的时候,需要进行持锁,尤其在耗时较长的操作中;避免还没有进入处理函数,就因为autoSleep进程没有disable从而进入休眠;另外对于在休眠过程中需要唤醒系统的应用来说setExactAndAllowWhileIdle,旧版本setExact在系统处于Doze的时候,同样会被忽略;

二:setForeground问题:

当API从25升至28时,会出现前台服务设置异常问题,修复如下:

一:针对Permission Denial: startForeground ,AndroidManifest.xml需添加属性

1
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

二:针对invalid channel for service notification,需进行channel创建,如下所示:

1
2
3
4
5
6
7
8
9
10
NotificationChannel notificationChannel = new NotificationChannel(
DESKCLOCK_NOTIFICATION_CHANNEL,
"deskclock",
NotificationManager.IMPORTANCE_HIGH);

NotificationCompat.Builder notification = new NotificationCompat.Builder(service,DESKCLOCK_NOTIFICATION_CHANNEL)

NotificationManager notificationManager =
(NotificationManager) service.getSystemService(Service.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(notificationChannel);

三:应用在后台的时候PendingIntent 无法startService,所以上述代码流程闹钟应用在后台是不正常的;

  1. android提供了一套JobIntentService的机制,对于响应不是很及时的可以改成这种接口;
  2. 可将PendingIntent改写为BrocastRecevicer然后在OnReceive中startForegroundService将service带到前台;前台服务的创建需要注意两个细节,可参考此文档Android8.0使用通知创建前台服务
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
一:Pending改成以下形式
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, instance.hashCode(),
stateChangeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
二:在广播中开启服务startForegroundService
public class AlarmReceiver extends BroadcastReceiver {


@Override
public void onReceive(Context context, Intent intent) {
final int alarmState = intent.getIntExtra(AlarmStateManager.ALARM_STATE_EXTRA, -1);
final long instanceId = AlarmInstance.getId(intent.getData());
Intent in = AlarmInstance.createIntent(context, AlarmService.class, instanceId);
.....
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
context.startForegroundService(in);
}
else{
context.startService(in);
}
}
}
三:在服务中startForeground
public int onStartCommand(Intent intent, int flags, int startId) {
...
if(Build.VERSION.SDK_INT>=26){
setForeground();
}
...
}

AlarmManager

主要代码分布:

core/java/android/app/AlarmManager.java

services/core/java/com/android/server/AlarmManagerService.java

services/core/jni/com_android_server_AlarmManagerService.cpp

安卓是怎么实现对闹钟进行监听和注册,分发的呢?

闹钟的监听

SystemServer在startOtherServices时,会调用SystemServiceManager中的startService,此时AlarmManagerService中的onStart函数就会被调用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void onStart() {
//调用native的init
mNativeData = init();
//因为内核不会保存时区,所以每次重启都需要重新设置
setTimeZoneImpl(SystemProperties.get(TIMEZONE_PROPERTY));
//如果获取到的时间小于固件生成的时间,那么利用ro.build.date.utc的时间去更新内核RTC的时间;
//没有的话不是则按照系统当前时间即可;这样能保证起来的系统时间是比较新的
setKernelTime(systemBuildTime);
//注册闹钟服务,用来接收DATE_CHANGED,更新日历时间
mClockReceiver = mInjector.getClockReceiver(this);
//监听量灭屏广播,
new InteractiveStateReceiver();
//监听应用协助重启广播
new UninstallReceiver();
//启动闹钟线程
AlarmThread waitThread = new AlarmThread();
waitThread.start();
//发布相关服务,安卓为了提升通信效率。将service分成binder service和local service
//(用于本进程通信)对应的获取接口为getLocalService(AlarmManagerInternal.class)
publishLocalService(AlarmManagerInternal.class, new LocalService());
//非本进程接口,(AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE)
publishBinderService(Context.ALARM_SERVICE, mService);
}

在start初始化的时候,我们主要注意的是init流程里面建立的对alarm事件的监听,以及在AlarmThread里面对闹钟时间的响应;

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

static jlong android_server_AlarmManagerService_init(JNIEnv*, jobject){
...
TimerFds fds;
epollfd = epoll_create(fds.size());
fds[i] = timerfd_create(android_alarm_to_clockid[i], TFD_NONBLOCK);
//wall_clock这里会支持的RTCid,这个rtc可以用来距离启动和休眠时的时间
static const clockid_t android_alarm_to_clockid[N_ANDROID_TIMERFDS] = {
CLOCK_REALTIME_ALARM, //对应安卓闹钟type:RTC_WAKEUP 常用于应用闹钟;
// A settable system-wide real-time clock
CLOCK_REALTIME, //对应安卓闹钟type:RTC

CLOCK_BOOTTIME_ALARM, //ELAPSED_REALTIME_WAKEUP //用于计时器,因为不能随系统时间更改
//单调递增的时钟,包括休眠时候的时间
CLOCK_BOOTTIME, //ELAPSED_REALTIME

//单调递增的时钟,不包括休眠时候的时间,这种没有看到安卓有进行使用
CLOCK_MONOTONIC,

//We also need an extra CLOCK_REALTIME fd which exists specifically to be
//canceled on RTC changes.
CLOCK_REALTIME,
};

简单而言,就是创建了对这几种闹钟类型的监听,但允许设置的时钟类型仅有4种;
然后在waitForAlarm中等待这几类闹钟事件上报,并上报有闹钟时间的类型
int AlarmImpl::waitForAlarm()
{
....
int nevents = epoll_wait(epollfd, events, N_ANDROID_TIMERFDS, -1);
//如果闹钟在底层被取消了,返回闹钟事件需要更新,没有的话则返回闹钟的类型;
ssize_t err = read(fds[alarm_idx], &unused, sizeof(unused));
...
}

闹钟的注册

接回apk设置alarm流程往下梳理注册流程:

1
2
3
4
apk调用:
setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, timeInMillis, pendingIntent);

setImpl(type, triggerAtMillis, WINDOW_EXACT, 0, FLAG_ALLOW_WHILE_IDLE, operation, null, null, null, null, null);

我们这里主要关注两个参数:flag和type

flag讲解:

flag主要影响的是idle状态是否需要唤醒

1
2
3
4
1.FLAG_STANDALONE = 1;用于标识该alarm不会被加入到其他alarm集合中去;
2.FLAG_WAKE_FROM_IDLE = 2;当系统处于Idle状态仍然能唤醒系统;
3.FLAG_ALLOW_WHILE_IDLE = 4;在idle下alarm仍会被执行,并且不会退出idle状态;
4.FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED = 8;与3类似,但运行不受任何约束

TYPE讲解:

应用能设置的类型为:RTC_WAKEUP,RTC,ELAPSED_REALTIME_WAKEUP,ELAPSED_REALTIME.这四种;

但是在AlarmManagerService会RTC类型的时间进行转换,convertToElapsed,最终设置的类型转换成ELAPSED_REALTIME类型,所以将闹钟的类型只有ELAPSED_REALTIME_WAKEUP,ELAPSED_REALTIME;

1563011053168

流程总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1.set流程里面会权限进行检查,根据是否为系统应用或者白名单里面的应用,对flag进行调整;
2.setImpl
1.闹钟的时间间隔不得低于5s
2.convertToElapsed(triggerAtTime, type),将绝对时间转换为相对时间,也即是开机时间
3.比对窗口时间,如果窗口时间为0,精确执行,小于0,通过当前时间减去触发时间得到一个窗口时间,如果大于0则把最早触发时间+窗口时间为最晚的时间;
3.setImplLocked
1.removeLocked(operation, directReceiver)
2.构造一个逻辑闹钟alarm,setImplLocked(a, false, doValidate)
4.setImplLocked
1.如果是DeviceIdleController(管理doze模式)设置的闹钟,会被模糊提前;
2.adjustDeliveryTimeBasedOnBucketLocked
androidP对应用进行了分组,
1.活跃:用户正在使用;闹钟延迟0min
2.工作集:经常在运行,但并未处于活跃状态;延迟6min;
3.常用:应用会被定期使用,但不是每天都必须使用;30min;
4.极少使用:应用不经常使用;2h;
5.从未使用;10d
3.insertAndBatchAlarmLocked
1.如果是带有FLAG_STANDALONE标志的,新建一个Batch;
2.如果不是的话会根据两个闹钟设置的时TriggerTime和最大允许的闹钟发生时间(max triigertime);两个 之间取交集(narrow batch)作为闹钟触发时间如果找不到则继续新建;
4.rescheduleKernelAlarmsLocked
从Batch list里面找到近的闹钟,通过timerfd设置到内核;
5.updateNextAlarmClockLocked
更新用户下一次的闹钟时间

如图为batch的形成过程:

1562911201925

闹钟时间分发

对于时间事件的分发,我们主要看AlarmThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private class AlarmThread extends Thread
{
while (true)
{
int result = waitForAlarm();
if (WAKEUP_STATS) {
...
//可以记录一天内的闹钟唤醒历史,可以用来统计闹钟优化的程度;
recordWakeupAlarms(mAlarmBatches, nowELAPSED, nowRTC);
...
}
//并获取到点闹钟列表,根据当前是否处于Doze模式或者app standby更新闹钟的响应时间
boolean hasWakeup = triggerAlarmsLocked(triggerList, nowELAPSED);
//将获取到的闹钟进行派发
deliverAlarmsLocked(triggerList, nowELAPSED);
这几个在set的时候也会被调用不再赘述
reorderAlarmsBasedOnStandbyBuckets(triggerPackages);
rescheduleKernelAlarmsLocked();
updateNextAlarmClockLocked();
}
}

Alarm消息的传递流程如下所示,如果设置的时候传入的是PendingIntent则会进入send流程,如果设置的时候传入的是Listener那么就会走到onAlarm的流程;

1563105032995

内核闹钟流程

当用户层设置闹钟的时候,内核不会马上设置到RTC寄存器;而是在suspend的时候从队列里面取出最早的时钟,然后设置进去;通过trace我们就能直接看出这种关系来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
timerfd_settime
do_timerfd_settime
timerfd_setup
alarm_start
alarmtimer_enqueue(base, alarm);
timerqueue_add(&base->timerqueue, &alarm->node);
3) | alarm_start() {
3) 2.125 us | alarmtimer_enqueue();
3) + 16.500 us | }
3) | alarm_start() {
3) 3.291 us | alarmtimer_enqueue();
3) + 21.791 us | }
alarmtimer_suspend() {
rtc_timer_start() {
rtc_timer_enqueue() {
__rtc_set_alarm() {
sunxi_rtc_setalarm();
}
}

关机闹钟的实现

当梳理完应用到内核的完整路径后,我们就可以借助这套机制完成我们的所要的需求;

这里主要利用的有两点:

一:DeskClock这类的闹钟应用在设置闹钟的时候会把AlarmClockInfo闹钟信息保存在系统中;此时,我们会将闹钟信息保存,当关机的时候再设置到RTC寄存器中;

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
43
 private void updateNextAlarmInfoForUserLocked(int userId,
AlarmManager.AlarmClockInfo alarmClock) {
if (alarmClock != null) {
if (DEBUG_ALARM_CLOCK) {
Log.v(TAG, "Next AlarmClockInfoForUser(" + userId + "): " +
formatNextAlarm(getContext(), alarmClock, userId));
}
mNextAlarmClockForUser.put(userId, alarmClock);
+ mShutdownReceiver.setTime(alarmClock.getTriggerTime()/1000 - 90);
} else {
if (DEBUG_ALARM_CLOCK) {
Log.v(TAG, "Next AlarmClockInfoForUser(" + userId + "): None");
}
mNextAlarmClockForUser.remove(userId);
+ mShutdownReceiver.setTime(0);
}

mPendingSendNextAlarmClockChangedForUser.put(userId, true);
mHandler.removeMessages(AlarmHandler.SEND_NEXT_ALARM_CLOCK_CHANGED);
mHandler.sendEmptyMessage(AlarmHandler.SEND_NEXT_ALARM_CLOCK_CHANGED);
}

class ShutdownReceiver extends BroadcastReceiver {
private long mTime = 0;
public ShutdownReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SHUTDOWN);
getContext().registerReceiver(this, filter);
updateNextRtcAlarm(0);
}

@Override
public void onReceive(Context context, Intent intent) {
Slog.i(TAG, "AlarmManagerService receive shutting down set rtc alarm time: " + mTime);
synchronized (mLock) {
updateNextRtcAlarm(mTime);
}
}

public void setTime(long time) {
mTime = time;
}
}

二:timerfd和epoll_wait,在关机充电的时候,需要判断关机时间到来,当闹钟时间到的时候;我们会重启系统;进入android;

关键代码:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
void *alarm_thread_handler(void *arg)
{
(void)arg;
int ret = 0;
struct epoll_event events[EPOLL_LISTEN_CNT];
struct timeval now_tv = { 0, 0 };
while (true) {
//等待闹钟事件来临,重启系统
int nevents = epoll_wait(epollfd, events, EPOLL_LISTEN_CNT, -1);
if (nevents < 0) {
LOG(WARNING) << __func__ <<" ++++"<< __LINE__ << " event:"<< nevents<< "errno: "<< errno << "\n";
continue;
}
unsigned long long wakeups;

if (read(wakealarm_fd, &wakeups, sizeof(wakeups)) == -1) {
LOGE("wakealarm_event: read wakealarm fd failed\n");
continue;
}
gettimeofday(&now_tv, NULL);
LOG(WARNING) << __func__ <<" " << "rebooting" << "now" << now_tv.tv_sec <<"\n";
request_suspend(false);
reboot(RB_AUTOBOOT);
}

return NULL;
}
static void init_shutdown_alarm(void)
{
long alarm_secs, alarm_in_booting = 0;
struct timeval now_tv = { 0, 0 };
struct timespec ts;
struct epoll_event ev;
//获取闹钟时间
alarm_secs = get_wakealarm_sec();
// have alarm irq in booting ?
alarm_in_booting = is_alarm_in_booting();
gettimeofday(&now_tv, NULL);

LOG(WARNING) << "alarm_in_booting: "<< alarm_in_booting << "alarm_secs " << alarm_secs << "now" << now_tv.tv_sec << "\n";
// alarm interval time == 0 and have no alarm irq in booting
if (alarm_secs <= 0 && (alarm_in_booting != 1))
return;
if (alarm_secs)
ts.tv_sec = alarm_secs;
else
ts.tv_sec = (long)now_tv.tv_sec + 1;

ts.tv_nsec = 0;

struct itimerspec spec;
memset(&spec, 0, sizeof(spec));
memcpy(&spec.it_value, &ts, sizeof(spec.it_value));

//timerfd_init
wakealarm_fd = timerfd_create(CLOCK_REALTIME_ALARM, 0);
if (wakealarm_fd <= 0) {
LOGE("%s, %d, alarm_fd=%d and exit\n", __func__, __LINE__, wakealarm_fd);
return ;
}

if (timerfd_settime(wakealarm_fd, TFD_TIMER_ABSTIME, &spec, NULL) == -1){
LOGE("timerfd_settime failed Error[%d:%s]\n",errno,strerror(errno));
close(wakealarm_fd);
return ;
};
//epoll_init
epollfd = epoll_create(EPOLL_LISTEN_CNT);
if (epollfd > 0) {
ev.events = EPOLLIN | EPOLLWAKEUP;
//add wakealarm_fd to epollfd
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, wakealarm_fd, &ev) == -1) {
LOGE("epoll_ctl failed; errno=%d\n", errno);
return;
}
} else {
LOGE("epoll_create failed; errno=%d\n", errno);
return;
}
pthread_create(&tid_alarm, NULL, alarm_thread_handler, NULL);
return;
}

其他

系统时间设置与RTC时间

1
2
3
4
5
6
7
8
9
10
11
12
在启动或者联网的时候,安卓会帮我们将网络时间设置到RTC中,前面的过程我们先跳过直接讲最后的
在framework/base/services/core/jni/com_android_server_AlarmManagerService.cpp中
1.通过setKernelTimeZone将时区设置到内核 通过getprop persist.sys.timezone 可以获取设置的时区,如无则默认没有0时区
2.通过setKernelTime更新时间到RTC中
设置的流程如下:
setKernelTime
-->setTime
setTimeofday设置墙上时间
gmtimer_r 将当前时间转换成格林威治时间;
ioctl(fd, RTC_SET_TIME, &rtc)将转化后的格林威治时间写入RTC中
所以当我们看cat /proc/driver/rtc时间的时候,就是当前时间与时区计算到的格林时间,
在看这个rtc节点的时候会出现与系统时间差上时区;

代办

谷歌闹钟管理优化梳理;

参考资料

Android组件系列–Intent详解

Intent的基本使用

Android消息机制,从java层到Native层剖析

Android事件总线(二)EventBus3.0源码解析

Android BroadcastReceiver使用详解

后台执行限制

Android广播机制

Google出品的序列化神奇Protocol Buffer使用攻略

理解AlarmManager机制

说说PendingIntent的内部机制

Vsync流程

Posted on 2019-06-18 | In display

Vsync

垂直同步,扫描一帧的同步时间,如屏幕的刷新率为60Hz,那么周期为1/60s,也就是16.66ms一次的时间间隔;

在了解安卓的显示流程的时候,网上一般都会贴个图说是通过VSYNC驱动下的绘制,合成,显示的流程线方式;

1558945552666

实际上由于app和sf都存在着存在一个相位偏移,所以并不是安全按照Vsync的时序来完成这些操作的;

那什么是VSYNC呢?为了有最基本的认识,让我们从底层代码开始讲起,借助VSYNC我们也会大致了解和体会到显示相关的代码框架;

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
./drivers/video/fbdev/sunxi/disp2/disp/dev_disp.c
在fb_init的时候,我们就会创建这个内核线程;
fb_init
g_fbi.vsync_task[i] = kthread_create(vsync_thread, (void*)i, task_name);

static int vsync_thread(void *parg)
{
unsigned long disp = (unsigned long)parg;

while (1) {

vsync_proc(disp);
-->snprintf(buf, sizeof(buf), "VSYNC%d=%llu", disp, ts);
envp[0] = buf;
envp[1] = NULL;
kobject_uevent_env(&g_fbi.dev->kobj, KOBJ_CHANGE, envp);
set_current_state(TASK_INTERRUPTIBLE);
schedule();
if (kthread_should_stop())
break;
set_current_state(TASK_RUNNING);
}

return 0;
}
在这个线程里面会不断的上报Vsync的信号,然后休眠;那什么时候唤醒呢?

disp_lcd_enable
disp_sys_register_irq(lcdp->irq_no,0,disp_lcd_event_proc,(void*)lcd,0,0);
disp_lcd_event_proc
sync_event_proc
ret = gdisp.init_para.vsync_event(disp);
vsync_event
drv_disp_vsync_event
wake_up_process(g_fbi.vsync_task[sel]);

根据irq_no和dts可以获知是注册的TCON的中断,也即对应着屏幕的刷新率;

如果我们使用ftrace抓取workqueue的使用情况的话,就能看到有如下的打印

1
2
$ echo workqueue:workqueue_queue_work > /sys/kernel/debug/tracing/set_event
$ cat /sys/kernel/debug/tracing/trace_pipe > out.txt

vsync

对于底层的disp,除了提供VSYNC信号,另外,他还提供了disp_ioctl这套系统调用接口,太多的话就不做阐述,如果后面有哪个接口比较重要再做描述;

HWC

代码目录在hareware/aw/hwc2中

hwc提供了安卓标准的hwc的相关接口实现(hwc2_function_pointer_t);安卓通过HIDL调用到hwc的相关function;然后再通过这些接口实现了对kernel/driver的调用;在hwc中有两个主要的线程,负责监听底层的事件,还有送显;

1558932920872

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
threadResouce/hwc_event_thread.cpp
eventThreadLoop
vsyncUeventParse(context, msg);
callVsync(context, vsync_id, timestamp);
在线程中,如果捕捉到Vsyn的Uevent时间,然后进行回调;

hwc中callback的注册流程:
int32_t hwc_register_callback(hwc2_device_t* device, int32_t descriptor,
hwc2_callback_data_t callbackData, hwc2_function_pointer_t pointer)

registerEventCallback(int bitMapDisplay, int32_t descriptor, int zOrder,
hwc2_callback_data_t callback_data, hwc2_function_pointer_t pointer)
registerEventCallback(0x3, descriptor, 0, callbackData, pointer);

surfaceflinge会通过HIDL的方式注册这个回调函数,这个我们后面再讲;

HIDL

那么HIDL怎么玩的?

1558945098496

如上所示就是一个HIDL的玩法;以vsync的callback注册流程为例;

SF_BE:SurfaceFlinger采用的是前后端设计,与HWC相关的逻辑都会放到SurfaceFlingeBE中

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
代码路径:framwwork/nativeservices/surfaceflinger/
SurfaceFlinger.cpp
SurfaceFlinger::init(){
...
mCompositionEngine->setHwComposer(getFactory().createHWComposer(getBE().mHwcServiceName));
mCompositionEngine->getHwComposer().registerCallback(this, getBE().mComposerSequenceId);
...
}

void SurfaceFlinger::onVsyncReceived(int32_t sequenceId, hwc2_display_t hwcDisplayId,
int64_t timestamp) {
ATRACE_NAME("SF onVsync");

Mutex::Autolock lock(mStateLock);
// Ignore any vsyncs from a previous hardware composer.
if (sequenceId != getBE().mComposerSequenceId) {
return;
}

if (!getHwComposer().onVsync(hwcDisplayId, timestamp)) {
return;
}

if (hwcDisplayId != getHwComposer().getInternalHwcDisplayId()) {
// For now, we don't do anything with external display vsyncs.
return;
}

bool periodChanged = false;
mScheduler->addResyncSample(timestamp, &periodChanged);
if (periodChanged) {
mVsyncModulator.onRefreshRateChangeDetected();
}
}

HWC2_Client:属于surfaceflinge进程,通过Binder和HWC2的HAL Server交互,命名空间是HWC2

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
代码路径:services/surfaceflinger/DisplayHardware/
HWComposer.cpp
void HWComposer::registerCallback(HWC2::ComposerCallback* callback,
int32_t sequenceId) {
mHwcDevice->registerCallback(callback, sequenceId);
}

std::unique_ptr<HWC2::Device> mHwcDevice;

services/surfaceflinger/DisplayHardware/HWC2.cpp
void Device::registerCallback(ComposerCallback* callback, int32_t sequenceId) {
if (mRegisteredCallback) {
ALOGW("Callback already registered. Ignored extra registration "
"attempt.");
return;
}
mRegisteredCallback = true;
sp<ComposerCallbackBridge> callbackBridge(
new ComposerCallbackBridge(callback, sequenceId));
mComposer->registerCallback(callbackBridge);
}
class ComposerCallbackBridge : public Hwc2::IComposerCallback {
Return<void> onVsync(Hwc2::Display display, int64_t timestamp) override
{
mCallback->onVsyncReceived(mSequenceId, display, timestamp);
return Void();
}
}

services/surfaceflinger/DisplayHardware/ComposerHal.cpp
void Composer::registerCallback(const sp<IComposerCallback>& callback)
{
auto ret = mClient->registerCallback(callback);
if (!ret.isOk()) {
ALOGE("failed to register IComposerCallback");
}
}

HWC SERVER:

client调用到server中标准的接口

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
43

代码路径:hardware/interfaces/graphics/composer/2.1/

utils/hal/include/composer-hal/2.1/ComposerClient.h
Return<void> registerCallback(const sp<IComposerCallback>& callback) override {
// no locking as we require this function to be called only once
mHalEventCallback = std::make_unique<HalEventCallback>(callback, mResources.get());
mHal->registerEventCallback(mHalEventCallback.get());
return Void();
}

utils/passthrough/include/composer-passthrough/2.1/HwcHal.h
void registerEventCallback(hal::ComposerHal::EventCallback* callback) override {
...
mDispatch.registerCallback(mDevice, HWC2_CALLBACK_VSYNC, this,
reinterpret_cast<hwc2_function_pointer_t>(vsyncHook));
...
}

static void vsyncHook(hwc2_callback_data_t callbackData, hwc2_display_t display,
int64_t timestamp) {
auto hal = static_cast<HwcHalImpl*>(callbackData);
hal->mEventCallback->onVsync(display, timestamp);
}
mDispatch是个结构体
struct {
...
HWC2_PFN_REGISTER_CALLBACK registerCallback;
...
} mDispatch = {};

template <typename T>
bool initDispatch(hwc2_function_descriptor_t desc, T* outPfn) {
auto pfn = mDevice->getFunction(mDevice, desc);
if (pfn) {
*outPfn = reinterpret_cast<T>(pfn);
return true;
} else {
ALOGE("failed to get hwcomposer2 function %d", desc);
return false;
}
}
这里之后我们就可以看到代码是走到各自厂家封装的函数了;

HWC VENDER:

提供hwc2_function_pointer_t的实现;

1
2
3
4
5
6
7
8
9
10
11
12
13
代码路径:hardware/aw/hwc2
hwc.cpp
hwc2_function_pointer_t hwc_device_getFunction(struct hwc2_device* device,
int32_t /*hwc2_function_descriptor_t*/ descriptor)
{
...
case HWC2_FUNCTION_REGISTER_CALLBACK:
return asFP<HWC2_PFN_REGISTER_CALLBACK>(
hwc_register_callback);
}
...

}

总结:

1.安卓提供了一套标准的hwc2_function_pointer_t给厂家去实现,然后自己就可以通过HIDL接口进行调用;

2.在Vsync时间来临时,会通过回调调到SurfaceFlinger的实现函数onVsyncReceived,然后又会通过唤醒DispSync这个线程,通过postEvent传递VSYNC事件,然后MessageQueue收到这个消息后,就会调用对应的hanlder函数,最终调用到SurfaceFlinger中onMessageReceiver处理,这里就会去决策是否要进行合成了;细节后面会讲到,我们现在只需要知道有VSYNC会来的时候会调到SurfaceFlinge的回调就行;

MessageQueue

surfaceflinger启动

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
system/core/rootdir/init.rc
on boot
class_start core

frameworks/native/services/surfaceflinger/surfaceflinger.rc
service surfaceflinger /system/bin/surfaceflinger

./services/surfaceflinger/main_surfaceflinger.cpp
int main(int, char**) {
startGraphicsAllocatorService();
ProcessState::self()->setThreadPoolMaxThreadCount(4);
sp<ProcessState> ps(ProcessState::self());
ps->startThreadPool();//设定surfaceflinger进程的binder线程池个数上限为4,并启动binder线程池

sp<SurfaceFlinger> flinger = surfaceflinger::createSurfaceFlinger();
setpriority(PRIO_PROCESS, 0, PRIORITY_URGENT_DISPLAY);//设置进程优先级
set_sched_policy(0, SP_FOREGROUND);//设置为前台进程

flinger->init(); //初始化
sp<IServiceManager> sm(defaultServiceManager());
sm->addService(String16(SurfaceFlinger::getServiceName()), flinger, false,
IServiceManager::DUMP_FLAG_PRIORITY_CRITICAL | IServiceManager::DUMP_FLAG_PROTO);
sched_setscheduler(0, SCHED_FIFO, &param)//设置进程运行策略

startDisplayService();
flinger->run(); //实行surfaceflinger中的run方法
}

先将创建的线程给打出来,后面有涉及到的线程作用也会提及

1558615784026

MessageQueue创建

类介绍

1
2
3
4
5
6
7
class SurfaceFlinger : public BnSurfaceComposer, 实现SurfaceComposer接口
public PriorityDumper, //实现SurfaceFlinger的信息dump
IBinder::DeathRecipient,
private HWC2::ComposerCallback 实现以下三种事件回调
//onHotplugReceived 屏幕热插拔事件
//onRefreshReceived 当图层的配置参数有变动的时候,SurfaceFlinger前面给的数据不能用的时候
//onVsyncReceived 接受底层硬件上报的垂直同步信息

onFirstRef

1
2
3
4
5
6
7
8
9
10
flinger的数据类型为sp强指针类型,当首次被强指针引用的时候则执行onFirstRef
onFirstRef
mEventQueue->init(this);

services/surfaceflinger/Scheduler/MessageQueue.cpp
void MessageQueue::init(const sp<SurfaceFlinger>& flinger) {
mFlinger = flinger;
mLooper = new Looper(true);
mHandler = new Handler(*this);
}

img

简单介绍一下就是looper不断从MessageQueue中取出一个Message,然后交给其对应的Hanlder;handler通过Looper sendMessgae 到队列中,也负责处理相关的消息,处理的消息为INVALIDATE,REFRESH当Vsync来的时候会调到callback,onVsyncReceived;接下来就是消息的收发是怎么驱动起来的了~

线程创建

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
void SurfaceFlinger::init() {
....
//创建EventControlThread来控制Vsync的开关, DispSyncThread软件产生的Vsync的线程
mScheduler =
getFactory().createScheduler([this](bool enabled) { setPrimaryVsyncEnabled(enabled); },
mRefreshRateConfigs);
--> make_unique<impl::DispSync>("SchedulerDispSync")
-->new DispSyncThread(name, mTraceDetailedInfo);
-->primaryDispSync->init
-->mThread->run("DispSync", PRIORITY_URGENT_DISPLAY + PRIORITY_MORE_FAVORABLE);
--> mEventControlThread = std::make_unique<impl::EventControlThread>(function);


//获取Vsync周期
auto resyncCallback =
mScheduler->makeResyncCallback(std::bind(&SurfaceFlinger::getVsyncPeriod, this));

//SF EventThread该线程用于SurfaceFlinger接受Vsync用于渲染和合成
mSfConnectionHandle = mScheduler->createConnection("sf",
mPhaseOffsets->getCurrentSfOffset(),resyncCallback, [this](nsecs_t timestamp) {
mInterceptor->saveVSyncEvent(timestamp);
});
-->makeEventThread
--> std::make_unique<DispSyncSource>(dispSync, phaseOffsetNs, true, connectionName);
std::make_unique<impl::EventThread>(std::move(eventThreadSource),
std::move(interceptCallback), connectionName); -->mThread = std::thread([this]() NO_THREAD_SAFETY_ANALYSIS {
std::unique_lock<std::mutex> lock(mMutex);
threadMain(lock);
});
....
}

线程流程图

img

对于我们而言需要知道的就是onVsyncReceive后会经过addResyncSample->updateModel 来到DispSyncThread线程的一直走到onVsyncEvent然后走到mSFEventThread的sendEvent,这个时候我们的MessageQueue就会收到消息,派发INVALID的消息然后决策是否需要刷新;

虽然说系统每秒有60个HW Vsync,但不代表APP和Vsync在每个Vsync都要更新画面,因为在Android里面,是根据software VSYN(Vsync-sf和Vsync-app)来更新画面,Software Vsync根据HwVsync过去发生的时间推测未来发生的时间,因此当APP或SF利用requestNextVsync的时候才会触发Vsync-sf或VSYNC-app;当SW Vsync和硬件Vsync误差无法接受的时候,就会重新打开硬件Vsync,来重新调节SW vsync

消息处理

INVALIDATE消息:用于处理Layer或者Display属性的变化以及Layer对应的buffer的更新

1
2
1.Layer或者Display属性的更新通过调用handleMessageTransaction()处理
2.buffer的更新通过调用handleMessageInvalidate()处理

当做完这一步后会去决策是否要进行刷新;

REFRESH消息:表示SurfaceFlinger需要进行一次合成操作(Refresh),通过handleMessageRefresh()实现;主要有三种情况:

1
2
3
1.Layer属性的变化导致window state发生变化
2.Layer有新的buffer到来
3,HWC请求进行一次repaint

如果这三种情况之一发生,则置refreshNeeded为true,调用signalRresh发出MessgaeQueue:REFRESH消息

当Vsync信号来之前,Layer或者Display属性的变化会做本地保存,只有当Vsyn信号到来时,SurfaceFlinger才会通过INVALIDATE和REFRESH消息来做统一的合并渲染和输出的处理工作;

1559196614903

代码流程如下:

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
43
void SurfaceFlinger::onMessageReceived(int32_t what) NO_THREAD_SAFETY_ANALYSIS {
switch (what) {
case MessageQueue::INVALIDATE: {
if (hwcFrameMissed && !gpuFrameMissed) {
signalLayerUpdate();
-->connection->resyncCallback();
break;
}
handleMessageTransaction()
handleMessageInvalidate()
-->handlePageFlip();
//从bufferqueue中取出下一个图形缓冲区,就好像翻页一样;
//该函数主要是从各个layer对应的BufferQueue中拿图形缓冲区资料,并根据内容更新脏数据
-->invalidateLayerStack
-->signalRefresh
}
case MessageQueue::REFRESH: {
handleMessageRefresh();

rebuildLayerStacks();
//遍历所有layer按照Z序找到可见图层和可见区域。核心算法有region类完成。
//它拥有一个私有成员变量mStorage(Vector<Rect>类型),并封装了与,或,异或操作函数,管
//理一组合法有序的矩形区域。
//通过region可以方便的计算出多个图层叠加后的可见图层,可见区域及其大小位置

calculateWorkingSet();
//遍历所有可见图层,在hal创建影子图层参数,例如hal没有该图层即调用createLayer创建,然
//后分配合成策略,决策由GPU还是DE合成;
prepareFrame(display);
//通过调用hwc validate函数,会调用hwc中的AssignLayer函数,根据厂商的DE情况,决策是
//否是DE合成还是GPU
doComposition(display, repaintEverything);
//合成,如果GPU合成则调用OpenGLES合成到FB,否则全部可见图层参数送到DE,即调用hal的
//hwc_presentDisplay函数,调用presentDisplay获取fence; submitLayerToDisplay将显示
//Layer加到链表中然后唤醒hwc_submit_thread,
//调用setupLayer设置好送显的图层参数,再等待生成者的fence释放完成后;
//调用commitToDisplay送显,最后displayToSrceen调用ioctl完成

postComposition();
//完成合成后的一些状态处理
break;
}
}

总结

以下是一帧的大概显示流程,后续有机会在详细讲下技术细节:

​ 事件驱动:vsync-app驱动应用层绘图完毕后会回调onFrameAvaliable到SF的layer里面更新layer状态,然后调用requestVsync把mSFEventThread唤醒(应用若长期不送帧,会进入休眠状态),在下一个Vsync信号来时会往MessageQueue(MQ)队列添加invalidata消息。MQ是SF的主线程,它主要处理invalidate和refresh消息。当MQ收到Refresh消息后,就会真正的进入合成和送显的流程;关键流程就是以上所说的rebuildLayerStacks, calculateWorkingSet, prepareFrame, doComposition, postComposition

​ SF进行Refresh时,通过VrModeSwitch()判断是否处于虚拟桌面,调用prepareFrame(对应HWC的de2TryToAssignLayer)询问HWC各个图层的合成方式。

​ 接着调用drawMesh()进行重绘合成(OPENGL)和调用postFrameBuffer()提交合成的画布或这一帧。SF提交是不代表GPU已经处理完成,所以不直接送显;而是通过submitLayerToDisplay()把这一帧放入链表中,就直接返回接着处理下一帧。

​ HWC::DisplayOpr有一个线程,submitThreadLoop()循环通过从链表中拿出一帧数据,通过commitToDisplay()异步向display下发,下发前等GPU完成即fence释放;

其他

如何强制GPU合成:

1
2
3
4
5
6
service call SurfaceFlinger 1008 i32 1

确认是否生效:
venus-a1:/ # dumpsys SurfaceFlinger | grep "h/w composer"
h/w composer state:
h/w composer disable

配置关闭硬件VSYNC

1
2
方案目录的mk文件中添加如下:
TARGET_RUNNING_WITHOUT_SYNC_FRAMEWORK := true

参考资料

Android消息机制(一):概述设计架构

Android P 图形显示系统(一)硬件合成HWC2

android 8.0 Hwcomposer2 HIDL流程

Android OTreble架构-HIDL源代码分析

Android SurfaceFlinger学习之路(十一)合成layer之准备合成)

opengl学习总结

Posted on 2019-05-13 | In opengl

opengl流程

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口确实2D像素组,这导致OpenGL大部分的工作都是关于把3D坐标转变为适应屏幕的2D像素;3D转成2D坐标的处理过程是OpenGl的图形渲染管线(Graphic Pipeline,大多译为管线,实际上指的是一堆原始图形数据途径一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的;图形渲染管线可以划分为两个主要部分,第一部分把你3D的坐标转换成2D坐标;第二部分是把2D坐标转变成实际的有颜色的像素;

1
2D坐标和像素不同,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到屏幕/窗口分辨率的限制

openGL首先接收用户提供的几何数据(顶点和几何图元),并且将它输入到一系列着色器阶段中进行处理,包括:顶点着色(vertex shader ),细分着色,以及最后的几何着色,然后被送入光栅化单元(resterizer)光栅化单元负责对所有剪切区域(clipping region)内的图元生成片元数据,然后对每个生成的片元都执行一个片元着色器;

1561280730893

下面有个更清晰容易理解的图~

顶点着色器(Vertex Shader),它把一个单独的顶点(Vertex)作为输入,顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理;如:顶点变换,法向量变化和单位化,生成相应的纹理坐标,纹理坐标变换,雾坐标,光照计算; 当需要计算顶点在屏幕上的位置,就会涉及到矩阵变换

图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(r如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定的图形,如上图就是一个三角形;

几何着色器(Geometry Shader)几何图形把图元形式的一系列顶点的集合作为输入,它可以通过产生新的顶点构造出新的图元来生成其他形状;这个着色阶段是可选

之后就会传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment).在这之前还会线做下裁切(Cliping)会丢弃超出视图以外的所有像素,用来提升效率;

片段着色器的主要目的是计算一个像素的最终颜色,这也是OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照,阴影,光的颜色等等),这些数据可以用来计算最终像素的颜色;片元着色器的作用有:雾;提取纹理单元,用于纹理贴图;颜色混合,等

最后的测试和混合简单而言是根据Zorder和Alpha值进行的图形合成过程;

先有个大概流程的印像,接下来我们会通过一些例子慢慢将流程梳理清楚;先从opengl的基本库开始讲起;

Opengl相关库

opengl基本函数库用来描述图元(graphics output promitive),属性(attribute),几何变换(geometric transformation),观察变换(viewing transform)和进行其他的操作;由于OpenGl被设计成硬件无关型,因此输入和输出函数等许多操作均不包括在其基础库中;而是放在OpenGl开发的辅助库中;

Opengl基本库(也称OpenGL核心库):常见有gl开头的这些函数的写法glBegin, glClear, glCopyPixels, glPolygonMode;变量中GL_2D,GL_RGB,GL_CCW;GL_POLYGON,GL_AMBIENT_AND_DIFFUSE;

相关库:OpenGL实用函数(openGL Utility,GLU)提供了一些例程,可以设置观察和投影矩阵,利用线条和多边形近似法来描述复杂对象,使用线性近似法显示二次曲线和样条曲线,处理表面绘制操作,以及完成其他复杂任务。每一个OpenGL实现中都包括了GLU库,所有的函数名都用glu开头。窗口显示系统,opengl实用函数工具包(OpenGL Utility Toolkit,GLUT)提供了与任意屏幕窗口系统进行交互的函数库。GLUT库函数以glut为前缀,该库也包含了描述与绘制二次和样条曲线及曲面的方法;

说这么多,先来个demo~

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
#include <GL/GLUT.h>
void init(void)
{
glClearColor(1.0, 1.0, 1.0, 1.0);//使用RGB颜色值将显示窗口的背景颜色设定为白色,参数解析前面为RGB,最后为alpha值

glMatrixMode(GL_PROJECTION);//设置投影矩阵,
gluOrtho2D(0.0, 200.0, 0.0, 150.0);//正交投影见过世界坐标系二维矩阵区域的内容映射到屏幕上,区域的x的坐标值从0.0到200,y坐标从0到150.只要再该矩形内定义的对象,都会出现在显示窗口
}

void lineSegment(void)
{
glClear(GL_COLOR_BUFFER_BIT);//用来指定它是颜色缓存中的位值

glColor3f(0.0, 0.4, 0.2);//设置线段的颜色
glBegin(GL_LINES);
glVertex2i(180, 15);
glVertex2i(10, 145);
glEnd();

glFlush();

}

void main(int argc, char** argv)
{
glutInit(&argc, argv); //初始化glut.该函数也能处理命令行参数
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); //指定显示窗口使用单个缓存和用RGB的颜色模型旋转颜色
glutInitWindowPosition(50, 100);//窗口显示的地方在在显示器的左上角向右50,向下100像素开始显示
glutInitWindowSize(400, 300);//设定显示窗口的初始宽度和高度的像素数
glutCreateWindow("An Example"); //窗口创建的时候给个标题
init();
glutDisplayFunc(lineSegment);//显示lineSegment线段
glutMainLoop();//激活已创建的窗口和图形显示的内容
}

显示效果如下:

demo

通过这个demo我们大致了解到opengl的大致使用步骤

  1. 初始化物体渲染所对应的状态。
  2. 设置需要渲染的物体

接着demo下面我们引入几个概念:

MVP

demo中的init函数中glMatrixMode(GL_PROJECTION);//设置投影矩阵,这个涉及到了投影矩阵,对于opengl常见的就是要进行矩阵变换;我们这里稍微带下

矩阵变换

  1. 红色坐标系位模型坐标系,指定了模型各个点的位置,每个点是(x,y,z)组成的数组;由于后面的矩阵都是4*4,所以在后面补上1;
  2. 模型变换,将模型坐标转化成世界坐标,把物体在世界坐标系的位置拆分成旋转,平移,缩放的表达式;
  3. 视图变换:指定一个相机的位置和角度,然后去观察世界坐标系下的物体;
  4. 投影变换:把前面三位空间的坐标系投影到二维屏幕的坐标系,除了屏幕的横纵坐标,另外一个维度就是垂直屏幕方向的坐标,就是之后可以写入深度缓冲区的值。将三维坐标转换到二维屏幕,主要分为正交投影和透视投影,都是用相似三角形算比例;
  5. 视口变换,这里只是一个非常简单的XoY平面上的缩放;它决定了最终渲染到平面的哪一块,所以用之前的缩放同样的处理就能得到相应矩阵;这里相当于我们经常在ui中遇到的缩放因子;

于是乎:

1
变换后的坐标=视口矩阵 * 投影矩阵(P) * 视图矩阵(V) * 模型矩阵(M) * 模型点坐标

对应的VR里面左右眼的矩阵模型公式:

1
leftEyeMvp = projectMatrix*projRot(投影矩阵) * mHeadOrient*mleftEye(视口矩阵) * mode(模型点)

通过这个矩阵我们就能从三维空间映射到我们的屏幕上,由于左右眼的视口矩阵不同产生的视觉差,让我们又产生了3d的效果;

这里有两个工具可以更好的理解这一概念:

MODELVIEW

http://www.songho.ca/opengl/files/matrixModelView.zip

PROJECTION

http://www.songho.ca/opengl/files/matrixProjection.zip

知道了矩阵变换后;我们还会经常遇到着色器这个名字;那这个是什么呢?

着色器

着色器(Shader)是运行在GPU上的小程序,这些小程序为图形渲染管线的某个特定部分而运行;在openGL中着色器就相当于画笔,而顶点vertices相当于图形(把一个个点按顺序用线连接起来就是一个图形),着色器OpenGL分成两个部分,一个用于绘制顶点的顶点着色器VerticesShader,一个用于顶点连线后所包围的区域填充颜色的片元着色器,可以理解为windows画图中的填充工具;

1560837291736

我们常见的着色器普遍认为有三种,在移动设备上使用较多的OpenGL的GLSL,在桌面使用较多微软推的DirectX的HSLS(High level shader language);还有NVIDIA公司的CG(C for Graphic);谷歌后面会推的Vulkan,着色器语言是基于GLSL为基础进行增强;由于本文基于的是OpenGL所以就是用的GLSL;

语言总是苍白的,来段代码吧~~

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#include <stdio.h>
#include <stdlib.h>
#include <GL/glew.h>
#include <GL/glut.h>


float g_lightPos[4] = {1,0.5,1,0};

void changeSize(int w, int h) {

// Prevent a divide by zero, when window is too short
// (you cant make a window of zero width).
if(h == 0)
h = 1;

float ratio = 1.0* w / h;

// Reset the coordinate system before modifying
glMatrixMode(GL_PROJECTION);
glLoadIdentity();

// Set the viewport to be the entire window
glViewport(0, 0, w, h);

// Set the correct perspective.
gluPerspective(45,ratio,1,1000);
glMatrixMode(GL_MODELVIEW);
}


void renderScene(void) {

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glLoadIdentity();
gluLookAt(0.0,0.0,5.0,0.0,0.0,-1.0,0.0f,1.0f,0.0f);

glLightfv(GL_LIGHT0, GL_POSITION, g_lightPos);
glutSolidTeapot(1);

glutSwapBuffers();
}

void processNormalKeys(unsigned char key, int x, int y) {

if (key == 27)
exit(0);
}

char* readShaderSource(const char *fileName)
{
FILE *fp;
char *content = NULL;
int count=0;

if (fileName != NULL)
{
fp = fopen(fileName,"rt");

if (fp != NULL)
{
fseek(fp, 0, SEEK_END);
count = ftell(fp);
rewind(fp);
if (count > 0)
{
content = (char *)malloc(sizeof(char) * (count+1));
count = fread(content,sizeof(char),count,fp);
content[count] = NULL;
}
fclose(fp);
}
}
return content;
}

GLuint genShader(GLenum type,const char* fileName,char*& log)
{
//创建着色器对象
GLuint shader = glCreateShader(type);
//从文件中读取着色器的实现代码
char* shaderSource = readShaderSource(fileName);
if( !shaderSource )
return 0;
const char* ptrShaderSource = shaderSource;
//将着色器的实现代码与创建的着色器对象绑定
glShaderSource(shader,1,&ptrShaderSource,NULL);
free(shaderSource);
//编译着色器对象
glCompileShader(shader);
GLint status = 0;
//查看编译状态
glGetShaderiv(shader,GL_COMPILE_STATUS,&status);
if( !status )
{
GLint length;
//读取日志信息的长度
glGetShaderiv(shader,GL_INFO_LOG_LENGTH,&length);
log = (GLchar*)malloc(length);
//读取日志信息
glGetShaderInfoLog(shader,length,&length,log);
#if 1
printf("%s\n",log);
#endif
//删除着色器对象
glDeleteShader(shader);
return 0;
}
return shader;
}

GLuint linkProgram(GLuint* shader,int shaderNum,char*& log)
{
//创建着色器程序
GLuint program = glCreateProgram();
int i;
//往着色器程序中加入着色器对象
for( i=0 ; i<shaderNum ; i++ )
glAttachShader(program,shader[i]);
//链接着色器程序
glLinkProgram(program);
GLint status;
//查看链接状态
glGetProgramiv(program,GL_LINK_STATUS,&status);
if( !status )
{
GLint length;
//读取日志信息的长度
glGetProgramiv(program,GL_INFO_LOG_LENGTH,&length);
log = (GLchar*)malloc(length);
//读取日志信息
glGetProgramInfoLog(program,length,&length,log);
#if 1
printf("%s\n",log);
#endif
//删除着色器对象
glDeleteProgram(program);
return 0;
}
return program;
}

void useProgram(GLuint program)
{
//运行创建成功的着色器程序
glUseProgram(program);
}

bool setShaders()
{
char* log = NULL;
//创建一个顶点着色器对象
GLuint vertexShader = genShader(GL_VERTEX_SHADER,"toon.vert",log);
if( !vertexShader )
{
free(log);
return false;
}
//创建一个片断着色器对象
GLuint fragmentShader = genShader(GL_FRAGMENT_SHADER,"toon.frag",log);
if( !fragmentShader )
{
free(log);
glDeleteShader(vertexShader);
return false;
}

//把创建好的顶点和片断着色器对象链接到着色器程序中
GLuint shader[2] = {vertexShader,fragmentShader};
GLuint program = linkProgram(shader,2,log);
if( !program )
{
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
free(log);
return false;
}
//使用创建成功的着色器程序
useProgram(program);

return true;
}

int main(int argc, char **argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(100,100);
glutInitWindowSize(320,320);
glutCreateWindow("Shader-Demo");

glutDisplayFunc(renderScene);
glutReshapeFunc(changeSize);
glutKeyboardFunc(processNormalKeys);

glEnable(GL_DEPTH_TEST);
glClearColor(1.0,1.0,1.0,1.0);


glewInit();
//判断是否支持GLSL
if (GLEW_ARB_vertex_shader && GLEW_ARB_fragment_shader)
printf("Ready for GLSL\n");
else
{
printf("No GLSL support\n");
exit(1);
}

setShaders();
glutMainLoop();

return 0;
}

顶点着色器

1
2
3
4
5
6
7
8
9
varying vec3 normal, lightDir;

void main()
{
lightDir = normalize(vec3(gl_LightSource[0].position));
normal = normalize(gl_NormalMatrix * gl_Normal);

gl_Position = ftransform();
}

当我们讨论到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的

片源着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

varying vec3 normal, lightDir;

void main()
{
float intensity;
vec3 n;
vec4 color;

n = normalize(normal);
intensity = max(dot(lightDir,n),0.0);

if (intensity > 0.98)
color = vec4(0.8,0.8,0.8,1.0);
else if (intensity > 0.5)
color = vec4(0.4,0.4,0.8,1.0);
else if (intensity > 0.25)
color = vec4(0.2,0.2,0.4,1.0);
else
color = vec4(0.1,0.1,0.1,1.0);

gl_FragColor = color;
}

效果图如下所示:

1561355152918

总结

着色器的创建流程 genShader:

  1. 创建一个着色器对象 glCreateShader;
  2. 将着色器的实现代码与创建的着色器对象绑定 glShaderSource;
  3. 把着色器源代码编译为目标代码 glCompileShader;
  4. 验证是否编译通过 glGetShaderiv;

接着,把创建好的着色器加入到着色程序中

  1. 创建一个着色程序 glCreateProgram;
  2. 把适当的着色对象连接到这个着色程序中 glAttachShader;
  3. 链接到这个着色器程序 glLinkProgram;
  4. 验证这着色器结点已经成功完成 glGetProgramiv;
  5. 使用着色器进行顶点或者片元处理 useProgram;

对于安卓来讲,会把这些接口都进行封装,简化使用;

参考资料

搞懂矩阵运算

第三课:矩阵

着色器

openGL ES学习之着色器语言

Android OpenGL ES从白痴到入门

openGl 矩阵变换

openGL着色器介绍

OpenGL替代者-Vulkan

是立体还是平面

休眠唤醒cma内存泄漏

Posted on 2019-04-27 | In display

现象分析

现场排查

起因keeptesting休眠唤醒出现vmap allocation申请内存失败,log打印如下

1
2
3
4
5
6
7

[ 7499.814118] [ion_debug]In ion_buffer_destroy: release buf size 249856, phys_addr 70bc0000, process[1947:composer@2.1-se]
[ 7499.899841] vmap allocation for size 5537792 failed: use vmalloc=<size> to increase size
[ 7499.910499] platform cma: Fail to allocate buffer
[ 7499.941821] vmap allocation for size 5537792 failed: use vmalloc=<size> to increase size
[ 7499.952483] platform cma: Fail to allocate buffer
[ 7499.983029] vmap allocation for size 5537792 failed: use vmalloc=<size> to increase size

通过查看显示相关的进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
t7-p3:/ # procrank
PID Vss Rss Pss Uss Swap PSwap USwap ZSwap cmdline

2387 1050156K 141496K 43392K 32760K 0K 0K 0K 0K com.android.systemui

------ ------ ------ ------ ------ ------ ------
479314K 354572K 708K 708K 708K 534K TOTAL

ZRAM: 676K physical used for 896K in swap (754616K total swap)
RAM: 1006160K total, 50708K free, 2984K buffers, 513540K cached, 1016K shmem, 46584K slab

查看对应的dma_buf分配情况,发现个数不断的增大

procmem -p 2387 | grep dmabuf | grep 1948 | wc -l

通过在ion_map的时候将进程信息添加到对应的ion debug节点中,unmap再删除,看到异常的dmabuf对应的进程;对应的是2880 systemui和launcher

1
2
3
4
5
6
7
8
9
t7-p3:/ # cat /sys/kernel/debug/ion/heaps/cma | grep dmabuf | grep 1994
allocator@2.0-s 1966 1994752 0 1 | 2217/android.display dmabuf:172 | 3426/Binder:3413_2 dmabuf:172 |
allocator@2.0-s 1966 1994752 0 1 | 3168/Binder:2880_6 dmabuf:d3 | 9217/Binder:3413_4 dmabuf:d3 |
allocator@2.0-s 1966 1994752 0 1 | 9347/Binder:2880_9 dmabuf:13e | 3426/Binder:3413_2 dmabuf:13e |
allocator@2.0-s 1966 1994752 0 1 | 2217/android.display dmabuf:17a | 9217/Binder:3413_4 dmabuf:17a | 2894/Binder:2880_1 dmabuf:17a |
allocator@2.0-s 1966 1994752 0 1 | 2217/android.display dmabuf:16a | 2894/Binder:2880_1 dmabuf:16a | 3426/Binder:3413_2 dmabuf:16a |
allocator@2.0-s 1966 1994752 0 1 | 3426/Binder:3413_2 dmabuf:105 | 9168/Binder:2880_7 dmabuf:105 |
allocator@2.0-s 1966 1994752 0 1 | 2895/Binder:2880_2 dmabuf:a5 | 9217/Binder:3413_4 dmabuf:a5 |
allocator@2.0-s 1966 1994752 0 1 | 9347/Binder:2880_9 dmabuf:da | 9217/Binder:3413_4 dmabuf:da |

跟踪logcat中gralloc alloc的时候的打印,发现异常截图size与截图的大小一致;

1
2
3
03-26 19:34:17.096  1980  2160 D SurfaceFlinger: captureLayers here sourcecrop = 0 0 - 1920 720
03-26 19:34:17.108 2186 2212 E KernelCpuSpeedReader: Failed to read cpu-freq: /sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state (No such file or directory)
03-26 19:34:17.109 1965 2107 I [Gralloc]: ion_alloc from ion_client:5 via heap type DMA(mask:16) for 1990720 Bytes cached buffer successfully, usage = 0x00000333

内存回收

首先怀疑是否为安卓内存没有回收导致

强制触发内存回收

方式一:kill -10 pid
方式二:am dumpheap com.test.test /sdcard/test.hprof
方式三:在进程onReceive中添加System.gc() ;然后在adb shell中输入 am  broadcast -a INTENT_ACTION_NAME_HERE

安卓系统内存回收可以分为三种情况:

第一,用户程序调用StartAcitity(),使当前的活动的Activity被覆盖;
第二,用户按BACK键,退出当前应用程序;
第三:启动一个新的应用程序。这些能够触发内存回收的时间最终调用的函数接口就是activityIdleInternal

通过kill -10 pid 的方式,触发安卓内存进行回收,发现内存没有释放;

从内核debug节点也看到此问题是上层systemui和launcher映射的fd没有释放导致;

休眠截屏流程

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
services/core/java/com/android/server/wm/TaskSnapshotController.java
/**
* Called when screen is being turned off.
*/
void screenTurningOff(ScreenOffListener listener) {
if (shouldDisableSnapshots()) {
listener.onScreenOff();
return;
}

// We can't take a snapshot when screen is off, so take a snapshot now!
mHandler.post(() -> {
try {
synchronized (mService.mWindowMap) {
mTmpTasks.clear();
mService.mRoot.forAllTasks(task -> {
if (task.isVisible()) {
mTmpTasks.add(task);
}
});
snapshotTasks(mTmpTasks);
}
} finally {
listener.onScreenOff();
}
});
}

在休眠老化过程中,将shouldDisableSnapshots默认配置为true,没有出现内存泄漏,更加确定与截屏流程有关;

进一步确认:
配合dumpsys命令,传入虚拟内存地址和长度,最终调用如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void native_dumpAddr(JNIEnv* /* env */, jobject /* clazz */, jint addr, jint len) {
std::stringstream ss;
ss << "/storage/emulated/0/" << std::hex << addr;
const char * path = ss.str().c_str();

int fd = open(path, O_CREAT | O_WRONLY | O_NOFOLLOW | O_CLOEXEC | O_APPEND, 0666);
if (fd < 0) {
ALOGI("error opening: %s: %s", path, strerror(errno));
return;
}
write(fd, (void *)addr, len);

close(fd);
}

直接在泄漏进程中直接dump虚拟内存内容,转换成图片格式后,确认为截图;

测试场景脚本化

在出现异常的现场时,如果能简化到某些行为,可以使debug的范围很大程度的缩小;在验证问题是否解决的时,也更容易判断

1
2
3
4
5
6
7
8
9
10
11
12
13
while true;
do
am start -n com.android.calculator2/com.android.calculator2.Calculator
sleep 0.5
am start -n com.android.settings/com.android.settings.Settings
sleep 0.5
done
或
while true;
do
input keyevent POWER
sleep 1
done

怀疑方向

从泄漏问题来看,有两点有疑问,一: 主界面为什么不泄露,设置界面泄漏?二:为什么与截屏流程有关,且影响到systemUI和launcher?

主界面没有泄漏原因

从log上看主界面之所以没有泄漏,是因为主界面不走截屏的流程;那么下一步的动作就是看为什么主界面没有截屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** Activity type is currently not defined. */
public static final int ACTIVITY_TYPE_UNDEFINED = 0;
/** Standard activity type. Nothing special about the activity... */
public static final int ACTIVITY_TYPE_STANDARD = 1;
/** Home/Launcher activity type. */
public static final int ACTIVITY_TYPE_HOME = 2;
/** Recents/Overview activity type. There is only one activity with this type in the system. */
public static final int ACTIVITY_TYPE_RECENTS = 3;
/** Assistant activity type. */
public static final int ACTIVITY_TYPE_ASSISTANT = 4;

int getSnapshotMode(Task task) {
final AppWindowToken topChild = task.getTopChild();
if (!task.isActivityTypeStandardOrUndefined() && !task.isActivityTypeAssistant()) {
return SNAPSHOT_MODE_NONE;//主界面
} else if (topChild != null && topChild.shouldUseAppThemeSnapshot()) {
return SNAPSHOT_MODE_APP_THEME;
} else {
return SNAPSHOT_MODE_REAL;//设置界面
}
}

结合打印和代码可以发现,由于主界面activity属性为ACTIVITY_TYPE_HOME,故不截屏;而设置界面中,activity属性为ACTIVITY_TYPE_STANDARD;故进行截屏;

dumpsys window

window

并无出现截图数量递增的情况;

安卓P上默认无论是否为低内存设备不再提供禁止截屏的接口,把截屏的第一帧动画当做第一帧进行显示;低内存只能更改第一帧的采样率

services/core/java/com/android/server/wm/TaskSnapshotController.java
    final float scaleFraction = isLowRamDevice ? 1f : 1f;

截屏作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
./services/core/java/com/android/server/am/ActivityRecord.java showStartingWindow
showStartingWindow
addStartingWindow //添加启动窗口
final TaskSnapshot snapshot = mService.mTaskSnapshotController.getSnapshot(
mContainer.getTask().mTaskId, mContainer.getTask().mUserId,
false /* restoreFromDisk */, false /* reducedResolution */);//休眠的时候系统会通过TaskController进行截图

final int type = getStartingWindowType(newTask, taskSwitch, processRunning,
allowTaskSnapshot, 、activityCreated, fromRecents, snapshot);
获取启动窗口的类型,如果启动串口的类型是STARTING_WINDOW_TYPE_SNAPSHOT时,使用snapshot内容作为启动窗口内容
一般我们从recents界面进入应用时,或者应用被压入后台再次点击桌面图标进入应用时走该流程;
窗口类型:
STARTING_WINDOW_TYPE_NONE:不添加starting window
STARTING_WINDOW_TYPE_SNAPSHOT:使用任务快照作为starting window的显示内容
STARTING_WINDOW_TYPE_SPLASH_SCREEN:默认行为,该界面显示的内容搜应用主题相关属性设置的影响

一般情况下我们可以通过设置theme的windowDisablePreview的属性为true禁止显示starting window,如果我们设置了应用窗口背景透明或者使用了float属性为true类型的窗口时,这个时候启动应用时不会添加starting window的。
对于启动窗口的内容何时使用snapshot内容合适受应用的theme影响。我们可以以应用启动方式不同来做一判断,一般应用冷启动和温启动的时候staring window的显示容易受theme影响,应用通过Recent启动或者热启动时,starting window会显示snapshot的内容
if (type == STARTING_WINDOW_TYPE_SNAPSHOT) {
return createSnapshot(snapshot);
}

即截图的图片在启动动画的时候会用来做第一帧动画;如果有keyguard的时候,唤醒启动的时候则会去那休眠前截好的图作为第一帧动画显示;没有则不用走到这里来;T7没有keyguard,故休眠唤醒的流程也不会经过这里;

SystemUi截屏相关流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
启动流程:
startOtherServices()
startSystemUi(context, windowManagerF);
intent.setComponent(new ComponentName("com.android.systemui","com.android.systemui.SystemUIService"));
context.startServiceAsUser(intent, UserHandle.SYSTEM);

src/com/android/systemui/SystemUIService.java
onCreate()
((SystemUIApplication) getApplication()).startServicesIfNeeded();
mServices[i].start();
这时候就会去起recent的服务

截图相关流程
getThumbnail 只是提供缩略图的作用
snapshot = ActivityManager.getService().getTaskSnapshot(taskId, reducedResolution);
return task.getSnapshot(reducedResolution);
task = mStackSupervisor.anyTaskForIdLocked(taskId,
MATCH_TASK_IN_STACKS_OR_RECENT_TASKS);
return task.getSnapshot(reducedResolution);

在休眠唤醒的流程里面,添加打印,并未发现有systemui进入到getSnapshot的相关操作。至此在应用排查阶段一直没有进展;另外烧录gsi同样能发现内存泄漏的问题;怀疑还是自己平台修改导致的可能性比较大;故在目光转移至gralloc分配流程;

进程的地址空间

进程的地址空间(address space)由允许进程使用的全部线性地址(memery region,其含义通常所指的虚拟内存的一个区间,可以称为虚存区 VMA Virtual Memory Area )组成。每个进程所看到的线性地址集成是不同的,一个进程所使用的地址和另外一个进程所使用的地址之间没有什么关系。后面我们还会看到,内核可以通过增加或删除某些线性地址区间来动态地修改进程的地址空间;

内存描述符:与进程地址空间有关的全部信息都包含在一个叫做内存描述符(memory descriptor)的数据结构中,这个类型为mm_struct,进程描述符的mm字段就指向这个内存结构;

ION基本概念

ION作用:用于用户空间的进程之间或者内核空间的模块之间进行内存共享;而且这种共享是0拷贝的;

对于ION的基本概念可以看下篇文章;

内存管理–ION

free时机

一般我们认为当ref_count = 0的时候就会去release这块内存

1
2
3
4
5
6
7
8
9
10
drivers/staging/android/ion/ion.c
ion_buffer_put(struct ion_buffer *buffer)
return kref_put(&handle->ref, _ion_handle_destroy);
return kref_sub(kref, 1, release);
if (atomic_sub_and_test((int) count, &kref->refcount)) {
release(kref);
ion_buffer_destroy
buffer->heap->ops->free(buffer);
vfree(buffer->pages);
kfree(buffer);

可以看到ion buffer的free动作是在在引用计数为0的时候去清理的;实际此时当不同进程都map到同一个buffer的时候,这时候还有一个引用计数会决定是否真正的close;

通过ioct free的时候

Object Operations Ion buffer ref count Ion buf status
allocator@2.0-s(alloc_device_alloc) 1.ion_alloc 1 allocated
2.ion share:dmabuf fd1 2 —
surfaceflinger gralloc_register_buffer
(map)
2
surfaceflinger gralloc_lock 2
surfaceflinger gralloc_unlock 2
surfaceflinger gralloc_unregister_buffer
(unmap)
2
allocator@2.0-s(alloc_device_free) 3.close(ion buffer个数不减少) 2 —
4.ion_free 1(2 -> 1) using
systemUI gralloc_register_buffer
(map)
1 using
systemUI FinalizerDaemon
gralloc_unregister_buffer
(unmap)
0(1 -> 0) freed

内存映射

mmap是一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和虚拟内存

mmap流程

mmap三个阶段:

一:进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;

二:调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

三:进程发起对这片映射空间的访问,引发缺页异常;实现文件内容到物理内存(主存)的拷贝

内存映射类型:

私有内存映射:多个进程会创建一个新的映射,各个进程不共享,也不会反应到物理文件中,比如linux.so动态库就是采用这种形式映射到各个进程虚拟地址空间中;

私有匿名映射:mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存

共享文件映射:多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件会反映到实际物理文件中,他也是进程通信的机制

共享匿名映射:这种机制在fork的时候不会采用写时复制,父子进程完全共享同样的物理内存,这也就实现了父子间通信(IPC)

匿名的意思是需不需要一个fd,正常来讲,当需要进行内存映射的时候,我们都需要依赖一个文件才能实现;通常需要open一个temp文件,穿件后unlink,close掉,比较麻烦;可以直接使用匿名映射来代替,linux也提供了这么一套机制给我们;无需依赖文件即可创建。:

如:int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

映射流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DPATH="/sys/kernel/debug/tracing"
echo > $DPATH/trace
echo nop > $DPATH/current_tracer


echo > $DPATH/set_ftrace_filter
echo "SyS_mmap_pgoff SyS_old_mmap SyS_munmap SyS_open filp_open " >> $DPATH/set_ftrace_filter
echo "do_brk elf_map load_elf_binary" >> $DPATH/set_ftrace_filter
echo "do_mmap do_munmap get_unmapped_area mmap_region vm_mmap vm_munmap vm_mmap_pgoff" >> $DPATH/set_ftrace_filter
echo "__split_vma unmap_region split_vma" >> $DPATH/set_ftrace_filter

echo function_graph > $DPATH/current_tracer
echo 2542 > $DPATH/set_ftrace_pid

echo 1 > $DPATH/tracing_on
cat $DPATH/trace > /data/trace.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 mappedAddress = (unsigned char *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, hnd->share_fd, 0);

1) | SyS_mmap_pgoff() {
1) | vm_mmap_pgoff() {
--> | do_mmap_pgoff
1) | do_mmap() {
1) + 27.334 us | get_unmapped_area();
1) + 15.792 us | mmap_region();
1) + 53.583 us | }
1) + 60.958 us | }
1) + 66.250 us | }

munmap(base, size)

1) | SyS_munmap() {
1) | do_munmap() {
1) + 85.084 us | unmap_region();
1) ! 105.667 us | }
1) ! 111.458 us | }

do_map

1
2
3
4
5
6
7
8
9
10
11
12
do_mmap
//获取未映射的vma
addr = get_unmapped_area(file, addr, len, pgoff, flags);
//先检查vma分配后是否超过进程限制,之后再rb_tree中找到满足本次申请条件vma的前区vma区(内核有很多红黑树存储的数据结构,尤其在内存中)
//如果两个vma能合并就合并成一个,否则就新申请的vma插入rb_tree中。
//在整个过程中,我们能看到的只有vma的申请和磁盘文件的关联
addr = mmap_region(file, addr, len, vm_flags, pgoff);
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX, NULL);
vma->vm_file = get_file(file);
atomic_long_inc(&f->f_count);
error = file->f_op->mmap(file, vma);通过fd进行内存映射

unmap流程

1
2
3
4
5
6
7
8
9
10
11
12
do_munmap
vma = find_vma(mm, start); //找到起始地址落在哪个vma内
unmap_region(mm, vma, prev, start, end);
error = __split_vma(mm, vma, start, 0); //上面有讲到如果两个vma能合并就合并,解映射的时候分离出来
remove_vma_list(mm, vma);
vma = remove_vma(vma);
fput(vma->vm_file);
atomic_long_dec_and_test(&file->f_count)
schedule_delayed_work
delayed_fput
__fput
file->f_op->release(inode, file);

释放时的log

1
2
3
4
5
6
7
8
9
10
11
12
13
[ 1689.870445] ion_buffer_put count:0 phys_adr 72100000 
[ 1689.877035] CPU: 2 PID: 3353 Comm: FinalizerDaemon Tainted: G O 4.9.118 #2
[ 1689.885982] Hardware name: sun8iw17
[ 1689.889931] [<c0111184>] (unwind_backtrace) from [<c010cd1c>] (show_stack+0x20/0x24)
[ 1689.898604] [<c010cd1c>] (show_stack) from [<c0478c5c>] (dump_stack+0x78/0x94)
[ 1689.906703] [<c0478c5c>] (dump_stack) from [<c06fb5f4>] (ion_buffer_put+0xc0/0xd4)
[ 1689.915177] [<c06fb5f4>] (ion_buffer_put) from [<c06fb7fc>] (ion_dma_buf_release+0x1c/0x20)
[ 1689.924520] [<c06fb7fc>] (ion_dma_buf_release) from [<c0585b60>] (dma_buf_release+0x64/0x17c)
[ 1689.934049] [<c0585b60>] (dma_buf_release) from [<c02843cc>] (__fput+0xf4/0x1d0)
[ 1689.942319] [<c02843cc>] (__fput) from [<c0284518>] (____fput+0x18/0x1c)
[ 1689.949806] [<c0284518>] (____fput) from [<c013f574>] (task_work_run+0xc8/0xd8)
[ 1689.957976] [<c013f574>] (task_work_run) from [<c010c458>] (do_work_pending+0xac/0xcc)
[ 1689.966819] [<c010c458>] (do_work_pending) from [<c0107f60>] (slow_work_pending+0xc/0x20)

只有当映射同一块进程都解除映射后,file->f_count,为0;才会调到ion_buffer_put,此时ion_buffer计数为0,释放该buffer

userspace 使用

mali-utgard/gralloc/src/gralloc_module.cpp

进程创建buffer的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
static struct hw_module_methods_t gralloc_module_methods =
{
.open = gralloc_device_open
};

gralloc_device_open
alloc_device_open
m->ion_client = ion_open();
alloc_device_alloc
gralloc_alloc_buffer
ret = aw_ion_alloc(&(m->ion_client), size, 0, heap_mask, flags, &(ion_hnd), usage);
ret = ion_share(m->ion_client, ion_hnd, &shared_fd);
cpu_ptr = mmap(NULL, size, map_mask, MAP_SHARED, shared_fd, 0);

结构体gralloc_module_t定义在文件hardware/libhardware/include/hardware/gralloc.h中,它主要是定义了四个用来操作图形缓冲区的成员函数,如下所示:

1
2
3
4
5
6
7
8
9
private_module_t::private_module_t()
{
```
base.registerBuffer = gralloc_register_buffer;
base.unregisterBuffer = gralloc_unregister_buffer;
base.lock = gralloc_lock;
base.unlock = gralloc_unlock;
```
}

registerBuffer和unregisterBuffer分别用来注册和注销一个指定的图形缓冲区,这个指定的图形缓冲区使用一个buffer_handle_t句柄来描述。所谓注册图形缓冲区,实际上就是将一块图形缓冲区映射到一个进程的地址空间,注销图形缓冲区则是相反的过程;成员函数lock和unlock分别用来锁定和解锁一个缓冲区,例如向一块图形缓冲写入内容的时候,需要将图形缓冲区锁定。用来避免访问冲突;

gralloc 流程debug

对于systemui来讲,在此过程并不会去申请buffer,而是通过内存映射,所以我们主要关注register和unregiser的流程

添加引用计数:

gralloc_register_buffer
     if (size == 1990720) {
                   getNameByPid(hnd->pid,task_name);       
                   count = count + 1;
                   AERR(" 0x%p process %d  task_name:%s size:%d share_fd:0x%x count:%d ", hnd, hnd->pid, task_name, size, hnd->share_fd, count);
     }

gralloc_unregister_buffer        
if (size == 1990720) {
                   getNameByPid(hnd->pid,task_name);       
                   count= count -1;
                   AERR(" 0x%p process %d  task_name:%s size:%d share_fd:0x%x count:%d ", hnd, hnd->pid, task_name, size, hnd->share_fd, count);
}

应用计数log:

1
2
3
gralloc_unregister_buffer:345  0x0xa96c52c0 process 2327  task_name:ndroid.systemui size:1990720 share_fd:0x5b count:2
gralloc_unregister_buffer:345 0x0xaeca7400 process 2327 task_name:ndroid.systemui size:1990720 share_fd:0x59 count:1
gralloc_unregister_buffer:345 0x0xaeccd7f0 process 2327 task_name:ndroid.systemui size:1990720 share_fd:0x4c count:0

通过打印发现,systemUI的register动作和unregister的动作是匹配的,count也能清零;但依然进程中的dmabuf异常存在泄漏

对应的dmabuf如下:

1
2
t7-p3:/ # procmem  -p 2327 | grep dmabuf
1948K 0K 0K 0K 0K 0K 0K 0K anon_inode:dmabuf-c3

且kill -10 后系统也没有完成回收,说明此时已经存在内存泄漏

查看unregister相关代码发现

1
2
3
4
5
6
7
8
9
10
gralloc_unregister_buffer
....
/* if handle is still locked, the unmapping would not happen until unlocked*/
if (!(hnd->lockState & private_handle_t::LOCK_STATE_WRITE))
{
unmap_buffer(hnd);
}

hnd->lockState |= private_handle_t::LOCK_STATE_UNREGISTERED;
....

当存在LOCK_WRITE标识的时候,此时不会被unmap,那么什么时候这个标志是什么时候被赋值的呢?surfaceFlinger在创建这块内存时,对于surfaceFlinger而言;由于截图的动作比较频繁,会调用lock为buffer添加锁保护,当写完之后unlock;这时候systemUI恰好映射到这块内存恰好会将其标志拷贝;对于systemUI而言,本身不会有写的操作,故更不会有unlock的动作;所以就发生了泄漏;解决这个问题的方法。就是在进程映射内存的时候;将这个标志清除掉;

1
2
gralloc_register_buffer
hnd->lockState &= ~(private_handle_t::LOCK_STATE_WRITE);

安卓O同样存在同样的问题,但由于项目一开始为了减少设备的内存使用,所以禁止了相关截图的功能,在androidP上,谷歌没有提供相关的全局禁止功能,故问题就暴露出来;

补丁如下:

1
2
3
4
5
在方案下面的:venus_a1.mk
Disable the task snapshots feature
PRODUCT_PROPERTY_OVERRIDES += \

persist.enable_task_snapshots = false

补充(代办)

上层的gralloc流程

systemUI如何map到surfaceFlinger

参考资料

gralloc流程

安卓性能优化:关于内存泄漏的知识都在这里了

Activity启动窗口和TaskSnapshot

Android操作系统的内存回收机制

信号处理

Android SurfaceFlinger学习之路

认真分析mmap:是什么 为什么 怎么用

Linux内存管理(9)mmap补充

匿名映射

显示硬件篇

Posted on 2019-02-27 | In display

lcd成像

由于市场上只有高端产品如手机屏幕才用OLED,但大部分依然是LCD,故将以LCD成像原理开启此篇;

lcd成像原理

偏振光:指单方向的光,自然光为散光,如果不经过偏振片,可以自由穿透液晶,这样就没办法起到选通作用;

偏振片在3D电影院中的应用:

为什么我们去3D影院需要带3D眼镜呢?3D眼镜其实就是两个不同方向的偏振片,我们人眼之所以看到的物体有3D效果,原因在于左眼和右眼存在一定的距离,当人眼将这两种图像传递给大脑后,大脑就会合成一个3D的图像,所以我们如果想看到3D的图像,必定是传递到我们的眼睛的图形存在偏移;在电影院中,3D电影就是横向和纵向的光源叠加,经过3D眼睛时,假设左眼镜片为只能透过横偏振光,右眼只能透过纵偏振光,那么通过左右眼的时候就会有两个图像;这样人眼就能看到3D效果;

接下来我们就可以大胆猜想下OLED,由于自发光的特性,那么背光和偏振片都是不必要的;这样就可以做的更薄;由于没有背光电路,OLED可以做到不漏光,而且响应速度更快,较LCD更适用于VR产品上;

lcd常用到的知识

  1. 残影: 是指画面切换之后一个画面不会立即消失,而是慢慢不见的现象;
  2. 坏点:是指液晶屏上无法控制的恒亮或者恒暗的点;
  3. mura: 显示器亮度不均匀造成各种痕迹的现象;
  4. 色饱和度(色域): 色饱和度是指显示器色彩鲜艳的程度,显示器是由红,绿,蓝三种颜色来组合成任意颜色光;
  5. 亮度: 亮度是显示器在白色画面下的明亮程度,俗话说一亮遮三丑
  6. 视角:液晶显示器由于天生的物理特性,使得使用者从不同角度看画质会有所变化;与正眼相比,斜看的时候,转到当前画面品质已经变化到无法接受的临界角度。称为显示器的视角
  7. Gamma Curve:是指不同灰阶与亮度的关系曲线。把0~255灰阶当x轴,亮度当y轴,画出来的线就叫做gamma curve;因为人眼对不同亮度有不同的辨识效果,比如说低亮度的辨识能力较高

常用的显示接口

A83的结构图

A83结构图
从A83的结构图可以看到支持的显示接口有RGB,LVDS,HDMI,MIPI

LCD常用接口

接口 接口定义 接口介绍
RGB RGB颜色是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的 接口一般为3.3V电平,需要同步信号,需要时刻刷新图像数据,需要适当的timing
LVDS Low Voltage Differential Signaling,是一种低压差分信号技术接口。它是美国NS公司为克服以TTL电平方式传输宽带高码率数据时功耗大、EMI电磁干扰大等缺点而研制的一种数字视频信号传输方式 高速率(一般655Mbps)低电压,低功耗,低EMI(摆幅350mv)抗干扰能力强,差分信号
MIPI MIPI联盟定义了一套接口标准,把移动设备内部的接口如摄像头、显示屏、基带、射频接口等标准化,从而增加设计灵活性,同时降低成本、设计复杂度、功耗和EMI,在移动设备十分常见 高速:1Gbps/Lane,4Gbps吞吐量
低功耗:200mV差分摆幅,200mv共模电压
噪声抑制
更少的pin,PCB layout更方便
HDMI High-Definition Multimedia Interface,数字接口,同时传输视频和音频,传输非压缩视频数据和压缩/非压缩的数字音频数据 a. TMDS Transition Minimized Differential Signal 8bit~10bit直流平衡编码
每个时钟周期传输10bit数据
b. EDID and DDC 实现设备间只能连接
c. Transfer Video and Audio 更低成本、连接更方便
d. HDCP High-Bandwidth Digital Content Protection

更多的时候,分辨率能支持的大小还是有IC本身设计决定的,与协议本身并没有太大的关系;

还有一个edp接口,这个接口也是超清的接口,目前的话在pc上居多,但对于移动设备,目前还是mipi的居多;有些IC甚至没有edp接口;

显示硬件流程框图

harware

DE合成或从dram中取出显示需要的数据,TCON0中包含RGB和LVDS,可以直接接显示器,MIPI相当于一个独立的IP,将TCON中的RGB信号转换成MIPI信号,再接到显示器;下面将按照显示流程框图对每个模块进行介绍;

DE(display egine)

如下为DE硬件结构框图:从结构图我们大致可以猜测到DE就是做些图形的处理和合成;

如图,该DE主要由两个RT-Mixer(实时合成模块),一个是Mux(解复用模块),一个Write-back(回写模块),主要用于debug,输入依赖内存(DMA)总线,显示内容输出到TCON,再到HDMI/CVBS做显示,回写内容输出返回内存总线。 其中RT-Mixer模块为一个核,可以认为是一个独立的DE,通常DE0,DE1对应的就是RT-Mixer0和RT-Mixer1。

RT-Mixer有四个通道,分别从DMA获取图层内容,然后通过overlay(覆盖)和scale(缩放)模块后依次进行双双混合,最后送给TCON显示。四个通道中,有三个是UI通道和一个视频通道,两种类型通道使用的overlay和scale模块是不一样的,另外视频通道送混合前会进行proc1的处理;

类似的,RT-Mixer1有两个通道,其中一个UI通道和一个视频通道,它们的功能和RT-Mixer0是一样的;

Routing(通道选择)模块比较简单就是通过寄存器值把对应的4个通道输出到不同的通道,以便后续做混合,它的作用就是配置通道的Zorder.

Blender 进行alpha叠加

RT-mixer

如图所示,视频的通道还会先经过proc1

proc1

  1. FCE:Fresh and Contrast enhancement(鲜艳度和对比度增强)
  2. BWS:Black and White Stretch(黑白电平拉伸) 又称灰度拉伸,就是检测到图像的灰度如果集中在某个区域,则将灰色值放大,从而获得比较好的视觉效果,例如可以把偏暗的图片变得明亮一些;
  3. LTI:Lumiance transient improvement(瞬态亮度改善) 让图像中亮度变化过渡分界处变得更加清晰,例如黑白交界处会更加尖锐
  4. PEAK:Luminance Peak(亮度峰化),将输入亮度分量通过带通和高通滤波器获取到增益后叠加到原输入亮度,以增加亮度的峰化值;
  5. ASE:Adaptive Saturation Enhancement(饱和度增强) 主要是对颜色信号进行自适应调整,使画面看起来更鲜艳,亮丽;
  6. FCC:Fancy color curvature change(丽色曲率改善) 主要是通过颜色空间将RGB转换成HSV,对颜色曲率进行调整,再转换回RGB输出;

proc1
如上图,在RT-Mixer0最后的proc2模块是CSC:Color Space Conversion(颜色空间转换)。由于前端输入格式不同,或者一些图像处理算法需要把图像转成另外的格式,所以需要在输出到显示器前格式统一转换成RGB

TCON(TimingControl)

tcon结构图

从结构图可以推测下,TCON主要的主要事情就是准备显示数据,还有显示上的时序;从TCON出来的数据就包含这RGB和LVDS这两种信号;如果需要转换成MIPI还需要经过硬件上的MIPI模块进行处理;
TCON有时还负责一些数据处理,如抖色(dither function),简单来讲就是增加像素的位数,以便显示效果能够更加细腻;现在主流的储存/显示方式是用8位记录每种颜色,三种颜色加起来一共24位(再加上用来定义透明度的8位,有时也被称作32位),每位有256级,三种颜色相乘,一共16,777,216种颜色。这就是所谓的8位色深了,这个标准一般被称作True Color,苹果则通俗的称其Millionsof Colors。不过最后苹果被好多消费者告了,因为其实苹果早年打着Millions of Colors旗号销售的很多电脑的屏幕都不是真正的24位,而是成本更低的18位;但其他厂家也是如此,只不过大家对苹果的期望更高一点而已;靠抖色从18扩大到24位;从而图像的验色可以更加的细腻;另外还有3D fuction的支持,将一张图片,以左右眼的数据进行输出,以便让用户看到3D的效果,这个也是在这里进行处理的,如果要支持这种3D模式,同样的需要佩戴眼镜还有屏幕也需要进行支持;

MIPI(Mobile Industry Processor Interface)

在移动平台上,由于此接口较为常见,所以我们将此接口拿出来讲下;
MIPI是2003年由ARM,Nokia,ST,TI等公司成立的一个联盟,目的是把手机内部的接口如摄像头,显示屏接口,视频/基带接口标准化,,从而增加设计灵活性,同时降低成本、设计复杂度、功耗和EMI。。MIPI联盟下面有不同的WorkGroup,分别定义了一系列的手机内部接口标准,比如摄像头接口CSI,显示接口DSI,视频接口DingRF,麦克风、喇叭接口等;由于本篇内容主要将的是显示,故我们就从MIPI-DSI讲起;

MIPI-DSI是一种应用于显示技术的串行接口,兼容DPI(显示像素接口,Display Pixel Interface),DBI(显示总线接口,Display Bus Interface)和DCS(显示命令集,Display Command Set),以串行的方式发送像素信息或指令给外围,而且在传输的过程中享有自己独立的通信协议,包括数据包格式和纠错检错机制;

MIPI-DSI具备高速和低速模式两种模式,全部数据通道都可以用于单向的高速数据传输,但只有一个数据通道才可用于低速双向传输,从属端的状态信息,像素等式通过该数据通道返回,时钟通道专用于高速传输数据的过程中传输同步时钟信号,此外,一个主机端可允许同时与多个从属端进行通信;下图为MIPI接口的数据链路图示:

mipi数据链路

简易物理设备图示

CSI_DSI.jpg

mipi屏参考电路

电源和背光电路

lcd_backlight

屏电路

lcd_mipi

从硬件原理图看mipi屏主要有三个主要的电路,供电:屏供电:VCC-LCD. MIPI信号供电:VCC-LCD-LOGIC;背光电路:通过lcd-pwm控制VCC-LED+,VCC-LED- ; MIPI信号:DSI-D0N,DSI-D0P,….,DSI-CKN,DSI-CKP;

lcd屏电路硬件简示

lcd_hardware

按照之前的MIPI数据链路,将SOC的MIPI信号转化成屏可以识别的信号,然后进行显示

综上,就是从DE->TCON->MIPI->LCD的硬件流程图;

参考资料

Android LCD(一):LCD基本原理篇

LCD主流显示接口介绍

高兴说 显示行业进阶篇之(三)

行動裝置顯示器邁向超高畫質 eDP躍升NB/平板主流介面

硝烟四起!各大3D显示器技术详细解读

MIPI接口介绍

LCD之mipi DSI接口驱动调试流程

内核的休眠流程

Posted on 2018-12-13 | In standby

内核休眠流程

当安卓合适的时机(wakeup_count = save_count && inpr == 0),与内核一致,在没有wakeup事件的时候,走进一步的流程,发echo mem > /sys/power/state命令后,内核开始处理休眠流程

./kernel/power/main.c 
state_store
    ->pm_suspend
        ->enter_state

休眠流程:

按照内核划分的休眠测试环节分为,FREEZER(进程冻结),DEVICES(设备休眠),PLATFORM(关闭不必要的中断),CPUS(关闭noboot cpu),core(关闭cpu中断,通知cpus进入休眠处理流程)几个阶段

kernel/power/suspend.c

enter_state
    suspend_prepare(suspend_state_t state)
    error = suspend_devices_and_enter(state);

test_freezer(冻结流程)

suspend_prepare(suspend_state_t state)    
        pm_prepare_console //如果需要的话重定向内核的kmsg  ??为什么要做切换
        error = pm_notifier_call_chain(pm_suspend_prepare); //用途一:有些设备需要在freeze进程之前就suspend;用途二:如果有些设备的reseym动作需要较多的延时么么resume的时候会在进程恢复之前,会阻止所有进程的恢复?更有甚者需要设备等待某个进程的数据才能resume; 
        //例子如下:drivers/video/fbdev/omap2/omapfb/dss/core.c
        error = suspend_freeze_processes(); //进程冻结,将用户进程和内核线程置于“可控”的暂停状态

在此对进程冻结进行展开描述

为什么需要进程冻结

  1. 会破坏文件系统,在系统创建hibernate image到cpu down之间,如果有进程还在修改文件系统的内容,这将会导致系统恢复之后无法完全恢复文件系统;
  2. 有可能导致创建hibernation image失败,创建hibernation image需要足够的内存空间,这期间如果还有进程在申请内存,就可能导致创建失败
  3. 干扰设备的resume和suspend,在cpu down之前,device suspend期间,如果进程还在访问设备,尤其是访问竞争资源,就有可能引起设备suspend异常
  4. 有可能导致进程感知休眠。系统休眠的理想状态下是所有任务对休眠过程无感知,睡眠之后会自动恢复工作,有些进程,需要所有cpu online才能正常工作,如果进程不冻结,那么休眠过程中就会异常;

冻结进程的主要流程

suspend_freeze_processes
——>    freeze_processes()   -> pm_freezing = true   
                         ->try_to_freeze_tasks ->  freeze_task ->  fake_signal_wake_up(p); 冻结用户进程



->freeze_kernel_threads()  ->pm_nosig_freezing = true 
                           ->try_to_freeze_tasks -> freeze_task ->    freeze_workqueues_begin();   冻结workqueue;
                                                                    wake_up_state(p, task_interruptible); 冻结内核线程

冻结的对象:可以被调度执行的实体,包括用户进程,内核线程和workqueue.

1.用户进程冻结流程

用户进程默认是可以被冻结的,借助信号处理机制,设置任务的tif_sigpending位,但不传递系统,然后唤醒任务;这样任务在返回用户态时就会进入信号处理流程,检查系统freeze状态,并做相应的处理;

fake_signal_wake_up -> signal_wake_up(p, 0) -> signal_wake_up_state(t, resume ? task_wakekill : 0); 
                                                    -> set_tsk_thread_flag(t, tif_sigpending);

信号的处理时机

理解信号异步机制的关键是信号的响应时机,我们对一个进程发送一个信号后,其实并没有硬中断发生,只是简单把信号挂载到目标进程的信号pending队列上去,信号真正得到执行的时机是进程执行完异常/中断返回到用户态的时刻;

让信号看起来是一个异步中断的关键是,正常的用户进程是会频繁的在用户态和内核态之间切换的(这种切换包括:系统调用,缺页异常,系统中断..),所以信号能很快得到执行。这样也带来一个问题,内核进程是不响应信号的,除非它刻意的去查询。所以通常情况下我们无法通过kill命令去杀死一个内核进程;

信号响应时机:

信号响应时机

arch/arm64/kernel/signal.c    
do_notify_resume -> do_signal(regs) -> get_signal
                                            task_work_run
                                            try_to_freeze

                                    -> handle_signal

2.内核线程冻结

内核线程和workqueue默认是不能被冻结的,少数内核线程和workqueue在创建的时指定了freezable标志。这些任务需要对freeze状态进行判断,当系统进入freezing时,可以通过调用freezing来判断freezing状态,并主动调用,try_to_freeze进入冻结;

代码范例如下:
static int autohotplug_thread_task(void *data)
{
    set_freezable();
    while (1) {
            if (freezing(current)) {
                    if (try_to_freeze())
                            continue;
            }    

            if (hotplug_enable)
                    autohotplug_governor_judge();

            set_current_state(task_interruptible);
            schedule();
            if (kthread_should_stop())
                    break;
            set_current_state(task_running);
    }    

    return 0;
}

3.workqueue冻结

work_queue通过max_active属性,如果max_active=0则不能入队新的work,所有的work延后执行;

freeze_workqueues_begin ->     workqueue_freezing = true; 
                            pwq_adjust_max_active(pwq); 
                        if (!freezable || !workqueue_freezing){
                                pwq->max_active = wq->saved_max_active;
                        } else {
                             pwq->max_active = 0;
                        }

schedule_work -> queue_work -> queue_work_on ->
 __queue_work 
    ...
    如果还没有达到max_active,将work挂载到worklist
          if (likely(pwq->nr_active < pwq->max_active)) {
                trace_workqueue_activate_work(work);
                pwq->nr_active++;
                worklist = &pwq->pool->worklist;
                if (list_empty(worklist))
                        pwq->pool->watchdog_ts = jiffies;
       否则将work挂载到临时队列 pwq->delayed_works
            } else {
                work_flags |= work_struct_delayed;
                worklist = &pwq->delayed_works;
            }                       
  ...

需要注意事项:

  1. workqueue的冻结只针对带有WQ_FREEZABLE;所以如果是schedule_work这种默认使用的是system_wq类型的workqueue冻结进程的时候并不会进行冻结;
  2. 对于schedule_delay_work的动作,如果休眠的时候不需要执行,则需要进行cancel_delayed_work_sync的动作,防止阻止系统休眠;

冻结进程何时被打断

进程怎么阻止休眠:无论通过wake_lock接口,__pm_stay_awake等,最终调用的都是wakeup_source_activate,都是增加inpr表示有事件在处理不准休眠;

wakeup_source_activate
    /* increment the counter of events in progress. */
    cec = atomic_inc_return(&combined_event_count);   也即解下来要讲的变量inpr

在系统休眠过程中:上层会判断cnt == saved_count, inpr == 0;底层也是如此
 bool pm_wakeup_pending(void)                                                                                     
 {                                                                                                                
     unsigned long flags;                                                                                     
     bool ret = false;                                                                                        

     spin_lock_irqsave(&events_lock, flags);                                                                  
     printk("++%s %d++\n",__func__, events_check_enabled);                                                    
     if (events_check_enabled) {                                                                              
             unsigned int cnt, inpr;                                                                          

             split_counters(&cnt, &inpr);    
             用户空间还有内核都是判断这两个变量;如果写入的值和cnt不同,说明读写的过程中出现产生了events;
             inpr表示用唤醒事件在处理,这两种都会阻止系统休眠                                                                
             ret = (cnt != saved_count || inpr > 0);                                                          
             events_check_enabled = !ret;                                                                     
             printk("+++%s cnt:%d saved_count:%d inpr:%d ret:%d++\n",__func__, cnt, saved_count, inpr, ret);  
     }                                                                                                        
     spin_unlock_irqrestore(&events_lock, flags);                                                             

     if (ret) {                                                                                               
             pr_info("pm: wakeup pending, aborting suspend\n");                                               
             pm_print_active_wakeup_sources();                                                                
     }                                                                                                        

     return ret || pm_abort_suspend;                                                                          
 }

device_suspend(设备休眠)

suspend_devices_and_enter
--> platform_suspend_begin
    suspend_console();
    dpm_suspend_start(pmsg_suspend);
        -->dpm_prepare(state);
        -->dpm_suspend(state);
            device_suspend(dev);    
                -->async_schedule(async_suspend, dev); 
                //在设备中如果通过device_enable_async_suspend设置过,async_schedule使设备走异步的休眠唤醒流程;
                -->__device_suspend(dev, pm_transition, false);
                        dpm_watchdog_set//设置超时watchdog,若休眠时间过长,则打印打钱的设置和函数栈默认超时时间为120s

                -->    dpm_run_callback(callback, dev, state, info);    
                        由于设备模型有bus、driver、device等多个层级,而suspend接口可能由任意一个层级实现。这里的优先顺序
                        是指,只要优先级高的层级注册了suspend,就会优先使用它,而不会使用优先级低的prepare。优先顺序为:dev->pm_domain->ops、dev->type->pm、dev->class->pm、dev->bus->pm、dev->driver->pm(这个优先顺序同样适用于其它callbacks)。 
                echo 1 > /sys/power/pm_print_times //在每个设备发起休眠唤醒流程的时候就能打印出来
                --> async_synchronize_full (等待所有异步动作都做完) 

platform(关闭不需要唤醒的中断)

suspend_enter(state, &wakeup);
    -->platform_suspend_prepare(state);        
       dpm_suspend_late(pmsg_suspend);
       platform_suspend_prepare_late(state);
       dpm_suspend_noirq(pmsg_suspend)
               -->cpuidle_pause();
               device_wakeup_arm_wake_irqs();
                    如果设备具有唤醒功能(dev->power.can_wakeup,device_init_wake 的时候就会使能)那么设置唤醒标志(irqd_wakeup_state)
                    -->enable_irq_wake->irq_set_irq_wake->set_irq_wake_real->irq_set_wake->sunxi_irq_set_wake 
                    -->arisc_set_wakeup_source传递到cpus
                  suspend_device_irqs();
                    -->没有唤醒标志的 enable_irq_wake,  request_irq(irq, xxx_isr, flag | IRQF_NO_SUSPEND`, xxx, xxx)的时候,此时中断会被清除,此时切断的是irq device和irq controller之间的联系
                        这两者的区别在于enable_irq_wake还会走到synchronize_irq(irq),等待中断处理完毕

              device_suspend_noirq(dev) //处理关闭中断后的事情    
       platform_suspend_prepare_noirq(state)            

cpus(关闭no bootcpu)

disable_nonboot_cpus() //关闭no boot cpu
    freeze_secondary_cpus(0)
            _cpu_down(cpu, 1, cpuhp_offline);

core(关闭cpu中断,通知arisc走休眠处理流程)

arch_suspend_disable_irqs(); //此时关闭的是irq controller和cpu的联系,在这个时候中断便不能唤醒cpu了;
     local_irq_disable();
syscore_suspend();//主要调用sched_clock_suspend,timekeeping_suspend,irq_gc_suspend,fw_suspend,cpu_pm_suspend
          //echo Y > /sys/module/kernel/parameters/initcall_debug 通过这个命令可以看到相关的调用
    suspend_ops->enter(state);    
        sunxi_suspend_enter//向小cpus传递电源相关参数
            arm_cpuidle_suspend
                 cpuidle_ops[cpu].suspend(index)
                    psci_cpu_suspend_enter
                        cpu_suspend(3, psci_suspend_finisher)
                    svc:psci_fn_cpu_suspend

arisc(小cpu的休眠处理流程)

extended_super_standby_entry(pmessage)
    esstandby_process_init(request, para, config) 
        dram进入自刷新;
        将系统相关的clk切换到低频状态
        将系统相关的电源关闭
    get_wakeup_src(para)
        轮询检测中断源

休眠唤醒流程

suspend_resume_trace

思考题:

1:进行如下操作后

echo 1 > /sys/power/wake_lock

echo mem > /sys/power/state

此时系统能否休眠??

能,events_check_enabled此时的值为false,故系统会直接进入休眠状态;只有上层程序在调用save_wakeup_count的时候,会去使能events_check_enabled,从这个层面讲,休眠的时候需不需要被唤醒事件打断,其实是看用户自身是否需要这种同步机制;

有正在处理的事件inpr,为什么还要cnt作为判断进入休眠唤醒的标志呢?

一般而言inpr已经足够表示系统有事件在进行,不要进入休眠;引入这个条件笔者认为只是为了能更新该值,保证wakeup_event都能被统计到;
当cnt != save_cnt 
打印如下 last active wakeup source: axp22_wakeup_source;表明是上次是被谁影响到休眠

2: 为什么即有request_irq(irq, xxx_isr, flag | IRQF_NO_SUSPEND, xxx, xxx)又有,enanle_irq_wake?

http://www.wowotech.net/forum/viewtopic.php?id=20
这里有相关的讨论,大意是
这两个方法,做的是同一件事情,都是控制“request line和irq controller之间的联系”。
IRQF_NO_SUSPEND是旧方法;
enable_irq_wake是新方法。
之有的driver都调用这两个接口,有两种可能:一是为兼容;而是不太理解,为保险起见,都调用。
从代码上看request的时候带的参数IRQF_OF_SUSPEND只能在内核休眠的时候起到阻止休眠的功能,而enable_irq_wake则可以在深度休眠后仍然能唤醒;
当休眠到core阶段的时候cpu的irq和fiq都已关闭,但仍保存着wakeup信号线,通过wakeup信号线唤醒从而唤醒cpu,当cpu中断使能开启后再响应中断
不建议使用IRQF_NO_SUSPEND的方式,原因有三个,一:对于电平触发的会在唤醒的是时候由于中断处理线程得不到调度导致pending存在,阻塞系统唤醒;
二:enable_irq_wake能通过disable_irq_wake的方式动态处理开关是否需要唤醒系统,
或者在probe的时候通过device_init_wakeup的方式添加设备唤醒系统的属性,这样无需调用enable_irq_wake就能使能设备休眠唤醒系统属性 
三:enable_irq_wake的设备,中断底半部处理函数即使调用sleep函数,系统仍会等到中断处理完成才睡眠不会漏处理

3:怎么对休眠流程进行分解debug

内核在休眠的流程中加入了trace point可以根据这个去定位是哪个流程去定位哪个流程异常:
    echo 1 > /sys/kernel/debug/tracing/events/power/suspend_resume/enable
    echo > /sys/kernel/debug/tracing/trace //清空trace
    echo 1 > /sys/kernel/debug/tracing/tracing_on
    按power键进入休眠的流程
    按power键唤醒
    cat  /sys/kernel/debug/tracing/trace
    查看在哪个流程就退出

如当怀疑休眠流程在device_suspend中挂死的;
可以echo devices > /sys/power/pm_test
然后修改dpm_suspend设备的休眠流程,让其逐一进入休眠后退出,观察是哪个设备休眠唤醒后系统异常;
其他处于内核阶段的类似;

echo N > /sys/module/printk/parameters/console_suspend //休眠的时候保持终端打开    
如果没有异常,那么可以怀疑是否由于cpus中dram进入自刷新后异常导致;或者系统电上下电导致;或者时钟频率不稳导致;    
在cpus中挂死,即走到arisc的休眠流程中,表现为串口log处于深度休眠,且无任何异常打印;
在重启的log中:
可以观察rtc寄存器的值:正常的值如下:
[371]rtc[0] value = 0x00000000
[374]rtc[1] value = 0x00000000
[377]rtc[2] value = 0x00000000
[380]rtc[3] value = 0x0000a101
[383]rtc[4] value = 0x00000000
[386]rtc[5] value = 0x00000000
此时观察rtc[3],查看相关代码定位挂死的地方
如怀疑dram导致的异常,可进行DRAM_CRC的校验,如下校验1G的空间
usage:
        echo  1 0x40000000 0x40000000 > sys/devices/platform/soc/soc@03000000:arisc/dram_crc_paras

        1:           enable
        0x40024080:  dram crc start address
        0xf0000:     dram crc length(byte)
venus-a1:/ # cat ./sys/devices/platform/soc/soc@03000000:arisc/dram_crc_result
dram info:
 enable 0
  error 0
 total count 0
error count 0
src:40000000
len:0x100000

4.查看内核锁

cat /sys/kernel/debug/wakeup_sources
如果debug下没有节点,需先进行挂载:mount -t debugfs none /mnt ,然后在mnt目录下查看
早期的linux版本,3.4
cat /proc/wakelocks    

5.查看唤醒源

cat /sys/power/pm_wakeup_irq 这个会保存深度休眠的时候唤醒系统的中断号
配合 cat /proc/interrupts可获取当前唤醒系统的唤醒源是哪个
原理如下
在supend_device_irq的流程中能够wakeup系统的都会被置上IRQD_WAKEUP_ARMED的标志,然后就关闭中断;如果在中断处理流程里面的
会通过synchronize_irq等待中断线程处理完;
resume_irq的时候后重新enable_irq进入中断处理流程,通过pm_system_irq_wakeup标记第一次的唤醒源;

6.具有唤醒系统能力设备代码demo

static int gpio_keys_probe(struct platform_device *pdev)
{
    ......
    button->wakeup = of_property_read_bool(pp, "wakeup-source"); //获取dts中配置的设备是否需要从深度休眠中唤醒

    button->gpio = of_get_gpio_flags(pp, 0, (enum of_gpio_flags*)&flags); 

    bdata->gpiod = gpio_to_desc(button->gpio);                          

    irq = gpiod_to_irq(bdata->gpiod); 

    request_irq(irq, gpio_key_irq_handle, IRQF_TRIGGER_RISING, "gpio-key", bdata); //如果想缺省bdata可以使用devm_request_irq,api,无需IRQF_NO_SUSPEND
    ......
    device_init_wakeup(&pdev->dev, wakeup);//为设备注册wakeup_source,申明设备为设备添加可唤醒标志

    if (device_may_wakeup(dev)) {
    for (i = 0; i < pdata->nbuttons; i++) {
    struct gpio_button_data *bdata = &ddata->data[i];
    error = dev_pm_set_wake_irq(dev, bdata->irq);//把中断号配置到wakeup_source中,这样在休眠的时候系统会帮你将该中断设置为深度休眠可唤醒源
    }
    }
        ......
}

7.项目经验总结

1. 硬件需确保电源信号稳定性;
2. 在项目开发过程中,休眠唤醒稳定性测试,在每次迭代的时候尽量做到一周进行一次多台机器休眠唤醒老化测试,测试老化的时候,应加大负载,如同时播放视频;
   以便增加问题的复现概率

参考资料

linux进程冻结技术

Linux电源管理(6)_Generic PM之Suspend功能

Concurrency Managed Workqueue之(一):workqueue的基本概念

Linux 的并发可管理工作队列机制探讨

linux workqueue

中断唤醒系统流程

安卓休眠流程

Posted on 2018-12-13 | In standby

安卓电源状态

首先安卓将电源分为以下几种情况:

1.sleep:休眠

从代码看,安卓将系统的休眠原因分为以下几种:
goToSleepNoUpdateLocked
    GO_TO_SLEEP_REASON_DEVICE_ADMIN//Going to sleep due to device administration policy 
    GO_TO_SLEEP_REASON_TIMEOUT //Going to sleep due to screen timeout
    GO_TO_SLEEP_REASON_LID_SWITCH //Going to sleep due to lid switch 滑盖
    GO_TO_SLEEP_REASON_POWER_BUTTON //Going to sleep due to power button
    GO_TO_SLEEP_REASON_SLEEP_BUTTON //Going to sleep due to sleep button 头显的远近
    GO_TO_SLEEP_REASON_HDMI //Going to sleep due to HDMI standby
    GO_TO_SLEEP_REASON_ACCESSIBILITY//Going to sleep by an accessibility service request
    如果不是以上几种情况,则默认为应用导致的休眠:
    GO_TO_SLEEP_REASON_APPLICATION//Going to sleep by application request

2.nap:进入屏保

默认屏幕超时后进入休眠,如果希望进入的是屏保模式
则需要在overlay/frameworks/base/core/res/res/values/config.xml中将屏保模式打开
<bool name="config_dreamsActivatedOnSleepByDefault">true</bool>    

3.wakeup:唤醒

wakeUpNoUpdateLocked
从以下状态中唤醒,reason不固定:
    WAKEFULNESS_ASLEEP:
    WAKEFULNESS_DREAMING:
    WAKEFULNESS_DOZING:

4.shutdown:关机
5.reboot:重启

在此前的内核篇,我们讲到了用户层会去轮询查看是否有唤醒事件需要处理,如果没有则echo mem > /sys/power/state,从而进入内核的休眠唤醒流程;所以在本篇我们主要将这两个流程是怎么衔接上的~

由于通过调用api进行休眠唤醒的流程更为简单,所以我们看下老化apk是如何进行调用休眠唤醒的api,然后进行老化休眠唤醒实验的;

老化休眠唤醒代码demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
休眠动作:
单点击开始后,设置闹钟和走休眠流程:
runSleepWakeUpTest()
mAlarmMgr.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()
+ (mSleepTime * 1000),
pendIntent);

standby();
pm.goToSleep(SystemClock.uptimeMillis());

唤醒动作:
onReceive

PowerManager pm = (PowerManager) this
.getSystemService(Context.POWER_SERVICE);
if (getAndroidSDKVersion() >= 17) {
pm.wakeUp(SystemClock.uptimeMillis());

} else {
pm.userActivity(SystemClock.uptimeMillis(), false);
}

从调用流程可以看到就是不断的调用goToSleep,然后设置一个闹钟,当闹钟来临的时候唤醒系统;

framework相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
core/java/android/os/PowerManager.java
public void goToSleep(long time) {
goToSleep(time, GO_TO_SLEEP_REASON_APPLICATION, 0);
}

./services/core/java/com/android/server/power/PowerManagerService.java
public void goToSleep(long eventTime, int reason, int flags) {
if (eventTime > SystemClock.uptimeMillis()) {
throw new IllegalArgumentException("event time must not be in the future");
}
goToSleepInternal(eventTime, reason, flags, uid);
}

private void goToSleepInternal(long eventTime, int reason, int flags, int uid) {
synchronized (mLock) {
if (goToSleepNoUpdateLocked(eventTime, reason, flags, uid)) {
updatePowerStateLocked();
}
}
}

sleep流程

当屏幕超时,按power键休眠,和应用主动调goToSleep;无论是哪种方式,如果系统启动没有异常,都会走到PowerManagerService的核心函数,updatePowerStateLocked;按照安卓注释,将updatePowerStateLocked 分为五个阶段,我们也按照五个阶段对该函数的功能进行讲解;

Phase 0: Basic state updates

1.USB插播亮屏入口点
 //如果需要插拔事件不影响到屏幕的亮灭,在overlay/frameworks/base/core/res/res/values/config.xml下
 //修改<bool name="config_unplugTurnsOnScreen">true</bool>从字面意思貌似是拔出的时候不亮屏;
 //mWakeUpWhenPluggedOrUnpluggedConfig = resources.getBoolean(                                                                                                                                               
            com.android.internal.R.bool.config_unplugTurnsOnScreen);//从这里就能完整看到这个配置的用途,插拔都会影响到屏幕

2.更新低电量模式        
当电池电量状态发生变化的时候,才能调到该方法
updateIsPoweredLocked(mDirty);

判断系统是否在setting中设置了充电时保持屏幕亮屏
updateStayOnLocked(mDirty);

updateScreenBrightnessBoostLocked(mDirty);  

Phase 1: Update wakefulness

wakefulness,安卓将休眠唤醒的电源状态分为四个:

类型 状态介绍
WAKEFULNESS_ASLEEP 设备处于sleep状态,只能被wakeUp函数唤醒,屏幕会被关闭,设备先进入doze状态
WAKEFULNESS_AWAKE 设备处于awake状态,可通过goToSleep的接口将设备置于休眠状态,当用户超时时间到了设备将开始dreaming或者进入休眠流程
WAKEFULNESS_DREAMING 设备处于dreaming状态,会被wakeUp唤醒,当dreaming时间结束或者
WAKEFULNESS_DOZING 打盹
// Loop because the wake lock and user activity computations are influenced
// by changes in wakefulness. 
//此循环只会循环两次,然后就退出;
for (;;) {
    updateWakeLockSummaryLocked(dirtyPhase1);
        getWakeLockSummaryFlags(wakeLock);
        adjustWakeLockSummaryLocked(mWakeLockSummary);
    updateUserActivitySummaryLocked(now, dirtyPhase1);
    if (!updateWakefulnessLocked(dirtyPhase1)) {
                 break;  
     }           
 }

1.updateWakeLockSummaryLocked(dirtyPhase1); ;

简单先介绍安卓的锁:

类型 属性 功能介绍
PARTIAL_WAKE_LOCK WAKE_LOCK_CPU 保持CPU运行,屏幕和键盘灯允许关闭。用户按power键之后,屏幕和键盘灯会关闭,CPU keep on,直到所有该类型所被释放
FULL_WAKE_LOCK WAKE_LOCK_SCREEN_BRIGHT WAKE_LOCK_BUTTON_BRIGH 保证屏幕和键盘灯亮(at full brightness)。用户按power键之后,CPU和屏幕键盘灯都会被关闭
SCREEN_BRIGHT_WAKE_LOCK WAKE_LOCK_SCREEN_BRIGHT 保证屏幕亮(full brightness),键盘灯允许关闭。用户按power键之后,CPU和屏幕都会被关闭
SCREEN_DIM_WAKE_LOCK WAKE_LOCK_SCREEN_BRIGHT 保证屏幕亮(full brightness),键盘灯允许关闭。用户按power键之后,CPU和屏幕都会被关闭
PROXIMITY_SCREEN_OFF PROXIMITY_SCREEN_OFF pSensor导致的灭屏情况下系统不会进入休眠,正常情况下不影响系统休眠
DOZE_WAKE_LOCK WAKE_LOCK_DOZE 使屏幕进入low power状态,允许cpu挂起。只有在电源管理进入doze模式时生效
DRAW_WAKE_LOCK WAKE_LOCK_DRAW 保持设备awake状态已完成绘制事件,只在doze模式下生效

该函数的主要作用为,遍历申请的所有锁,根据当前mWakefulness的状态(asleep,dozing,wake,dream),过滤不需要的锁,如DOZE模式下过滤WAKE_LOCK_SCREEN_BRIGHT,WAKE_LOCK_SCREEN_DIM,WAKE_LOCK_BUTTON_BRIGHT,而ASLEEP模式下在基础上过滤WAKE_LOCK_PROXIMITY_SCREEN_OFF,在Dream模式下会添加WAKE_LOCK_CPU.

2.updateUserActivitySummaryLocked(now, dirtyPhase1);

mUserActivitySummary的种类如下:

类型 类型介绍
USER_ACTIVITY_SCREEN_BRIGHT 点亮屏幕
USER_ACTIVITY_SCREEN_DIM 屏幕变暗
USER_ACTIVITY_SCREEN_DREAM 屏保模式
这里有三个时间参数:
sleepTimeout:设备完全休眠的时间,该值可以理解为保持唤醒或屏保的最大值或者上限,并且该值要大 
             Settings.System.SCREEN_OFF_TIMEOUT,默认为-1;表示禁用此功能项;
screenOffTimeoutSetting:表示设备在一段不活动进入睡眠或者屏保的时间,也称为用户超时时间,但屏幕不一定关闭,可能进入屏保
screenDimDuration 亮屏后不操作,多久变暗;
根据这三个时间参数计算nextTimeout的时间:
         scheduleUserInactivityTimeout(nextTimeout);

故该函数的主要作用为,更新用户活动时间,当设备和用户有交互的时,都会根据当前时间和休眠时长,dim时长,所处状态而计算下次休眠的时间,从而完成活动超时时的操作,如由亮屏进入Dim的时长,Dim到灭屏的时长,亮屏到屏保的时长;

3.updateWakefulnessLocked

if (shouldNapAtBedTimeLocked()) {                                            
changed = napNoUpdateLocked(time, Process.SYSTEM_UID);
    -->mSandmanSummoned = true;
       setWakefulnessLocked(WAKEFULNESS_DREAMING, 0);
       -->mNotifier.onWakefulnessChangeStarted(wakefulness, reason);                     
 } else {                                                                      
changed = goToSleepNoUpdateLocked(time,                                   
        PowerManager.GO_TO_SLEEP_REASON_TIMEOUT, 0, Process.SYSTEM_UID);  
       --> mSandmanSummoned = true;
           setWakefulnessLocked(WAKEFULNESS_DOZING, reason);
           -->mNotifier.onWakefulnessChangeStarted(wakefulness, reason); 

 }                                                                             

该函数的主要作用在与更新电源状态;

Phase2:Lock profiles that became inactive/not kept awake.

updateProfilesLocked(now); //估计是cts的时候使用,这个不再赘述

Phase 3: Update display power state.

updateDisplayPowerStateLocked(dirtyPhase2); 
-->mDisplayPowerRequest.policy = getDesiredScreenPolicyLocked(); 
-->requestPowerState
    -->return mDisplayPowerController.requestPowerState(request,waitForNegativeProximity);
        -->requestPowerState
            -->sendUpdatePowerStateLocked
                -->Message msg = mHandler.obtainMessage(MSG_UPDATE_POWER_STATE);
                    mHandler.sendMessage(msg);
handleMessage(Message msg)
   -->updatePowerState();
      -->animateScreenStateChange
        -->setScreenState
        -->mPowerState.setScreenState(state);(DPC)
            -->scheduleScreenUpdate-->postScreenUpdateThreadSafe-->setState->mLock.notifyAll
                -->mBlanker.requestDisplayState(DPS)
            -->callbacks.onDisplayStateChange(state)  -->根据亮灭屏走PowerManagerService enable/disable autosuspend
            -->requestGlobalDisplayStateInternal(state, brightness=0) -->mTempDisplayStateWorkQueue.get(i).run()(DMS)
            -->SurfaceControl.setDisplayPowerMode(LDA)

        -->blockScreenOn 在亮屏的流程里面监听窗口是否绘制完成,
           是的话重新发起updatePowerState,设置mColorFadeLevel为1;此时才会去设置背光,这时候对于用户来说屏幕才是最终的亮了
           否的话此时的背光值一直会为0;


        -->mWindowManagerPolicy.screenTurningOff(mPendingScreenOffUnblocker)
        在灭屏的同时还发起了activity的pause流程
        -->updateScreenOffSleepToken
        -->acquireSleepToken
        -->updateSleepIfNeededLocked
        -->goingToSleepLocked
        -->checkReadyForSleepLocked
        -->putStacksToSleepLocked
        -->goToSleepIfPossible
        -->startPausingLocked
        -->schedulePauseAcvity --> sendMessage
        -->handleMessage -->handlePauseActivity-->performPauseActivity-->performPauseActivityIfNeed
        -->callActivityOnPause -->activity.performPause--> onPause
        唤醒的时候应用resume的流程
        ActivityStackSupervisor.java
        SleepTokenImpl.release
        -->removeSleepTokenLocked
        -->updateSleepIfNeededLocked
        -->applySleepTokensLocked
        -->resumeFocusedStackTopActivityLockedi
        -->resumeTopActivityUncheckedLocked
        -->resumeTopActivityInnerLocked

函数作用:决策屏幕的量灭状态

如果当前是WAKEFULNESS_ASLEEP状态,直接设置屏幕为POLICY_OFF

如果包含以下一种状态,就设置屏幕为POLICY_BRIGHT

1.mWakeLockSummary 如果有 WAKE_LOCK_SCREEN_BRIGHT类型的wakeLock 

2.mUserActivitySummary 屏幕状态为USER_ACTIVITY_SCREEN_BRIGHT

3.当前系统未启动完成

4.当前处于最大屏幕亮度

可以看出屏幕的状态和前面设置的wakeLock,stayon,userActivity,screenBrightness等有关

Phase 4: Update dream state (depends on display ready signal).

updateDreamLocked(dirtyPhase2, displayBecameReady);
-->scheduleSandmanLocked();
    -->if (!mSandmanScheduled) {
         Message msg = mHandler.obtainMessage(MSG_SANDMAN);
      }

函数作用:决定是否进入屏保状态

Phase 5: Send notifications, if needed.

finishWakefulnessChangeIfNeededLocked();

函数作用:发出wakefulness发生改变的通知

Phase 6: Update suspend blocker.

// Because we might release the last suspend blocker here, we need to make sure
// we finished everything else first!  
updateSuspendBlockerLocked(); 
    -->needWakeLockSuspendBlocker = ((mWakeLockSummary & WAKE_LOCK_CPU) != 0);//根据是否有CPU的wakelock,来决定cpu是否保持唤醒
    -->needDisplaySuspendBlockerLocked//根据前面屏幕的状态,屏幕是否需要亮屏,来决定是否需要持持有屏幕的锁             

函数作用:进行锁的申请和释放

分析完整个函数的调用流程,依然找不到怎么跟内核休眠的流程衔接起来;那么安卓是什么是否才会去调用内核休眠流程对接起来的?原来安卓在上面的函数调到灭屏的接口时候,才会去使能autosuspend的流程;想想也没有什么毛病,毕竟对于用户来说灭屏时标识着系统要走到休眠的标志;

代码流程如下:

onDisplayStateChange
-->if (state == Display.STATE_OFF) {
    if (!mDecoupleHalAutoSuspendModeFromDisplayConfig) {
           setHalInteractiveModeLocked(true);
    }
    }
 nativeSetAutoSuspend(enable);
    enabelAutosuspend
        suspendControl->enableAutoSuspend 使能autoSuspend进程,进程会一直检测系统是否无锁
        gSuspendBlocker->release释放锁
对应的灭屏disable的流程
    disableAutoSuspend
    suspendHal->acquireWakeLock(WakeLockType:PARTIAL,"PowerManager.SuspendLockout");所以一旦亮屏我们都会在内核的wake_lock节点看到这个锁,当然等到安卓把这个锁放在上层申请
    的话就看不到了 

为了呼应上次内核篇上讲的安卓等待的时机,我们在这里把读写wakeup count内核的的相关操作也po出来;

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
43
44
45
ReadFdToString(wakeup_count_fd, &wakeup_count)
bool pm_get_wakeup_count(unsigned int *count, bool block)
{
unsigned int cnt, inpr;

if (block) {
DEFINE_WAIT(wait);

for (;;) {
prepare_to_wait(&wakeup_count_wait_queue, &wait,
TASK_INTERRUPTIBLE);
split_counters(&cnt, &inpr);
if (inpr == 0 || signal_pending(current))
break;

schedule();
}
finish_wait(&wakeup_count_wait_queue, &wait);
}

split_counters(&cnt, &inpr);
*count = cnt;
return !inpr;
}
可以看到在读函数里面也会去判断是否有唤醒事件在处理,如果有唤醒事件那么就会不断的阻塞在获取进程当中;

WriteStringToFd(wakeup_count, wakeup_count_fd)
bool pm_save_wakeup_count(unsigned int count)
{
unsigned int cnt, inpr;
unsigned long flags;

events_check_enabled = false;
spin_lock_irqsave(&events_lock, flags);
split_counters(&cnt, &inpr);
if (cnt == count && inpr == 0) {
saved_count = count;
events_check_enabled = true;
}
spin_unlock_irqrestore(&events_lock, flags);
return events_check_enabled;
}
可以看到在写函数中,呼应到了我们内核篇中,当判断是否save_count == wakeup_count && inpr==0这两个判断为没有唤醒事件,之后就开始写相关的节点,

success = WriteStringToFd(sleep_state, state_fd);//echo mem > /sys/power/state 发起内核进入休眠流程;

总之,当安卓灭屏后,才会使能autosuspend不断轮询是否有锁也即唤醒事件,没有才会下发mem到写到内核节点state中;

安卓按键亮灭屏唤醒流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
亮屏:
InputManagerService interceptKeyBeforeQueueing
InputManagerCallback interceptKeyBeforeQueueing
PhoneWindowManager interceptKeyBeforeQueueing
inputceptPowerKeyDown

wakeUpFromPowerKey
wakeUp
PowerManager wakeUp
PowerManagerService wakeUp
灭屏:
InputManagerService interceptKeyBeforeQueueing
InputManagerCallback interceptKeyBeforeQueueing
PhoneWindowManager interceptKeyBeforeQueueing
inputceptPowerKeyUp
powerPress
goToSleepFromPowerButton
goToSleep
PowerManager goToSleep
PowerManagerService goToSleep

安卓的持锁流程

1
2
3
4
5
6
7
8
9
10
11
12
PhoneWindowManager mPowerKeyWakeLock.acquire    
PowerManager acquireLocked -->mService.acquireWakeLock
PowerManarService acquireWakeLockInternal
-->new WakeLock ,mWakeLock.add将申请的锁放到PowerManager统计,这一步并不会真正申请锁
-->updatePowerState
-->updateWakeLockSummaryLocked adjustWakeLockSummary 汇总所有申请到的锁,根据睡眠状态,决定后续是否申请锁,信息保存在mWakeLockSummary
-->updateSuspendBlockerLocked-->mWakeLockSuspendBlocker.acquire -->nativeAcquireSuspendBlocker

jni acquire_wake_lock
hal power.cpp suspendService->acquireWakeLock
system SystemSuspend new WakeLock --> mSystemSuspend->incSuspendCounter -->WriteStringToFd(name, mWakeLockFd) 往内核wakeLocks写锁
mSuspendCounter++ 这个是另外一套,安卓后续计划通过suspendCounter而不是写内核节点持锁的方式

autospend 流程

androidR上,谷歌说要弃用/sys/power/wake_lock节点;安卓此前申请锁的流程在hardware/libhardware_legacy/power.cpp;原理就是去往wake_lock节点写值;
现在的话想将这个流程放在system/hardware/interfaces/suspend中进行实现;

在这里只能说谷歌终于想通了要把这套放在用户层,毕竟曾经为了wake_lock,一直在争吵,最后还是被谷歌给干进内核,现在的话又抽出来放在用户层里面;具体细节可以参照这篇文章Linux电源管理

谷歌列举这样的好处是:

  1. 减少对内核的依赖
  2. 在没有debugfs的时候用户层仍有能力去debug和log suspend blocks
  3. 当进程消亡的时候可以进行锁通过binder的管理进行释放 dangling wake locks

通过mSuspendCounter去统计当前持锁的数量,当mSuspendCounter为0的时候,说明上层持锁为0

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

void SystemSuspend::initAutosuspend() {
std::thread autosuspendThread([this] {
while (true) {
std::this_thread::sleep_for(mSleepTime); //如果失败了,会更改autosuspend的时间,100ms~1min
lseek(mWakeupCountFd, 0, SEEK_SET);
const string wakeupCount = readFd(mWakeupCountFd);
if (wakeupCount.empty()) {
PLOG(ERROR) << "error reading from /sys/power/wakeup_count";
continue;
}

auto counterLock = std::unique_lock(mCounterLock);
mCounterCondVar.wait(counterLock, [this] { return mSuspendCounter == 0; });//申请锁的时候mSuspendCounter会增加;释放的时候mSuspendCounter会减少
// The mutex is locked and *MUST* remain locked until we write to /sys/power/state.
// Otherwise, a WakeLock might be acquired after we check mSuspendCounter and before we
// write to /sys/power/state.

if (!WriteStringToFd(wakeupCount, mWakeupCountFd)) {
PLOG(VERBOSE) << "error writing from /sys/power/wakeup_count";
continue;
}
bool success = WriteStringToFd(kSleepState, mStateFd);
counterLock.unlock();

if (!success) {
PLOG(VERBOSE) << "error writing to /sys/power/state";
}

mControlService->notifyWakeup(success);

updateSleepTime(success);
}
});
autosuspendThread.detach();
LOG(INFO) << "automatic system suspend enabled";
}
关机充电判断休眠唤醒原理大同小异
system/core/libsuspend/autosuspend.c
int autosuspend_enable(void)
{
autosuspend_init();
autosuspend_ops = autosuspend_wakeup_count_init();
autosuspend_ops->enable();
autosuspend_wakeup_count_enable()
}
system/core/libsuspend/autosuspend_wakeup_count.cpp
autosuspend_wakeup_count_enable()
autosuspend_init();
创建一个进程:suspend_thread_func 这个进程主要就是用来做轮询的动作
sem_post(&suspend_lockout);
suspend_thread_func
while (true) {
if (!ReadFdToString(wakeup_count_fd, &wakeup_count)) { //读取wakeup count值,如果成功,将读取的值回写,否则说明正在处理wakeup events,continue
continue;
}
int ret = sem_wait(&suspend_lockout);//通过信号量来控制这个流程是否要继续走下去,当disable的时候信号量为0,进程会阻塞在这里等待;
if (WriteStringToFd(wakeup_count, wakeup_count_fd)) { //回写后,判断返回值是否成功,如果不成功(说明读写过程中产生了wakeup events),继续读,写,直到成功。成功后,可以触发电源状态切换
success = WriteStringToFd(sleep_state, state_fd);
}
ret = sem_post(&suspend_lockout);
}

其他

如何设置安卓休眠超时时间?

settings get system screen_off_timeout
settings put system screen_off_timeout 2147483647 //设置永不休眠 

设置屏保命令
settings put secure screensaver_activate_on_sleep 1 打开屏保
设置插入适配器后不休眠
settings put global stay_on_while_plugged_in 1
安卓系统不休眠debug

查看上层有没有释放锁
    dumpsys power | grep -Hrsn 'LOCK'
PARTIAL_WAKE_LOCK              'messaging_service_start_wakelock' 
查看active_since这一项,看哪个锁不为0
      cat /sys/kernel/debug/wakeup_sources

上层传向底层的锁
cat /sys/power/wake_lock
当这几步查找后都没有对应的锁时,可以通过/sys/power/pm_test结点,操作设备进入对应的休眠阶段,详见linux休眠篇章

进入深度休眠后被唤醒问题debug

一般而言,平台会提供深度休眠后如何查看系统唤醒源,如全志平台的查看方式为
    一:休眠的时候不关闭终端
    echo N > /sys/module/printk/parameters/console_suspend
    二:唤醒后查看对应的唤醒源
    查看唤醒时候打印:
        platform wakeup, standby wakesource is:0x10000
    在allwinner对应的pm.h里面就可以看到对应的唤醒源编号,如linux4.9的在driver/soc/sunxi/pm.h 
    其他版本可以直接搜索关键字找到对应的唤醒源编号;
        #define CPUS_WAKEUP_DESCEND     (1<<16)
    而CPUS的GPIO一般是WIFI中断管脚在连接,故此时可以断开wifi连接查看系统唤醒是否依然异常
另外如果当log出现为闹钟唤醒:
    platform wakeup, standby wakesource is:0x100000
    查看对应唤醒源编号
    #define CPUS_WAKEUP_ALM0        (1<<20)
这种情况下我们一般需要通过去查看到底是哪个应用去唤醒系统的:这个时候我们可以借助这个应用 BetterBatteryStats2.0
    1.下载地址:
    http://cn.apkhere.com/app/com.asksven.betterbatterystats
    安装该软件:
    2.adb install com.asksven.betterbatterystats_2.2.2.0_paid-www.apkhere.com.apk
    3.获取权限:
    adb -d shell pm grant com.asksven.betterbatterystats android.permission.BATTERY_STATS
    4.若依然不可以:
    setenforce 0:取消selinux
    getenforce 查看是否为安全模式 Permissive 则是非安全;
    观察各项参数:

rtc闹钟设置与查看

venus-a3:/ # cat /proc/driver/rtc
rtc_time        : 05:44:49
rtc_date        : 2019-04-26
alrm_time       : 00:00:00
alrm_date       : 1970-01-01
alarm_IRQ       : no
alrm_pending    : no
update IRQ enabled      : no
periodic IRQ enabled    : no
periodic IRQ frequency  : 1
max user IRQ frequency  : 64
24hr            : yes

设置当前时间+100000后唤醒
venus-a3:/ # echo +100000 > /sys/devices/platform/soc/rtc/rtc/rtc0/wakealarm
venus-a3:/ # cat /proc/driver/rtc
rtc_time        : 05:47:15
rtc_date        : 2019-04-26
alrm_time       : 09:33:52
alrm_date       : 2019-04-27
alarm_IRQ       : yes
alrm_pending    : no
update IRQ enabled      : no
periodic IRQ enabled    : no
periodic IRQ frequency  : 1
max user IRQ frequency  : 64
24hr            : yes

echo 0取消

查看闹钟应用设置闹钟:
venus-a3:/ # settings list system | grep  next
       next_alarm_formatted=Wed 11:50 AM

怎么捕捉休眠唤醒的systrace
休眠前
atrace –async_start -b 8192 -o /data/trace2.dat gfx view wm am power
唤醒后
atrace –async_stop

参考资料

Android7.0 PowerManagerService 之亮灭屏(二) PMS 电源状态管理updatePowerStateLocked()

Android 保持屏幕常亮的几种方法

Android7.0 PowerManagerService(3)核心函数updatePowerStateLocked的主要流程

Android7.0 PowerManagerService亮灭屏分析(一)

wakelock介绍

linux笔记–rtc子系统

Android7.0亮屏流程分析

锁屏分析/)

ARM体系架构概述

Posted on 2018-11-17 | In cpu

ARM体系架构的发展

本文将从以下几点去展开介绍arm体系架构的发展:

  1. 指令集架构
  2. arm公司的发展历史以及授权模式
  3. arm的体系架构

arm公司的发展历史

arm的发展历程在此文章讲的非常详细,故不再赘述一文带你了解ARM的发展历程

arm授权模式

在传统PC领域,半导体有两种路子可以走,一种是Intel这种,从头到尾一条龙,架构和芯片设计,生产一律不靠任何人; 这样做需要极其雄厚,全方位的实力做保障,得有钱,有人,有技术,在半导体技术日益复杂的今天,能这么做屈指可数;好处就是利润率比较高,想卖多少钱就多少;当然技术上得做到领先甚至于垄断的地位优势才比较明显;

另外一种则无工厂模式(Fabless),NVIDIA,AMD,这类企业都是自己设计芯片,制造交给代工厂,比如台积电,联电,GlobalFoundries,三星电子。好处很明显,负担轻,但是在半导体这种工艺在功耗与性能中扮演重要角色的行业,你设计出来的是否能设计出来,怎么设计出来,很大程度看代工厂的能耐;幸好这些代工厂也十分给力,不断的逼近物理极限;使得AMD最近也能慢慢的赶上挤牙膏的Intel;

arm就不一样了,它不制造,不销售芯片,只是自己设计IP,包括指令集,微处理器,GPU,总线,然后谁要的话就买arm的授权;授权模式分为此三种:

处理器授权

ARM设计好一颗CPU或者GPU,armv7架构对应的IP为Cortex-A5/A7/A9/A12/A15/A17这几个核心架构,对应armv8-A就有Cortext-A35/A53/A57/A72/A73;ARMv8.2A指令集:Cortext-A55/A75;然后授权卖给伙伴,买下它们后,只能按照图纸实现,能发挥的不多,如何实现就比较随便,如配置哪些模块,几个核心,多少缓存,多高频率,什么工艺,谁来代工等等;

如果想优化,但是技术有限,那么可以买arm的处理器优化包/物理IP包授权(POP)
如果只是想更快速搞出产品,那么更干脆了,arm已经帮你制定好代工厂处理器类型和工艺了;
代表商家有联发科,展讯,联芯,全志,瑞芯微,炬力等;这些设计公司获得的是软核或者这点硬核授权,通过购买的CPU核,与GPU核,以及通过一定的流程,集成出SOC;

购买处理器授权的本质上就大同小异了,可以做出差异化的只能在一些IP上做出差异化;

架构/指令授权

这种授权方式价格比较贵,为防止碎片化的情况出现还有可能处于技术上的保护,arm禁止对指令集进行修改或者添加,但其他公司是否确切遵守了,这个就不得而知了;总之这种授权需要具有非常强的技术实力,也不过15家,如我们熟知的高通,苹果,三星,华为等公司;

除了版税,那些购买了arm IP授权的,还需要为每一个芯片支付版税;

在这里有些问题就应该提出来思考了,为什么都是arm的指令集,苹果的芯片却可以吊打现在的Android厂商的芯片呢?评价CPU性能指标在以下几个方面:

一:指令集宽度(ISSUE)6

二:乱序指令执行缓冲区(Recorder Buffer)(192)

三:内存加载延迟(Load Latency):4

四:分支预测错误代价(Misprediction Penalty):16(一般介于14~19)

不得不说,苹果很早就开始布局CPU的设计之路,具有极强的芯片设计能力,在厂家都是基于arm的公版进行设计,苹果就已经通过购买arm指令集,然后进行自己的CPU设计,在14年的时候A7处理器,作为第一个ARM 64位CPU商用,而且将指令集宽度位6,作为对比arm目前的指令集为3;由于封闭式的开发,不像高通,需要考虑各个厂家的需求和成本,苹果有更高的溢价能力,可以用面积去换取性能与功耗,并根据自己的系统去做定制化;

指令集架构(ISA)

CPU执行计算任务时都需要遵从一定的规范,程序在被执行时都需要先翻译成CPU可以理解的语言。这种规范就是指令集(ISA,Intruction Set Architecrure);

例如以下的机器码:1110 0001 1010 0000 0010 0000 0000 0001

比如有个CPU定位1110 0001就是ADD指令,1010 0000对应的是存储数据的寄存器R2;0010 0000对应的寄存器R0;
0000 0001对应是寄存器R1,故意思可以是ADD R2 R0 R1,将R0,R1的值加起来放到R2;指令集就是定义一套约定俗成的CPU运行规则,对于编程人员,面对的汇编指令;而对于CPU来说,就是怎么去理解这段二进制码,不同的CPU对这段二进制代码不同,故就有了不同的指令集架构;

指令集一般分为两种:精简指令集(RISC:reduced instruction set computer)和复杂指令集(CISC:Complex Instrution Set Computer),以洗衣机洗衣服为例,RISC架构为,加水->漂洗->风干;而复杂指令集则可以发出洗衣服的指令,从而让洗衣机自动帮忙做这一整套的流程;CISC可以通过一条复杂的指令来完成许多事情,性能在处理复杂的任务时,会比RISC更为高效;但随之而来的是面积和功耗的提升;基于2/8理论,程序大部分时间(80%)都是在做重复而简单的事情;所以RISC架构就应运而生,更为精简的指令,将任务更多的放在了编译器这块,通过复杂的指令转化成简单指令的组合,在一定程度上增加了代码量,但使其大部分场合能比CISC取得更小的面积和功耗;

采用精简指令集的微处理,常见为ARM,MIPS,Power Architecture(包括PowerPC,PowerXCell),SPARC,RISC-V等而采用CISC则为X86和AMD

至于arm为代表RISC架构与以X86为代表的CISC之间的优劣比较还有是否存在替代一说,笔者认为,不会,而且从目前的发展来看,已经出现了你中有我,我中有你的局面;
更进一步的探讨可以看下知乎上关于精简指令集与负杂指令集的讨论,我也很赞同其中一个答主的观点,这已经不是个技术问题了,而是一个商业的问题

总结完RISC和CISC之间的关系,再讲讲基于RISC阵营里面的ARM和最近比较火热的RISC-V,arm大家都熟悉,而RISC-V又是个什么东西呢?

RISC-V是加州大学伯克利分校的开源指令集,由计算机架构的宗师级任务David Patterson领衔打造,通过将核心指令集以及
关键IP开源,意图改变半导体生态;详细的可以看这篇文档;名家专栏丨一文看懂RISC-V

总结如下:RISC-V具有以下优点:1.可模块化配置的指令集。 2.支持可扩展的指令集 3.一套指令集支持所有架构,基本指令集仅40余条指令,以此为共用基础,加上其他常用模块子集指令总指令集也仅几十条 4.硬件设计和编译器非常简单

笔者认为,RISC-V目前作为一个像linux看齐的硬件开源组织,linux之所以能健康长足发展,在于社区成千上万的内核爱好者的不断贡献,使得linux能够蓬勃发展;而硬件则不同,在这动辄上百万的流片费的无数摆在硬件设计者面前的坎,是否会有公司或者团体愿意将自己用血与泪验证的IP,贡献到社区,如果没有做到这一点,即将面临由于厂家扩展指令不同的碎片化问题解决,还有RISC-V后面的长足发展,将付之空谈;这样RISC-V架构只能作为几个大厂找到的低廉的替代arm方案的选择,而无法真正做到普惠;

arm的体系架构

时间 架构 主要更新
1985 ARMv1 只有26位的寻值空间,没有用于商业产品
1986 ARMv2 首颗量产的ARM处理器,包括32位乘法指令和协处理器指令
1990 ARMv3 具有片上高速缓存,MMU和写缓冲,寻址空间增大到32位
1993 ARMv4 ARM7,ARM8,ARM9和Strong ARM采用这种架构。增加了16 Thumb指令集
1998 ARMv5 ARM7(EJ),ARM9(E),ARM10(E)和Xscale采用这种了该架构,改进了ARM/Thumb状态之间的切换效率,此外还引入DSP指令和支持JAVA
2001 ARMv6 ARM11,强化了图形处理性能,通过追加有效进行多媒体处理的SIMD将语音及图像的处理功能大大提高。此外ARM在这个系列中引入混合16位/32位的Thumb-2指令集
2004 ARMv7 Cortex-M3/4/7,Cortex-R4/5/6/7,Cortex-A8/9都是基于该架构,该架构包括NEON技术扩展,可将DSP和媒体处理吞吐量高达400%,并提供改进的浮点支持以满足下一代3D图形和游戏以及传统嵌入式控制应用的需要
2007 ARMv6-M 专门为低成本,高性能的设备而设计,Cortex-M0/1即采用该架构
2011 ARMv8 Cortex-A32/35/53/57/72/73采用此架构,第一款支持64位的处理器架构

ARM架构发展图:

Arm架构发展

arm体系的CPU工作模式:

1.用户模式(usr):正常的程序执行状态
2.快速中断模式(fiq):用于支持高速数据传输或者通道处理
3.中断模式(irq):用于普通中断处理
4.管理模式(svc):操作系统使用的保护模式
5.系统模式(sys):运行具有特权的操作系统的任务;
6.数据访问终止模式(abt):数据或指令与预取终止时进入该模式
7.未定义指令终止模式(und):未定义的指令执行时进入该模式;

linux如何从用户态进入内核态
分为两种:主动式和被动式、

1.主动式:就是linux用户在(ARM在用户模式下)工作,通过发起用户态程序发起命令请求,ARM响应进入特权模式进而Linux切入内核态,就是系统调用;系统调用可被堪称一个内核与用户空间程序交互的接口;把用户进程的请求传达 给内核,待内核把请求处理完毕后再将处理结果送回给用户空间;
系统调用的主要用途:

一:控制硬件-系统调用往往作为硬件资源和用户空间的抽象接口,比如读写文件用到的write/read调用

二:设置系统状态或读取内核数据;

2.被动式:就是Linux在用户态(ARM在用户模式)工作,没有主动发起请求,而被动地进入内核态,包括硬件中断和程序异常;

参考资料:

arm汇编基础教程

给开源架构泼泼冷水

完全看懂ARM处理器:RISC与CISC是什么?历史,架构一次性看透

ARMv8-a

ARM介绍1:发展史
ARM介绍2:授权模式

ARMV8

从arm到三星,苹果是如何走向自研芯片的

一样是ARM架构,为何苹果处理器效能就是压下其他人
浅析arm的异常,中断和arm工作模式的联系

123

dongka

21 posts
11 categories
13 tags
© 2020 dongka
Powered by Hexo
|
Theme — NexT.Pisces v5.1.4