Android PMS原理总结

486 阅读6分钟

一 system_server启动PMS

Android的所有Java服务都是通过system_server进程启动的,并且驻留在system_server进程中。SystemServer进程在启动时,通过创建一个ServerThread线程启动所有服务。

1.1 startBootstrapServices()

system_server的startBootstrapServices()函数会启动一些引导服务,比如:

  • ActivityManagerService

  • PowerManagerService

  • DisplayManagerService

  • SensorService

其中PackageManagerService就在这里启动。

private void startBootstrapServices() {
        //启动installer服务
        Installer installer = mSystemServiceManager.startService(Installer.class);
        // We need the default display before we can initialize the package manager.
        mSystemServiceManager.startBootPhase(SystemService.PHASE\_WAIT\_FOR\_DEFAULT\_DISPLAY);
        
        //处于加密状态则仅仅解析核心应用
        // Only run "core" apps if we're encrypting the device.
        String cryptState = SystemProperties.get("vold.decrypt");
        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;
        }
        // 创建PMS对象 - 启动入口
        traceBeginAndSlog("StartPackageManagerService");
        mPackageManagerService = PackageManagerService.main(mSystemContext, installer,
                mFactoryTestMode != FactoryTest.FACTORY\_TEST\_OFF, mOnlyCore);
        // 是否首次启动
        mFirstBoot = mPackageManagerService.isFirstBoot();
        
        // 获取PackageManager
        mPackageManager = mSystemContext.getPackageManager();
        Trace.traceEnd(Trace.TRACE\_TAG\_SYSTEM\_SERVER);
}

1.2 startOtherService()

system_server的startOtherService()方法会启动其他服务,在这里也会对PMS做一些处理

private void startOtherServices() {
    ......
    if (!mOnlyCore) {
        ........
        try {
            //将调用performDexOpt:Performs dexopt on the set of packages
            mPackageManagerService.updatePackagesIfNeeded();
        }.......
        ........
        try {
            //执行Fstrim,执行磁盘维护操作
            //可能类似于TRIM技术,将标记为删除的文件,彻底从硬盘上移除
            //而不是等到写入时再移除,目的是提高写入时效率
            mPackageManagerService.performFstrimIfNeeded();
        }.........
        .......
        try {
            mPackageManagerService.systemReady();
        }........
        .......
    }
}

PMS启动后将参与一些系统优化的工作,然后调用SystemReady方法通知系统服务进入就绪状态

在system_server进程启动过程,涉及PMS服务的主要几个处理:

  • PMS.main()

  • PMS.performDexOpt()

  • PMS.systemReady()

二 PMS.main的入口

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

在这个main入口中,直接实例化一个PMS服务,并将PMS服务放入ServiceManager中,便于管理,那么重点就是实例化PMS

代码比较多,选择一些重点

new PackageManagerService(context, installer, factoryTest, onlyCore);

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;
        mFactoryTest = factoryTest;
        mOnlyCore = onlyCore;
        mLazyDexOpt = "eng".equals(SystemProperties.get("ro.build.type"));
    
    	// displayMetrics是一个描述界面显示,尺寸,分辨率,密度的类
        mMetrics = new DisplayMetrics();
    
    	// Settings是Android的全局管理者,用于协助PMS保存所有的安装包信息
        mSettings = new Settings(context);
        mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM\_UID,
                ApplicationInfo.FLAG\_SYSTEM|ApplicationInfo.FLAG\_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.phone", RADIO\_UID,
                ApplicationInfo.FLAG\_SYSTEM|ApplicationInfo.FLAG\_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.log", LOG\_UID,
                ApplicationInfo.FLAG\_SYSTEM|ApplicationInfo.FLAG\_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.nfc", NFC\_UID,
                ApplicationInfo.FLAG\_SYSTEM|ApplicationInfo.FLAG\_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH\_UID,
                ApplicationInfo.FLAG\_SYSTEM|ApplicationInfo.FLAG\_PRIVILEGED);
        mSettings.addSharedUserLPw("android.uid.shell", SHELL\_UID,
                ApplicationInfo.FLAG\_SYSTEM|ApplicationInfo.FLAG\_PRIVILEGED);

        .......

        mInstaller = installer;

    	// 获取默认的显示信息,保存到mMetrics
        getDefaultDisplayMetrics(context, mMetrics);

    	// 获取系统配置信息
        SystemConfig systemConfig = SystemConfig.getInstance();
        mGlobalGids = systemConfig.getGlobalGids();
        mSystemPermissions = systemConfig.getSystemPermissions();
        mAvailableFeatures = systemConfig.getAvailableFeatures();

        synchronized (mInstallLock) {
        // writer
        synchronized (mPackages) {
            mHandlerThread = new ServiceThread(TAG,
                    Process.THREAD\_PRIORITY\_BACKGROUND, true /\*allowIo\*/);
            mHandlerThread.start();
            mHandler = new PackageHandler(mHandlerThread.getLooper());
            Watchdog.getInstance().addThread(mHandler, WATCHDOG\_TIMEOUT);
			
            // 创建各种目录,此时,到了我们熟悉的Android目录了
            File dataDir = Environment.getDataDirectory();
            mAppDataDir = new File(dataDir, "data");
            mAppInstallDir = new File(dataDir, "app");
            mAppLib32InstallDir = new File(dataDir, "app-lib");
            mAsecInternalPath = new File(dataDir, "app-asec").getPath();
            mUserAppDataDir = new File(dataDir, "user");
            mDrmAppPrivateInstallDir = new File(dataDir, "app-private");

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

此方法中,做了大概如下操作

  • 构造DisplayMetrics类:描述界面显示,尺寸,分辨率,密度。构造完后并获取默认的信息保存到变量mMetrics中

  • 构造Settings类:这个是Android的全局管理者,用于协助PMS保存所有的安装包信息

  • 保存Installer对象

  • 获取系统配置信息:SystemConfig构造函数中会通过readPermissions()解析指定目录下的所有xml文件,然后把这些信息保存到systemConfig中,涉及的目录有如下:

    • /system/etc/sysconfig
    • /system/etc/permissions
    • /oem/etc/sysconfig
    • /oem/etc/permissions
  • 创建data下的各种目录,比如data/app, data/app-private等

其中我们最为熟悉的就是最后一点,创建各种Android目录结构

各种第三方应用都是安装在mAppInstallDir目录中,直接搜索mAppInstallDir,得知在scanDirLI()函数中调用

扫描指定文件目录下的apk文件

scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime)

private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) {
        final File\[\] files = dir.listFiles();
        if (ArrayUtils.isEmpty(files)) {
            Log.d(TAG, "No files in app dir " + dir);
            return;
        }

        if (DEBUG\_PACKAGE\_SCANNING) {
            Log.d(TAG, "Scanning app dir " + dir + " scanFlags=" + scanFlags
                    + " flags=0x" + Integer.toHexString(parseFlags));
        }

        for (File file : files) {
            final boolean isPackage = (isApkFile(file) || file.isDirectory())
                    && !PackageInstallerService.isStageName(file.getName());
            if (!isPackage) {
                // Ignore entries which are not packages
                continue;
            }
            try {
                scanPackageLI(file, parseFlags | PackageParser.PARSE\_MUST\_BE\_APK,
                        scanFlags, currentTime, null);
            } catch (PackageManagerException e) {
                Slog.w(TAG, "Failed to parse " + file + ": " + e.getMessage());

                // Delete invalid userdata apps
                if ((parseFlags & PackageParser.PARSE\_IS\_SYSTEM) == 0 &&
                        e.error == PackageManager.INSTALL\_FAILED\_INVALID\_APK) {
                    logCriticalInfo(Log.WARN, "Deleting invalid package at " + file);
                    if (file.isDirectory()) {
                        FileUtils.deleteContents(file);
                    }
                    file.delete();
                }
            }
        }
    }

扫描这个文件夹下面的所有文件,并且判断是否为apk文件,如果不是继续循环,如果是,则扫描这个路径下的文件,调用scanPackageLI()

解析package信息,其中最重要的一个类为PackageParser,这个类和插件化技术处理联系紧密

private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags,
            long currentTime, UserHandle user) throws PackageManagerException {
	if (DEBUG\_INSTALL) Slog.d(TAG, "Parsing: " + scanFile);
    parseFlags |= mDefParseFlags;
    PackageParser pp = new PackageParser();
    pp.setSeparateProcesses(mSeparateProcesses);
    pp.setOnlyCoreApps(mOnlyCore);
    pp.setDisplayMetrics(mMetrics);
    
    .......   
    
    final PackageParser.Package pkg;
    try {
       // 1
       pkg = pp.parsePackage(scanFile, parseFlags);
    } catch (PackageParserException e) {
       throw PackageManagerException.from(e);
    }
}

其中1位置使用PackageParser对象解析对应scanFile文件下的资源,最终返回一个pkg对象,pkg为PackageParser类中的内部类Package

Package:

public final static class Package {

        public String packageName;
        /\*\* Names of any split APKs, ordered by parsed splitName \*/
        public String\[\] splitNames;

        // TODO: work towards making these paths invariant

        /\*\*
         \* Path where this package was found on disk. For monolithic packages
         \* this is path to single base APK file; for cluster packages this is
         \* path to the cluster directory.
         \*/
        public String codePath;

        /\*\* Path of base APK \*/
        public String baseCodePath;
        /\*\* Paths of any split APKs, ordered by parsed splitName \*/
        public String\[\] splitCodePaths;

        /\*\* Flags of any split APKs; ordered by parsed splitName \*/
        public int\[\] splitFlags;

        public boolean baseHardwareAccelerated;

        // For now we only support one application per package.
        public final ApplicationInfo applicationInfo = new ApplicationInfo();

        public final ArrayList<Permission> permissions = new ArrayList<Permission>(0);
        public final ArrayList<PermissionGroup> permissionGroups = new ArrayList<PermissionGroup>(0);
        public final ArrayList<Activity> activities = new ArrayList<Activity>(0);
        public final ArrayList<Activity> receivers = new ArrayList<Activity>(0);
        public final ArrayList<Provider> providers = new ArrayList<Provider>(0);
        public final ArrayList<Service> services = new ArrayList<Service>(0);
        public final ArrayList<Instrumentation> instrumentation = new ArrayList<Instrumentation>(0);

		.......   
        .......   
        .......   
}

其中有我们熟悉的activities,receivers,providers,services四大组件集合,Package对象就是包含了我们Android的所有资源信息

PackageParser中的parsePackage()方法--->parseMonolithicPackage()

    public Package parsePackage(File packageFile, int flags) throws PackageParserException {
        if (packageFile.isDirectory()) {
            return parseClusterPackage(packageFile, flags);
        } else {
            return parseMonolithicPackage(packageFile, flags);
        }
    }


    @Deprecated
    public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
        if (mOnlyCoreApps) {
            final PackageLite lite = parseMonolithicPackageLite(apkFile, flags);
            if (!lite.coreApp) {
                throw new PackageParserException(INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED,
                        "Not a coreApp: " + apkFile);
            }
        }
		
        // 1
        final AssetManager assets = new AssetManager();
        try {
            final Package pkg = parseBaseApk(apkFile, assets, flags);
            pkg.codePath = apkFile.getAbsolutePath();
            return pkg;
        } finally {
            IoUtils.closeQuietly(assets);
        }
    }

在代码中位置1 ,初始化一个AssetManager对象,并作为参数传入parseBaseApk()函数,最终返回一个package对象

由此可见,parseBaseApk函数也是对这个apk文件的资源进行解析处理

注意:此时new出来的assetManager并不能直接加载apk资源,必须还要去调用addAssetPath方法

parseBaseApk():

private Package parseBaseApk(File apkFile, AssetManager assets, int flags)
            throws PackageParserException {
        final String apkPath = apkFile.getAbsolutePath();

        mParseError = PackageManager.INSTALL\_SUCCEEDED;
        mArchiveSourcePath = apkFile.getAbsolutePath();

        if (DEBUG\_JAR) Slog.d(TAG, "Scanning base APK: " + apkPath);

    	// 1 
        final int cookie = loadApkIntoAssetManager(assets, apkPath, flags);

        Resources res = null;
        XmlResourceParser parser = null;
        try {
            res = new Resources(assets, mMetrics, null);
            assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                    Build.VERSION.RESOURCES\_SDK\_INT);
            // 2 
            parser = assets.openXmlResourceParser(cookie, ANDROID\_MANIFEST\_FILENAME);

            final String\[\] outError = new String\[1\];
            
            // 3
            final Package pkg = parseBaseApk(res, parser, flags, outError);
            if (pkg == null) {
                throw new PackageParserException(mParseError,
                        apkPath + " (at " + parser.getPositionDescription() + "): " + outError\[0\]);
            }

            pkg.baseCodePath = apkPath;
            pkg.mSignatures = null;

            return pkg;

        } catch (PackageParserException e) {
            throw e;
        } catch (Exception e) {
            throw new PackageParserException(INSTALL\_PARSE\_FAILED\_UNEXPECTED\_EXCEPTION,
                    "Failed to read manifest from " + apkPath, e);
        } finally {
            IoUtils.closeQuietly(parser);
        }
    }

位置1代码为

private static int loadApkIntoAssetManager(AssetManager assets, String apkPath, int flags)
            throws PackageParserException {
        if ((flags & PARSE\_MUST\_BE\_APK) != 0 && !isApkPath(apkPath)) {
            throw new PackageParserException(INSTALL\_PARSE\_FAILED\_NOT\_APK,
                    "Invalid package file: " + apkPath);
        }

        // The AssetManager guarantees uniqueness for asset paths, so if this asset path
        // already exists in the AssetManager, addAssetPath will only return the cookie
        // assigned to it.
        int cookie = assets.addAssetPath(apkPath);
        if (cookie == 0) {
            throw new PackageParserException(INSTALL\_PARSE\_FAILED\_BAD\_MANIFEST,
                    "Failed adding asset path: " + apkPath);
        }
        return cookie;
}

此时,可以通过assetManager加载apk资源,包括res,androidMainfest.xml文件等

位置2 打开androidMainfest文件,也就是解析关键资源入口就找到了

位置3 将解析后的androidMainfest文件返回的parser对象传入parseBaseApk函数

private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags,
            String\[\] outError) throws XmlPullParserException, IOException {

	....... 
    ....... 
	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("application")) {
                if (foundApp) {
                    if (RIGID\_PARSER) {
                        outError\[0\] = "<manifest> has more than one <application>";
                        mParseError = PackageManager.INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED;
                        return null;
                    } else {
                        Slog.w(TAG, "<manifest> has more than one <application>");
                        XmlUtils.skipCurrentTag(parser);
                        continue;
                    }
                }

                foundApp = true;
                //1
                if (!parseBaseApplication(pkg, res, parser, attrs, flags, outError)) {
                    return null;
                }
            }
        ....... 
    }
    ....... 
}

在这个方法中,解析了AndroidMainfest中的application节点,这是我们需要关心的重点。

直接看位置1

private boolean parseBaseApplication(Package owner, Resources res,
            XmlPullParser parser, AttributeSet attrs, int flags, String\[\] outError)
        throws XmlPullParserException, IOException {
	  ....... 
      .......
      while ((type = parser.next()) != XmlPullParser.END\_DOCUMENT
                && (type != XmlPullParser.END\_TAG || parser.getDepth() > innerDepth)) {
            if (type == XmlPullParser.END\_TAG || type == XmlPullParser.TEXT) {
                continue;
            }

            String tagName = parser.getName();
            if (tagName.equals("activity")) {
                // 1
                Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false,
                        owner.baseHardwareAccelerated);
                if (a == null) {
                    mParseError = PackageManager.INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED;
                    return false;
                }

                owner.activities.add(a);

            } else if (tagName.equals("receiver")) {
               // 2
                Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true, false);
                if (a == null) {
                    mParseError = PackageManager.INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED;
                    return false;
                }

                owner.receivers.add(a);

            } else if (tagName.equals("service")) {
                Service s = parseService(owner, res, parser, attrs, flags, outError);
                if (s == null) {
                    mParseError = PackageManager.INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED;
                    return false;
                }

                owner.services.add(s);

            } else if (tagName.equals("provider")) {
                Provider p = parseProvider(owner, res, parser, attrs, flags, outError);
                if (p == null) {
                    mParseError = PackageManager.INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED;
                    return false;
                }

                owner.providers.add(p);

            } else if (tagName.equals("activity-alias")) {
                Activity a = parseActivityAlias(owner, res, parser, attrs, flags, outError);
                if (a == null) {
                    mParseError = PackageManager.INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED;
                    return false;
                }

                owner.activities.add(a);

            } else if (parser.getName().equals("meta-data")) {
                // note: application meta-data is stored off to the side, so it can
                // remain null in the primary copy (we like to avoid extra copies because
                // it can be large)
                if ((owner.mAppMetaData = parseMetaData(res, parser, attrs, owner.mAppMetaData,
                        outError)) == null) {
                    mParseError = PackageManager.INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED;
                    return false;
                }

            } else if (tagName.equals("library")) {
                sa = res.obtainAttributes(attrs,
                        com.android.internal.R.styleable.AndroidManifestLibrary);

                // Note: don't allow this value to be a reference to a resource
                // that may change.
                String lname = sa.getNonResourceString(
                        com.android.internal.R.styleable.AndroidManifestLibrary\_name);

                sa.recycle();

                if (lname != null) {
                    lname = lname.intern();
                    if (!ArrayUtils.contains(owner.libraryNames, lname)) {
                        owner.libraryNames = ArrayUtils.add(owner.libraryNames, lname);
                    }
                }

                XmlUtils.skipCurrentTag(parser);

            } else if (tagName.equals("uses-library")) {
                sa = res.obtainAttributes(attrs,
                        com.android.internal.R.styleable.AndroidManifestUsesLibrary);

                // Note: don't allow this value to be a reference to a resource
                // that may change.
                String lname = sa.getNonResourceString(
                        com.android.internal.R.styleable.AndroidManifestUsesLibrary\_name);
                boolean req = sa.getBoolean(
                        com.android.internal.R.styleable.AndroidManifestUsesLibrary\_required,
                        true);

                sa.recycle();

                if (lname != null) {
                    lname = lname.intern();
                    if (req) {
                        owner.usesLibraries = ArrayUtils.add(owner.usesLibraries, lname);
                    } else {
                        owner.usesOptionalLibraries = ArrayUtils.add(
                                owner.usesOptionalLibraries, lname);
                    }
                }

                XmlUtils.skipCurrentTag(parser);

            } else if (tagName.equals("uses-package")) {
                // Dependencies for app installers; we don't currently try to
                // enforce this.
                XmlUtils.skipCurrentTag(parser);

            } else {
                if (!RIGID\_PARSER) {
                    Slog.w(TAG, "Unknown element under <application>: " + tagName
                            + " at " + mArchiveSourcePath + " "
                            + parser.getPositionDescription());
                    XmlUtils.skipCurrentTag(parser);
                    continue;
                } else {
                    outError\[0\] = "Bad element under <application>: " + tagName;
                    mParseError = PackageManager.INSTALL\_PARSE\_FAILED\_MANIFEST\_MALFORMED;
                    return false;
                }
            }
        }
}

可以看到上面就是我们最为熟悉的配置文件中的各种节点配置,包括四大组件,meta-data

等。

其中有一点可以注意下:activity节点和receiver节点处理类型都为Activity对象,这是因为activity和receiver节点结构一样,类似于JavaBean结构。

activity和receiver节点结构:

		<activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <receiver android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
            </intent-filter>
        </receiver>

节点中都有intent-filter,action节点。那么在上面parseBaseApplication()函数代码块中位置1和位置2的函数parseActivity()如下:

private Activity parseActivity(Package owner, Resources res,
            XmlPullParser parser, AttributeSet attrs, int flags, String\[\] outError,
            boolean receiver, boolean hardwareAccelerated)
            throws XmlPullParserException, IOException {
 	......
        
    // 1
    Activity a = new Activity(mParseActivityArgs, new ActivityInfo());
    
    ......
        
    while ((type=parser.next()) != XmlPullParser.END\_DOCUMENT
               && (type != XmlPullParser.END\_TAG
                       || parser.getDepth() > outerDepth)) {
            if (type == XmlPullParser.END\_TAG || type == XmlPullParser.TEXT) {
                continue;
            }

            if (parser.getName().equals("intent-filter")) {
                //2
                ActivityIntentInfo intent = new ActivityIntentInfo(a);
                if (!parseIntent(res, parser, attrs, true, intent, outError)) {
                    return null;
                }
                if (intent.countActions() == 0) {
                    Slog.w(TAG, "No actions in intent filter at "
                            + mArchiveSourcePath + " "
                            + parser.getPositionDescription());
                } else {
                    a.intents.add(intent);
                }
            } else if (parser.getName().equals("meta-data")) {
                if ((a.metaData=parseMetaData(res, parser, attrs, a.metaData,
                        outError)) == null) {
                    return null;
                }
            }
        }
    ......
}

13行直接匹配解析intent-filter节点,同时将parseIntent()函数返回的intent-filter放入activity对象中的intents集合中。

在第8行中,这个activity对象中会将信息转为ActivityInfo,其中包括各种信息:name,

packageName等等

到这里基本一次解析就结束了,会将得到的信息存储到对应的PackageParser中的对应的集合中,并将这部分信息传递到PMS中

那么一样的道理,在其他文件路径下的处理也类似,比如/data/app-private目录

以上,为PMS原理简单流程,后续可补充。

关注公众号:Android老皮
解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版

内容如下

1.Android车载应用开发系统学习指南(附项目实战)
2.Android Framework学习指南,助力成为系统级开发高手
3.2023最新Android中高级面试题汇总+解析,告别零offer
4.企业级Android音视频开发学习路线+项目实战(附源码)
5.Android Jetpack从入门到精通,构建高质量UI界面
6.Flutter技术解析与实战,跨平台首要之选
7.Kotlin从入门到实战,全方面提升架构基础
8.高级Android插件化与组件化(含实战教程和源码)
9.Android 性能优化实战+360°全方面性能调优
10.Android零基础入门到精通,高手进阶之路

敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔