Android系统-包管理机制(一)PMS服务启动

Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
mSystemServiceManager.startService(LightsService.class);
mDisplayManagerService = mSystemServiceManager.startService(DisplayManagerService.class);
mSystemServiceManager.startBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);

// Only run “core” apps if we’re encrypting the device.
// 表示加密了设备,这时mOnlyCore的值为true,表示只运行“核心”程序,创建一个极简的启动环境。
String cryptState = SystemProperties.get(“vold.decrypt”);

mIsAlarmBoot = SystemProperties.getBoolean(“ro.alarm_boot”, false);
if (ENCRYPTING_STATE.equals(cryptState)) {
Slog.w(TAG, “Detected encryption in progress - only parsing core apps”);
mOnlyCore = true;
} else if (ENCRYPTED_STATE.equals(cryptState)) {
Slog.w(TAG, “Device encrypted - only parsing core apps”);
mOnlyCore = true;
} else if (mIsAlarmBoot) {
mOnlyCore = true;
}

if (RegionalizationEnvironment.isSupported()) {
Slog.i(TAG, “Regionalization Service”);
RegionalizationService regionalizationService = new RegionalizationService();
ServiceManager.addService(“regionalization”, regionalizationService);
}

// Start the package manager.
//启动PMS
traceBeginAndSlog(“StartPackageManagerService”);
//创建PMS
mPackageManagerService = PackageManagerService.main(mSystemContext, installer,
mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
// 表示PMS是否首次被启动,这个参数会在WMS创建时使用
mFirstBoot = mPackageManagerService.isFirstBoot();
mPackageManager = mSystemContext.getPackageManager();
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);

if (!mOnlyCore) {
boolean disableOtaDexopt = SystemProperties.getBoolean(“config.disable_otadexopt”,
false);
if (!disableOtaDexopt) {
traceBeginAndSlog(“StartOtaDexOptService”);
try {
OtaDexoptService.main(mSystemContext, mPackageManagerService);
} catch (Throwable e) {
reportWtf(“starting OtaDexOptService”, e);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
}

traceBeginAndSlog(“StartUserManagerService”);
mSystemServiceManager.startService(UserManagerService.LifeCycle.class);
Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
AttributeCache.init(mSystemContext);
mActivityManagerService.setSystemProcess();
startSensorService();
}

PMS

构造方法

frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java

public static PackageManagerService main(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
// Self-check for initial settings.
PackageManagerServiceCompilerMapping.checkProperties();
//初始化PMS
PackageManagerService m = new PackageManagerService(context, installer,
factoryTest, onlyCore);
m.enableSystemUserPackages();
//将package服务注册到ServiceManager大管家
ServiceManager.addService(“package”, m);
return m;
}

PMS的构造方法一共有700多行,在代码中,将PMS的构造流程分为了5个阶段,每个阶段会使用EventLog.writeEvent打印系统日志。

  1. BOOT_PROGRESS_PMS_START(开始阶段)
  2. BOOT_PROGRESS_PMS_SYSTEM_SCAN_START(扫描系统阶段)
  3. BOOT_PROGRESS_PMS_DATA_SCAN_START(扫描Data分区阶段)
  4. BOOT_PROGRESS_PMS_SCAN_END(扫描结束阶段)
  5. BOOT_PROGRESS_PMS_READY(准备阶段)

1、开始阶段

BOOT_PROGRESS_PMS_START

public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
//打印开始日志
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_START,
SystemClock.uptimeMillis());

if (mSdkVersion <= 0) {
Slog.w(TAG, “**** ro.build.version.sdk not set!”);
}

mContext = context;

mPermissionReviewRequired = context.getResources().getBoolean(
R.bool.config_permissionReviewRequired);

mFactoryTest = factoryTest;
mOnlyCore = onlyCore;
//用于存储屏幕的相关信息
mMetrics = new DisplayMetrics();
//创建Settings对象 (1)
mSettings = new Settings(mPackages);
// 添加system, phone, log, nfc, bluetooth, shell这六种shareUserId到mSettings;
mSettings.addSharedUserLPw(“android.uid.system”, Process.SYSTEM_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw(“android.uid.phone”, RADIO_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw(“android.uid.log”, LOG_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw(“android.uid.nfc”, NFC_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw(“android.uid.bluetooth”, BLUETOOTH_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw(“android.uid.shell”, SHELL_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

mInstaller = installer;
//创建Dex优化工具类
mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context,
dexopt”);
mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());

mOnPermissionChangeListeners = new OnPermissionChangeListeners(
FgThread.get().getLooper());

getDefaultDisplayMetrics(context, mMetrics);
//得到全局系统配置信息
SystemConfig systemConfig = SystemConfig.getInstance();
//获取全局的groupId
mGlobalGids = systemConfig.getGlobalGids();
//获取系统权限
mSystemPermissions = systemConfig.getSystemPermissions();
mAvailableFeatures = systemConfig.getAvailableFeatures();

mProtectedPackages = new ProtectedPackages(mContext);

//安装APK时需要的锁,保护所有对installd的访问。
synchronized (mInstallLock) {
//更新APK时需要的锁,保护内存中已经解析的包信息等内容
synchronized (mPackages) {
//创建后台线程ServiceThread
mHandlerThread = new ServiceThread(TAG,
Process.THREAD_PRIORITY_BACKGROUND, true /allowIo/);
mHandlerThread.start();
//创建PackageHandler绑定到ServiceThread的消息队列
mHandler = new PackageHandler(mHandlerThread.getLooper());
mProcessLoggingHandler = new ProcessLoggingHandler();
//将PackageHandler添加到Watchdog的检测集中
Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);

mDefaultPermissionPolicy = new DefaultPermissionGrantPolicy(this);

//在Data分区创建一些目录
File dataDir = Environment.getDataDirectory();
mAppInstallDir = new File(dataDir, “app”);
mAppLib32InstallDir = new File(dataDir, “app-lib”);
mEphemeralInstallDir = new File(dataDir, “app-ephemeral”);
mAsecInternalPath = new File(dataDir, “app-asec”).getPath();
mDrmAppPrivateInstallDir = new File(dataDir, “app-private”);
mRegionalizationAppInstallDir = new File(dataDir, “app-regional”);

//创建多用户管理服务
sUserManager = new UserManagerService(context, this, mPackages);

mFoundPolicyFile = SELinuxMMAC.readInstallPolicy();

//解析packages.xml等文件的信息,保存到Settings的对应字段中。packages.xml中记录系统中所有安装的应用信息,包括基本信息、签名和权限。如果packages.xml有安装的应用信息,readLPw方法会返回true,mFirstBoot的值为false,说明PMS不是首次被启动。
mFirstBoot = !mSettings.readLPw(sUserManager.getUsers(false));

在开始阶段,创建了很多PMS中的关键对象并赋值给PMS中的成员变量

mSettings

用于保存所有包的动态设置。

Settings(Object lock) {
this(Environment.getDataDirectory(), lock);
}

Settings(File dataDir, Object lock) {
mLock = lock;

mRuntimePermissionsPersistence = new RuntimePermissionPersistence(mLock);

mSystemDir = new File(dataDir, “system”);
mSystemDir.mkdirs(); //创建/data/system
FileUtils.setPermissions(mSystemDir.toString(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG
|FileUtils.S_IROTH|FileUtils.S_IXOTH,
-1, -1);
mSettingsFilename = new File(mSystemDir, “packages.xml”);
mBackupSettingsFilename = new File(mSystemDir, “packages-backup.xml”);
mPackageListFilename = new File(mSystemDir, “packages.list”);
FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID);

mStoppedPackagesFilename = new File(mSystemDir, “packages-stopped.xml”);
mBackupStoppedPackagesFilename = new File(mSystemDir, “packages-stopped-backup.xml”);
}

此处mSystemDir是指目录/data/system,在该目录有以下5个文件:

文件功能
packages.xml记录所有安装app的信息
packages-backup.xml备份文件
packages-stopped.xml记录系统被强制停止的文件
packages-stopped-backup.xml备份文件
packages.list记录应用的数据信息
mInstaller

Installer继承自SystemService,和PMS、AMS一样是系统的服务(引导服务),PMS很多的操作都是由Installer来完成的,比如APK的安装和卸载。在Installer内部,通过socket与installd通信,(貌似8.0以上改成了IInstalld和installd进行Binder通信),由位于nativie层的installd来完成具体的操作。

systemConfig

用于得到全局系统配置信息。比如系统的权限就可以通过SystemConfig来获取。

mPackageDexOptimizer

Dex优化的工具类。

mHandler(PackageHandler类型)

PackageHandler继承自Handler,PMS通过PackageHandler驱动APK的复制和安装工作。 PackageHandler处理的消息队列如果过于繁忙,有可能导致系统卡住, 因此将它添加到Watchdog的监测集中。 Watchdog主要有两个用途,一个是定时检测系统关键服务(AMS和WMS等)是否可能发生死锁,还有一个是定时检测线程的消息队列是否长时间处于工作状态(可能阻塞等待了很长时间)。如果出现上述问题,Watchdog会将日志保存起来,必要时还会杀掉自己所在的进程,也就是SystemServer进程。

sUserManager(UserManagerService类型)

多用户管理服务。

2、扫描系统阶段

//打印扫描系统阶段日志
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SYSTEM_SCAN_START,
startTime);

// Set flag to monitor and not change apk file paths when
// scanning install directories.
final int scanFlags = SCAN_NO_PATHS | SCAN_DEFER_DEX | SCAN_BOOTING | SCAN_INITIAL;

final String bootClassPath = System.getenv(“BOOTCLASSPATH”);
final String systemServerClassPath = System.getenv(“SYSTEMSERVERCLASSPATH”);

if (bootClassPath == null) {
Slog.w(TAG, “No BOOTCLASSPATH found!”);
}

if (systemServerClassPath == null) {
Slog.w(TAG, “No SYSTEMSERVERCLASSPATH found!”);
}

final List allInstructionSets = InstructionSets.getAllInstructionSets();
final String[] dexCodeInstructionSets =
getDexCodeInstructionSets(
allInstructionSets.toArray(new String[allInstructionSets.size()]));

/**

  • Ensure all external libraries have had dexopt run on them.
    */
    if (mSharedLibraries.size() > 0) {
    // NOTE: For now, we’re compiling these system “shared libraries”
    // (and framework jars) into all available architectures. It’s possible
    // to compile them only when we come across an app that uses them (there’s
    // already logic for that in scanPackageLI) but that adds some complexity.
    for (String dexCodeInstructionSet : dexCodeInstructionSets) {
    for (SharedLibraryEntry libEntry : mSharedLibraries.values()) {
    final String lib = libEntry.path;
    if (lib == null) {
    continue;
    }

try {
// Shared libraries do not have profiles so we perform a full
// AOT compilation (if needed).
int dexoptNeeded = DexFile.getDexOptNeeded(
lib, dexCodeInstructionSet,
getCompilerFilterForReason(REASON_SHARED_APK),
false /* newProfile */);
if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) {
mInstaller.dexopt(lib, Process.SYSTEM_UID, dexCodeInstructionSet,
dexoptNeeded, DEXOPT_PUBLIC /dexFlags/,
getCompilerFilterForReason(REASON_SHARED_APK),
StorageManager.UUID_PRIVATE_INTERNAL,
SKIP_SHARED_LIBRARY_CHECK);
}
} catch (FileNotFoundException e) {
Slog.w(TAG, "Library not found: " + lib);
} catch (IOException | InstallerException e) {
Slog.w(TAG, "Cannot dexopt " + lib + "; is it an APK or JAR? "

  • e.getMessage());
    }
    }
    }
    }

//在/system中创建framework目录
File frameworkDir = new File(Environment.getRootDirectory(), “framework”);

final VersionInfo ver = mSettings.getInternalVersion();
mIsUpgrade = !Build.FINGERPRINT.equals(ver.fingerprint);

// when upgrading from pre-M, promote system app permissions from install to runtime
mPromoteSystemApps =
mIsUpgrade && ver.sdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1;

// When upgrading from pre-N, we need to handle package extraction like first boot,
// as there is no profiling data available.
mIsPreNUpgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N;

mIsPreNMR1Upgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N_MR1;

// save off the names of pre-existing system packages prior to scanning; we don’t
// want to automatically grant runtime permissions for new system apps
if (mPromoteSystemApps) {
Iterator pkgSettingIter = mSettings.mPackages.values().iterator();
while (pkgSettingIter.hasNext()) {
PackageSetting ps = pkgSettingIter.next();
if (isSystemApp(ps)) {
mExistingSystemPackages.add(ps.name);
}
}
}

// Collect vendor overlay packages. (Do this before scanning any apps.)
// For security and version matching reason, only consider
// overlay packages if they reside in the right directory.
String overlayThemeDir = SystemProperties.get(VENDOR_OVERLAY_THEME_PROPERTY);
//扫描/vendor/overlay目录下的文件
if (!overlayThemeDir.isEmpty()) {
scanDirTracedLI(new File(VENDOR_OVERLAY_DIR, overlayThemeDir), mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_TRUSTED_OVERLAY, scanFlags | SCAN_TRUSTED_OVERLAY, 0);
}
scanDirTracedLI(new File(VENDOR_OVERLAY_DIR), mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_TRUSTED_OVERLAY, scanFlags | SCAN_TRUSTED_OVERLAY, 0);

// Find base frameworks (resource packages without code).
//收集包名:/system/framework
scanDirTracedLI(frameworkDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_IS_PRIVILEGED,
scanFlags | SCAN_NO_DEX, 0);

// Collected privileged system packages.
//收集私有的系统包名:/system/priv-app
final File privilegedAppDir = new File(Environment.getRootDirectory(), “priv-app”);
scanDirTracedLI(privilegedAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_IS_PRIVILEGED, scanFlags, 0);

// Collect ordinary system packages.
//收集一般的系统包名:/system/app
final File systemAppDir = new File(Environment.getRootDirectory(), “app”);
scanDirTracedLI(systemAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

// Collect all vendor packages.
//收集所有的供应商包名:/vendor/app
File vendorAppDir = new File(“/vendor/app”);
try {
vendorAppDir = vendorAppDir.getCanonicalFile();
} catch (IOException e) {
// failed to look up canonical path, continue with original one
}
scanDirTracedLI(vendorAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

// Collect all OEM packages.
//收集所有OEM包名:/oem/app
final File oemAppDir = new File(Environment.getOemDirectory(), “app”);
scanDirTracedLI(oemAppDir, mDefParseFlags
| PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

// Collect all Regionalization packages form Carrier’s res packages.
if (RegionalizationEnvironment.isSupported()) {
Log.d(TAG, “Load Regionalization vendor apks”);
final List RegionalizationDirs =
RegionalizationEnvironment.getAllPackageDirectories();
for (File f : RegionalizationDirs) {
File RegionalizationSystemDir = new File(f, “system”);
// Collect packages in /system/priv-app
scanDirLI(new File(RegionalizationSystemDir, “priv-app”),
PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR
| PackageParser.PARSE_IS_PRIVILEGED, scanFlags, 0);
// Collect packages in /system/app
scanDirLI(new File(RegionalizationSystemDir, “app”),
PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR,
scanFlags, 0);
// Collect overlay in /system/vendor
scanDirLI(new File(RegionalizationSystemDir, “vendor/overlay”),
PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR,
scanFlags | SCAN_TRUSTED_OVERLAY, 0);
}
}

// Prune any system packages that no longer exist.
// 这个列表代表有可能有升级包的系统App
final List possiblyDeletedUpdatedSystemApps = new ArrayList();
if (!mOnlyCore) {
Iterator psit = mSettings.mPackages.values().iterator();
while (psit.hasNext()) {
PackageSetting ps = psit.next();

/*

  • If this is not a system app, it can’t be a
  • disable system app.
    */
    if ((ps.pkgFlags & ApplicationInfo.FLAG_SYSTEM) == 0) {
    continue;
    }

/*

  • If the package is scanned, it’s not erased.
    /
    final PackageParser.Package scannedPkg = mPackages.get(ps.name);
    if (scannedPkg != null) {
    /
  • If the system app is both scanned and in the
  • disabled packages list, then it must have been
  • added via OTA. Remove it from the currently
  • scanned package so the previously user-installed
  • application can be scanned.
    */
    if (mSettings.isDisabledSystemPackageLPr(ps.name)) { //1
    logCriticalInfo(Log.WARN, "Expecting better updated system app for "
  • ps.name + “; removing system app. Last known codePath=”
  • ps.codePathString + “, installStatus=” + ps.installStatus
  • “, versionCode=” + ps.versionCode + “; scanned versionCode=”
  • scannedPkg.mVersionCode);
    //将这个系统App的PackageSetting从PMS的mPackages中移除
    removePackageLI(scannedPkg, true);
    //将升级包的路径添加到mExpectingBetter列表中
    mExpectingBetter.put(ps.name, ps.codePath);
    }

continue;
}

if (!mSettings.isDisabledSystemPackageLPr(ps.name)) {
psit.remove();
logCriticalInfo(Log.WARN, "System package " + ps.name

  • " no longer exists; it’s data will be wiped");
    // Actual deletion of code and data will be handled by later
    // reconciliation step
    } else {
    final PackageSetting disabledPs = mSettings.getDisabledSystemPkgLPr(ps.name);
    //这个系统App升级包信息在mDisabledSysPackages中,但是没有发现这个升级包存在
    if (disabledPs.codePath == null || !disabledPs.codePath.exists()) {//2
    possiblyDeletedUpdatedSystemApps.add(ps.name);
    }
    }
    }
    }

//look for any incomplete package installations
//清理所有安装不完整的包
ArrayList deletePkgsList = mSettings.getListOfIncompleteInstallPackagesLPr();
for (int i = 0; i < deletePkgsList.size(); i++) {
// Actual deletion of code and data will be handled by later
// reconciliation step
final String packageName = deletePkgsList.get(i).name;
logCriticalInfo(Log.WARN, "Cleaning up incompletely installed app: " + packageName);
synchronized (mPackages) {
mSettings.removePackageLPw(packageName);
}
}

//delete tmp files
//删除临时文件
deleteTempPackageFiles();

// Remove any shared userIDs that have no associated packages
mSettings.pruneSharedUsersLPw();

系统扫描阶段的主要工作有以下3点:

  1. 创建/system的子目录,比如/system/framework、/system/priv-app和/system/app等等
  2. 扫描系统文件,比如/vendor/overlay、/system/framework、/system/app等等目录下的文件。
  3. 对扫描到的系统文件做后续处理。

主要来说第3点,一次OTA升级对于一个系统App会有三种情况:

  • 这个系统APP无更新。
  • 这个系统APP有更新。
  • 新的OTA版本中,这个系统APP已经被删除。

当系统App升级,PMS会将该系统App的升级包设置数据(PackageSetting)存储到Settings的mDisabledSysPackages列表中(具体见PMS的replaceSystemPackageLIF方法),mDisabledSysPackages的类型为ArrayMap<String, PackageSetting>。mDisabledSysPackages中的信息会被PMS保存到packages.xml中的<updated-package>标签下(具体见Settings的writeDisabledSysPackageLPr方法)。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

在此为大家准备了四节优质的Android高级进阶视频:

架构师项目实战——全球首批Android开发者对Android架构的见解

附相关架构及资料

image.png

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

在此为大家准备了四节优质的Android高级进阶视频:

架构师项目实战——全球首批Android开发者对Android架构的见解

附相关架构及资料

[外链图片转存中…(img-MMQcitvX-1711814758765)]

往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值