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

Android8 binder源码学习分析笔记(二)

前文回顾:

Android8 binder源码学习分析笔记(一)-CSDN博客文章浏览阅读800次,点赞16次,收藏13次。Binder是Android核心的进程间通信(IPC)机制,它实现了不同进程间安全高效的方法调用和数据传输。AIDL(Android接口定义语言)简化了Binder的使用,开发者只需定义接口,系统会自动生成代理和存根代码。Binder的关键优势包括:高性能(仅需一次数据拷贝)、安全性(支持身份校验和权限控制)和面向对象特性。AIDL生成的代码包含Stub(服务端基础类)和Proxy(客户端代理类),通过transact和onTransact方法实现跨进程调用。这种机制既保证了进程隔离,又满足了应用间的通信需 https://blog.csdn.net/g_i_a_o_giao/article/details/151158611上篇文章介绍了binder是什么、AIDL的相关的知识以及应用中如何使用AIDL进行进程间通信。那么这篇文章将分析一下在framework源码中binder的使用案例。

startActivity(Intent intent)是我们最常用的方法之一,那么我们来看看它在framework层的实现原理是什么。
在Activity的startActivity方法中,会调用重载的startActivity方法,然后再去调用startActivityForResult方法,在这个方法中最重要的是调用Instrument.java的execStartActivity方法。

Activity.java:/*** 启动一个新的Activity,与{@link #startActivity(Intent, Bundle)}相同,但没有指定选项。** @param intent 要启动的Intent意图* @throws android.content.ActivityNotFoundException 如果找不到对应的Activity* @see #startActivity(Intent, Bundle)* @see #startActivityForResult*/
@Override
public void startActivity(Intent intent) {this.startActivity(intent, null); // 调用重载方法,options参数为null
}@Override
public void startActivity(Intent intent, @Nullable Bundle options) {// 通知自动填充客户端控制器关于启动Activity的事情getAutofillClientController().onStartActivity(intent, mIntent);if (options != null) {// 如果有options,调用startActivityForResult并传入optionsstartActivityForResult(intent, -1, options);} else {// 注意:我们希望通过此调用保持与可能已重写该方法的应用程序的兼容性startActivityForResult(intent, -1);}
}public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,@Nullable Bundle options) {if (mParent == null) {// 转换Springboard活动选项(如果有)options = transferSpringboardActivityOptions(options);// 通过Instrumentation执行实际的Activity启动Instrumentation.ActivityResult ar =mInstrumentation.execStartActivity(this, // 当前ContextmMainThread.getApplicationThread(), // 应用的ApplicationThread(Binder对象)mToken, // Activity的IBinder令牌this, // 当前Activityintent, // 要启动的IntentrequestCode, // 请求代码options); // 选项// 如果有返回结果,发送结果if (ar != null) {mMainThread.sendActivityResult(mToken, mEmbeddedID, requestCode, ar.getResultCode(),ar.getResultData());}// 如果requestCode >= 0,表示需要返回结果if (requestCode >= 0) {// 如果此启动请求结果,我们可以在收到结果之前避免使Activity可见。// 在onCreate(Bundle savedInstanceState)或onResume()期间设置此代码将在此期间保持Activity隐藏,// 以避免闪烁。只有在请求结果时才能这样做,因为这保证我们会在Activity完成时收回信息,// 无论发生什么情况。mStartedActivity = true; // 设置标志表示已启动Activity}// 取消输入并启动退出过渡动画cancelInputsAndStartExitTransition(options);// TODO 考虑清除/刷新其他事件源和子窗口的事件} else {// 如果有父Activity,通过父Activity的方法启动if (options != null) {mParent.startActivityFromChild(this, intent, requestCode, options);} else {// 注意:我们希望通过此方法保持与现有应用程序的兼容性mParent.startActivityFromChild(this, intent, requestCode);}}
}

那我们继续分析execStartActivity方法。我们看到了熟悉的代码IApplicationThread。这是本进程的Ibinder对象,而之前的文章我们已经分析过,在进程启动的时候,已经通过attachApplication(mAppThread)方法将这个binder对象发送给了AMS。(具体可以查看文章Android8 从系统启动到用户见到第一个Activity的流程源码分析(三)-CSDN博客)

IApplicationThread whoThread = (IApplicationThread) contextThread;

再看另一个核心步骤:ActvityManager.getService().startActivity()方法。

/*** 执行由应用程序发起的startActivity调用。* 默认实现负责更新任何活动的{@link ActivityMonitor}对象,并将此调用分派给系统活动管理器;* 您可以重写此方法以监视应用程序启动活动,并修改启动时发生的情况。** <p>此方法返回一个{@link ActivityResult}对象,您可以在拦截应用程序调用时使用它,* 以避免执行启动活动操作,但仍返回应用程序期望的结果。* 为此,重写此方法以捕获启动活动调用,使其返回包含您希望应用程序看到的结果的新ActivityResult,* 并且不要调用父类。请注意,仅当<var>requestCode</var> >= 0时应用程序才期望结果。** <p>如果没有找到运行给定Intent的Activity,此方法会抛出{@link android.content.ActivityNotFoundException}。** @param who 启动Activity的Context* @param contextThread 启动Activity的Context的主线程* @param token 向系统标识谁正在启动Activity的内部令牌;可能为null* @param target 哪个Activity正在执行启动(从而接收任何结果);如果此调用不是从Activity发出的,可能为null* @param intent 要启动的实际Intent* @param requestCode 此请求结果的标识符;如果调用者不期望结果,则小于零* @param options 附加选项* @return 要强制返回特定结果,返回包含所需数据的ActivityResult对象;否则返回null。默认实现始终返回null。* @throws android.content.ActivityNotFoundException* @see Activity#startActivity(Intent)* @see Activity#startActivityForResult(Intent, int)* {@hide} 这是一个隐藏的系统API*/
@UnsupportedAppUsage
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {// 将contextThread转换为IApplicationThread接口,这是应用与AMS通信的Binder接口IApplicationThread whoThread = (IApplicationThread) contextThread;// 获取来源信息(用于Analytics等)Uri referrer = target != null ? target.onProvideReferrer() : null;if (referrer != null) {intent.putExtra(Intent.EXTRA_REFERRER, referrer);}// 处理ActivityMonitor - 用于测试和监控Activity启动if (mActivityMonitors != null) {synchronized (mSync) {final int N = mActivityMonitors.size();for (int i=0; i<N; i++) {final ActivityMonitor am = mActivityMonitors.get(i);ActivityResult result = null;// 检查是否需要忽略特定Intentif (am.ignoreMatchingSpecificIntents()) {if (options == null) {options = ActivityOptions.makeBasic().toBundle();}result = am.onStartActivity(who, intent, options);}// 如果监控器返回了结果,直接返回而不继续启动if (result != null) {am.mHits++;return result;} else if (am.match(who, null, intent)) {// 如果监控器匹配此启动请求am.mHits++;if (am.isBlocking()) {// 如果监控器是阻塞的,返回结果或nullreturn requestCode >= 0 ? am.getResult() : null;}break;}}}}try {// 准备Intent以便离开当前进程intent.migrateExtraStreamToClipData(who);  // 处理内容URI权限intent.prepareToLeaveProcess(who);         // 安全检查// 关键调用:通过Binder IPC与ActivityManagerService通信int result = ActivityManager.getService().startActivity(whoThread,                    // 应用线程接口(IApplicationThread)who.getOpPackageName(),       // 调用包名who.getAttributionTag(),      // 归属标签(用于权限跟踪)intent,                       // 要启动的Intentintent.resolveTypeIfNeeded(who.getContentResolver()), // Intent的MIME类型token,                        // Activity令牌target != null ? target.mEmbeddedID : null, // 嵌入式IDrequestCode,                  // 请求代码0,                            // 开始标志null,                         // 分析信息(ProfilingInfo)options);                     // 启动选项// 通知启动活动结果(主要用于测试)notifyStartActivityResult(result, options);// 检查启动结果,如果失败会抛出异常checkStartActivityResult(result, intent);} catch (RemoteException e) {// 处理远程调用异常throw new RuntimeException("Failure from system", e);}return null;
}

那么再去看看这个ActivityManager中的getService方法,在这个方法中,我们看到了熟悉的ServiceManager.getService(Context.ACTIVITY_SERVER)方法。之前我们也分析过,每启动一个系统服务,都会调用ServiceManager.addService()将binder对象添加。(详情请查看文章:Android8 SystemServer 启动源码分析笔记(三)-CSDN博客)

 public static IActivityManager getService() {return IActivityManagerSingleton.get();}private static final Singleton<IActivityManager> IActivityManagerSingleton =new Singleton<IActivityManager>() {@Overrideprotected IActivityManager create() {final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);final IActivityManager am = IActivityManager.Stub.asInterface(b);return am;}};

那么我们去ServiceManager中查看这个getService方法。主要是获取Ibinder对象,如果获取不到就从native层获取。之前文章已经分析过,在AMS启动以后,就通过 ServiceManager.addService(Context.ACTIVITY_SERVICE, this, true);添加了binder对象。

 public static IBinder getService(String name) {try {// 从缓存中获取,获取不到就创建IBinder service = sCache.get(name);if (service != null) {return service;} else {// 从native层获取return Binder.allowBlocking(getIServiceManager().getService(name));}} catch (RemoteException e) {Log.e(TAG, "error in getService", e);}return null;}private static IServiceManager getIServiceManager() {if (sServiceManager != null) {return sServiceManager;}// Find the service manager// 与native层通信获取Ibinder对象sServiceManager = ServiceManagerNative.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));return sServiceManager;}

那么再回到最开始的ActvityManager.getService().startActivity()方法。将这个进程的IAppliactionThread(IBinder)对象作为参数,调用了AMS的IBinder中的startActivity方法。

那我们去看看frameworks/base/core/java/android/app/IActivityManager.aidl中是如何定义这个binder接口的,可以看到这里的参数都是in单向。

 int startActivity(in IApplicationThread caller, in String callingPackage, in Intent intent,in String resolvedType, in IBinder resultTo, in String resultWho, int requestCode,int flags, in ProfilerInfo profilerInfo, in Bundle options);

再去看看AMS中对于这个接口的实现逻辑:在这个方法中会调用startActivityAsUser方法,在这个方法中又会去调用ActivityStarter.java的startActivityMayWait方法。

@Overridepublic final int startActivity(IApplicationThread caller, String callingPackage,Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) {return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,resultWho, requestCode, startFlags, profilerInfo, bOptions,UserHandle.getCallingUserId());}@Overridepublic final int startActivityAsUser(IApplicationThread caller, String callingPackage,Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,int startFlags, ProfilerInfo profilerInfo, Bundle bOptions, int userId) {enforceNotIsolatedCaller("startActivity");userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),userId, false, ALLOW_FULL_ONLY, "startActivity", null);// TODO: Switch to user app stacks here.return mActivityStarter.startActivityMayWait(caller, -1, callingPackage, intent,resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,profilerInfo, null, null, bOptions, false, userId, null, "startActivityAsUser");}

我们继续去ActivityStarter中的startActivityMayWait方法中去看一下。主要是通过传递的IApplicationThread对象获取对应的ProcessRecord对象。

AMS中的getRecordForAppLocked方法,主要是根据传递的IBinder寻找ProcessRecord对象。

final ProcessRecord getRecordForAppLocked(IApplicationThread thread) {if (thread == null) {return null;}int appIndex = getLRURecordIndexForAppLocked(thread);if (appIndex >= 0) {return mLruProcesses.get(appIndex);}// Validation: if it isn't in the LRU list, it shouldn't exist, but let's// double-check that.final IBinder threadBinder = thread.asBinder();final ArrayMap<String, SparseArray<ProcessRecord>> pmap = mProcessNames.getMap();for (int i = pmap.size()-1; i >= 0; i--) {final SparseArray<ProcessRecord> procs = pmap.valueAt(i);for (int j = procs.size()-1; j >= 0; j--) {final ProcessRecord proc = procs.valueAt(j);if (proc.thread != null && proc.thread.asBinder() == threadBinder) {Slog.wtf(TAG, "getRecordForApp: exists in name list but not in LRU list: "+ proc);return proc;}}}return null;}

接下来最重要的还是调用了startActivityLocked方法。这些在之前的文章都已经分析过Activity的启动流程了,就不再进行分析了。

/*** 启动一个Activity,并可能等待其结果。这是Activity启动流程中最核心的方法之一。* * @param caller 调用者的ApplicationThread,用于标识来源进程* @param callingUid 调用者的UID* @param callingPackage 调用者的包名* @param intent 要启动的Intent* @param resolvedType 解析后的MIME类型* @param voiceSession 语音交互会话(用于语音操作)* @param voiceInteractor 语音交互器* @param resultTo 接收结果的Activity的Binder令牌* @param resultWho 结果接收者的标识符* @param requestCode 请求代码(用于startActivityForResult)* @param startFlags 启动标志* @param profilerInfo 性能分析器信息* @param outResult 用于存储等待结果的输出参数* @param globalConfig 全局配置* @param bOptions 启动选项Bundle* @param ignoreTargetSecurity 是否忽略目标Activity的安全限制* @param userId 用户ID* @param inTask 要在其中启动的Task记录* @param reason 启动原因(用于调试和日志)* @return 启动结果代码*/
final int startActivityMayWait(IApplicationThread caller, int callingUid,String callingPackage, Intent intent, String resolvedType,IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,IBinder resultTo, String resultWho, int requestCode, int startFlags,ProfilerInfo profilerInfo, WaitResult outResult,Configuration globalConfig, Bundle bOptions, boolean ignoreTargetSecurity, int userId,TaskRecord inTask, String reason) {// 1. 安全检查:拒绝可能泄露的文件描述符if (intent != null && intent.hasFileDescriptors()) {throw new IllegalArgumentException("File descriptors passed in Intent");}// 通知指标记录器Activity正在启动mSupervisor.mActivityMetricsLogger.notifyActivityLaunching();// 检查Intent是否明确指定了组件(Activity类)boolean componentSpecified = intent.getComponent() != null;// 保存Intent副本供即时应用使用final Intent ephemeralIntent = new Intent(intent);// 创建Intent副本,避免修改客户端原始对象intent = new Intent(intent);// 2. 处理即时应用安装器组件的特殊情况if (componentSpecified&& intent.getData() != null&& Intent.ACTION_VIEW.equals(intent.getAction())&& mService.getPackageManagerInternalLocked().isInstantAppInstallerComponent(intent.getComponent())) {// 拦截直接指向即时安装器的Intent// 即时安装器不应该用原始URL启动;而是调整Intent使其看起来像"正常"的即时应用启动intent.setComponent(null /*component*/);componentSpecified = false;}// 3. 解析Intent,找到匹配的ActivityResolveInfo rInfo = mSupervisor.resolveIntent(intent, resolvedType, userId);if (rInfo == null) {// 4. 特殊处理:托管配置文件的情况UserInfo userInfo = mSupervisor.getUserInfo(userId);if (userInfo != null && userInfo.isManagedProfile()) {// 对于托管配置文件,如果尝试从已解锁的父配置文件启动加密 unaware 的应用// 允许它解析,因为用户将通过确认凭证来解锁配置文件UserManager userManager = UserManager.get(mService.mContext);boolean profileLockedAndParentUnlockingOrUnlocked = false;long token = Binder.clearCallingIdentity();try {UserInfo parent = userManager.getProfileParent(userId);profileLockedAndParentUnlockingOrUnlocked = (parent != null)&& userManager.isUserUnlockingOrUnlocked(parent.id)&& !userManager.isUserUnlockingOrUnlocked(userId);} finally {Binder.restoreCallingIdentity(token);}if (profileLockedAndParentUnlockingOrUnlocked) {// 同时匹配DIRECT_BOOT_AWARE和UNAWARE组件rInfo = mSupervisor.resolveIntent(intent, resolvedType, userId,PackageManager.MATCH_DIRECT_BOOT_AWARE| PackageManager.MATCH_DIRECT_BOOT_UNAWARE);}}}// 5. 解析目标Activity的具体信息ActivityInfo aInfo = mSupervisor.resolveActivity(intent, rInfo, startFlags, profilerInfo);// 6. 转换选项Bundle为ActivityOptions对象ActivityOptions options = ActivityOptions.fromBundle(bOptions);// 进入同步代码块,保护AMS状态的完整性synchronized (mService) {// 7. 获取真实的调用者PID和UIDfinal int realCallingPid = Binder.getCallingPid();final int realCallingUid = Binder.getCallingUid();int callingPid;if (callingUid >= 0) {callingPid = -1;} else if (caller == null) {callingPid = realCallingPid;callingUid = realCallingUid;} else {callingPid = callingUid = -1;}// 8. 检查配置是否将要变更final ActivityStack stack = mSupervisor.mFocusedStack;stack.mConfigWillChange = globalConfig != null&& mService.getGlobalConfiguration().diff(globalConfig) != 0;// 清除调用者身份,以系统身份执行后续操作final long origId = Binder.clearCallingIdentity();try {// 9. 处理重量级进程的特殊情况if (aInfo != null &&(aInfo.applicationInfo.privateFlags& ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) {// 这可能是一个重量级进程!检查是否已经有另一个不同的重量级进程在运行if (aInfo.processName.equals(aInfo.applicationInfo.packageName)) {final ProcessRecord heavy = mService.mHeavyWeightProcess;if (heavy != null && (heavy.info.uid != aInfo.applicationInfo.uid|| !heavy.processName.equals(aInfo.processName))) {// 10. 如果已有其他重量级进程运行,准备启动切换器Activityint appCallingUid = callingUid;if (caller != null) {ProcessRecord callerApp = mService.getRecordForAppLocked(caller);if (callerApp != null) {appCallingUid = callerApp.info.uid;} else {Slog.w(TAG, "Unable to find app for caller " + caller+ " (pid=" + callingPid + ") when starting: "+ intent.toString());ActivityOptions.abort(options);return ActivityManager.START_PERMISSION_DENIED;}}// 创建PendingIntent用于启动原始目标ActivityIIntentSender target = mService.getIntentSenderLocked(ActivityManager.INTENT_SENDER_ACTIVITY, "android",appCallingUid, userId, null, null, 0, new Intent[] { intent },new String[] { resolvedType }, PendingIntent.FLAG_CANCEL_CURRENT| PendingIntent.FLAG_ONE_SHOT, null);// 11. 构建启动重量级切换器Activity的IntentIntent newIntent = new Intent();if (requestCode >= 0) {// 调用者请求结果newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_HAS_RESULT, true);}newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_INTENT,new IntentSender(target));if (heavy.activities.size() > 0) {ActivityRecord hist = heavy.activities.get(0);newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_CUR_APP,hist.packageName);newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_CUR_TASK,hist.getTask().taskId);}newIntent.putExtra(HeavyWeightSwitcherActivity.KEY_NEW_APP,aInfo.packageName);newIntent.setFlags(intent.getFlags());newIntent.setClassName("android",HeavyWeightSwitcherActivity.class.getName());// 12. 替换Intent为启动切换器Activity的Intentintent = newIntent;resolvedType = null;caller = null;callingUid = Binder.getCallingUid();callingPid = Binder.getCallingPid();componentSpecified = true;// 重新解析Intent(现在指向切换器Activity)rInfo = mSupervisor.resolveIntent(intent, null /*resolvedType*/, userId);aInfo = rInfo != null ? rInfo.activityInfo : null;if (aInfo != null) {aInfo = mService.getActivityInfoForUser(aInfo, userId);}}}}// 13. 调用核心的启动方法final ActivityRecord[] outRecord = new ActivityRecord[1];int res = startActivityLocked(caller, intent, ephemeralIntent, resolvedType,aInfo, rInfo, voiceSession, voiceInteractor,resultTo, resultWho, requestCode, callingPid,callingUid, callingPackage, realCallingPid, realCallingUid, startFlags,options, ignoreTargetSecurity, componentSpecified, outRecord, inTask,reason);// 14. 如果配置将要变更,现在更新配置if (stack.mConfigWillChange) {// 如果调用者也希望切换到新配置,现在执行// 这允许一个干净的切换,因为我们正在等待当前Activity暂停(所以我们不会销毁它),// 并且还没有启动下一个ActivitymService.enforceCallingPermission(android.Manifest.permission.CHANGE_CONFIGURATION,"updateConfiguration()");stack.mConfigWillChange = false;mService.updateConfigurationLocked(globalConfig, null, false);}// 15. 处理等待结果的情况if (outResult != null) {outResult.result = res;if (res == ActivityManager.START_SUCCESS) {// 将等待对象添加到列表,并等待Activity启动完成mSupervisor.mWaitingActivityLaunched.add(outResult);do {try {mService.wait(); // 等待通知} catch (InterruptedException e) {}} while (outResult.result != START_TASK_TO_FRONT&& !outResult.timeout && outResult.who == null);if (outResult.result == START_TASK_TO_FRONT) {res = START_TASK_TO_FRONT;}}// 16. 处理任务前置的情况if (res == START_TASK_TO_FRONT) {final ActivityRecord r = outRecord[0];if (r.nowVisible && r.state == RESUMED) {// Activity已经可见并处于 resumed 状态outResult.timeout = false;outResult.who = r.realActivity;outResult.totalTime = 0;outResult.thisTime = 0;} else {// 等待Activity可见outResult.thisTime = SystemClock.uptimeMillis();mSupervisor.waitActivityVisible(r.realActivity, outResult);do {try {mService.wait(); // 等待通知} catch (InterruptedException e) {}} while (!outResult.timeout && outResult.who == null);}}}// 17. 通知指标记录器Activity启动完成mSupervisor.mActivityMetricsLogger.notifyActivityLaunched(res, outRecord[0]);return res;} finally {// 18. 恢复调用者身份Binder.restoreCallingIdentity(origId);}}
}


文章转载自:

http://1BAiSnga.cfcdr.cn
http://Sp4OdaCZ.cfcdr.cn
http://GJYUk1XT.cfcdr.cn
http://5y97U8vb.cfcdr.cn
http://h4qtKYcb.cfcdr.cn
http://EisRY9eI.cfcdr.cn
http://uYuSJJdh.cfcdr.cn
http://ZKQ1JCii.cfcdr.cn
http://sBop93H5.cfcdr.cn
http://jFjPwBfr.cfcdr.cn
http://haBQrrQf.cfcdr.cn
http://sSn73vcq.cfcdr.cn
http://rDT3ukU2.cfcdr.cn
http://7O3flg8v.cfcdr.cn
http://N6QDZU4H.cfcdr.cn
http://WszwmlOL.cfcdr.cn
http://kpNTSgrm.cfcdr.cn
http://2xYInr4m.cfcdr.cn
http://uvP4wIpz.cfcdr.cn
http://V6hCL2t2.cfcdr.cn
http://cJFm4RLu.cfcdr.cn
http://SFgd7PM2.cfcdr.cn
http://AmY4SKAD.cfcdr.cn
http://CS5bUt3N.cfcdr.cn
http://P7amnoAH.cfcdr.cn
http://sbIOh10R.cfcdr.cn
http://iOYw2BEY.cfcdr.cn
http://F0yr19IX.cfcdr.cn
http://h7lHJW0r.cfcdr.cn
http://Ui9tnN3j.cfcdr.cn
http://www.dtcms.com/a/368501.html

相关文章:

  • 【51单片机8*8点阵显示箭头动画详细注释】2022-12-1
  • 笔记三 FreeRTOS中断
  • 【连载 2/9】大模型应用:(二)初识大模型(35页)【附全文阅读】
  • 为什么动态视频业务内容不可以被CDN静态缓存?
  • 【视频系统】技术汇编
  • 如何提升技术架构设计能力?
  • 【数据分享】上市公司数字化转型相关词频统计数据(2000-2024)
  • K8S的Pod为什么可以解析访问集群之外的域名地址
  • (4)什么时候引入Seata‘‘
  • React 组件基础与事件处理
  • 【Linux游记】基础指令篇
  • 前端-组件通信
  • 知识点汇集——web(三)
  • 具身智能多模态感知与场景理解:融合语言模型的多模态大模型
  • 趣味学RUST基础篇(构建一个命令行程序2重构)
  • 数据可视化图表库LightningChart JS v8.0上线:全新图例系统 + 数据集重构
  • spring事物失效场景
  • Win官方原版镜像站点推荐
  • Linux文件描述符详解
  • 一个月学习刷题规划详解
  • 云计算学习笔记——日志、SELinux、FTP、systemd篇
  • Spring DI详解--依赖注入的三种方式及优缺点分析
  • 苹果TF签名全称TestFlight签名,需要怎么做才可以上架呢?
  • 小团队如何高效完成 uni-app iOS 上架,从分工到工具组合的实战经验
  • 华为认证HCIA备考知识点 :IP路由基础(含配置案例)
  • AI测试:自动化测试框架、智能缺陷检测、A/B测试优化
  • 从零到上线:直播美颜SDK中人脸美型功能的技术实现与效果优化
  • 大数据毕业设计选题推荐-基于大数据的高级大豆农业数据分析与可视化系统-Hadoop-Spark-数据可视化-BigData
  • 自演化大语言模型的技术背景
  • 3D目标跟踪重磅突破!TrackAny3D实现「类别无关」统一建模,多项SOTA达成!