当前位置: 首页 > news >正文

展开说说:Android服务之bindService解析

前面两篇文章我们分别总结了Android四种Service的基本使用以及源码层面总结一下startService的执行过程,本篇继续从源码层面总结bindService的执行过程。

本文依然按着是什么?有什么?怎么用?啥原理?的步骤来分析。

bindService使用方法和调用流程都与startService时有很多相似之处,方便的话请先阅读上一篇《展开说说:Android服务之startService解析》。

  1. 是什么

调用bindService()来创建,调用方可以通过一个IBinder接口和service进行通信,需要通过ServiceConnection建立连接多用于有交互的场景。

只能调用方通过unbindService()方法来断开连接。调用方可以和Service通讯,并且一个service可以同时和多个调用方存在绑定关系解除绑定也需要所有调用全部解除绑定之后系统会销毁service

2、有什么

Service和Activity一样也有自己的生命周期,也需要在AndroidManifest.xml中注册。另外bindService的使用比startService要复杂一些:第一需要中创建一个Binder子类并定义方法来给使用者调用在onBind方法中返回它的实例;第二使用者需要创建ServiceConnection对象,并在onServiceConnected回调方法调用Binder子类中定义方法。

2.1 在AndroidManifest.xml中注册

和startService注册流程一样:

 <service android:name="com.example.testdemo.service.ServiceJia" />

2.2 bindService时Service的生命周期

与startService时执行的生命周期有些不同。

onCreate   

它只在Service刚被创建的时刻被调用,Service在运行中,这个方法将不会被调用。也就是只有经历过onDestroy生命周期以后再次。

onBind

当另一个组件调用 bindService()想要与Service绑定(例如执行 RPC)时执行,在此方法的实现中,必须通过返回 IBinder 提供一个接口,供客户端用来与服务通信。您必须始终实现此方法;但是,如果您不想允许绑定,则应返回 null。这个方法默认时返回null。

onUnbind

调用方调用 unbindService() 来解除Service绑定时执行

onDestroy

所有绑定到Service的调用方都解绑以后,则系统会销毁该服务。

onRebind

当Service中的onUnbind方法返回true,并且Service调用unbindService之后并没有销毁,此时重新绑定时将会触发onRebind。Service执行过onBindonUnbind返回true并且还没执行onDestroy,等再次bindService就会执行它。

日志打印

bindService
2024-12-01 11:19:14.434 30802-30802/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: onCreate:


2023-12-01 11:19:14.436 30802-30802/com.example.testdemo3 E/com.example.testdemo3.activity.ServiceActivity: onServiceConnected:


2023-12-01 11:19:14.436 30802-30802/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: JiaBinder  --doSomething: start conncetion    //这里不是生命周期,是binder对象调用binder内方法的打印证明完成交互


unbindService
2023-12-01 11:21:10.705 12765-12765/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: onUnbind:


2023-12-01 11:21:10.705 12765-12765/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: onDestroy:

  1. 怎么用

因为是有交互的嘛,因此肯定比那些启动以后就成了甩手掌柜的startService使用稍微负责一些,第一需要中创建一个Binder子类并定义方法来给使用者调用在onBind方法中返回它的实例;第二使用者需要创建ServiceConnection对象,并在onServiceConnected回调方法调用Binder子类中定义方法。

具体可以参考前面的《展开说说:Android四大组件之Service使用》已经总结了使用方法,这里不在赘述。

  1. 啥原理,SDK版本API 30

bindService调用流程都与startService时有很多相似之处,方便的话请先阅读上一篇《展开说说:Android服务之startService解析》。

bindService的启动方法是调用

Intent serviceIntent = new Intent(ServiceActivity.this, ServiceJia.class);
bindService(serviceIntent,serviceConnection, Context.BIND_AUTO_CREATE)

然后我们顺着bindService方法开始解析源码,Go :

4.1 从ContexWrapper的bindService开始,同startService:

@Override
public boolean bindService(Intent service, ServiceConnection conn,int flags) {return mBase.bindService(service, conn, flags);
}

4.2 ContextImpl类bindService

mBase的类型是Context,但实际代码逻辑是在它的实现类ContextImpl类。

 @Overridepublic boolean bindService(Intent service, ServiceConnection conn, int flags) {warnIfCallingFromSystemProcess();return bindServiceCommon(service, conn, flags, null, mMainThread.getHandler(), null,getUser());
}private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags,String instanceName, Handler handler, Executor executor, UserHandle user) {// Keep this in sync with DevicePolicyManager.bindDeviceAdminServiceAsUser.IServiceConnection sd;if (conn == null) {throw new IllegalArgumentException("connection is null");}if (handler != null && executor != null) {throw new IllegalArgumentException("Handler and Executor both supplied");}if (mPackageInfo != null) {if (executor != null) {sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), executor, flags);} else {sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);}} else {throw new RuntimeException("Not supported in system context");}validateServiceIntent(service);try {IBinder token = getActivityToken();if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null&& mPackageInfo.getApplicationInfo().targetSdkVersion< android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {flags |= BIND_WAIVE_PRIORITY;}service.prepareToLeaveProcess(this);int res = ActivityManager.getService().bindIsolatedService(mMainThread.getApplicationThread(), getActivityToken(), service,service.resolveTypeIfNeeded(getContentResolver()),sd, flags, instanceName, getOpPackageName(), user.getIdentifier());if (res < 0) {throw new SecurityException("Not allowed to bind to service " + service);}return res != 0;} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}

bindService调用bindServiceCommon方法。将ServiceConnection 转为Binder的实现类IServiceConnection方便跨进程的远程服务的回调自己定义的方法。

4.3 来到LoadedApk

 final @NonNull LoadedApk mPackageInfo;

因此来到LoadedApk 查看getServiceDispatcher方法:

@UnsupportedAppUsage
public final IServiceConnection getServiceDispatcher(ServiceConnection c,Context context, Handler handler, int flags) {return getServiceDispatcherCommon(c, context, handler, null, flags);
}private IServiceConnection getServiceDispatcherCommon(ServiceConnection c,Context context, Handler handler, Executor executor, int flags) {synchronized (mServices) {LoadedApk.ServiceDispatcher sd = null;ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> map = mServices.get(context);if (map != null) {if (DEBUG) Slog.d(TAG, "Returning existing dispatcher " + sd + " for conn " + c);sd = map.get(c);}if (sd == null) {if (executor != null) {sd = new ServiceDispatcher(c, context, executor, flags);} else {sd = new ServiceDispatcher(c, context, handler, flags);}if (DEBUG) Slog.d(TAG, "Creating new dispatcher " + sd + " for conn " + c);if (map == null) {map = new ArrayMap<>();mServices.put(context, map);}map.put(c, sd);} else {sd.validate(context, handler, executor);}return sd.getIServiceConnection();}
}

private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices
    = new ArrayMap<>();

mServices记录了应用当前活动的ServiceConnection和ServiceDispatcher的映射关系,不知是否记得ActivityThread中也有一个final ArrayMap<IBinder, Service> mServices = new ArrayMap<>(); 记录了IBinder,和Service的映射关系。

继续说LoadedApk中的service哈,上面代码会判断是否存在相同的ServiceConnection,如果不存在就创建新ServiceDispatcher实例并将其存储在mService中,key时ServiceConnection,value为ServiceDispatcher,ServiceDispatcher内部存储了ServiceConnection和InnerConnection对象。在调用bindService以后Service和调用方成功建立连接时系统会通过InnerConnection调用ServiceConnection中的onServiceConnected方法,此时我们就可以利用传过来的IBinder调用Service中的方法完成交互了。这个过程支持跨进程IPC通信,比如两个进程使用AIDL通信。

4.4 ContextImpl类bindService的bindIsolatedService

返回头看上面4.2中的bindService方法,继续向下看会调用ActivityManagerService的bindIsolatedService方法:

synchronized(this) {return mServices.bindServiceLocked(caller, token, service,resolvedType, connection, flags, instanceName, callingPackage, userId);}

4.5 来到ActiveService类的bindServiceLocked

继续调用本类的bringUpServiceLocked

bringUpServiceLocked(serviceRecord,
        serviceIntent.getFlags(),
        callerFg, false, false);

在调用本类realStartServiceLocked

realStartServiceLocked(r, app, execInFg);

一般来说源码中当一个方法多次穿梭调用之后突然带上了real,那一定是离真相不远了。

app.thread.scheduleCreateService(r, r.serviceInfo,
        mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
        app.getReportedProcState());

这一行就很熟悉了,和上一篇startService一样,调用的ApplicationThread来创建Service实例并调用它的onCreate生命周期。

上一篇分析这个方法之下是调用onStartCommand生命周期

,没错这里也不例外下面requestServiceBindingsLocked(r, execInFg)也会去调用ApplicationThread的scheduleBindService:

r.app.thread.scheduleBindService(r, i.intent.getIntent(), rebind,
        r.app.getReportedProcState());

4.6来到ApplicationThread

利用它封装的handler发送BIND_SERVICE消息:

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;if (DEBUG_SERVICE)Slog.v(TAG, "scheduleBindService token=" + token + " intent=" + intent + " uid="+ Binder.getCallingUid() + " pid=" + Binder.getCallingPid());sendMessage(H.BIND_SERVICE, s);}

接收消息:

case BIND_SERVICE:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind");handleBindService((BindServiceData)msg.obj);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;

关键来咯:

 private void handleBindService(BindServiceData data) {Service s = mServices.get(data.token);if (DEBUG_SERVICE)Slog.v(TAG, "handleBindService s=" + s + " rebind=" + data.rebind);if (s != null) {try {data.intent.setExtrasClassLoader(s.getClassLoader());data.intent.prepareToEnterProcess();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);}} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}} catch (Exception e) {if (!mInstrumentation.onException(s, e)) {throw new RuntimeException("Unable to bind to service " + s+ " with " + data.intent + ": " + e.toString(), e);}}}
}

上面代码显示根据token获取Service对象,然后判断首次绑定就调用onBind生命周期,已经绑定过就调用onReBind生命周期,返回的IBinder对象就可以用来调用Service中的方法了。但是为了让调用方拿到这个IBinder就同过onServiceConnected方法回调回去,这个工作就有ActivityManagerService的publishService方法来完成。

4.6 来到ActivityManagerService

 public void publishService(IBinder token, Intent intent, IBinder service) {// Refuse possible leaked file descriptorsif (intent != null && intent.hasFileDescriptors() == true) {throw new IllegalArgumentException("File descriptors passed in Intent");}synchronized(this) {if (!(token instanceof ServiceRecord)) {throw new IllegalArgumentException("Invalid service token");}mServices.publishServiceLocked((ServiceRecord)token, intent, service);}}

然后它有调用了ActiveService的publishServiceLocked方法来处理:

void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {final long origId = Binder.clearCallingIdentity();try {if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "PUBLISHING " + r+ " " + intent + ": " + service);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);if (!filter.equals(c.binding.intent.intent)) {if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Not publishing to: " + c);if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Bound intent: " + c.binding.intent.intent);if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Published intent: " + intent);continue;}if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Publishing to: " + c);try {c.conn.connected(r.name, service, false);} catch (Exception e) {Slog.w(TAG, "Failure sending service " + r.shortInstanceName+ " to connection " + c.conn.asBinder()+ " (in " + c.binding.client.processName + ")", e);}}}}serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false);}} finally {Binder.restoreCallingIdentity(origId);}
}

它在for循环里调用一行代码:

c.conn.connected(r.name, service, false);

顺着代码看:

Conn的类型是是在ConnectionRecord类定义的IServiceConnection

final IServiceConnection conn;  // The client connection.

Service就是建立连接的Ibinder实例。

4.7再次来到LoadedApk类

看一下IServiceConnectionconnected方法:

private static class InnerConnection extends IServiceConnection.Stub {@UnsupportedAppUsagefinal WeakReference<LoadedApk.ServiceDispatcher> mDispatcher;InnerConnection(LoadedApk.ServiceDispatcher sd) {mDispatcher = new WeakReference<LoadedApk.ServiceDispatcher>(sd);}public void connected(ComponentName name, IBinder service, boolean dead)throws RemoteException {LoadedApk.ServiceDispatcher sd = mDispatcher.get();if (sd != null) {sd.connected(name, service, dead);}}
}

而它又调用了ActivityThread类,mActivityThread就是其中Handler子类 H ,这一步就是为了利用Handler在主线程回调给调用方的onServiceConnected

public void connected(ComponentName name, IBinder service, boolean dead) {if (mActivityExecutor != null) {mActivityExecutor.execute(new RunConnection(name, service, 0, dead));} else if (mActivityThread != null) {mActivityThread.post(new RunConnection(name, service, 0, dead));} else {doConnected(name, service, dead);}
}

RunConnection 的实现:

private final class RunConnection implements Runnable {RunConnection(ComponentName name, IBinder service, int command, boolean dead) {mName = name;mService = service;mCommand = command;mDead = dead;}public void run() {if (mCommand == 0) {doConnected(mName, mService, mDead);} else if (mCommand == 1) {doDeath(mName, mService);}}final ComponentName mName;final IBinder mService;final int mCommand;final boolean mDead;
}

回调onServiceConnected彻底呼应上了

public void doConnected(ComponentName name, IBinder service, boolean dead) {ServiceDispatcher.ConnectionInfo old;ServiceDispatcher.ConnectionInfo info;synchronized (this) {if (mForgotten) {// We unbound before receiving the connection; ignore// any connection received.return;}old = mActiveConnections.get(name);if (old != null && old.binder == service) {// Huh, already have this one.  Oh well!return;}if (service != null) {// A new service is being connected... set it all up.info = new ConnectionInfo();info.binder = service;info.deathMonitor = new DeathMonitor(name, service);try {service.linkToDeath(info.deathMonitor, 0);mActiveConnections.put(name, info);} catch (RemoteException e) {// This service was dead before we got it...  just// don't do anything with it.mActiveConnections.remove(name);return;}} else {// The named service is being disconnected... clean up.mActiveConnections.remove(name);}if (old != null) {old.binder.unlinkToDeath(old.deathMonitor, 0);}}// If there was an old service, it is now disconnected.if (old != null) {mConnection.onServiceDisconnected(name);}if (dead) {mConnection.onBindingDied(name);}// If there is a new viable service, it is now connected.if (service != null) {mConnection.onServiceConnected(name, service);} else {// The binding machinery worked, but the remote returned null from onBind().mConnection.onNullBinding(name);}
}

至此bindService的绑定流程分析完毕!

才疏学浅,如有错误,欢迎指正,多谢。

相关文章:

展开说说:Android服务之bindService解析

前面两篇文章我们分别总结了Android四种Service的基本使用以及源码层面总结一下startService的执行过程&#xff0c;本篇继续从源码层面总结bindService的执行过程。 本文依然按着是什么&#xff1f;有什么&#xff1f;怎么用&#xff1f;啥原理&#xff1f;的步骤来分析。 b…...

node-sass 老版本4.14.0 安装失败解决办法

旧项目 npm install 发现 node-sass 安装 失败 切换淘宝镜像之后 不能完全解决问题。因为需要编译&#xff0c;本地没有Python环境不能实现 安装node-sass时&#xff0c;在install阶段会从Github上下载一个叫binding.node的文件&#xff0c;而「GitHub Releases」里的文件…...

最近很火的字幕截图生成器

网址 https://disksing.com/fake-screenshot/ 最近很火的字幕截图生成器&#xff0c;对于自媒体来说真的太实用了 另外透露一下&#xff0c;你仔细研究就会发现&#xff0c;这是个纯前端的项目...

使用RabbitMQ实现可靠的消息传递机制

使用RabbitMQ实现可靠的消息传递机制 大家好&#xff0c;我是微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 1. RabbitMQ简介 RabbitMQ是一个开源的消息代理软件&#xff0c;实现了高级消息队列协议&#xff08;AMQP&…...

Function Call ReACT,Agent应用落地的加速器_qwen的function calling和react有什么不同

探索智能体Agent的未来之路&#xff1a;Function Call与ReACT框架的较量&#xff0c;谁能引领未来&#xff1f; 引言 各大平台出现智能体应用创建&#xff0c;智能体逐渐落地&#xff0c;背后的使用哪种框架&#xff1f; 随着各大平台&#xff0c;例如百度千帆APPbuilder、阿…...

Java的JSONPath(fastjson)使用总结

背景 最近使用json实现复杂业务配置, 因为功能需要解析读取json的中节点数据。如果使用循环或者stream处理&#xff0c;可以实现&#xff0c;但是都过于麻烦。在想能否使用更简单json读取方式&#xff0c;正好发现fastjson支持该功能&#xff0c;本文做一个记录 案例说明 示…...

【大模型】大语言模型:光鲜背后的阴影——事实准确性和推理能力的挑战

大语言模型&#xff1a;光鲜背后的阴影——事实准确性和推理能力的挑战 引言一、概念界定二、事实准确性的局限2.1 训练数据的偏差2.2 知识的时效性问题2.3 复杂概念的理解与表述 三、推理能力的局限3.1 表层理解与深层逻辑的脱节3.2 缺乏常识推理3.3 无法进行长期记忆和连续推…...

Java面向对象练习(1.手机类)(2024.7.4)

手机类 package Phone;public class Phone {private String brand;private int price;private String color;public Phone(){}public Phone(String brand, int price, String color){this.brand brand;this.price price;this.color color;}public void setBrand(String bra…...

智慧生活新篇章,Vatee万腾平台领航前行

在21世纪的科技浪潮中&#xff0c;智慧生活已不再是一个遥远的梦想&#xff0c;而是正逐步成为我们日常生活的现实。从智能家居的温馨便捷&#xff0c;到智慧城市的高效运转&#xff0c;科技的每一次进步都在为我们的生活增添新的色彩。而在这场智慧生活的变革中&#xff0c;Va…...

Spring Cloud Gateway报sun.misc.Unsafe.park(Native Method)

项目引入spring cloud gateway的jar报&#xff0c;启动的时候报&#xff1a; [2024-07-05 10:10:16.162][main][ERROR][org.springframework.boot.web.embedded.tomcat.TomcatStarter][61]:Error starting Tomcat context. Exception: org.springframework.beans.factory.Bean…...

select single , select endselect

select single , select endselect single 根据条件找到一条数据&#xff0c;就出来了。 select endselect是在里面循环&#xff0c;每次找一条&#xff0c;依次放到into table中&#xff0c;或者放到into work area中&#xff0c;下面append table 。 实际开发中不建议这么操…...

后端学习(一)

添加数据库包&#xff1a; 数据库连接时 发生错误&#xff1a; 解决方式&#xff1a; SqlConnection conn new SqlConnection("serverlocalhost;databaseMyBBSDb;uidsa;pwd123456;Encryptfalse;") ;conn.Open();SqlCommand cmd new SqlCommand("SELECT * FROM…...

【活动行】参与上海两场线下活动,教育生态行业赛总决赛活动和WAIC人工智能大会活动 - 上海活动总结

目录 背景决赛最后一公里领域范围 决赛作品AI智教相机辅导老师Copilot辅导老师Copilot雅思写作竞技场 优秀作品总结 背景 决赛 百度发起的千帆杯教育生态行业赛于2024年7月4日进行线下决赛&#xff0c;博主虽然没能进入决赛&#xff0c;但也非常荣幸能够以嘉宾身份到现场给进…...

conda 安装设置

安装anaconda 推荐官网下载和安装,最新版本是anaconda3+python3.11,个人选择。有可能找不到 Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror Tips:小白一定要全部勾选,特别第二项“add anaconda3 to my path environment variable…...

用PlantUML和语雀画UML类图

概述 首先阐述一下几个简单概念&#xff1a; UML&#xff1a;是统一建模语言&#xff08;Unified Modeling Language&#xff09;的缩写&#xff0c;它是一种用于软件工程的标准化建模语言&#xff0c;旨在提供一种通用的方式来可视化软件系统的结构、行为和交互。UML由Grady…...

uniapp微信小程序电子签名

先上效果图&#xff0c;不满意可以直接关闭这页签 新建成单独的组件&#xff0c;然后具体功能引入&#xff0c;具体功能点击签名按钮&#xff0c;把当前功能页面用样式隐藏掉&#xff0c;v-show和v-if也行&#xff0c;然后再把这个组件显示出来。 【签名-撤销】原理是之前绘画时…...

MetaPoint_速读

Meta-Point Learning and Refining for Category-Agnostic Pose Estimation https://arxiv.org/abs/2404.14808https://github.com/chenbys/metapointabstract 这篇文章介绍了一种名为Meta-Point Learning and Refining的框架&#xff0c;用于实现类别不可知的姿势估计。该框…...

数据库逆向工程工具reverse_sql

reverse_sql 是一个用于解析和转换 MySQL 二进制日志&#xff08;binlog&#xff09;的工具。它可以将二进制日志文件中记录的数据库更改操作&#xff08;如插入、更新、删除&#xff09;转换为反向的 SQL 语句&#xff0c;以便对系统或人为产生的误操作进行数据回滚和恢复。 *…...

四大内网穿透利器对比

本文精选四款市场上的佼佼者——巴比达、花生壳、Frp及NatApp&#xff0c;详细剖析它们的特点与优势&#xff0c;助力企业和个人用户精准选择&#xff0c;其中特别强调了巴比达在企业级安全访问方面的突出贡献。 1. 巴比达 特点 深度安全防护&#xff1a;巴比达提供全方位安…...

【LeetCode】每日一题:跳跃游戏 II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n - 1] 的最小…...

SpringBoot拦截器

目录 一、拦截器快速入门 &#xff08;1&#xff09;什么是拦截器 &#xff08;2&#xff09;拦截器的使用步骤 1、定义拦截器 &#x1f340;preHandle() 方法 &#x1f340;postHandle() 方法 &#x1f340;afterCompletion() 方法 2、注册配置拦截器 二、拦截器详解…...

uniapp中实现跳转链接到游览器(安卓-h5)

uniapp中实现跳转链接到游览器&#xff08;安卓-h5&#xff09; 项目中需要做到跳转到外部链接&#xff0c;网上找了很多都不是很符合自己的要求&#xff0c;需要编译成app后是跳转到游览器打开链接&#xff0c;编译成web是在新窗口打开链接。实现的代码如下&#xff1a; 效果&…...

WPF UI 界面布局 魔术棒 文字笔记识别 技能提升 布局功能扩展与自定义 继承Panel的对象,测量与排列 系列七

应用开发第一步 功能分类&#xff1a;页面上的功能区域划分。。。。需求分析 业务逻辑 数据流 功能模块 UI/UX 编码 测试 发布 功能开发与布局 不用显式的方式设定元素的尺寸 不使用屏幕坐标来指定位置 Grid 功能最强大&#xff0c;布局最灵活的容器…...

文件格式是.pb应该怎么查看?

文件格式为.pb的文件&#xff0c;通常是Google Protocol Buffers&#xff08;简称PB&#xff09;序列化后的二进制文件。要查看.pb文件的内容&#xff0c;可以采用以下方法&#xff1a; 1. **直接打开&#xff08;不推荐&#xff09;**&#xff1a; - 直接打开.pb文件通常会显示…...

android2024 gradle8 Processor和ksp两种编译时注解实现

android编译时注解&#xff0c;老生常谈&#xff0c;外面的例子都是bindView&#xff0c;脑壳看疼了&#xff0c;自己学习和编写下。 而且现在已经进化到kotlin2.0&#xff0c;google也逐渐放弃kapt&#xff0c;进入维护状态。所以要好好看看本贴。 参考我的工程&#xff1a; h…...

elementui的table的@selection-change阻止事件改变

说明&#xff1a; 最近有个不想说的&#xff08;xxx&#xff09;业务&#xff0c;在表格勾选每一行的时候要触发一系列查询功能&#xff0c;查询失败还要把那个勾勾回退。真实蛋疼&#xff01;表格勾选的默认selection-change是change事件&#xff0c;一般change事件是在完成之…...

空间数据采集与管理:为什么选择ArcGISPro和Python?

你还在为找不到合适的数据而苦恼吗&#xff1f;你还在面对大量数据束手无策&#xff0c;不知如何处理吗&#xff1f;对于从事生产和科研的人员来说&#xff0c;空间数据的采集与管理是地理信息系统&#xff08;GIS&#xff09;和空间分析领域的关键环节。通过准确高效地采集和管…...

案例精选 | 聚铭综合日志分析系统为江苏省电子口岸构建高效安全的贸易生态

江苏省电子口岸有限公司&#xff0c;成立于2009年&#xff0c;由江苏省贸促会携手南京海关、江苏检验检疫局及江苏海事局等部门共同出资组建。公司承载着推动江苏乃至长三角地区国际贸易便利化的重大使命&#xff0c;致力于打造一个集先进性、创新性、高效性于一体的电子口岸综…...

TCP粘包

目录 TCP粘包产生的原因 TCP粘包的现象 TCP粘包的解决方案 TCP粘包是指在TCP通信中,发送方发送的多个数据包在接收方被错误地合并成一个数据包的现象。tcp粘包在发送端和接收端都有可能发生。发送端粘包:发送端需要等缓冲区满才发送出去,造成粘包。接收方粘包:接收方不及…...

数据泄露态势(2024年5月)

监控说明&#xff1a;以下数据由零零信安0.zone安全开源情报系统提供&#xff0c;该系统监控范围包括约10万个明网、深网、暗网、匿名社交社群威胁源。在进行抽样事件分析时&#xff0c;涉及到我国的数据不会选取任何政府、安全与公共事务的事件进行分析。如遇到影响较大的伪造…...