我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android 13添加自定义native服务

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android 13添加自定义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",    class="lazy" data-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: [        ],    },    class="lazy" data-srcs: [        ":beanservice_aidl",    ],    shared_libs: [        "libbase",        "libcutils",        "libutils",        "liblog",        "libbinder",        "libgui",    ],    cflags: [        "-Werror",        "-Wall",        "-Wextra",    ],}filegroup {    name: "beanservice_aidl",    class="lazy" data-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",    class="lazy" data-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: [        ],    },    class="lazy" data-srcs: [        ":beanservice_aidl",    ],    shared_libs: [        "libbase",        "libcutils",        "libutils",        "liblog",        "libbinder",        "libgui",    ],    cflags: [        "-Werror",        "-Wall",        "-Wextra",    ],}filegroup {    name: "beanservice_aidl",    class="lazy" data-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",    class="lazy" data-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

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android 13添加自定义native服务

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android 13添加自定义native服务

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

Android自定义Notification添加点击事件

前言在上一篇文章中《Notification自定义界面》中我们实现了自定义的界面,那么我们该怎么为自定义的界面添加点击事件呢?像酷狗在通知栏 有“上一首”,“下一首”等控制按钮,我们需要对按钮的点击事件进行响应,不过方法和之前的点击设置不一
2023-05-30

在React Native中添加自定义字体的方法详解

ReactNative添加自定义字体指南:在ReactNative中添加自定义字体至关重要,允许您使用符合品牌特色的唯一字体。步骤:获取字体文件(.ttf或.otf)。将文件添加到项目assets/fonts文件夹。创建字体常量,引用文件路径。使用useFonts钩子加载字体。在样式中使用字体名称应用。考虑因素:平台支持:确保字体适用于目标平台。许可证:检查字体许可限制。性能:仅加载必需字体,避免影响性能。命名约定:使用一致的命名约定。可扩展性:后续添加字体时,按照相同步骤更新字体常量。
在React Native中添加自定义字体的方法详解
2024-04-02

android为自定义CompoundButton添加涟漪效果

要为自定义的CompoundButton添加涟漪效果,可以按照以下步骤进行:1. 创建一个新的drawable文件ripple_effect.xml,用于定义涟漪效果的样式和颜色:```android:color="@color/rippl
2023-09-16

CentOS 7怎么添加自定义系统服务

小编给大家分享一下CentOS 7怎么添加自定义系统服务,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!Centos7开机启动项分为system和user两种类型
2023-06-10

Android 自定义输入手机号自动添加分隔符

比较简单的一个控件,就是加些逻辑处理而已,以前貌似是直接监听的,封装起来方便点public class AccountTxtView extends android.support.v7.widget.AppCompatEditText {
2023-05-31

CentOS7 systemd添加自定义系统服务的方法

systemd:CentOS 7的服务systemctl脚本存放在:/usr/lib/systemd/,有系统(system)和用户(user)之分,即:/usr/lib/systemd/system ,/usr/lib/systemd/u
2022-06-04

Android中怎么通过自定义ImageView添加文字说明

本篇文章为大家展示了Android中怎么通过自定义ImageView添加文字说明,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。MyImageTextViewNew.javapublic class
2023-05-30

如何在Android应用中添加一个自定义弹框

如何在Android应用中添加一个自定义弹框?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。实现步骤:1.xml布局实现
2023-05-31

Android 中怎么在有序广播中添加自定义权限

这篇文章给大家介绍Android 中怎么在有序广播中添加自定义权限,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。Android 中在有序广播中添加自定义权限的实例有序广播说明:有序广播因为要处理消息的处理结果,所以要复
2023-05-30

Android动画效果之自定义ViewGroup添加布局动画(五)

前言:前面几篇文章介绍了补间动画、逐帧动画、属性动画,大部分都是针对View来实现的动画,那么该如何为了一个ViewGroup添加动画呢?今天结合自定义ViewGroup来学习一下布局动画。本文将通过对自定义图片选择控件设置动画为例来学习布
2022-06-06

Android自定义ImageView实现在图片上添加图层效果

首先我们先看下效果图实现思路 这是两张前后对比图,右边第二张图里面的已抢光标签图片当已经没有商品的时候就会显示了,在每个图片的中心位置,第一想法是在ImageView的外层再套一层RelativeLayout 实现方法
2022-06-06

android自定义RadioGroup可以添加多种布局的实现方法

android自带的RadioGroup是继承自LinearLayout,如果布局的时候不是直接写radiobutton,即radiobutton外面还包了一层容器,这时分组是不成功的,因为查找不到radiobutton,如果要实现这种效果
2022-06-06

怎么自行给指定的SAP OData服务添加自定义日志记录功能

这篇文章给大家分享的是有关怎么自行给指定的SAP OData服务添加自定义日志记录功能的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。有的时候,SAP标准的OData实现或者相关的工具没有提供我们想记录的日志功能,
2023-06-04

Windows任务栏快捷菜单列表怎自定义添加?

将桌面众多的软件快捷方式添加到任务栏文件夹中编程客栈,既可以让桌面整洁,又可以让方便找出要使用的软件。 编程客栈 1、在任意位置新建文件夹,本经验在D盘新建名为“shortcut”的文件夹。编程2、将桌面快捷方式剪切
2023-06-10

Android中如何实现自定义ImageView添加文字设置按下效果

这篇文章主要为大家展示了“Android中如何实现自定义ImageView添加文字设置按下效果”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Android中如何实现自定义ImageView添加文
2023-05-30

Android怎么实现在ServiceManager中加入自定义服务的方法

这篇文章主要介绍了Android怎么实现在ServiceManager中加入自定义服务的方法,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。具体如下:当我们要使用android
2023-05-30

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录