返回顶部
首页 > 资讯 > 精选 >Android 13添加自定义native服务
  • 248
分享到

Android 13添加自定义native服务

androidjava开发语言 2023-08-19 14:08:34 248人浏览 安东尼
摘要

欢迎加入我的知识星球Android系统开发指南 欢迎关注微信公众号 无限无羡 欢迎关注知乎账号 无限无羡 文章目录 native服务添加selinux权限配置通过binder访问服务 native服务添加 native服务就是用

欢迎加入我的知识星球Android系统开发指南

欢迎关注微信公众号 无限无羡
欢迎关注知乎账号 无限无羡

native服务添加

native服务就是用c++写的系统服务,通过init进程启动,可以实现binder接口供client调用。
下面我们以实现一个beanserver的后台服务为例:

  1. 首先需要写一个rc文件
// 文件路径根据自己需求放置// vendor/zzh/native-service/bean-server/beanserver.rcservice beanserver /system/bin/beanserver    class main
  1. 写服务的main函数
// vendor/zzh/native-service/bean-server/main_beanserver.cpp#define LOG_TAG "beanserver"//#define LOG_NDEBUG 0#include using namespace Android;int main(int arGC __unused, char** argv __unused){    ALOGD(" beamserver start......");    return 0;}

这个服务我们启动后只是打印了一行日志就退出了,具体可以根据自己的需求加入自己开机处理的业务。

  1. 编写Android.bp
cc_binary {// 最终会生成到/system/bin/beanserver    name: "beanserver",    srcs: ["main_beanserver.cpp"],    header_libs: [    ],    shared_libs: [        "liblog",        "libutils",        "libui",        "libgui",        "libbinder",        "libhidlbase",    ],    compile_multilib: "first",    cflags: [        "-Wall",        "-Wextra",        "-Werror",        "-Wno-unused-parameter",    ],// 最终会生成到/system/etc/init/beanserver.rc// init进程启动时会解析/system/etc/init/目录下的rc文件    init_rc: ["beanserver.rc"],    vintf_fragments: [    ],}

beanserver.rc在经过编译后会生成到/system/etc/init/beanserver.rc,然后init进程启动的时候就会解析该文件启动进程。

init进程解析/system/etc/init/的代码

// system/core/init/init.cpp// 可以看到init会解析多个目录下的rc文件static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {    Parser parser = CreateParser(action_manager, service_list);    std::string bootscript = GetProperty("ro.boot.init_rc", "");    if (bootscript.empty()) {        parser.ParseConfig("/system/etc/init/hw/init.rc");        if (!parser.ParseConfig("/system/etc/init")) {            late_import_paths.emplace_back("/system/etc/init");        }        // late_import is available only in Q and earlier release. As we don't        // have system_ext in those versions, skip late_import for system_ext.        parser.ParseConfig("/system_ext/etc/init");        if (!parser.ParseConfig("/vendor/etc/init")) {            late_import_paths.emplace_back("/vendor/etc/init");        }        if (!parser.ParseConfig("/odm/etc/init")) {            late_import_paths.emplace_back("/odm/etc/init");        }        if (!parser.ParseConfig("/product/etc/init")) {            late_import_paths.emplace_back("/product/etc/init");        }    } else {        parser.ParseConfig(bootscript);    }}
  1. 加到编译镜像环境
// device/generic/car/emulator/aosp_car_emulator.mk// 找到自己项目的mk文件加入PRODUCT_PACKAGES += beanserver

这样才会将beanserver编译到镜像中。

  1. 编译镜像验证
    编译完启动时看到有如下报错日志:
03-29 07:13:38.364     0     0 I init    : Parsing file /system/etc/init/beanserver.rc...03-29 07:13:41.706     0     0 E init    : Could not start service 'beanserver' as part of class 'core': File /system/bin/beanserver(labeled "u:object_r:system_file:s0") has incorrect label or no domain transition from u:r:init:s0 to another SElinux domain defined. Have you configured your service correctly? https://source.android.com/security/selinux/device-policy#label_new_services_and_address_denials. Note: this error shows up even in permissive mode in order to make auditing denials possible.

提示缺少selinux权限配置,我们可以在Google官网看到label_new_services_and_address_denials

下面我们配置一下selinux权限

selinux权限配置

  1. 编写自定义服务的te文件
// 我这里将新加的selinux配置放在了自己创建目录中// 注意文件的首尾保留一行空行// vendor/zzh/sepolicy/vendor/beanserver.te# beanservertype beanserver, domain, coredomain;type beanserver_exec, exec_type, file_type, system_file_type;init_daemon_domain(beanserver)
  1. 编写file_contexts文件
// vendor/zzh/sepolicy/vendor/file_contexts// 注意文件的首尾保留一行空行/system/bin/beanserver   u:object_r:beanserver_exec:s0

添加到sepolicy编译规则

// device/generic/car/emulator/aosp_car_emulator.mkBOARD_VENDOR_SEPOLICY_DIRS += vendor/zzh/sepolicy/vendor
  1. 编译后启动模拟器
    这个时候能看到服务启动了,但是发现beanserver一直在重启,日志如下:

    经过排查发生这个现象的原因有两个:
    (1)我们的进程启动后只是打印了一行日志就return 0退出了,通过查看init的代码发现,init进程会通过signal 9去kill掉退出的进程,这里总感觉有点矛盾,可能时init担心进程自己退出的不彻底,所以来个signal 9? init 里的代码如下:
// system/core/init/sigchld_handler.cppstatic pid_t ReapOneProcess() {    siginfo_t siginfo = {};    // This returns a zombie pid or infORMs us that there are no zombies left to be reaped.    // It does NOT reap the pid; that is done below.    if (TEMP_FAILURE_RETRY(waitid(P_ALL, 0, &siginfo, WEXITED | WNOHANG | WNOWAIT)) != 0) {        PLOG(ERROR) << "waitid failed";        return 0;    }    auto pid = siginfo.si_pid;    if (pid == 0) return 0;    // At this point we know we have a zombie pid, so we use this scopeguard to reap the pid    // whenever the function returns from this point forward.    // We do NOT want to reap the zombie earlier as in Service::Reap(), we kill(-pid, ...) and we    // want the pid to remain valid throughout that (and potentially future) usages.    auto reaper = make_scope_guard([pid] { TEMP_FAILURE_RETRY(waitpid(pid, nullptr, WNOHANG)); });    std::string name;    std::string wait_string;    Service* service = nullptr;    if (SubcontextChildReap(pid)) {        name = "Subcontext";    } else {        service = ServiceList::GetInstance().FindService(pid, &Service::pid);        if (service) {            name = StringPrintf("Service '%s' (pid %d)", service->name().c_str(), pid);            if (service->flags() & SVC_EXEC) {                auto exec_duration = boot_clock::now() - service->time_started();                auto exec_duration_ms =                    std::chrono::duration_cast<std::chrono::milliseconds>(exec_duration).count();                wait_string = StringPrintf(" waiting took %f seconds", exec_duration_ms / 1000.0f);            } else if (service->flags() & SVC_ONESHOT) {                auto exec_duration = boot_clock::now() - service->time_started();                auto exec_duration_ms =                        std::chrono::duration_cast<std::chrono::milliseconds>(exec_duration)    .count();                wait_string = StringPrintf(" oneshot service took %f seconds in background",               exec_duration_ms / 1000.0f);            }        } else {            name = StringPrintf("Untracked pid %d", pid);        }    }// beanserver退出后,si_code为CLD_EXITED    if (siginfo.si_code == CLD_EXITED) {        LOG(INFO) << name << " exited with status " << siginfo.si_status << wait_string;    } else {        LOG(INFO) << name << " received signal " << siginfo.si_status << wait_string;    }    if (!service) {        LOG(INFO) << name << " did not have an associated service entry and will not be reaped";        return pid;    }// 这里将siginfo传入Reap函数    service->Reap(siginfo);    if (service->flags() & SVC_TEMPORARY) {        ServiceList::GetInstance().RemoveService(*service);    }    return pid;}
void Service::Reap(const siginfo_t& siginfo) {// 如果没有oneshot属性或者设置了restart的属性,则会调用SIGKILL杀掉进程// 这里的oneshot和restart指的是rc文件里面的配置,我们的rc文件只声明了class main// 如果没有声明oneshot属性,则服务被杀后会重新启动,如果配置了oneshot则只会启动一次    if (!(flags_ & SVC_ONESHOT) || (flags_ & SVC_RESTART)) {        KillProcessGroup(SIGKILL, false);    } else {        // Legacy behavior from ~2007 until Android R: this else branch did not exist and we did not        // kill the process group in this case.        if (SelinuxGetVendorAndroidVersion() >= __ANDROID_api_R__) {            // The new behavior in Android R is to kill these process groups in all cases.  The            // 'true' parameter instructions KillProcessGroup() to report a warning message where it            // detects a difference in behavior has occurred.            KillProcessGroup(SIGKILL, true);        }    }    ...}

为了保证服务不退出,我们先在main函数里加个死循环看下效果

int main(int argc __unused, char** argv __unused){    ALOGD(" beamserver start......");    while (true) {        sleep(3);        ALOGD("beanserver_thread...........");    }    return 0;}

编译后启动模拟器看下启动日志:

可以看到目前服务已经正常了,这个服务将一直在后台运行。
如果我们只想开机后执行一些操作后退出,那也可以,我们修改下看看效果。
修改beanserver.rc文件:

service beanserver /system/bin/beanserver    class main    oneshot //只启动一次

修改main_beanserver.cpp文件:

#define LOG_TAG "beanserver"//#define LOG_NDEBUG 0#include using namespace android;void quickSort(int arr[], int left, int right) {    int i = left, j = right;    int tmp;    int pivot = arr[(left + right) / 2];        while (i <= j) {        while (arr[i] < pivot)            i++;        while (arr[j] > pivot)            j--;        if (i <= j) {            tmp = arr[i];            arr[i] = arr[j];            arr[j] = tmp;            i++;            j--;        }    };        if (left < j)        quickSort(arr, left, j);    if (i < right)        quickSort(arr, i, right);}int main(int argc __unused, char** argv __unused){    ALOGD(" beamserver start...");    int data[] = {9, 21, 3, 66, 100, 8, 36};    quickSort(data, 0, 7);    for (int i = 0; i < 7; i++) {        ALOGD("data[%d]=%d", i, data[i]);    }    return 0;}

将一组数据用快速排序进行排序后输出,然后服务退出,通过日志可以看到,服务退出后init发送了signal 9,到此服务进程退出。

通过binder访问服务

先看下文件目录:

如果想要被别的进程调用,就需要实现binder接口,这样才算是一个完整的系统服务。

  1. 定义aidl接口
// vendor/zzh/native-service/bean-server/libbeanservice/aidl/com/zzh/IBeanService.aidlpackage com.zzh;interface IBeanService {    void sayHello();}
  1. 加入Android.bp中进行编译
// vendor/zzh/native-service/bean-server/libbeanservice/Android.bpcc_library_shared {    name: "libbeanservice_aidl",    aidl: {        export_aidl_headers: true,        local_include_dirs: ["aidl"],        include_dirs: [        ],    },    srcs: [        ":beanservice_aidl",    ],    shared_libs: [        "libbase",        "libcutils",        "libutils",        "liblog",        "libbinder",        "libgui",    ],    cflags: [        "-Werror",        "-Wall",        "-Wextra",    ],}filegroup {    name: "beanservice_aidl",    srcs: [        "aidl/com/zzh/IBeanService.aidl",    ],    path: "aidl",}

make libbeanservice_aidl 后可以看到out下生成的文件:
IBeanService.cpp
IBeanService.h
BpBeanService.h
BnBeanService.h

  1. 实现服务,继承BnBeanService
    BeanService.h
// vendor/zzh/native-service/bean-server/libbeanservice/BeanService.h#ifndef BEANSERVICE_H#define BEANSERVICE_H#include #include namespace android {class BeanService:    public BinderService<BeanService>,    public virtual ::com::zzh::BnBeanService,    public virtual IBinder::DeathRecipient{public:    // Implementation of BinderService    static char const* getServiceName() { return "bean.like"; }    // IBinder::DeathRecipient implementation    virtual void        binderDied(const wp<IBinder> &who);        BeanService();    ~BeanService();    virtual binder::Status  sayHello();};}#endif

这里BeanService继承了BinderService,必须重写getServiceName函数用来返回服务的名称,后续注册服务时会用到这个名字。

BeanService.cpp

// vendor/zzh/native-service/bean-server/libbeanservice/BeanService.cpp#define LOG_TAG "BeanService"//#define LOG_NDEBUG 0#include "BeanService.h"#include namespace android {using binder::Status;BeanService::BeanService() {}BeanService::~BeanService() {}void BeanService::binderDied(const wp<IBinder> &who) {    ALOGE("%s: Java client's binder died, removing it from the list of active clients, who=%p",            __FUNCTION__, &who);}Status BeanService::sayHello() {    ALOGD(" BeanService::sayHello ");    return Status::ok();}}

Android.bp

// vendor/zzh/native-service/bean-server/libbeanservice/Android.bpcc_library_shared {    name: "libbeanservice",    srcs: [        "BeanService.cpp",    ],    header_libs: [    ],    shared_libs: [        "libbeanservice_aidl",        "libbase",        "libui",        "liblog",        "libutils",        "libbinder",        "libcutils",    ],    static_libs: [    ],    include_dirs: [    ],    export_shared_lib_headers: [        "libbinder",        "libbeanservice_aidl",    ],    export_include_dirs: ["."],    cflags: [        "-Wall",        "-Wextra",        "-Werror",        "-Wno-ignored-qualifiers",    ],}cc_library_shared {    name: "libbeanservice_aidl",    aidl: {        export_aidl_headers: true,        local_include_dirs: ["aidl"],        include_dirs: [        ],    },    srcs: [        ":beanservice_aidl",    ],    shared_libs: [        "libbase",        "libcutils",        "libutils",        "liblog",        "libbinder",        "libgui",    ],    cflags: [        "-Werror",        "-Wall",        "-Wextra",    ],}filegroup {    name: "beanservice_aidl",    srcs: [        "aidl/com/zzh/IBeanService.aidl",    ],    path: "aidl",}
  1. 注册服务
// vendor/zzh/native-service/bean-server/main_beanserver.cpp#define LOG_TAG "beanserver"//#define LOG_NDEBUG 0#include "BeanService.h"#include using namespace android;int main(int argc __unused, char** argv __unused){    ALOGD(" beamserver start...");    signal(SIGPIPE, SIG_IGN);    // Set 5 threads for HIDL calls. Now cameraserver will serve HIDL calls in    // addition to consuming them from the Camera HAL as well.    hardware::configurerpcThreadpool(5,  false);    sp<ProcessState> proc(ProcessState::self());    sp<IServiceManager> sm = defaultServiceManager();    ALOGI("ServiceManager: %p", sm.get());    BeanService::instantiate();    ALOGI("ServiceManager: %p done instantiate", sm.get());    ProcessState::self()->startThreadPool();    IPCThreadState::self()->joinThreadPool();}

BeanService::instantiate()这个函数会调用父类BinderService的方法,最终会调用publish方法进行服务注册。

template<typename SERVICE>class BinderService{public:// 进行服务注册的方法,被instantiate()调用    static status_t publish(bool allowIsolated = false,int dumpFlags = IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT) {        sp<IServiceManager> sm(defaultServiceManager());        // 这里进行服务注册,SERVICE::getServiceName()这个就是BeanService重写的方法,返回"bean.like"        return sm->addService(String16(SERVICE::getServiceName()), new SERVICE(), allowIsolated,  dumpFlags);    }    static void publishAndJoinThreadPool(            bool allowIsolated = false,            int dumpFlags = IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT) {        publish(allowIsolated, dumpFlags);        joinThreadPool();    }// 调用publish()方法    static void instantiate() { publish(); }    static status_t shutdown() { return NO_ERROR; }   ...}
  1. 编译后启动模拟器,看日志:

接下来根据提示配置selinux权限
adb pull /sys/fs/selinux/policy
adb logcat -b events -d | audit2allow -p policy
输出信息如下:

#============= beanserver ==============allow beanserver servicemanager:binder call;#============= hal_audio_caremu ==============allow hal_audio_caremu audiOServer:fifo_file write;allow hal_audio_caremu default_prop:file read;#============= init ==============allow init vendor_toolbox_exec:file execute_no_trans;

我们将规则加入beanserver.te即可:
selinux一般情况下并不能一次加完,每次都是添加了log中所报的权限后,再次运行时又会有新的权限错误,我们按照上面的方法逐一添加即可,最终的te文件如下:

// vendor/zzh/sepolicy/vendor/beanserver.te# beanservertype beanserver, domain, coredomain;type beanserver_exec, exec_type, file_type, system_file_type;init_daemon_domain(beanserver)allow beanserver servicemanager:binder { call transfer };allow beanserver beanserver_service:service_manager add;

除了上述修改外,还需要做如下修改:

// vendor/zzh/sepolicy/public/service.tetype beanserver_service,       service_manager_type;
// vendor/zzh/sepolicy/private/service_contextsbean.like u:object_r:beanserver_service:s0
// device/generic/car/emulator/aosp_car_emulator.mkSYSTEM_EXT_PUBLIC_SEPOLICY_DIRS += vendor/zzh/sepolicy/publicSYSTEM_EXT_PRIVATE_SEPOLICY_DIRS += vendor/zzh/sepolicy/private

简单解释一下,我们需要为定义的服务定义一个标签,ServiceManager执行addService操作时会进行检查,如果不定义标签的话会使用默认的default_android_service,但Selinux是不允许以这个标签add service的。

// system/sepolicy/public/domain.te# Do not allow service_manager add for default service labels.# Instead domains should use a more specific type such as# system_app_service rather than the generic type.# New service_types are defined in {,hw,vnd}service.te and new mappings# from service name to service_type are defined in {,hw,vnd}service_contexts.neverallow * default_android_service:service_manager *;neverallow * default_android_vndservice:service_manager *;neverallow * default_android_hwservice:hwservice_manager *;

再次编译启动模拟器:


到此,我们的服务已经成功的添加到ServiceManager中。

  1. Client通过binder访问服务

我们写一个demo进行访问服务
测试文件:

// vendor/zzh/native-service/bean-server/client/BeanServer_client_test.cpp#define LOG_TAG "beanclient"//#define LOG_NDEBUG 0#include #include using namespace android;using com::zzh::IBeanService;int main(int argc __unused, char** argv __unused) {    ALOGD(" beanclient start...");      // 先获取IServiceManager    sp<IServiceManager> sm = defaultServiceManager();     sp<IBinder> binder;    do {    // 通过服务名称拿到代理对象        binder = sm->getService(String16("bean.like"));        if (binder != 0) {            break;        }        usleep(500000); // 0.5s    } while (true);// 转换成IBeanService    sp<IBeanService>   beanService = interface_cast<IBeanService>(binder);    // 通过代理调用服务端函数    beanService->sayHello();        return 0;}

Android.bp

// vendor/zzh/native-service/bean-server/client/Android.bpcc_binary {    name: "beanserver_client",    srcs: ["BeanServer_client_test.cpp"],    cflags: [        "-Wall",        "-Wextra",        "-Werror",        "-Wno-unused-parameter",    ],    compile_multilib: "first",    shared_libs: [        "libbeanservice_aidl",        "libbase",        "libcutils",        "libutils",        "liblog",        "libbinder",        "libgui",    ],    include_dirs: ["."],}

编译到系统目录

// device/generic/car/emulator/aosp_car_emulator.mkPRODUCT_PACKAGES += beanserver_client

也可以不加上面的编译选项,直接make beanserver_client,然后将生成的产物adb push到系统目录也可以。

启动模拟器,在终端执行beanserver_client,查看日志:

可以看出是不允许shell访问beanserver服务,这里在实际业务中要对client端配置selinux,由于我们这里是测试,就不赘述了。直接setenforce 0进行测试

susetenforce  0

然后再重新执行beanservere_client

调用成功!

为了方便大家清晰的理解代码结果,下面列出了本例中的代码目录:

zzh@ubuntu:~/work/android/aosp/android-13.0.0_r35/vendor/zzh$ tree.├── native-service│   └── bean-server│       ├── Android.bp│       ├── beanserver.rc│       ├── client│       │   ├── Android.bp│       │   └── BeanServer_client_test.cpp│       ├── libbeanservice│       │   ├── aidl│       │   │   └── com│       │   │       └── zzh│       │   │           └── IBeanService.aidl│       │   ├── Android.bp│       │   ├── BeanService.cpp│       │   └── BeanService.h│       └── main_beanserver.cpp└── sepolicy    ├── private    │   └── service_contexts    ├── public    │   └── service.te    └── vendor        ├── beanserver.te        └── file_contexts11 directories, 13 files

上述文件我已经整理好,大家想要的话可以关注我的微信公众号无限无羡,回复"nt" 即可获取,获取后只需修改部分名称,添加编译选项到自己项目的配置文件即可。

来源地址:https://blog.csdn.net/weixin_41678668/article/details/129828093

--结束END--

本文标题: Android 13添加自定义native服务

本文链接: https://lsjlt.com/news/375623.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Android 13添加自定义native服务
    欢迎加入我的知识星球Android系统开发指南 欢迎关注微信公众号 无限无羡 欢迎关注知乎账号 无限无羡 文章目录 native服务添加selinux权限配置通过binder访问服务 native服务添加 native服务就是用...
    99+
    2023-08-19
    android java 开发语言
  • Android自定义Notification添加点击事件
    前言在上一篇文章中《Notification自定义界面》中我们实现了自定义的界面,那么我们该怎么为自定义的界面添加点击事件呢?像酷狗在通知栏 有“上一首”,“下一首”等控制按钮,我们需要对按钮的点击事件进行响应,不过方法和之前的点击设置不一...
    99+
    2023-05-30
  • android为自定义CompoundButton添加涟漪效果
    要为自定义的CompoundButton添加涟漪效果,可以按照以下步骤进行:1. 创建一个新的drawable文件ripple_ef...
    99+
    2023-09-16
    android
  • android 12.0 添加自定义系统服务接口给app调用
    1.前言 0 定制化开发中,在app需要调用系统层的一些功能的时候,由于受限于系统api权限或者某些api被隐藏了,所以需要添加自定义系统服务给app调用 首先要自定义服务 然后给app调用就好 2.自定义服务功能实现 添加自定义系统服务A...
    99+
    2023-09-12
    android system systemserver 自定义服务 添加自定义服务
  • CentOS 7怎么添加自定义系统服务
    小编给大家分享一下CentOS 7怎么添加自定义系统服务,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!Centos7开机启动项分为system和user两种类型 ...
    99+
    2023-06-10
  • Android 自定义输入手机号自动添加分隔符
    比较简单的一个控件,就是加些逻辑处理而已,以前貌似是直接监听的,封装起来方便点public class AccountTxtView extends android.support.v7.widget.AppCompatEditText {...
    99+
    2023-05-31
    android 分隔符 roi
  • CentOS7 systemd添加自定义系统服务的方法
    systemd: CentOS 7的服务systemctl脚本存放在:/usr/lib/systemd/,有系统(system)和用户(user)之分,即:/usr/lib/systemd/system ,/usr/l...
    99+
    2022-06-04
    CentOS7 systemd 系统服务 CentOS7 添加自定义系统服务
  • Android Studio如何为Activity添加自定义注解信息
    普通Java-Kotlin类添加注释 添加类时注释作者信息和日期时间 依次打开 File—>Settings—>editor—>File and Code Tem...
    99+
    2024-04-02
  • Android 13.0 添加自定义服务,并生成jar给第三方app调用
    1.概述 在13.0系统产品定制化开发中,由于需要新增加自定义的功能,所以要增加自定义服务,而app上层通过调用自定义服务,来调用相应的功能,所以系统需要先生成jar,然后生成jar 给上层app调用,接下来就来分析实现的步骤,然后来实现相...
    99+
    2023-10-21
    android jar java 生成jar包 系统源码生成jar包
  • win8.1怎么添加自定义磁贴?
    在METRO界面添加自定义快捷方式磁贴 1、创建需要磁贴的自定义快捷方式 2、进入METRO应用程序界面,右击一个“记事本”应用程序,点击“打开文件位置” ...
    99+
    2022-06-04
    自定义
  • android自定义加减按钮
    本文实例为大家分享了android自定义加减按钮的具体代码,供大家参考,具体内容如下 1、定义两个shape: my_button_shape_normal.xml: <...
    99+
    2022-06-06
    按钮 Android
  • C#为控件添加自定义事件及自定义触发
    先随便搞个事件吧 public class TestEventrgs : EventArgs { private string _name; ...
    99+
    2024-04-02
  • Android中怎么通过自定义ImageView添加文字说明
    本篇文章为大家展示了Android中怎么通过自定义ImageView添加文字说明,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。MyImageTextViewNew.javapublic c...
    99+
    2023-05-30
    android imageview
  • 如何在Android应用中添加一个自定义弹框
    如何在Android应用中添加一个自定义弹框?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。实现步骤:1.xml布局实现<&#63;xml vers...
    99+
    2023-05-31
    android roi
  • SCWS中怎么添加自定义词典
    本篇文章为大家展示了SCWS中怎么添加自定义词典,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。require_once __DIR__.'/fun...
    99+
    2024-04-02
  • 怎么为Repository添加自定义方法
    这篇文章主要介绍“怎么为Repository添加自定义方法”,在日常操作中,相信很多人在怎么为Repository添加自定义方法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”怎么为Repository添加自定...
    99+
    2023-06-25
  • 如何为Repository添加自定义方法
    目录为Repository添加自定义方法一、为某个Repository添加自定义方法二、添加全局Repository继承jpa Repository 写自定义方法查询首先定义实体类是...
    99+
    2024-04-02
  • Hive中怎么添加自定义函数
    这篇文章主要讲解了“Hive中怎么添加自定义函数”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Hive中怎么添加自定义函数”吧!环境介绍:CentOS7+hive-1.1.0-cdh6.7....
    99+
    2023-06-03
  • jQuery中怎么添加自定义方法
    本文小编为大家详细介绍“jQuery中怎么添加自定义方法”,内容详细,步骤清晰,细节处理妥当,希望这篇“jQuery中怎么添加自定义方法”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。一、什么是jQuery自定义方...
    99+
    2023-07-06
  • Android自定义ProgressDialog加载图片
    为了提高用户体验,我们肯定希望该Dialog能更加炫酷,让用户看着更舒服。那如何做呢,当然是我们自己定义一个ProgressDialog了。 一、使用系统加载框 mDia...
    99+
    2022-06-06
    图片 progressdialog Android
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作