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

Android应用安装过程

Android 系统源码源码-应用安装过程

Android 中应用安装的过程就是解析 AndroidManifest.xml 的过程,系统可以从 Manifest 中得到应用程序的相关信息,比如 Activity、Service、Broadcast Receiver 和 ContentProvider 等。这些工作都是由 PackageManageService 负责的,也就是所谓的 PMS. 它跟 AMS 一样都是一种远程的服务,并且都是在系统启动 SystemServer 的时候启动的。下面我们通过源代码来分析下这个过程。

1、启动 PMS 的过程

系统在启动 SystemServer 的过程会启动 PMS,系统的启动过程可以参考下面这篇文章学习,

Android 系统源码-1:Android 系统启动流程源码分析

在启动 SystemServer 的时候会调用 startBootstrapServices() 方法启动引导服务。PMS 就是在这个方法中启动的,

    private void startBootstrapServices() {// ...mPackageManagerService = PackageManagerService.main(mSystemContext, installer,mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);mFirstBoot = mPackageManagerService.isFirstBoot();mPackageManager = mSystemContext.getPackageManager();// ...}

可以看出,系统是通过调用 PMS 的 main 方法来将其启动起来的。其 main 方法会先实例化一个 PMS 对象,然后调用 ServiceManager 的静态方法将其注册到 ServiceManager 中进行管理。

    public static PackageManagerService main(Context context, Installer installer,boolean factoryTest, boolean onlyCore) {PackageManagerServiceCompilerMapping.checkProperties();PackageManagerService m = new PackageManagerService(context, installer,factoryTest, onlyCore);m.enableSystemUserPackages();ServiceManager.addService("package", m);final PackageManagerNative pmn = m.new PackageManagerNative();ServiceManager.addService("package_native", pmn);return m;}

当我们需要使用 PMS 解析 APK 的时候就会从 ServiceManager 中获取。

在 PMS 的构造方法中有许多工作要完成。一个 APK 安装的主要分成下面几个步骤,

  1. 拷贝文件到指定的目录:默认情况下,用户安装的 APK 首先会被拷贝到 /data/app 目录下,/data/app 目录是用户有权限访问的目录,在安装 APK 的时候会自动选择该目录存放用户安装的文件,而系统的 APK 文件则被放到了 /system 分区下,包括 /system/app/system/vendor/app,以及 /system/priv-app 等等,该分区只有 ROOT 权限的用户才能访问,这也就是为什么在没有 Root 手机之前,我们没法删除系统出场的 APP 的原因了。
  2. 解压缩 APK,拷贝文件,创建应用的数据目录:为了加快 APP 的启动速度,APK 在安装的时候,会首先将 APP 的可执行文件 dex 拷贝到 /data/dalvik-cache 目录,缓存起来。然后,在 /data/data/ 目录下创建应用程序的数据目录 (以应用的包名命名),存放在应用的相关数据,如数据库、XML 文件、Cache、二进制的 so 动态库等。
  3. 解析 APK 的 AndroidManifest.xml 文件。
    public PackageManagerService(Context context, Installer installer,boolean factoryTest, boolean onlyCore) {// ....synchronized (mInstallLock) {synchronized (mPackages) {// Expose private service for system components to use.LocalServices.addService(PackageManagerInternal.class, new PackageManagerInternalImpl());sUserManager = new UserManagerService(context, this,new UserDataPreparer(mInsstaller, mInstallLock, mContext, mOnlyCore), mPackages);mPermissionManager = PermissionManagerService.create(context,new DefaultPermissionGrantedCallback() {@Overridepublic void onDefaultRuntimePermissionsGranted(int userId) {synchronized(mPackages) {mSettings.onDefaultRuntimePermissionsGrantedLPr(userId);}}}, mPackages /*externalLock*/);mDefaultPermissionPolicy = mPermissionManager.getDefaultPermissionGrantPolicy();mSettings = new Settings(mPermissionManager.getPermissionSettings(), mPackages);}}// ...mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context, "*dexopt*");DexManager.Listener dexManagerListener = DexLogger.getListener(this, installer, mInstallLock);mDexManager = new DexManager(mContext, this, mPackageDexOptimizer, installer, mInstallLock, dexManagerListener);mArtManagerService = new ArtManagerService(mContext, this, installer, mInstallLock);// ...synchronized (mInstallLock) {synchronized (mPackages) {// 创建消息mHandlerThread = new ServiceThread(TAG,Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);mHandlerThread.start();mHandler = new PackageHandler(mHandlerThread.getLooper());// ...// 扫描各个目录获取 APK 文件:VENDOR_OVERLAY_DIR           // framework 文件夹:frameworkDir// 系统文件夹:privilegedAppDir systemAppDir// 供应商的包:Environment.getVendorDirectory()// 原始设备制造商的包 :Environment.getOdmDirectory()// 原始设计商的包:Environment.getOdmDirectory()// 原始产品的包:// ....mInstallerService = new PackageInstallerService(context, this);final Pair<ComponentName, String> instantAppResolverComponent = getInstantAppResolverLPr();if (instantAppResolverComponent != null) {mInstantAppResolverConnection = new InstantAppResolverConnection(mContext, instantAppResolverComponent.first,instantAppResolverComponent.second);mInstantAppResolverSettingsComponent =getInstantAppResolverSettingsLPr(instantAppResolverComponent.first);} else {mInstantAppResolverConnection = null;mInstantAppResolverSettingsComponent = null;}updateInstantAppInstallerLocked(null);final Map<Integer, List<PackageInfo>> userPackages = new HashMap<>();final int[] currentUserIds = UserManagerService.getInstance().getUserIds();for (int userId : currentUserIds) {userPackages.put(userId, getInstalledPackages(/*flags*/ 0, userId).getList());}mDexManager.load(userPackages);} // synchronized (mPackages)} // synchronized (mInstallLock)// ....}

在构造方法中会扫描多个目录来获取 APK 文件,上述注释中我们已经给出了这些目录,及其获取的方式。当扫描一个路径的时候会使用 scanDirLI() 方法来完成扫描工作。

    private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime) {final File[] files = scanDir.listFiles();if (ArrayUtils.isEmpty(files)) {return;}try (ParallelPackageParser parallelPackageParser = new ParallelPackageParser(mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir, mParallelPackageParserCallback)) {int fileCount = 0;for (File file : files) {final boolean isPackage = (isApkFile(file) || file.isDirectory())&& !PackageInstallerService.isStageName(file.getName());if (!isPackage) {continue;}// 提交文件用来解析parallelPackageParser.submit(file, parseFlags);fileCount++;}for (; fileCount > 0; fileCount--) {// 获取解析的结果,即从队列阻塞队列中获取解析的结果ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();// ...if (throwable == null) {// TODO(toddke): move lower in the scan chain// Static shared libraries have synthetic package namesif (parseResult.pkg.applicationInfo.isStaticSharedLibrary()) {renameStaticSharedLibraryPackage(parseResult.pkg);}try {if (errorCode == PackageManager.INSTALL_SUCCEEDED) {scanPackageChildLI(parseResult.pkg, parseFlags, scanFlags, currentTime, null);}} catch (PackageManagerException e) {errorCode = e.error;}}// 。。。}}}

从上面的代码中可以看出,提交文件来解析以及获取解析都是通过 ParallelPackageParser 来完成的。它使用 submit() 方法来提交文件用来解析,使用 take() 方法获取解析的结果。这两个方法的定义如下,

    public void submit(File scanFile, int parseFlags) {mService.submit(() -> {ParseResult pr = new ParseResult();try {PackageParser pp = new PackageParser();pp.setSeparateProcesses(mSeparateProcesses);pp.setOnlyCoreApps(mOnlyCore);pp.setDisplayMetrics(mMetrics);pp.setCacheDir(mCacheDir);pp.setCallback(mPackageParserCallback);pr.scanFile = scanFile;pr.pkg = parsePackage(pp, scanFile, parseFlags);} catch (Throwable e) {pr.throwable = e;}try {mQueue.put(pr);} catch (InterruptedException e) {Thread.currentThread().interrupt();mInterruptedInThread = Thread.currentThread().getName();}});}public ParseResult take() {try {if (mInterruptedInThread != null) {throw new InterruptedException("Interrupted in " + mInterruptedInThread);}return mQueue.take();} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new IllegalStateException(e);}}

submit() 方法使用一个线程池来执行任务,也就是上面的 mService。它会将要解析的信息封装成 PackageParser 对象,然后把解析的结果信息封装成 ParseResult 放进一个阻塞队列中。当调用 take() 方法的时候会从该阻塞队列中获取解析的结果。

包信息的解析最终是通过 PackageParser 的 parsePackage() 方法来完成的。其定义如下,

    public Package parsePackage(File packageFile, int flags, boolean useCaches)throws PackageParserException {Package parsed = useCaches ? getCachedResult(packageFile, flags) : null;if (parsed != null) {return parsed;}long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;if (packageFile.isDirectory()) {parsed = parseClusterPackage(packageFile, flags);} else {// 是文件,所以走这条路线parsed = parseMonolithicPackage(packageFile, flags);}long cacheTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;cacheResult(packageFile, flags, parsed);return parsed;}

我们会在这方法中进入到 parseMonolithicPackage() 来对文件进行解析。

    public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {final PackageLite lite = parseMonolithicPackageLite(apkFile, flags);final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags);try {// 解析final Package pkg = parseBaseApk(apkFile, assetLoader.getBaseAssetManager(), flags);pkg.setCodePath(apkFile.getCanonicalPath());pkg.setUse32bitAbi(lite.use32bitAbi);return pkg;} catch (IOException e) {throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION);} finally {IoUtils.closeQuietly(assetLoader);}}

在这个方法中会使用 parseBaseApk() 来对 APK 文件进行解析,

    private Package parseBaseApk(File apkFile, AssetManager assets, int flags)throws PackageParserException {final String apkPath = apkFile.getAbsolutePath();String volumeUuid = null;if (apkPath.startsWith(MNT_EXPAND)) {final int end = apkPath.indexOf('/', MNT_EXPAND.length());volumeUuid = apkPath.substring(MNT_EXPAND.length(), end);}mParseError = PackageManager.INSTALL_SUCCEEDED;mArchiveSourcePath = apkFile.getAbsolutePath();XmlResourceParser parser = null;try {final int cookie = assets.findCookieForPath(apkPath);// 读取 AndroidManifest.xmlparser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);final Resources res = new Resources(assets, mMetrics, null);final String[] outError = new String[1];// 在这里进一步解析 Manifest 的各种信息final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError);pkg.setVolumeUuid(volumeUuid);pkg.setApplicationVolumeUuid(volumeUuid);pkg.setBaseCodePath(apkPath);pkg.setSigningDetails(SigningDetails.UNKNOWN);return pkg;} catch (PackageParserException e) {throw e;} catch (Exception e) {throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION);} finally {IoUtils.closeQuietly(parser);}}

这里的 ANDROID_MANIFEST_FILENAME 是一个字符串,这个字符串的定义是 AndroidManifest.xml,所以,我们找到了解析 Manifest 的地方。

然后方法会进入到 parseBaseApk() 方法中进一步对 Manifest 进行解析。其读取操作就是基本的 XML 解析的过程。它会使用内部定义的字符串常量从 Manifest 中获取应用的版本还有四大组件等信息。

解析完了 APK 之后会一路经过 return 语句返回到 scanDirLI() 方法中,当从阻塞队列中取出 Package 之后将会调用 scanPackageChildLI() 在该方法中会将解析的出的 APK 信息缓存到 PMS 中。

这样,在系统启动之后 PMS 就解析了全部的 APK 文件,并将其缓存到了 PMS 中。这样这些应用程序还无法展示给用户,所以需要 Launcher 桌面程序从 PMS 中获取安装包信息并展示到桌面上。

2、应用安装的过程

虽然 PMS 用来负责应用的安装和卸载,但是真实的工作却是交给 installd 来实现的。 installd 是在系统启动的时候,由 init 进程解析 init.rc 文件创建的。在早期版本的 Android 中,它使用 Socket 与 Java 层的 Installer 进行通信。在 9.0 的代码中,它使用 Binder 与 Java 层的 Installer 进行通信。当启动 Installd 的时候,将会调用其 main 方法,

int main(const int argc, char *argv[]) {return android::installd::installd_main(argc, argv);
}static int installd_main(const int argc ATTRIBUTE_UNUSED, char *argv[]) {int ret;int selinux_enabled = (is_selinux_enabled() > 0);setenv("ANDROID_LOG_TAGS", "*:v", 1);android::base::InitLogging(argv);SLOGI("installd firing up");union selinux_callback cb;cb.func_log = log_callback;selinux_set_callback(SELINUX_CB_LOG, cb);// 初始化全局信息if (!initialize_globals()) {exit(1);}// 初始化相关目录if (initialize_directories() < 0) {exit(1);}if (selinux_enabled && selinux_status_open(true) < 0) {exit(1);}if ((ret = InstalldNativeService::start()) != android::OK) {exit(1);}// 加入到 Binder 线程池当中IPCThreadState::self()->joinThreadPool();LOG(INFO) << "installd shutting down";return 0;
}

在启动 Installd 的时候会初始化各种相关的目录,这部分内容就不展开了。然后,它会调用 IPCThreadState::self()->joinThreadPool() 一行来将当前线程池加入到 Binder 线程池当中等待通信。

当 Java 层的 Installer 需要与之通信的时候,会调用 connect() 方法与之建立联系。其源码如下,这里会通过 ServiceManager 获取 installd 服务,然后将其转换成本地的服务进行 IPC 的调用。

    private void connect() {// 获取远程服务 IBinder binder = ServiceManager.getService("installd");if (binder != null) {try {binder.linkToDeath(new DeathRecipient() {@Overridepublic void binderDied() {connect();}}, 0);} catch (RemoteException e) {binder = null;}}if (binder != null) {// 转成本地服务进行 IPC 调用mInstalld = IInstalld.Stub.asInterface(binder);try {invalidateMounts();} catch (InstallerException ignored) {}} else {// 重连BackgroundThread.getHandler().postDelayed(() -> {connect();}, DateUtils.SECOND_IN_MILLIS);}}

Installer 与 PMC 类似,也是一种系统服务,它的启动的时刻与 PMS 基本一致,位于同一个方法中,并且其启动时刻位于 PMS 之前。

2、从 ADB 安装的过程

另外
有什么技术问题欢迎加我交流 qilebeaf
本人10多年大厂软件开发经验,精通Android,Java,Python,前端等开发,空余时间承接软件开发设计、课程设计指导、解决疑难bug、AI大模型搭建,AI绘图应用等。
欢迎砸单

相关文章:

Android应用安装过程

Android 系统源码源码-应用安装过程 Android 中应用安装的过程就是解析 AndroidManifest.xml 的过程&#xff0c;系统可以从 Manifest 中得到应用程序的相关信息&#xff0c;比如 Activity、Service、Broadcast Receiver 和 ContentProvider 等。这些工作都是由 PackageManage…...

Word中输入文字时,后面的文字消失

当在Word中输入文字时&#xff0c;如果发现后面的文字消失&#xff0c;通常是由以下3个原因造成的&#xff1a; 检查Insert键状态&#xff1a;首先确认是否误按了Insert键。如果是&#xff0c;请再次按下Insert键以切换回插入模式。在插入模式下&#xff0c;新输入的文字会插入…...

【LeetCode】合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 解题思路 水题&#xff0c;主要用于后面的链表的归并排序做了该题 AC代码 # Definition for singly-linked list. # class ListNode: # def __init__(self, val0, nex…...

分子AI预测赛Task1笔记

分子AI预测赛Task1笔记 实践步骤&#xff1a;跑通baseline → 尝试个人idea→尝试进阶baseline 一、跑通baseline 1、应当先下载数据库 下载相应的数据库 !pip install lightgbm openpyxl2、训练模型并预测结果 首先要导入相应的库和方法类&#xff0c;如pandas等 # 1. …...

ubuntu 安装并启用 samba

环境&#xff1a;ubuntu server 24.04 步骤如下&#xff1a; sudo apt update sudo apt install samba修改配置文件&#xff1a; sudo vi /etc/samba/smb.conf新增内容&#xff1a; [username]path /home/[username]available yesvalid users [username]read only nobrow…...

atcoder ABC 357-D题详解

atcoder ABC 357-D题详解 Problem Statement For a positive integer N, let VN​ be the integer formed by concatenating N exactly N times. More precisely, consider N as a string, concatenate N copies of it, and treat the result as an integer to get VN​. For…...

从单一到多元:EasyCVR流媒体视频汇聚技术推动安防监控智能升级

随着科技的飞速发展&#xff0c;视频已成为我们日常生活和工作中的重要组成部分。尤其在远程办公、在线教育、虚拟会议等领域&#xff0c;视频的应用愈发广泛。为了满足日益增长的视频需求&#xff0c;流媒体视频汇聚融合技术应运而生&#xff0c;它不仅改变了传统视频的观看和…...

Spring MVC数据绑定和响应——数据回写(二)JSON数据的回写

项目中已经导入了Jackson依赖&#xff0c;可以先调用Jackson的JSON转换的相关方法&#xff0c;将对象或集合转换成JSON数据&#xff0c;然后通过HttpServletResponse将JSON数据写入到输出流中完成回写&#xff0c;具体步骤如下。 1、修改文件DataController.java&#xff0c;在…...

怎么快速给他人分享图片?扫描二维码看图的简单做法

现在通过二维码来查看图片是一种很常见的方法&#xff0c;通过二维码来查看图片不仅能够减少对手机存储空间的占用&#xff0c;而且获取图片变得更加方便快捷&#xff0c;只需要扫码就能够查看图片&#xff0c;有利于图片的展现。很多的场景中都有图片二维码的应用&#xff0c;…...

【UML用户指南】-26-对高级行为建模-状态图

目录 1、概念 2、组成结构 3、一般用法 4、常用建模技术 4.1、对反应型对象建模 一个状态图显示了一个状态机。在为对象的生命期建模中 活动图展示的是跨过不同的对象从活动到活动的控制流 状态图展示的是单个对象内从状态到状态的控制流。 在UML中&#xff0c;用状态图…...

解决VSCode无法用ssh连接远程服务器的问题

原因&#xff1a; 因为windows自带的ssh无法连接远程服务器&#xff0c;需要用git底下的ssh.exe。 搜了很久&#xff0c;试过很多方法&#xff0c;包括替换掉环境变量中的ssh&#xff0c;但是都无效&#xff0c;最后发现是要在VSCode中配置需要使用哪个ssh.exe。 步骤&#…...

【区块链+基础设施】银联云区块链服务 | FISCO BCOS应用案例

为了顺应区块链基础设施化的发展趋势&#xff0c;中国银联推出了银联云区块链服务——UPBaaS&#xff0c;为金融行业采用区块链 技术提出了解决方案&#xff0c;微众银行为平台提供 FISCO BCOS 区块链开源技术支持。通过银联云区块链服务&#xff0c;用户可 以用可视化的方式创…...

Java SE入门及基础(61) 死锁 死锁发生条件

目录 死锁 1. 死锁的概念 2. 死锁发生条件 互斥条件 不可剥夺条件 请求与保持条件 循环等待 3. 案例分析 示例 分析 死锁 1. 死锁的概念 Deadlock describes a situation where two or more threads are blocked forever, waiting for each other 死锁描述了一种情…...

简单爬虫案例——爬取快手视频

网址&#xff1a;aHR0cHM6Ly93d3cua3VhaXNob3UuY29tL3NlYXJjaC92aWRlbz9zZWFyY2hLZXk9JUU2JThCJTg5JUU5JTlEJUEy 找到视频接口&#xff1a; 视频链接在photourl中 完整代码&#xff1a; import requestsimport re url https://www.kuaishou.com/graphql cookies {did: web_…...

42、nginx之nginx.conf

nginx----web服务器 一、nginx http就是apache&#xff0c;在国内很少。 nginx是开源的&#xff0c;是一款高性能&#xff0c;轻量级的web服务软件。 稳定性高&#xff0c;而且版本迭代比较快&#xff08;修复bug速度比较快&#xff0c;安全性快&#xff09; 消耗系统资源…...

高薪程序员必修课-java为什么要用并发编程

目录 前言 1. 提高性能和效率 2. 更好地响应用户 3. 优化I/O操作 具体示例 示例1&#xff1a;提高性能和效率 示例2&#xff1a;更好地响应用户 示例3&#xff1a;优化I/O操作 总结 前言 并发编程允许多个线程在同一时间执行任务。下面我们从多个原理角度来解释为什么J…...

postgreSQL学习

postgreSql学习 学习参考&#xff1a;1、命令1.1 登录1.2 关闭连接 2、常用数据类型2.1 数值类型2.2 字符串类型2.3 时间2.4 其他 3、自增主键4、sql4.1 库操作&#xff08;1&#xff09;创建新库&#xff08;2&#xff09;切换数据库&#xff08;3&#xff09;删库【谨慎&…...

【3】系统标定

文章目录 雷达标定相机主雷达标定底盘动力学标定车辆循迹验证建图 雷达标定 主要是为了获得到lidar到imu的tf关系。imu为父坐标lidar为子坐标。其他雷达标定到主lidar坐标系下。 标定的结果都是生成一个是四元数。 #mermaid-svg-crOWRnT4UE0jtJVy {font-family:"trebuch…...

网安小贴士(3)网安协议

一、前言 网络安全协议是构建安全网络环境的基础&#xff0c;它们帮助保护网络通信免受各种威胁和攻击。 二、定义 网络安全协议是指在计算机网络中用于确保网络通信和数据传输安全的协议。它们定义了在网络通信过程中的安全机制、加密算法、认证和授权流程等&#xff0c;以保…...

大数据面试题之HBase(1)

目录 介绍下HBase HBase优缺点 说下HBase原理 介绍下HBase架构 HBase读写数据流程 HBase的读写缓存 在删除HBase中的一个数据的时候&#xff0c;它什么时候真正的进行删除呢?当你进行删除操作&#xff0c;它是立马就把数据删除掉了吗? HBase中的二级索引 HBa…...

智慧医疗能源事业线深度画像分析(上)

引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...

测试markdown--肇兴

day1&#xff1a; 1、去程&#xff1a;7:04 --11:32高铁 高铁右转上售票大厅2楼&#xff0c;穿过候车厅下一楼&#xff0c;上大巴车 &#xffe5;10/人 **2、到达&#xff1a;**12点多到达寨子&#xff0c;买门票&#xff0c;美团/抖音&#xff1a;&#xffe5;78人 3、中饭&a…...

TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案

一、TRS收益互换的本质与业务逻辑 &#xff08;一&#xff09;概念解析 TRS&#xff08;Total Return Swap&#xff09;收益互换是一种金融衍生工具&#xff0c;指交易双方约定在未来一定期限内&#xff0c;基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)

骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术&#xff0c;它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton)&#xff1a;由层级结构的骨头组成&#xff0c;类似于人体骨骼蒙皮 (Mesh Skinning)&#xff1a;将模型网格顶点绑定到骨骼上&#xff0c;使骨骼移动…...

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题

【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要&#xff1a; 近期&#xff0c;在使用较新版本的OpenSSH客户端连接老旧SSH服务器时&#xff0c;会遇到 "no matching key exchange method found"​, "n…...

Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案

在大数据时代&#xff0c;海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构&#xff0c;在处理大规模数据抓取任务时展现出强大的能力。然而&#xff0c;随着业务规模的不断扩大和数据抓取需求的日益复杂&#xff0c;传统…...

论文阅读:Matting by Generation

今天介绍一篇关于 matting 抠图的文章&#xff0c;抠图也算是计算机视觉里面非常经典的一个任务了。从早期的经典算法到如今的深度学习算法&#xff0c;已经有很多的工作和这个任务相关。这两年 diffusion 模型很火&#xff0c;大家又开始用 diffusion 模型做各种 CV 任务了&am…...

背包问题双雄:01 背包与完全背包详解(Java 实现)

一、背包问题概述 背包问题是动态规划领域的经典问题&#xff0c;其核心在于如何在有限容量的背包中选择物品&#xff0c;使得总价值最大化。根据物品选择规则的不同&#xff0c;主要分为两类&#xff1a; 01 背包&#xff1a;每件物品最多选 1 次&#xff08;选或不选&#…...

Axure零基础跟我学:展开与收回

亲爱的小伙伴,如有帮助请订阅专栏!跟着老师每课一练,系统学习Axure交互设计课程! Axure产品经理精品视频课https://edu.csdn.net/course/detail/40420 课程主题:Axure菜单展开与收回 课程视频:...

【Java】Ajax 技术详解

文章目录 1. Filter 过滤器1.1 Filter 概述1.2 Filter 快速入门开发步骤:1.3 Filter 执行流程1.4 Filter 拦截路径配置1.5 过滤器链2. Listener 监听器2.1 Listener 概述2.2 ServletContextListener3. Ajax 技术3.1 Ajax 概述3.2 Ajax 快速入门服务端实现:客户端实现:4. Axi…...