AIDL使用及原理介绍

news2024/11/26 2:52:39

一、AIDL使用及理解

介绍参考官方文档:https://developer.android.com/develop/background-work/services/aidl?hl=zh-cn

1.1、aidl文件的产物

1.1.1 aidl文件

interface IDownloadIpc {
    //开始下载
    DownloadResponse start(in DownloadInfoParcel downloadInfo);
    //暂停下载
    DownloadResponse pause(in DownloadInfoParcel downloadInfo);
    //取消下载
    DownloadResponse cancel(in DownloadInfoParcel downloadInfo);
    //获取下载信息, DownloadInfo可以转换成ResourceDto
    List<DownloadInfoParcel> getDownLoadInfo();
    //注册下载监听回调
    void registerDownloadCallback(IDownloadIpcCallback callback);
    //反注册下载监听回调
    void unregisterDownloadCallback(IDownloadIpcCallback callback);
    //中心cta是否通过
    boolean isCtaPass();
}

1.1.2 aidl文件编译后后文件

在这里插入图片描述

分为三部分
Default实现
Stub实现
接口定义

1.2、线程及时序问题

1.2.1 client与server的一次连接中,任务是顺序执行的

/**
 * binder_proc_transaction() - sends a transaction to a process and wakes it up
 * @t:      transaction to send
 * @proc:   process to send the transaction to
 * @thread: thread in @proc to send the transaction to (may be NULL)
 */
static bool binder_proc_transaction(struct binder_transaction *t,
                    struct binder_proc *proc,
                    struct binder_thread *thread)
{
    //找到Server端的对应Binder服务在Binder驱动中对应的对象binder_node
    struct binder_node *node = t->buffer->target_node;
    //判断这次Binder调用是不是oneway
    bool oneway = !!(t->flags & TF_ONE_WAY);
    //初始化为false,用于标记当前Server端的对应Binder服务是否正在执行oneway的方法
    bool pending_async = false;
 
    binder_node_lock(node);
    //oneway == true
    if (oneway) {
        if (node->has_async_transaction) {
            //发现对应Binder服务正在执行oneway的方法,设置pending_async为true
            pending_async = true;
        } else {
            //发现对应Binder服务没有执行oneway的方法,设置has_async_transaction为1
            node->has_async_transaction = 1;
        }
    }
 
    binder_inner_proc_lock(proc);
 
    //oneway的调用thread为空,第1次oneway调用,pending_async为false
    if (!thread && !pending_async)
        thread = binder_select_thread_ilocked(proc);
 
    if (thread) {
        binder_enqueue_thread_work_ilocked(thread, &t->work);
    } else if (!pending_async) {
        binder_enqueue_work_ilocked(&t->work, &proc->todo);
    } else {
        //这次Binder work放到Binder Node的async_todo队列中,不会立刻执行
        binder_enqueue_work_ilocked(&t->work, &node->async_todo);
    }
 
    if (!pending_async)
        //需要唤醒thread执行工作队列中的Binder work
        binder_wakeup_thread_ilocked(proc, thread, !oneway /* sync */);
 
    binder_inner_proc_unlock(proc);
    binder_node_unlock(node);
 
    return true;
}

可以看到这里有两把锁:binder_inner_proc_lock和binder_node_lock,对于一次连接,服务端都有唯一的binder_node,由于binder_node_lock的存在,所以通信是顺序执行的。

1.2.2 Binder线程池

参考:进程的Binder线程池工作过程
默认地,每个进程的binder线程池的线程个数上限为15。
Binder设计架构中,只有第一个Binder主线程(也就是Binder_1线程)是由应用程序主动创建,Binder线程池的普通线程都是由Binder驱动根据IPC通信需求创建

1.3、Binder通信的最大容量限制

参考:一次Binder通信最大可以传输多大的数据?
一次Binder通信最大可以传输是1MB-8KB(PS:8k是两个pagesize,一个pagesize是申请物理内存的最小单元)

#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)//这里的限制是1MB-4KB*2
ProcessState::ProcessState(const char *driver)
{
    if (mDriverFD >= 0) {
        // mmap the binder, providing a chunk of virtual address space to receive transactions.
        // 调用mmap接口向Binder驱动中申请内核空间的内存
        mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
        if (mVMStart == MAP_FAILED) {
            // *sigh*
            ALOGE("Using %s failed: unable to mmap transaction memory.\n", mDriverName.c_str());
            close(mDriverFD);
            mDriverFD = -1;
            mDriverName.clear();
        }
    }
}

1.4、oneway 以及in、out、inout参数的理解

参考: https://blog.csdn.net/anlian523/article/details/98476033
oneway: 该关键字修饰的方法在服务端是异步执行的,往往没有返回值
in:客户端传递给服务端的参数,服务端需要获取参数的具体内容
out:客户端不用将参数的具体内容传递给服务端,服务端需要返回具体内容,客户端收到该内容后填充到参数中。
inout:in和out的融合

二、binder通信过程

2.1、获取远程Binder

Server提供服务往往以Service的形式,在onBind是将提供的服务以Binder形式返回给Client端:

override fun onBind(intent: Intent?): IBinder {
    LogUtility.i(TAG, "onBind")
    return downloadBinder
}

private val downloadBinder: Binder = object : IDownloadIpc.Stub() {
    override fun start(downloadInfo: DownloadInfoParcel?): DownloadResponse {
        val ctaPass = AppUtil.isCtaPass()
        LogUtility.w(TAG, "start ctaPass: $ctaPass, downloadInfo: $downloadInfo")
        if (!ctaPass) {
            return DownloadResponse(Constants.DOWNLOAD_FAILE_NO_CTA, "cta not pass")
        }
        (downloadProxy.getDownloadInfo(downloadInfo?.pkgName) as? LocalDownloadInfo)?.let {
            downloadProxy.download(it)
        }
        return DownloadResponse(0, "success")
    }
    //...
}

可以看到服务端返回的Binder对象实现了IDownloadIpc.Stub的接口,那Client是如何获取到Server的Binder的呢?
在这里插入图片描述

首先是Client发起绑定:

//ContextImpl.java
private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags,
        String instanceName, Handler handler, Executor executor, UserHandle user) {
    。。。
    if (mPackageInfo != null) {
        if (executor != null) {
            sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), executor, flags);
        } else {
            sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);
        }
    }
   
    try {
        。。。
        int res = ActivityManager.getService().bindIsolatedService(
            mMainThread.getApplicationThread(), getActivityToken(), service,
            service.resolveTypeIfNeeded(getContentResolver()),
            sd, flags, instanceName, getOpPackageName(), user.getIdentifier());
        
        return res != 0;
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

sd包装了ServiceConnection 对象,用于服务绑定成功后的回调。ActivityManager.getService()获取到ActivityManagerService,是一次IPC调用。ActivityManagerService将绑定逻辑放在ActiveServices中处理:

//ActiveServices
int bindServiceLocked(IApplicationThread caller, IBinder token, Intent service,
        String resolvedType, final IServiceConnection connection, int flags,
        String instanceName, String callingPackage, final int userId)
        throws TransactionTooLargeException {
    。。。
        if ((flags&Context.BIND_AUTO_CREATE) != 0) {
            s.lastActivity = SystemClock.uptimeMillis();
            needOomAdj = true;
            if (bringUpServiceLocked(s, service.getFlags(), callerFg, false,
                    permissionsReviewRequired, packageFrozen, true) != null) {
                mAm.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_BIND_SERVICE);
                return 0;
            }
        }
        。。。
        
        if (s.app != null && b.intent.received) {
            // Service is already running, so we can immediately
            // publish the connection.
            try {
                c.conn.connected(s.name, b.intent.binder, false);
            } catch (Exception e) {
                Slog.w(TAG, "Failure sending service " + s.shortInstanceName
                        + " to connection " + c.conn.asBinder()
                        + " (in " + c.binding.client.processName + ")", e);
            }

            // If this is the first app connected back to this binding,
            // and the service had previously asked to be told when
            // rebound, then do so.
            if (b.intent.apps.size() == 1 && b.intent.doRebind) {
                requestServiceBindingLocked(s, b.intent, callerFg, true);
            }
        } else if (!b.intent.requested) {
            requestServiceBindingLocked(s, b.intent, callerFg, false);
        }

    }

    return 1;
}
//ActiveServices
private String bringUpServiceLocked(ServiceRecord r, int intentFlags, boolean execInFg,
        boolean whileRestarting, boolean permissionsReviewRequired, boolean packageFrozen,
        boolean enqueueOomAdj)
        throws TransactionTooLargeException {
  。。。
    if (!isolated) {
        app = mAm.getProcessRecordLocked(procName, r.appInfo.uid);
        if (DEBUG_MU) Slog.v(TAG_MU, "bringUpServiceLocked: appInfo.uid=" + r.appInfo.uid
                    + " app=" + app);
        if (app != null) {
            final IApplicationThread thread = app.getThread();
            final int pid = app.getPid();
            final UidRecord uidRecord = app.getUidRecord();
            if (thread != null) {
                try {
                    app.addPackage(r.appInfo.packageName, r.appInfo.longVersionCode,
                            mAm.mProcessStats);
                    realStartServiceLocked(r, app, thread, pid, uidRecord, execInFg,
                            enqueueOomAdj);
                    return null;
                } 
            }
        }
    } 
  。。。
    return null;
}

在bringUpServiceLocked中调用realStartServiceLocked创建Service,创建成功后Service会回调onCreate()。创建完Service后,会调用requestServiceBindingLocked()方法绑定已经创建的服务,并回调ServiceConnection的onServiceConnected(name: ComponentName?, service: IBinder?)方法,将binder回传给Client。

//ActiveServices
private final boolean requestServiceBindingLocked(ServiceRecord r, IntentBindRecord i,
        boolean execInFg, boolean rebind) throws TransactionTooLargeException {
    
    if ((!i.requested || rebind) && i.apps.size() > 0) {
        try {
            bumpServiceExecutingLocked(r, execInFg, "bind",
                    OomAdjuster.OOM_ADJ_REASON_BIND_SERVICE);
            r.app.getThread().scheduleBindService(r, i.intent.getIntent(), rebind,
                    r.app.mState.getReportedProcState());
            if (!rebind) {
                i.requested = true;
            }
            i.hasBound = true;
            i.doRebind = false;
        }
    }
    return true;
}

r.app.getThread()返回IApplicationThread类型对象,IApplicationThread是AIDL生产的对象,因此这里也是IPC通信,最终调用到ApplicationThread的scheduleBindService()方法,最终调用handleBindService()方法:

//ActivityThread.java
public final class ActivityThread extends ClientTransactionHandler
        implements ActivityThreadInternal {
  private class ApplicationThread extends IApplicationThread.Stub {
    public final void scheduleBindService(IBinder token, Intent intent,
        boolean rebind, int processState) {
    updateProcessState(processState, false);
    BindServiceData s = new BindServiceData();
    s.token = token;
    s.intent = intent;
    s.rebind = rebind;
    sendMessage(H.BIND_SERVICE, s);
}
  }
  
private void handleBindService(BindServiceData data) {
    CreateServiceData createData = mServicesData.get(data.token);
    Service s = mServices.get(data.token);
 
    if (s != null) {
        try {
            data.intent.setExtrasClassLoader(s.getClassLoader());
            data.intent.prepareToEnterProcess(isProtectedComponent(createData.info),
                    s.getAttributionSource());
            try {
                if (!data.rebind) {
                    IBinder binder = s.onBind(data.intent);
                    ActivityManager.getService().publishService(
                            data.token, data.intent, binder);
                } else {
                    s.onRebind(data.intent);
                    ActivityManager.getService().serviceDoneExecuting(
                            data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
                }
            } 
        } 
    }
}
}

通过data.rebind判断是否是重新绑定,不是重新绑定则调用Service的onBind()方法,是重新绑定则调用Service的bind()方法。这点通过onBind()方法并获取一个Binder对象,并通过publishService()方法将该binder回调返回给客户端:

//ActivityManagerService
void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {
    final long origId = Binder.clearCallingIdentity();
    try {
        if (r != null) {
            Intent.FilterComparison filter
                    = new Intent.FilterComparison(intent);
            IntentBindRecord b = r.bindings.get(filter);
            if (b != null && !b.received) {
                b.binder = service;
                b.requested = true;
                b.received = true;
                ArrayMap<IBinder, ArrayList<ConnectionRecord>> connections = r.getConnections();
                for (int conni = connections.size() - 1; conni >= 0; conni--) {
                    ArrayList<ConnectionRecord> clist = connections.valueAt(conni);
                    for (int i=0; i<clist.size(); i++) {
                        ConnectionRecord c = clist.get(i);
                        c.conn.connected(r.name, service, false);
                        }
                    }
                }
            }
            serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false, false);
        }
    } finally {
        Binder.restoreCallingIdentity(origId);
    }
}

2.2、客户端调用服务端

参考:https://cloud.tencent.com/developer/article/2360820
在这里插入图片描述

绑定成后onServiceConnected()方法会回调

//CenterDownloadConnectionManager
private val con: ServiceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            downloadService = IDownloadIpc.Stub.asInterface(service)
     }

service是远程服务端的Binder代理对象,实际是一个BinderProxy对象。

//IDownloadIpc
public static com.nearme.gamespace.bridge.download.IDownloadIpc asInterface(android.os.IBinder obj)
{
  if ((obj==null)) {
    return null;
  }
  android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
  if (((iin!=null)&&(iin instanceof com.nearme.gamespace.bridge.download.IDownloadIpc))) {
    return ((com.nearme.gamespace.bridge.download.IDownloadIpc)iin);
  }
  return new com.nearme.gamespace.bridge.download.IDownloadIpc.Stub.Proxy(obj);
}

BinderProxy#queryLocalInterface()方法返回为null,因此会返回一个Proxy对象。

//IDownloadIpc
private static class Proxy implements com.nearme.gamespace.bridge.download.IDownloadIpc
{
  private android.os.IBinder mRemote;
  Proxy(android.os.IBinder remote)
  {
    mRemote = remote;
  }
  @Override public android.os.IBinder asBinder()
  {
    return mRemote;
  }
  public java.lang.String getInterfaceDescriptor()
  {
    return DESCRIPTOR;
  }
  //开始下载

  @Override public com.nearme.gamespace.bridge.download.DownloadResponse start(com.nearme.gamespace.bridge.download.DownloadInfoParcel downloadInfo) throws android.os.RemoteException
  {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    com.nearme.gamespace.bridge.download.DownloadResponse _result;
    try {
      _data.writeInterfaceToken(DESCRIPTOR);
      if ((downloadInfo!=null)) {
        _data.writeInt(1);
        downloadInfo.writeToParcel(_data, 0);
      }
      else {
        _data.writeInt(0);
      }
      boolean _status = mRemote.transact(Stub.TRANSACTION_start, _data, _reply, 0);
      if (!_status && getDefaultImpl() != null) {
        return getDefaultImpl().start(downloadInfo);
      }
      _reply.readException();
      if ((0!=_reply.readInt())) {
        _result = com.nearme.gamespace.bridge.download.DownloadResponse.CREATOR.createFromParcel(_reply);
      }
      else {
        _result = null;
      }
    }
    finally {
      _reply.recycle();
      _data.recycle();
    }
    return _result;
  }

这里的mRemote是一个BinderProxy对象,其transact()方法会通过JNI调用到Binder驱动层,然后调用的Server端的DownloadIpcService,调用Binder的transact()方法:

//Binder
public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply,
        int flags) throws RemoteException {
    if (false) Log.v("Binder", "Transact: " + code + " to " + this);

    if (data != null) {
        data.setDataPosition(0);
    }
    boolean r = onTransact(code, data, reply, flags);
    if (reply != null) {
        reply.setDataPosition(0);
    }
    return r;
}

Stud继承了Binder并重写了onTransact()方法:

public static abstract class Stub extends android.os.Binder implements com.nearme.gamespace.bridge.download.IDownloadIpc
{
  private static final java.lang.String DESCRIPTOR = "com.nearme.gamespace.bridge.download.IDownloadIpc";
  
  
  @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
  {
    java.lang.String descriptor = DESCRIPTOR;
    switch (code)
    {
      case TRANSACTION_start:
      {
        data.enforceInterface(descriptor);
        com.nearme.gamespace.bridge.download.DownloadInfoParcel _arg0;
        if ((0!=data.readInt())) {
          _arg0 = com.nearme.gamespace.bridge.download.DownloadInfoParcel.CREATOR.createFromParcel(data);
        }
        else {
          _arg0 = null;
        }
        com.nearme.gamespace.bridge.download.DownloadResponse _result = this.start(_arg0);
        reply.writeNoException();
        if ((_result!=null)) {
          reply.writeInt(1);
          _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
        }
        else {
          reply.writeInt(0);
        }
        return true;
      }
      
}

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

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

相关文章

【数据结构与算法】A*算法——自动寻路

这里写目录标题 一.为什么用A*算法二.A*算法的实现原理三.A*算法的实现1.初始化地图2.格子初始化3.两个列表4.起点到终点的路径5.起点到终点的最佳路径★6.资源的释放 四.完整代码1.Astar.h2.Astar.cpp3.main.cpp4.运行结果 一.为什么用A*算法 上节课我们已经讲了最短路径算法…

【Qt】内置对话框

一.Qt内置对话框 Qt 提供了多种可复⽤的对话框类型&#xff0c;即 Qt 标准对话框。Qt标准对话框全部继承于QDialog类。常⽤标准对话框如下&#xff1a; 二.内置对话框分类 1.消息对话框 QMessageBox 1.1 概念 消息对话框是应⽤程序中最常⽤的界⾯元素。消息对话框主要⽤于为…

SpringBoot集成微信小程序(二)【登录、获取头像昵称及手机号】

一、背景 小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识&#xff0c;快速建立小程序内的用户体系。 微信小程序官方文档&#xff1a;https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 二、技术栈 SpringBoot…

RFID在晶圆搬运机中的智能化实践

RFID在晶圆搬运机中的智能化实践 应用背景 在半导体制造行业&#xff0c;晶圆搬运是一个至关重要的环节&#xff0c;它不仅影响生产效率&#xff0c;还直接关系到产品的质量和成本。在如今的多品种少量生产中&#xff0c;要保障生产效率和品质&#xff0c;工序管理至关重要。…

Python环境安装及PIP安装(Mac OS版)

官网 https://www.python.org/downloads/ 安装python python-3.12.1-macos11.pkg下载后&#xff0c;安装一直下一步即可 验证是否安装成功&#xff0c;执行python3命令和pip3命令 配置环境变量 获取python3安装位置并配置在.bash_profile #查看python路径 which python3#…

卫星图像检测,分割,跟踪,超分辨率,数据集调研

卫星图像检测&#xff0c;分割&#xff0c;跟踪&#xff0c;超分辨率&#xff0c;数据集调研 超分辨率Image super-resolution: A comprehensive review, recent trends, challenges and applicationsA Review of GAN-Based Super-Resolution Reconstruction for Optical Remot…

ppt模板免费网站有哪些?自动美化工具推荐

新的8月&#xff0c;是时候以全新面貌迎接高效办公挑战了&#xff01; 想要你的PPT演示脱颖而出&#xff0c;却苦于找不到精美又免费的模板&#xff1f; 别担心&#xff0c;今天我来告诉你们&#xff1a;哪个软件有精美免费ppt模板&#xff1f; 今天我为你们精心汇总了6款PPT…

二维高斯拟合20240815

二维高斯拟合 高斯函数表达式 二维高斯函数是一个在二维空间中用来表示高斯分布的函数&#xff0c;常用于统计学、图像处理和机器学习等领域。其数学表达式通常为&#xff1a; f ( x , y ) 1 2 π σ x σ y 1 − ρ 2 exp ⁡ ( − 1 2 ( 1 − ρ 2 ) ( ( x − μ x ) 2 σ …

面向未来的算力网络连接技术架构

面向未来的算力网络连接发展特点与实践 物理层网络基础设施是算力网络的“最后一公里”&#xff0c;光纤光缆等物理层网络基础设施的发展有助于保障算力网络基础设施的稳健发展。物理层核心技术发生了巨大变化&#xff0c;在交换、传输等方面不断更新迭代&#xff0c;当前铜缆的…

【云原生】看一眼haproxy,秒变技术大佬(理论篇)

一、负载均衡 1、什么是负载均衡&#xff1f; 负载均衡&#xff1a;Load Balance&#xff0c;简称LB&#xff0c;是一种服务或基于硬件设备等实现的高可用反向代理技术&#xff0c;负载均衡将特定的业务(网络服务、网络流量等)分担给指定的一个或多个后端特定的服务器或设备&…

Secure CRT 9.x版本高亮着色配置文件

Secure CRT的网络配置文件高亮显示&#xff0c;还在完善&#xff0c;逐渐适配不同厂商 设备名字自动蓝色高亮显示设备接口名高亮显示IPv4地址、IPv6地址、MAC地址高亮显示掩码、反掩码高亮显示设备SN号高亮显示接口状态、设备状态等高亮显示各路由协议高亮显示 【下载地址】效果…

基于Spring Boot的疗养院管理系统的设计与实现

TOC springboot234基于Spring Boot的疗养院管理系统的设计与实现 第1章 绪论 1.1选题动因 当前的网络技术&#xff0c;软件技术等都具备成熟的理论基础&#xff0c;市场上也出现各种技术开发的软件&#xff0c;这些软件都被用于各个领域&#xff0c;包括生活和工作的领域。…

输出Docker容器的启动命令行脚本

当Docker容器启动后&#xff0c;如果忘记启动参数&#xff0c;比如目录挂载、端口映射等&#xff0c;可以通过Portainer等容器管理工具查看。但是&#xff0c;有时希望能获取容器启动的命令行&#xff0c;因为需要再启动一个类似容器&#xff0c;怎么办呢&#xff1f; 有一款工…

【Python养成】:输出两个等腰三角形且平行方向

项目场景&#xff1a; 遇到一个小小的高中小程序题目&#xff0c;有趣就做 了一下&#xff0c;供大家参考。 问题描述 要求使用python编程&#xff0c;实现如下图形。 原因分析&#xff1a; 其实就是 找规律和找空格。先左边后右边。具体解释可以找AI。太简单了没必要亲自讲解…

Linux 7 x86平台上安装达梦8数据库

1、环境描述 2、安装前准备 2.1 操作系统信息调研 Linux平台需要通过命令查看操作系统版本、位数、磁盘空间、内存等信息。 CPU信息 [rootray1 ~]# cat /proc/cpuinfo | grep -E "physical id|core id|cpu cores|siblings|cpu MHz|model name|cache size"|tail -n…

【PY模型训练】最终测试

Anacoda3 安装 1. conda -V 2.conda config --add channels https://mirrors.ustc.edu.cn/anaconda/pkgs/main/ 3.打开.condarc文件,将下面内容直接放入.condarc文件里面 channels: - conda-forge - defaults show_channel_urls: true default_ch…

【Qt】 QWidget的cursor属性

QWidget的cursor属性 cursor属性可以设置当鼠标指向不同控件&#xff0c;不同窗口会产生不同的效果。 API说明 cursor() 获取到当前 widget 的 cursor 属性, 返回 QCursor 对象. 当⿏标悬停在该 widget 上时, 就会显⽰出对应的形状. setCursor(const QCursor& cursor) …

CSS——less

一、less简介 1、less是一门css的预处理语言。是一个css的增强版&#xff0c;通过less可以编写更少的代码实现更强大的样式。 2、css原先也只支持变量的设置。 变量的设置可以便捷更改一样的样式&#xff0c;其中&#xff0c;calc&#xff08;&#xff09;是计算函数。 3、在le…

【6大设计原则】精通设计模式之里氏代换原则:从理论到实践,掌握代码演化的黄金法则

一、引言 1.1 设计模式的必要性 在软件开发的复杂性面前&#xff0c;设计模式提供了一套成熟的解决方案&#xff0c;它们是经过多年实践总结出来的&#xff0c;能够帮助我们应对各种编程难题。设计模式不仅仅是一种编程技巧&#xff0c;更是一种编程哲学&#xff0c;它能够提…