一文了解 Android 车机如何处理中控的旋钮输入?

news2024/12/29 2:03:39

前言

上篇文章《从实体按键看 Android 车载的自定义事件机制》带大家了解了 Android 车机支持自定义输入的机制 CustomInputService。事实上,除了支持自定义事件,对于中控上常见的音量控制、焦点控制的旋钮事件,Android 车机也是支持的。

那本篇文章带大家看下 Android 车机处理旋钮事件的内在原理:

  1. 定义
  2. 监听和订阅
  3. 接收
  4. 处理
  5. 模拟

1. 定义

和自定义输入所支持的事件一致,支持旋钮输入的事件类型也在如下文件 types.hal 中定义。

// hardware/interfaces/automotive/vehicle/2.0/types.hal
    /**
     * Property to feed H/W rotary events to android
     * ...
     */
    HW_ROTARY_INPUT = (
        0x0A20
        | VehiclePropertyGroup:SYSTEM
        | VehiclePropertyType:INT32_VEC
        | VehicleArea:GLOBAL),

enum RotaryInputType : int32_t {
    ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION = 0,
    ROTARY_INPUT_TYPE_AUDIO_VOLUME = 1,
};

HW_ROTARY_INPUT 代表该事件在底层的 Property 定义,供 VehicleHal 对其发起监听。

该事件涵盖了一些旋钮所必须的数据:

  • 第 0 位代表哪种旋钮硬件,由 RotaryInputType 枚举细分,包括控制焦点的旋钮 TYPE_SYSTEM_NAVIGATION 和控制音量的旋钮 TYPE_AUDIO_VOLUME
  • 第 1 位代表旋转计数,正数代表顺时针计数 clockwise,负数代表逆时针计数 counterclockwise
  • 第 2 位代表旋钮事件的目标屏幕 VehicleDisplay,默认是 MAIN,即 center console,中控屏幕
  • 第 3 位及以后代表持续计数事件之间的时间差,单位为 ns

2. 监听和订阅

上层处理事件输入的 CarInputService 在初始化的时候,会向调度车机输入的中间层 InputHalService 注册监听。

// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {
    ...
    @Override
    public void init() {
        if (!mInputHalService.isKeyInputSupported()) {
            return;
        }

        mInputHalService.setInputListener(this);
        ...
    }
    ...
}

InputHalService 判断支持旋钮输入的话,向和 HAL 层交互的 VehicleHal 注册 HW_ROTARY_INPUT Property 的订阅。

// packages/services/Car/service/src/com/android/car/hal/InputHalService.java
public class InputHalService extends HalServiceBase {
    ...
    public void setInputListener(InputListener listener) {
        ...
        boolean rotaryInputSupported;

        synchronized (mLock) {
            mListener = listener;
            ...
            rotaryInputSupported = mRotaryInputSupported;
        }
        ...
        if (rotaryInputSupported) {
            mHal.subscribeProperty(this, HW_ROTARY_INPUT);
        }
        ...
    }

    public boolean isRotaryInputSupported() {
        synchronized (mLock) {
            return mRotaryInputSupported;
        }
    }
    ...
}

3. 接收

当旋钮事件发生,将通过 HAL 层抵达上述订阅该 Property 的 VehicleHal,其将找出处理方 HalServiceBaseInputHalService 并继续分发。

// packages/services/Car/service/src/com/android/car/hal/VehicleHal.java
public class VehicleHal implements HalClientCallback {
    ...
    @Override
    public void onPropertyEvent(ArrayList<HalPropValue> propValues) {
        synchronized (mLock) {
            for (int i = 0; i < propValues.size(); i++) {
                HalPropValue v = propValues.get(i);
                int propId = v.getPropId();
                HalServiceBase service = mPropertyHandlers.get(propId);
                if (service == null) {
                    continue;
                }

                service.getDispatchList().add(v);
                mServicesToDispatch.add(service);
                VehiclePropertyEventInfo info = mEventLog.get(propId);
                if (info == null) {
                    info = new VehiclePropertyEventInfo(v);
                    mEventLog.put(propId, info);
                } else {
                    info.addNewEvent(v);
                }
            }
        }
        for (HalServiceBase s : mServicesToDispatch) {
            s.onHalEvents(s.getDispatchList());
            s.getDispatchList().clear();
        }
        mServicesToDispatch.clear();
    }
    ...
}

InputHalService 首先确保上层的 InputListener 确实存在,此后再检查该 HalProperty 是何种类型。HW_ROTARY_INPUT 旋钮事件的话调用 dispatchRotaryInput() 继续。

public class InputHalService extends HalServiceBase {
    ...
    @Override
    public void onHalEvents(List<HalPropValue> values) {
        InputListener listener;
        synchronized (mLock) {
            listener = mListener;
        }
        if (listener == null) {
            return;
        }

        for (int i = 0; i < values.size(); i++) {
            HalPropValue value = values.get(i);
            switch (value.getPropId()) {
                case HW_ROTARY_INPUT:
                    dispatchRotaryInput(listener, value);
                    break;
                ...
            }
        }
    }
    ...
}

dispatchRotaryInput() 将执行如下步骤:

  1. 检查必要数据是否齐全,即起码包括旋钮硬件类型、旋钮计数、目标屏幕这 3 位
  2. 按照 index 取出这三位数据
  3. 检查旋钮计数是否为 0,因为无法判断 0 是顺时针还是逆时针
  4. 检查目标屏幕是否为中控屏幕 MAIN、仪表屏幕 INSTRUMENT_CLUSTER 中的一个
  5. 检查旋钮计数的时间差数值位数是否匹配(比如:旋转了 3 格的话,那么时间差必须要占 2 位)
  6. 根据旋钮硬件类型转化为 CarInputManager 中定义的事件类型
    • 焦点控制的话转换为 INPUT_TYPE_ROTARY_NAVIGATION
    • 音量控制的话转换为 INPUT_TYPE_ROTARY_VOLUME
  7. 提取持续计数的时间差到 timestamps 数组中
  8. 根据旋钮计数方向,转换到的事件类型以及时间差数组封装 RotaryEvent 对象交由 InputListener 继续分发
public class InputHalService extends HalServiceBase {
    ...
    private void dispatchRotaryInput(InputListener listener, HalPropValue value) {
        int timeValuesIndex = 3;  // remaining values are time deltas in nanoseconds
        if (value.getInt32ValuesSize() < timeValuesIndex) {
            return;
        }

        int rotaryInputType = value.getInt32Value(0);
        int detentCount = value.getInt32Value(1);
        int vehicleDisplay = value.getInt32Value(2);
        long timestamp = value.getTimestamp();  // for first detent, uptime nanoseconds

        boolean clockwise = detentCount > 0;
        detentCount = Math.abs(detentCount);
        if (detentCount == 0) { // at least there should be one event
            return;
        }

        if (vehicleDisplay != VehicleDisplay.MAIN
                && vehicleDisplay != VehicleDisplay.INSTRUMENT_CLUSTER) {
            return;
        }
        if (value.getInt32ValuesSize() != (timeValuesIndex + detentCount - 1)) {
            return;
        }

        int carInputManagerType;
        switch (rotaryInputType) {
            case ROTARY_INPUT_TYPE_SYSTEM_NAVIGATION:
                carInputManagerType = CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION;
                break;
            case ROTARY_INPUT_TYPE_AUDIO_VOLUME:
                carInputManagerType = CarInputManager.INPUT_TYPE_ROTARY_VOLUME;
                break;
            default: ...
        }

        long[] timestamps = new long[detentCount];
        long uptimeToElapsedTimeDelta = CarServiceUtils.getUptimeToElapsedTimeDeltaInMillis();
        ...
            
        RotaryEvent event = new RotaryEvent(carInputManagerType, clockwise, timestamps);
        listener.onRotaryEvent(event, convertDisplayType(vehicleDisplay));
    }
    ...
}

4. 处理

监听章节里提到 InputListener 为 CarInputService,所以将传递到 CarInputService 的 onRotaryEvent() 进行处理。

onRotaryEvent() 先检查是否有使用 InputEventCapture 监听旋钮事件的 Service 存在:

  • 如果有监听,交由 Capture 该事件的 Service 专门处理
  • 如果没有,转换为 Android 标准 KeyEvent 进行处理
// packages/services/Car/service/src/com/android/car/CarInputService.java
public class CarInputService ... {
    ...
    @Override
    public void onRotaryEvent(RotaryEvent event, @DisplayTypeEnum int targetDisplay) {
        if (!mCaptureController.onRotaryEvent(targetDisplay, event)) {
            List<KeyEvent> keyEvents = rotaryEventToKeyEvents(event);
            for (KeyEvent keyEvent : keyEvents) {
                onKeyEvent(keyEvent, targetDisplay);
            }
        }
    }
    ...
}

专门处理

Car App 提供了一个专门控制焦点的 RotaryService,它在绑定时通过 CarInputManager 的 requestInputEventCapture() 申请监听了 INPUT_TYPE_ROTARY_NAVIGATION 类型的旋钮事件。

// packages/apps/Car/RotaryController/src/com/android/car/rotary/RotaryService.java
public class RotaryService ... {
    /** Input types to capture. */
    private final int[] mInputTypes = new int[]{
            CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION,
            ...};
    ...
    @Override
    public void onServiceConnected() {
        super.onServiceConnected();

        mCar = Car.createCar(this, null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER,
                (car, ready) -> {
                    mCar = car;
                    if (ready) {
                        mCarInputManager =
                                (CarInputManager) mCar.getCarManager(Car.CAR_INPUT_SERVICE);
                        ...
                        mCarInputManager.requestInputEventCapture(
                                CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
                                mInputTypes,
                                CarInputManager.CAPTURE_REQ_FLAGS_ALLOW_DELAYED_GRANT,
                                /* callback= */ this);
                    }
                });
        ...
    }
    ...
}

自然的,RotaryService 的 onRotaryEvent() 会得到调用,首先将检查目标屏幕是否符合预期,必须是 MAIN 即中控屏幕。通过的话,调用 handleRotaryEvent() 继续处理。

public class RotaryService ... {
    ...
    @Override
    public void onRotaryEvents(int targetDisplayType, @NonNull List<RotaryEvent> events) {
        if (!isValidDisplayType(targetDisplayType)) {
            return;
        }
        for (RotaryEvent rotaryEvent : events) {
            handleRotaryEvent(rotaryEvent);
        }
    }

    private static boolean isValidDisplayType(int displayType) {
        if (displayType == CarOccupantZoneManager.DISPLAY_TYPE_MAIN) {
            return true;
        }
        return false;
    }
    ...
}

handleRotaryEvent() 将检查 RotaryEvent 中的硬件 type,确保确实来自于焦点控制旋钮 INPUT_TYPE_ROTARY_NAVIGATION,通过的话调用 handleRotateEvent() 继续。

public class RotaryService ... {
    ...
    private void handleRotaryEvent(RotaryEvent rotaryEvent) {
        if (rotaryEvent.getInputType() != CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION) {
            return;
        }
        boolean clockwise = rotaryEvent.isClockwise();
        int count = rotaryEvent.getNumberOfClicks();

        long eventTime = rotaryEvent.getUptimeMillisForClick(count - 1);
        handleRotateEvent(clockwise, count, eventTime);
    }
    ...
}

handleRotateEvent() 主要是依据屏幕的设置和当前 focus 的 Node 情况来决定是调用 performScrollAction() 执行屏幕滚动,还是寻找到目标 Node 调用 performFocusAction() 来执行焦点的移动。

其本质上是通过 InputManager 向系统注入 SCROLL 触摸事件,或者通过 Accessibility 向上面的或下面的待 focus 的 AccessibilityNode 发送 FOCUS Action 操作。

public class RotaryService ... {
    ...
    private void handleRotateEvent(boolean clockwise, int count, long eventTime) {
        int rotationCount = getRotateAcceleration(count, eventTime);
        if (mInProjectionMode) {
            injectMotionEvent(DEFAULT_DISPLAY, clockwise ? rotationCount : -rotationCount);
            return;
        }
        if (initFocus() || mFocusedNode == null) {
            return;
        }

        if (mInDirectManipulationMode) {
            if (DirectManipulationHelper.supportRotateDirectly(mFocusedNode)) {
                performScrollAction(mFocusedNode, clockwise);
            } else {
                AccessibilityWindowInfo window = mFocusedNode.getWindow();
                if (window == null) {
                    L.w("Failed to get window of " + mFocusedNode);
                    return;
                }
                int displayId = window.getDisplayId();
                window.recycle();
                injectMotionEvent(displayId, clockwise ? rotationCount : -rotationCount);
            }
            return;
        }

        int remainingRotationCount = rotationCount;
        int direction = clockwise ? View.FOCUS_FORWARD : View.FOCUS_BACKWARD;
        Navigator.FindRotateTargetResult result =
                mNavigator.findRotateTarget(mFocusedNode, direction, rotationCount);
        if (result != null) {
            if (performFocusAction(result.node)) {
                remainingRotationCount -= result.advancedCount;
            }
            Utils.recycleNode(result.node);
        } else {
            L.w("Failed to find rotate target from " + mFocusedNode);
        }

        if (remainingRotationCount > 0 && isInFocusedWindow(mFocusedNode)) {
            AccessibilityNodeInfo scrollableContainer =
                    mNavigator.findScrollableContainer(mFocusedNode);
            if (scrollableContainer != null) {
                injectScrollEvent(scrollableContainer, clockwise, remainingRotationCount);
                scrollableContainer.recycle();
            }
        }
    }
    ...
}

标准处理

和导航旋钮事件不同,系统没有 Capture 音量旋钮事件 INPUT_TYPE_ROTARY_VOLUME 的 Service,那么它得执行标准处理。

首先,得将 RotatryEvent 转换为标准的按键编号 Key Code,具体的执行如下逻辑:

  1. 焦点控制按钮的话,依据方向 mapping 顺时针为焦点前进的 KEYCODE_NAVIGATE_NEXT,逆时针为焦点后退的 KEYCODE_NAVIGATE_PREVIOUS
  2. 音量控制按钮的话,mapping 为音量 +/- Key Code,顺时针为 KEYCODE_VOLUME_UP,逆时针则是 KEYCODE_VOLUME_DOWN
  3. 按照计数次数批量调用 createKeyEvent() 创建 KeyEvent 对象,并添加到待处理 keyEvents 列表中。
public class CarInputService ... {
    ...
    private static List<KeyEvent> rotaryEventToKeyEvents(RotaryEvent event) {
        int numClicks = event.getNumberOfClicks();
        int numEvents = numClicks * 2; // up / down per each click
        boolean clockwise = event.isClockwise();
        int keyCode;

        switch (event.getInputType()) {
            case CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION:
                keyCode = clockwise
                        ? KeyEvent.KEYCODE_NAVIGATE_NEXT
                        : KeyEvent.KEYCODE_NAVIGATE_PREVIOUS;
                break;
            case CarInputManager.INPUT_TYPE_ROTARY_VOLUME:
                keyCode = clockwise
                        ? KeyEvent.KEYCODE_VOLUME_UP
                        : KeyEvent.KEYCODE_VOLUME_DOWN;
                break;
            ...
        }

        ArrayList<KeyEvent> keyEvents = new ArrayList<>(numEvents);
        for (int i = 0; i < numClicks; i++) {
            long uptime = event.getUptimeMillisForClick(i);
            KeyEvent downEvent = createKeyEvent(/* down= */ true, uptime, uptime, keyCode);
            KeyEvent upEvent = createKeyEvent(/* down= */ false, uptime, uptime, keyCode);
            keyEvents.add(downEvent);
            keyEvents.add(upEvent);
        }
        return keyEvents;
    }    
    ...
}

接着,遍历准备好的 keyEvents 列表,逐个处理。

public class CarInputService ... {
    ...
    @Override
    public void onRotaryEvent(RotaryEvent event, @DisplayTypeEnum int targetDisplay) {
        if (!mCaptureController.onRotaryEvent(targetDisplay, event)) {
            List<KeyEvent> keyEvents = rotaryEventToKeyEvents(event);
            // 遍历列表,逐个处理
            for (KeyEvent keyEvent : keyEvents) {
                onKeyEvent(keyEvent, targetDisplay);
            }
        }
    }
    ...
}

CarInputService 的 onKeyEvent() 直接处理的 Code 只有激活语音助手的 KEYCODE_VOICE_ASSIST 和拨打电话的 KEYCODE_CALL。其他的 Key Code 执行一般处理:

  1. 如果目标屏幕是 INSTRUMENT_CLUSTER 即仪表屏幕的话,调用 handleInstrumentClusterKey()InstrumentClusterKeyListener 执行仪表上的事件,貌似是 Cluster app 完成,具体不再展开
  2. 检查是否有使用 InputEventCapture 监听 NAVIGATE_ 焦点控制、VOLUME_ 音量控制 KeyEvent 的 Service 存在,有的话回调 onKeyEvent() Callback
  3. 如果没有 Capture 处理的好,告知 KeyEventListener 进行兜底处理
public class CarInputService ... {
    ...
    @Override
    public void onKeyEvent(KeyEvent event, @DisplayTypeEnum int targetDisplayType) {
        // Special case key code that have special "long press" handling for automotive
        switch (event.getKeyCode()) {
            case KeyEvent.KEYCODE_VOICE_ASSIST:
                handleVoiceAssistKey(event);
                return;
            case KeyEvent.KEYCODE_CALL:
                handleCallKey(event);
                return;
            default:
                break;
        }

        assignDisplayId(event, targetDisplayType);

        // Allow specifically targeted keys to be routed to the cluster
        if (targetDisplayType == CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER
                && handleInstrumentClusterKey(event)) {
            return;
        }
        if (mCaptureController.onKeyEvent(targetDisplayType, event)) {
            return;
        }
        mMainDisplayHandler.onKeyEvent(event);
    }
    ...
}

KeyEventListener 在 CarInputService 初始化的时候指定,具体的就是通过 InputManagerHelper 注入 KeyEvent。

public class CarInputService ... {
    ...
    private final KeyEventListener mMainDisplayHandler;

    public CarInputService( ... ) {
        this(context, inputHalService, userService, occupantZoneService, bluetoothService,
                new Handler(CarServiceUtils.getCommonHandlerThread().getLooper()),
                context.getSystemService(TelecomManager.class),
                event -> InputManagerHelper.injectInputEvent(
                        context.getSystemService(InputManager.class), event),
                () -> Calls.getLastOutgoingCall(context),
                () -> getViewLongPressDelay(context),
                () -> context.getResources().getBoolean(R.bool.config_callButtonEndsOngoingCall),
                new InputCaptureClientController(context));
    }
    ...
}

InputManagerHelper 没啥特别的,直接调用 InputManager 的标准方法 injectInputEvent() 完成注入,后续由 InputManagerService 开始 Dispatch、Transport 等一系列处理。

// packages/services/Car/car-builtin-lib/src/android/car/builtin/input/InputManagerHelper.java
public class InputManagerHelper {
    ...
    public static boolean injectInputEvent(@NonNull InputManager inputManager,
            @NonNull android.view.InputEvent event) {
        return inputManager.injectInputEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
    }
}

5. 模拟

当旋钮按键环境尚未到位的时候,我们可以使用 adb 命令模拟旋钮事件来验证代码链路。

格式:

adb shell cmd car_service inject-rotary [-d display] [-i input_type] [-c clockwise] [-dt delta_times_ms]
  • display,目标屏幕:0 代表中控屏幕,1 代表仪表屏幕,默认是 0
  • input_type,按钮类型: 10 代表焦点控制,11 代表音量控制,默认是 10
  • clockwise,旋钮方向: true 代表顺时针方向,false 代表逆时针,默认是 false
  • delta_times_ms,持续旋转计数的时间间隔:多次旋转事件和当前时刻的间隔列表,按降序排列,默认是 0,表示只有一次旋转

下面将介绍几个命令示例,帮助大家更好地理解该命令的使用。

adb shell cmd car_service inject-rotary

没有指定任何参数,全部都是默认的操作,表示针对中控屏幕发送焦点控制的旋钮事件,方向为逆时针、焦点后退 1 格

adb shell cmd car_service inject-rotary -d 1 -i 11 -c true

表示针对仪表屏幕发送音量控制的旋钮事件,方向为顺时针、调低 1 格

adb shell cmd car_service inject-rotary -c true -dt 100 50

表示针对中控屏幕发送焦点控制的旋钮事件,方向为顺时针、3 次计数、焦点前进 3 格

结语

与自定义输入相比,旋钮事件的处理流程有细微差异,主要体现在 CarInputService 会针对音量、焦点两种的旋钮控制,存在特定的处理逻辑。最后,结合一张图回顾下整体流程:

  1. 支持音量控制焦点控制的两种旋钮硬件产生 HW_ROTARY_INPUT Propery 变化

  2. 由和 HAL 层交互的 VehicleHal 订阅到 Propery 变化,将事件提取为 HalPropValue 类型

  3. 并发送给车机输入的中间服务 InputHalService 接收和进一步地封装为 RotaryEvent 类型

  4. 分发到处理事件输入的专用服务 CarInputService

    a. 如果有 Capture 音量/焦点的 Rotary 事件的交由其专门处理:Car App 的 RotaryService,其将决定通过 InputManager 注入 SCROLL 滚动还是通过 Accessibility 触发焦点 Focus 操作;

    b. 如果没有,则执行标准处理:

    • 首先按照 Rotary 类型和旋钮方向、计数封装为 Android 标准 KeyEvent 列表
    • 如果目标屏幕为仪表的话,列表交由 Cluster App 处理
    • 反之检查是否有 Capture 该 KeyEvent 的 Service 需要处理
    • 最后交由 InputManager 逐个注入该 KeyEvent,继而由系统的 InputManagerService 进行调度

推荐阅读

  • 从实体按键看 Android 车载的自定义事件机制
  • 如何打造车载语音交互:Google Voice Interaction 给你答案
  • Android 车机初体验:Auto,Automotive 傻傻分不清楚?

参考文档

  • https://developer.android.google.cn/training/cars
  • https://source.android.google.cn/docs/devices/automotive/hmi/rotary_controller/app_developers

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/812349.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

测试|测试用例方法篇

测试|测试用例方法篇 文章目录 测试|测试用例方法篇1.测试用例的基本要素&#xff1a;测试环境&#xff0c;操作步骤&#xff0c;测试数据&#xff0c;预期结果…2.测试用例带来的好处3.测试用例的设计思路&#xff0c;设计方法&#xff0c;具体设计方法之间的关系**设计测试用…

linux设备驱动的poll与fasync

什么是fasync 在 Linux 驱动程序中&#xff0c;fasync 是一种机制&#xff0c;用于在异步事件发生时通知进程。它允许进程在等待设备事件时&#xff0c;不必像传统的轮询方式那样持续地查询设备状态。 具体来说&#xff0c;当进程调用 fcntl(fd, F_SETFL, O_ASYNC) 函数时&am…

lib-flexible修改配置适配更多不同分辨率

找到设置宽度的地方 然后根据你的屏幕最大多大呀&#xff0c;最小多小呀设置一下 if (width / dpr < 1980) { width 1980 * dpr; } else if (width / dpr > 5760) { width 5760 * dpr; }

Python 教程之标准库概览

概要 Python 标准库非常庞大&#xff0c;所提供的组件涉及范围十分广泛&#xff0c;使用标准库我们可以让您轻松地完成各种任务。 以下是一些 Python3 标准库中的模块&#xff1a; 「os 模块」 os 模块提供了许多与操作系统交互的函数&#xff0c;例如创建、移动和删除文件和…

【Linux】进程篇Ⅱ:进程开始、进程终止、进程等待

文章目录 五、fork 函数&#xff0c;创建进程写时拷贝 六、进程终止1. 退出码2. 如何终止程序 七、进程等待1. 概念2. wait 函数waitpid 函数 &#x1f53a; 3. 阻塞等待 五、fork 函数&#xff0c;创建进程 #include <unistd.h>   pid_t fork(void);   返回值&#xf…

动态SQL 语句-更复杂的查询业务需求也能轻松拿捏

文章目录 动态SQL 语句-更复杂的查询业务需求动态SQL-官方文档为什么需要动态SQL动态SQL-基本介绍基本介绍动态SQL 必要性解决方案分析 动态SQL 常用标签动态SQL-案例演示if 标签应用实例where 标签应用实例choose/when/otherwise 应用实例forEach 标签应用实例trim 标签应用实…

【C语言进阶篇】 你真的学会数组了嘛?数组笔试题万字解析(上)

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《C语言初阶篇》 《C语言进阶篇》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 &#x1f4cb; 前言&#x1f4ac; 数组笔试题解析&#x1f4ad; 一维数组笔试题✅ 一维数组笔试题解析✅ 一维数…

ShardingSphere 源码模块介绍

目录 Agent Db-Protocol Dialect-Exception Distribution Features Infra JDBC Kernel Mode Proxy Agent agent 模块提供了基于 Java Agent 实现的可观察框架 logging&#xff1a; 用于记录 ShardingSphere 的日志&#xff0c;支持文件metricrs&#xff1a;用于收集…

C++那些事之高性能SIMD

C那些事之高性能SIMD 最近在看相关向量化的内容&#xff0c;看起来有点头大&#xff0c;借此机会&#xff0c;学习一下高性能SIMD编程。 SIMD全称single-instruction multiple-data,单指令多数据。 在传统的计算机架构中&#xff0c;CPU一次只能处理一个数据元素。但是&#xf…

【机器学习】Feature scaling and Learning Rate (Multi-variable)

Feature scaling and Learning Rate 1、数据集2、学习率2.1 α \alpha α 9.9e-72.2 α \alpha α 9e-72.3 α \alpha α 1e-7 3、特征缩放3.1 特征缩放的原因3.2 Z-score 归一化3.3 预测3.4 损失等值线 导入所需的库 import numpy as np np.set_printoptions(precision…

【C++】类和对象-C++运算符重载

运算符重载 1.加号运算符重载 代码&#xff1a; #include <iostream> using namespace std; /******************************************/ //加号运算符重载class Person { public:int m_A;int m_B;//1、成员函数重载号(不能与下面方式2同时存在&#xff0c;否则代码报…

在docker中没有vi如何修改docker中的文件

今天在做学成在线的项目&#xff0c;遇到了一个问题&#xff0c;就是死活登不上xxl-job&#xff0c;按照之前遇到的nacos的问题&#xff0c;我怀疑很大概率是和当时的ip设置有关&#xff0c;不知道nacos的ip怎么修改的同学&#xff0c;可以看看这篇文章&#xff1a;关于docker中…

电子词典

项目要求&#xff1a; 1.登录注册功能&#xff0c;不能重复登录&#xff0c;重复注册。用户信息也存储在数据库中。 2.单词查询功能 3.历史记录功能&#xff0c;存储单词&#xff0c;意思&#xff0c;以及查询时间&#xff0c;存储在数据库 4.基于TCP&#xff0c;支持多客户…

【AI网站分享】

AI网站分享 1 AI应用2 AI 写作3 AI 编程4 AI设计5 AI作图6 AI训练模型7 AI影音编辑8 AI效率助手 网站链接&#xff1a; https://tools.haiyong.site/ai/ 网站中的内容大致可以分为八类&#xff1a;AI应用、AI写作、 AI 编程、 AI设计、 AI作图、AI训练模型、 AI影音编辑、 AI效…

线程属性——线程分离应用

文章目录 相关函数初始化释放线程属性的资源获取线程分离的状态属性设置线程分离的状态属性获取线程的栈的大小线程分离应用 相关函数 可以通过man pthread_attr_然后按两次table键查询和属性相关的函数 初始化 释放线程属性的资源 获取线程分离的状态属性 设置线程分离的状…

C# VS2022+WinForm+Oracle19.3+存储过程,SQL和代码分离

【我的目的】&#xff1a;SQL和代码分别存放在不同的地方&#xff0c;便于随时修改SQL的内容&#xff0c;也便于修改SQL的书写格式 方案1&#xff1a;把SQL存放在DataSet.xsd中实现SQL和代码分离 方案2&#xff1a;用存储过程实现SQL和代码分离 我最倾向方案1&#xff0c;利用…

链路 聚合

静态链路聚合&#xff1a;多数内网使用 。非物理直连建议与BFD联动 动态链路聚合LACP&#xff1a;是公有协议、内网、二层专线接口都能使用&#xff0c;现网多数使用此方式链路 聚合 PAGP&#xff1a;思科私有协议&#xff0c;只支持思科设备使&#xff0c;现网多数不用

Windows驱动开发

开发Windows驱动程序时&#xff0c;debug比较困难&#xff0c;并且程序容易导致系统崩溃&#xff0c;这时可以使用Virtual Box进行程序调试&#xff0c;用WinDbg在主机上进行调试。 需要使用的工具&#xff1a; Virtual Box&#xff1a;用于安装虚拟机系统&#xff0c;用于运…

谨防虚假发货!了解如何辨别真假发货单号

随着电子商务的发展&#xff0c;快递行业成为了一个不可忽视的重要环节。然而&#xff0c;虚假发货单号的出现给快递行业带来了一定的困扰。为了解决这个问题&#xff0c;一些快递批量查询高手软件开始应用于虚假发货单号的分析。本文将介绍这些软件如何分析出虚假发货单号&…

应用开发者的疑问:大模型是银弹吗?

被当成银弹的大模型 ChatGPT 火了之后&#xff0c;大模型似乎被当成了真正的银弹&#xff0c;所有的体验问题都想通过大模型解决&#xff1a; 能不能和大模型对话订机票&#xff1f;自然语言生成 SQL&#xff0c;简化报表分析工作&#xff1f;大模型帮老年人操作软件&#xff…