Android 启动时应用的安装解析过程《一》

应用对于Android系统来说至关重要,系统会有几个时机对APP进行解析,一个是APK安装的时候会进行解析,还有一个就是系统在重启之后会进行解析,这里就简单的记录一下重启的时候APK的解析过程。

一、SystemServer

系统在启动之后从内核层启动第一个用户控件Init进程,再通过Init进程启动系统中第一个java 进程zygote,随之zygote fock出SystemServer,SystemServer再开始启动一些列系统服务,其中就包括PackageMangerService,在startBootstrapServices函数中:

java 复制代码
public final void startBootstrapServices(TimingsTraceAndSlog timingsTraceAndSlog) {
.....
    // 先调用了PackageManagerService的main函数
 	Pair<PackageManagerService, IPackageManager> main = PackageManagerService.main(context, installer, domainVerificationService, z, this.mOnlyCore);
 	// main 函数创建了PackageManagerService 和IPackageManager 的实例
    this.mPackageManagerService = (PackageManagerService) main.first;
    IPackageManager iPackageManager = (IPackageManager) main.second;
    Watchdog.getInstance().resumeWatchingCurrentThread("packagemanagermain");
    // 该方法将在 BaseDexClassLoader 中安装一个报告器,同时还将强制报告系统服务器已加载的任何 dex 文件
    SystemServerDexLoadReporter.configureSystemServerDexReporter(iPackageManager);
    this.mFirstBoot = this.mPackageManagerService.isFirstBoot();
    this.mPackageManager = this.mSystemContext.getPackageManager();

二、PackageManagerService

在SystemServer中是先调用PackageManagerService的main函数来初始化它自己的,来看看它做了什么:

java 复制代码
public static Pair<PackageManagerService, IPackageManager> main(Context context,
            Installer installer, @NonNull DomainVerificationService domainVerificationService,
            boolean factoryTest, boolean onlyCore) {
		PackageManagerServiceCompilerMapping.checkProperties();
        final TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG + "Timing",
                Trace.TRACE_TAG_PACKAGE_MANAGER);
        t.traceBegin("create package manager");
        final PackageManagerTracedLock lock = new PackageManagerTracedLock();
        final Object installLock = new Object();
        // 创建自带handler的线程
        HandlerThread backgroundThread = new ServiceThread("PackageManagerBg",
                Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
        backgroundThread.start();
        Handler backgroundHandler = new Handler(backgroundThread.getLooper());
        // 创建PackageManagerServiceInjector 用来持有管理其他一系列重要的类实例
		PackageManagerServiceInjector injector = new PackageManagerServiceInjector(
                context, lock, installer, installLock, new PackageAbiHelperImpl(),
                backgroundHandler,
                SYSTEM_PARTITIONS,
                // 解析compnent的
                (i, pm) -> new ComponentResolver(i.getUserManagerService(), pm.mUserNeedsBadging),
                // 权限管理服务
                (i, pm) -> PermissionManagerService.create(context,
                        i.getSystemConfig().getAvailableFeatures()),
                // 用户管理服务
                (i, pm) -> new UserManagerService(context, pm,
                        new UserDataPreparer(installer, installLock, context, onlyCore),
                        lock),
               // 系统数据库,用来访问系统数据库的
                (i, pm) -> new Settings(Environment.getDataDirectory(),
                        RuntimePermissionsPersistence.createInstance(),
                        i.getPermissionManagerServiceInternal(),
                        domainVerificationService, backgroundHandler, lock),
                (i, pm) -> AppsFilterImpl.create(i,
                        i.getLocalService(PackageManagerInternal.class)),
                 // compat服务用来管理一些系统特性
                (i, pm) -> (PlatformCompat) ServiceManager.getService("platform_compat"),
                // 系统配置服务,预置的系统的权限、硬件feature还有其他的一些配置在这里面解析
                (i, pm) -> SystemConfig.getInstance(),
                // 在软件包上运行 dexopt 命令的辅助类。
                (i, pm) -> new PackageDexOptimizer(i.getInstaller(), i.getInstallLock(),
                        i.getContext(), "*dexopt*"),
                // dex 行为管理类会保存所有包的dex位置
                (i, pm) -> new DexManager(i.getContext(), i.getPackageDexOptimizer(),
                        i.getInstaller(), i.getInstallLock()),
                (i, pm) -> new ArtManagerService(i.getContext(), i.getInstaller(),
                        i.getInstallLock()),
                (i, pm) -> ApexManager.getInstance(),
                (i, pm) -> new ViewCompiler(i.getInstallLock(), i.getInstaller()),
                (i, pm) -> (IncrementalManager)
                        i.getContext().getSystemService(Context.INCREMENTAL_SERVICE),
                (i, pm) -> new DefaultAppProvider(() -> context.getSystemService(RoleManager.class),
                        () -> LocalServices.getService(UserManagerInternal.class)),
                (i, pm) -> new DisplayMetrics(),
                // 包解析类
                (i, pm) -> new PackageParser2(pm.mSeparateProcesses, pm.mOnlyCore,
                        i.getDisplayMetrics(), pm.mCacheDir,
                        pm.mPackageParserCallback) /* scanningCachingPackageParserProducer */,
                 (i, pm) -> new PackageParser2(pm.mSeparateProcesses, pm.mOnlyCore,
                        i.getDisplayMetrics(), null,
                        pm.mPackageParserCallback) /* scanningPackageParserProducer */,
                (i, pm) -> new PackageParser2(pm.mSeparateProcesses, false, i.getDisplayMetrics(),
                        null, pm.mPackageParserCallback) /* preparingPackageParserProducer */,
                // Prepare a supplier of package parser for the staging manager to parse apex file
                // during the staging installation.
                // 包安装服务
                (i, pm) -> new PackageInstallerService(
                        i.getContext(), pm, i::getScanningPackageParser),
                (i, pm, cn) -> new InstantAppResolverConnection(
                        i.getContext(), cn, Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE),
                (i, pm) -> new ModuleInfoProvider(i.getContext()),
                (i, pm) -> LegacyPermissionManagerService.create(i.getContext()),
                (i, pm) -> domainVerificationService,
                (i, pm) -> {
                    HandlerThread thread = new ServiceThread(TAG,
                            Process.THREAD_PRIORITY_DEFAULT, true /*allowIo*/);
                    thread.start();
                    return new PackageHandler(thread.getLooper(), pm);
                },
                 new DefaultSystemWrapper(),
                LocalServices::getService,
                context::getSystemService,
                (i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager(), pm),
                (i, pm) -> IBackupManager.Stub.asInterface(ServiceManager.getService(
                        Context.BACKUP_SERVICE)),
                (i, pm) -> new SharedLibrariesImpl(pm, i));
                if (Build.VERSION.SDK_INT <= 0) {
            Slog.w(TAG, "**** ro.build.version.sdk not set!");
        }
        // 初始化PackageManagerService
        PackageManagerService m = new PackageManagerService(injector, onlyCore, factoryTest, PackagePartitions.FINGERPRINT, Build.IS_ENG, Build.IS_USERDEBUG, Build.VERSION.SDK_INT, Build.VERSION.INCREMENTAL);
        t.traceEnd(); // "create package manager"
        final CompatChange.ChangeListener selinuxChangeListener = packageName -> {
            synchronized (m.mInstallLock) {
                final Computer snapshot = m.snapshotComputer();
                final PackageStateInternal packageState =
                        snapshot.getPackageStateInternal(packageName);
                if (packageState == null) {
                    Slog.e(TAG, "Failed to find package setting " + packageName);
                    return;
                }
                 AndroidPackage pkg = packageState.getPkg();
                SharedUserApi sharedUser = snapshot.getSharedUser(
                        packageState.getSharedUserAppId());
                String oldSeInfo = AndroidPackageUtils.getSeInfo(pkg, packageState);
                
                if (pkg == null) {
                    Slog.e(TAG, "Failed to find package " + packageName);
                    return;
                }
                final String newSeInfo = SELinuxMMAC.getSeInfo(pkg, sharedUser,
                        m.mInjector.getCompatibility());
                
                if (!newSeInfo.equals(oldSeInfo)) {
                    Slog.i(TAG, "Updating seInfo for package " + packageName + " from: "
                            + oldSeInfo + " to: " + newSeInfo);
                    m.commitPackageStateMutation(null, packageName,
                            state -> state.setOverrideSeInfo(newSeInfo));
                    m.mAppDataHelper.prepareAppDataAfterInstallLIF(pkg);
                }
            }
        };
		//监听selinux的一些变化
		injector.getCompatibility().registerListener(SELinuxMMAC.SELINUX_LATEST_CHANGES,
                selinuxChangeListener);
        injector.getCompatibility().registerListener(SELinuxMMAC.SELINUX_R_CHANGES,
                selinuxChangeListener);
        // ota完或者第一次开机为所有用户安装其被
        m.installAllowlistedSystemPackages();
        // 初始化 IPackageManagerImpl 
        IPackageManagerImpl iPackageManager = m.new IPackageManagerImpl();
        ServiceManager.addService("package", iPackageManager);
        // 初始化PackageManagerNative 
        final PackageManagerNative pmn = new PackageManagerNative(m);
        ServiceManager.addService("package_native", pmn);
        LocalManagerRegistry.addManager(PackageManagerLocal.class, m.new PackageManagerLocalImpl());
        return Pair.create(m, iPackageManager);
    }

简单的介绍了一下main,下面重头戏在PackageManagerService的初始化过程,它总共有两个构造函数,注意一个只做了赋值,我们这里调用的不是这个构造函数而是另一个

java 复制代码
public PackageManagerService(PackageManagerServiceInjector injector, boolean onlyCore,
            boolean factoryTest, final String buildFingerprint, final boolean isEngBuild,
            final boolean isUserDebugBuild, final int sdkVersion, final String incrementalVersion) {
	mIsEngBuild = isEngBuild;
	......
	// 这里部分主要是给PackageMangerService的一些属性赋值
	......
	// CHECKSTYLE:ON IndentationCheck
    t.traceEnd();
	t.traceBegin("addSharedUsers");
	// 注册各种UID
    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);
    mSettings.addSharedUserLPw("android.uid.se", SE_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.networkstack", NETWORKSTACK_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.uwb", UWB_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    t.traceEnd();
    ......
    // 又是一堆初始化赋值
    ......
    // 从systemconfig获取对于机器来说可用的硬件feature,可以在这里之前对feature进行修改
    t.traceBegin("get system config");
    SystemConfig systemConfig = injector.getSystemConfig();
    mAvailableFeatures = systemConfig.getAvailableFeatures();
    t.traceEnd();
    ......
    // 这里是读取在mac_permissions.xml下面配置的一些se策略可以有效的限制一些应用的权限
    SELinuxMMAC.readInstallPolicy();
    ......
    final VersionInfo ver = mSettings.getInternalVersion();
    // 指纹信息是否有更新
    mIsUpgrade =
            !buildFingerprint.equals(ver.fingerprint);
    if (mIsUpgrade) {
           PackageManagerServiceUtils.logCriticalInfo(Log.INFO, "Upgrading from "
                        + ver.fingerprint + " to " + PackagePartitions.FINGERPRINT);
   }
  
   mInitAppsHelper = new InitAppsHelper(this, mApexManager, mInstallPackageHelper,
                mInjector.getSystemPartitions());

    // when upgrading from pre-M, promote system app permissions from install to runtime
    // 是否从M升上来
    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.
   // 是否从N 升上来
   mIsPreNUpgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N;
   // 是否从n mr升上来
   mIsPreNMR1Upgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N_MR1;
  // 是否从Q升上来
   mIsPreQUpgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.Q;
   ......
   // 根据不同的版本类型创建缓存目录,如果已存在则不创建
   mCacheDir = PackageManagerServiceUtils.preparePackageParserCache(
                    mIsEngBuild, mIsUserDebugBuild, mIncrementalVersion);

   final int[] userIds = mUserManager.getUserIds();
   PackageParser2 packageParser = mInjector.getScanningCachingPackageParser();
   // 初始化安装系统级APP
   mOverlayConfig = mInitAppsHelper.initSystemApps(packageParser, packageSettings, userIds,
                    startTime);
   // 初始化安装非系统级APP
   mInitAppsHelper.initNonSystemApps(packageParser, userIds, startTime);
   packageParser.close();
   ......
}

可以从上面PackageManagerService的初始化可以看到,机器开机的时候安装APP的任务是交给InitAppsHelper这个类来做的,两个函数一个initSystemApps一个initNonSystemApps,分别是初始化系统app和非系统app,两个函数都是对系统中的应用进行安装解析,区别就是针对系统app还是非系统app,我们先从系统app来看看。

三、InitAppsHelper

java 复制代码
public OverlayConfig initSystemApps(PackageParser2 packageParser,
            WatchedArrayMap<String, PackageSetting> packageSettings,
            int[] userIds, long startTime) 

initSystemApps有四个参数,看下其中比较重要的两个参数怎么来的

1、PackageParser2

第一个参数实在PackageManagerService初始化的时候从mInjector获取的,而这里的PackageParser2是在PackaManagerService的main函数初始化的

java 复制代码
PackageParser2 packageParser = mInjector.getScanningCachingPackageParser();

2、WatchedArrayMap<String, PackageSetting>

首先这个参数是通过PackageMangerService的成员变量mSettings获取:

java 复制代码
final WatchedArrayMap<String, PackageSetting> packageSettings =
                mSettings.getPackagesLocked();

Settings.getPackagesLocked

java 复制代码
 WatchedArrayMap<String, PackageSetting> getPackagesLocked() {
        return mPackages;
    }

Settings.addPackageLPw 中添加数据

java 复制代码
 PackageSetting addPackageLPw(String name, String realName, File codePath, int uid, int pkgFlags,
                                 int pkgPrivateFlags, @NonNull UUID domainSetId) {
        PackageSetting p = mPackages.get(name);
        if (p != null) {
            if (p.getAppId() == uid) {
                return p;
            }
            PackageManagerService.reportSettingsProblem(Log.ERROR,
                    "Adding duplicate package, keeping first: " + name);
            return null;
        }
        p = new PackageSetting(name, realName, codePath, pkgFlags, pkgPrivateFlags, domainSetId)
                .setAppId(uid);
        if (mAppIds.registerExistingAppId(uid, p, name)) {
            mPackages.put(name, p);
            return p;
        }
        return null;
    }

Settings.readSettingsLPw,这个函数中对应用的一些配置信息进行解析,这些信息主要是存在/data/system目录下面

java 复制代码
 boolean readSettingsLPw(@NonNull Computer computer, @NonNull List<UserInfo> users,
            ArrayMap<String, Long> originalFirstInstallTimes) {
        mPendingPackages.clear();
        mInstallerPackages.clear();
        originalFirstInstallTimes.clear();

        ArrayMap<Long, Integer> keySetRefs = new ArrayMap<>();
        ArrayList<Signature> readSignatures = new ArrayList<>();
      	// 获取一系列配置文件,文件位于/data/system/下面,在setings初始化的时候被赋值
      	/**
      	mSystemDir = new File(dataDir, "system");
        mSystemDir.mkdirs();
		 mSettingsFilename = new File(mSystemDir, "packages.xml");
         mSettingsReserveCopyFilename = new File(mSystemDir, "packages.xml.reservecopy");
         mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
         下面就是对这些配置文件进行解析
		*/
        try (ResilientAtomicFile atomicFile = getSettingsFile()) {
            FileInputStream str = null;
            try {
                str = atomicFile.openRead();
                if (str == null) {
                    // Not necessary, but will avoid wtf-s in the "finally" section.
                    findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
                    findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
                    return false;
                }
                final TypedXmlPullParser parser = Xml.resolvePullParser(str);

                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG
                        && type != XmlPullParser.END_DOCUMENT) {
                    // nothing
                }

                if (type != XmlPullParser.START_TAG) {
                    mReadMessages.append("No start tag found in settings file\n");
                    PackageManagerService.reportSettingsProblem(Log.WARN,
                            "No start tag found in package manager settings");
                    Slog.wtf(PackageManagerService.TAG,
                            "No start tag found in package manager settings");
                    return false;
                }

                int outerDepth = parser.getDepth();
                while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                        && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
                    if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                        continue;
                    }

                    String tagName = parser.getName();
                    if (tagName.equals("package")) {
                    //读取包信息
                        readPackageLPw(parser, readSignatures, keySetRefs, users,
                                originalFirstInstallTimes);
                    } else if (tagName.equals("permissions")) {
                        mPermissions.readPermissions(parser);
                    } else if (tagName.equals("permission-trees")) {
                        mPermissions.readPermissionTrees(parser);
                    } else if (tagName.equals("shared-user")) {
                        readSharedUserLPw(parser, readSignatures, users);
                    } else if (tagName.equals("preferred-packages")) {
                        // no longer used.
                    } else if (tagName.equals("preferred-activities")) {
                        // Upgrading from old single-user implementation;
                        // these are the preferred activities for user 0.
                        readPreferredActivitiesLPw(parser, 0);
                    } else if (tagName.equals(TAG_PERSISTENT_PREFERRED_ACTIVITIES)) {
                        // TODO: check whether this is okay! as it is very
                        // similar to how preferred-activities are treated
                        readPersistentPreferredActivitiesLPw(parser, 0);
                    } else if (tagName.equals(TAG_CROSS_PROFILE_INTENT_FILTERS)) {
                        // TODO: check whether this is okay! as it is very
                        // similar to how preferred-activities are treated
                        readCrossProfileIntentFiltersLPw(parser, 0);
                    } else if (tagName.equals(TAG_DEFAULT_BROWSER)) {
                        readDefaultAppsLPw(parser, 0);
                    } else if (tagName.equals("updated-package")) {
                        readDisabledSysPackageLPw(parser, users);
                    } else if (tagName.equals("renamed-package")) {
                        String nname = parser.getAttributeValue(null, "new");
                        String oname = parser.getAttributeValue(null, "old");
                        if (nname != null && oname != null) {
                            mRenamedPackages.put(nname, oname);
                        }
                    } else if (tagName.equals("last-platform-version")) {
                        // Upgrade from older XML schema
                        final VersionInfo internal = findOrCreateVersion(
                                StorageManager.UUID_PRIVATE_INTERNAL);
                        final VersionInfo external = findOrCreateVersion(
                                StorageManager.UUID_PRIMARY_PHYSICAL);

                        internal.sdkVersion = parser.getAttributeInt(null, "internal", 0);
                        external.sdkVersion = parser.getAttributeInt(null, "external", 0);
                        internal.buildFingerprint = external.buildFingerprint =
                                XmlUtils.readStringAttribute(parser, "buildFingerprint");
                        internal.fingerprint = external.fingerprint =
                                XmlUtils.readStringAttribute(parser, "fingerprint");

                    } else if (tagName.equals("database-version")) {
                        // Upgrade from older XML schema
                        final VersionInfo internal = findOrCreateVersion(
                                StorageManager.UUID_PRIVATE_INTERNAL);
                        final VersionInfo external = findOrCreateVersion(
                                StorageManager.UUID_PRIMARY_PHYSICAL);

                        internal.databaseVersion = parser.getAttributeInt(null, "internal", 0);
                        external.databaseVersion = parser.getAttributeInt(null, "external", 0);

                    } else if (tagName.equals("verifier")) {
                        final String deviceIdentity = parser.getAttributeValue(null, "device");
                        mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity);
                    } else if (TAG_READ_EXTERNAL_STORAGE.equals(tagName)) {
                        // No longer used.
                    } else if (tagName.equals("keyset-settings")) {
                        mKeySetManagerService.readKeySetsLPw(parser, keySetRefs);
                    } else if (TAG_VERSION.equals(tagName)) {
                        final String volumeUuid = XmlUtils.readStringAttribute(parser,
                                ATTR_VOLUME_UUID);
                        final VersionInfo ver = findOrCreateVersion(volumeUuid);
                        ver.sdkVersion = parser.getAttributeInt(null, ATTR_SDK_VERSION);
                        ver.databaseVersion = parser.getAttributeInt(null, ATTR_DATABASE_VERSION);
                        ver.buildFingerprint = XmlUtils.readStringAttribute(parser,
                                ATTR_BUILD_FINGERPRINT);
                        ver.fingerprint = XmlUtils.readStringAttribute(parser, ATTR_FINGERPRINT);
                    } else if (tagName.equals(
                            DomainVerificationPersistence.TAG_DOMAIN_VERIFICATIONS)) {
                        mDomainVerificationManager.readSettings(computer, parser);
                    } else if (tagName.equals(
                            DomainVerificationLegacySettings.TAG_DOMAIN_VERIFICATIONS_LEGACY)) {
                        mDomainVerificationManager.readLegacySettings(parser);
                    } else {
                        Slog.w(PackageManagerService.TAG, "Unknown element under <packages>: "
                                + parser.getName());
                        XmlUtils.skipCurrentTag(parser);
                    }
                }

                str.close();
            } catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException e) {
                // Remove corrupted file and retry.
                atomicFile.failRead(str, e);

                // Ignore the result to not mark this as a "first boot".
                readSettingsLPw(computer, users, originalFirstInstallTimes);
            }
        }

        return true;
    }

从上面的函数可以看出Settings这个类的主要职责就是存储修改解析应用的配置信息,这里涉及的东西比较多,不再赘述有兴趣可以自行查阅相关源码。

java 复制代码
            mFirstBoot = !mSettings.readLPw(computer,
                    mInjector.getUserManagerInternal().getUsers(
                    /* excludePartial= */ true,
                    /* excludeDying= */ false,
                    /* excludePreCreated= */ false));

这里是Settings解析的开始在PackageMangerService的构造函数中,这是第二个参数的由来,接着第三个四个参数就比较简单分别是uid和时间戳。

篇幅有限下一篇展开细说

相关推荐
SRC_BLUE_1725 分钟前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
镭封5 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4875 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛5 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二5 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫6 小时前
Android开发中的隐藏控件技巧
android
Winston Wood8 小时前
Android中Activity启动的模式
android
众乐认证8 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto
三杯温开水8 小时前
新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)
android·运维·服务器