Android资源管理框架-------之Android中的资源包(二)
我们知道一个APK中主要包含了dex字节码、AndroidManifest.xml、res目录下的各种资源、以及resources.arsc等等,也就是说一般情况下我们的一个APK既是一个dex包,也是一个资源包。注意我们前面说的是一般情况下,既然有一般,那么肯定也就有二般~ ~。典型的二般情况有种:资源共享库和Android系统资源包。关于资源共享库,Android资源管理中的SharedLibrary和Dynamic Reference-------之资源共享库(一)这个系列的文章详细介绍了其概念、原理、实现、应用,如有兴趣,可以详细阅读;而Android系统资源包则是指framework-res.apk,这个APK里则是只有资源,系统的代码在用到这些资源的时候是可以从这个包中获取的,其实我们的APK也离不开这个包里的资源,比如我们在xml文件里用到的android:
打头的资源,在运行时都会去framework-res.apk里查找。
既然说到了系统资源包,那就顺便说一下android-sdk里的android.jar吧,它也是一个不太守规矩的家伙。你以为它就是个jar包?错!错!错!
我们看看android.jar里可不只有class文件这么简单,还有AndroidManifest.xml,还有res目录,而且AndroidManifest.xml和res目录下面的xml类型的资源还都是编译过的,最关键的是它还有resources.arsc!看到这些,我们能想到的就是,这哪里是一个普通的jar包,这分明是一个APK!不过,有一点和APK不一样,就是这里面直接放的是class文件,没有用dx工具转化成dex,毕竟这个是给我们应用开发的时候编译用的,弄成dex就不好编译了。不知道大家有没有疑问,android.jar里的资源和framework-res.apk里的资源有什么异同?其实,这两者里面的资源大部分都是相同的,差别在于,对于一些非公开的资源,android.jar里是引用不到的,而framework-res.apk里则包括了系统全部的资源,毕竟android.jar只是编译使用,不想公开的不放进去就可以了,但framework-res.apk则是运行时要加载的,少了资源会崩溃的。当然,android.jar里的class和framework.jar的异同也基本类似。另外还有一点,framework-res.apk的包名非常简洁,就叫android,我们引用Android系统资源的时候通常是形如android:color/white这种形式,这里冒号前面的android指的就是framework-res.apk的包名(更确切地说,xmlns:android="http://schemas.android.com/apk/res/android
这样的namespace中,res/androd
中的android指的是framework-res.apk的包名)。
说完Android的系统资源包,我们再说一般的应用包,也就是APK。一个APK里面只要有资源,那么它本身也是一个资源包。但是这并不意味着这个App在运行的时候只会依赖并加载它本身这一个资源包。相反,只依赖本身资源的APK,基本没有,一个App可以没有布局,没有界面,但总要有AndroidManifest.xml吧,这里面,它总会用到android打头的属性吧,那么不论在编译时还是运行时,这个App都会依赖framework-res.apk这个系统资源包了。其实,一个跑在mtk平台上的APK,很可能要加载四个资源包:Android本身的资源包framework-res.apk、mtk自己的系统资源包、手机厂商的系统资源包以及APK本身。如果考虑到资源共享库和RRO(Runtime Resources Overlay,Android资源管理中的Runtime Resources Overlay-------之概述(一)这里有RRO详细的介绍)包,那么这个APK要加载的资源包就会更多了。另外,我们说的加载一个系统包,是指加载包内的resources.arsc文件,而非一定是要加载其整个包。
既然一个应用在运行时要加载那么多的包,那么这些包被加载到了哪里呢?我们知道Resources
类是一个App资源相关的接口类,我们资源相关的大部分操作都要通过它来完成。不过,资源包的加载并不是通过resources
来完成的,而是更加low level的AssetManager
类,其实Resources
类是对AssetManager
类的一个封装,Resources
类的大部分功能都是通过AssetManager
来实现的。当然,Resources
中还封装了Theme相关的东西,关于Theme,Android资源管理中的Theme和Style-------之总述(一)系列文章已经详细描述,这里不再多说。
下面我们看一下AssetManager的构造方法,一共有两个,一个是public的,一个是private的:
//frameworks/base/core/java/android/content/res/AssetManager.java private AssetManager(boolean isSystem) { //...省略非核心代码 init(true); } public AssetManager() { synchronized (this) { //...省略非核心代码 init(false); ensureSystemAssets(); } } //如果系统的AssetManager对象没有创建,则创建之 private static void ensureSystemAssets() { synchronized (sSync) { //sSystem是系统的AssetManager对象 if (sSystem == null) { AssetManager system = new AssetManager(true); //创建java层的Global String Pool system.makeStringBlocks(null); sSystem = system; } } }
这两个方法构造方法都非常简洁,其中private的构造方法是用来创建系统资源对应的AssetManager
对象的,public的则是用来创建一般应用的AssetManager
对象的。其中每一个应用的AssetManager
对象中都有一个系统的AssetManager
对象sSystem。当我们创建一个非System的AssetManager对象时,如果System的AssetManager对象还没有创建,我们会把sSystem也给创建了。至于system.makeStringBlocks(null);
这句创建java 层Global String Pool的实现,我们后面再讲,这里先看init方法的实现,init是一个native方法,我们直接看native层:
//frameworks/base/core/jni/android_util_AssetManager.cppstatic void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem){ /** *RRO相关的处理,系统的overlay package的idmap是在这里直接做的,不再经过PMS, *如果没有生成framework-res.apk的idmap文件,则会在这里生成 */ if (isSystem) { verifySystemIdmaps(); } //创建native层的AssetManager对象 AssetManager* am = new AssetManager(); if (am == NULL) { jniThrowException(env, "java/lang/OutOfMemoryError", ""); return; } //给AssetManager对象添加默认的资源包了 am->addDefaultAssets(); ALOGV("Created AssetManager %p for Java object %p\n", am, clazz); //在java层的AssetManager中,有一个成员 mObject,记录native层创建的这个AssetManager对象的地址, //在这里给它赋值 env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));}
init
方法主要就是创建native层的AssetManager
对象,并把其地址存到java层的AssetManager
对象的mObject成员变量中,再有就是给AssetManager对象添加默认的资源包了,我们看看都添加了哪些资源包:
//frameworks/base/core/libs/androidfw/AssetManager.cppbool AssetManager::addDefaultAssets(){ const char* root = getenv("ANDROID_ROOT"); LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set"); //ALOGD("AssetManager-->addDefaultAssets CIP path not exsit!"); String8 path(root);///system //static const char* kSystemAssets = "framework/framework-res.apk"; //所以这里就是添加系统/system/framework/framework-res.apk这个资源包了 path.appendPath(kSystemAssets); //添加进去 bool isOK1 =addAssetPath(path, NULL); String8 path2(root); //static const char* kMediatekAssets = "framework/mediatek-res/mediatek-res.apk"; //所以这里就是添加MTK的/system/framework/mediatek-res/mediatek-res.apk这个资源包了 path2.appendPath(kMediatekAssets);bool isOK2 =addAssetPath(path2, NULL); if(!isOK2){ ALOGW("AssetManager-->addDefaultAssets isok2 is false");} //添加手机厂商自己的系统资源包,具体路径这里就不方便贴出来了 String8 path3(root); path3.appendPath(kBmigoAssets); bool isOK3 =addAssetPath(path3, NULL); if(!isOK3){ ALOGW("AssetManager-->add mogo-framework-res failed"); return isOK3; }
我们看到,不论是APK的AssetManager
,还是sSystem这个系统的AssetManager
,这里都会添加3个系统资源包,分别是Android本身的、MTK的、手机厂商的。也就是说,在构造一个java层的AssetManager
对象的时候,这三个资源包,都会作为系统资源包,添加到构造的AssetManager
对象中去。另外,我们的apk进程在构造完AssetManager
对象后,还会把自己添加到这个AssetManager
中,这样我们的App的AssetManager
中就有四个资源包了。
前面我们讲到system.makeStringBlocks(null);
时没有讲其实现,下面我们简单看一下它的实现。首先这一句是给system(AssetManager
的一个对象,表示是系统AssetManager
)创建Global String Pool。至于什么是Global String Pool,我们在讲resources.arsc文件时会详细来讲,这里我们只要知道当我们从资源管理框架查找某一资源,拿到的数据类型是字符串时,我们拿到的数据不是结果字符串,而是两个信息,第一个信息表示在哪个Global String Pool中,第二个信息表示结果字符串在这个Global String Pool中的索引,我们根据这两个信息到对应的Global String Pool中的对应位置,就可以拿到结果字符串了。
final void makeStringBlocks(StringBlock[] seed) { final int seedNum = (seed != null) ? seed.length : 0; /** * 总的Global String Pool的个数,由于一个资源包有且只有一个Global String Pool * 所以,也就是已经加载的资源包的个数,在这里我们添加了android、mtk、手机厂商三个资源包,所以num = 3 */ final int num = getStringBlockCount(); mStringBlocks = new StringBlock[num]; if (localLOGV) Log.v(TAG, "Making string blocks for " + this + ": " + num); for (int i=0; i<num; i++) { //seed会被放到最前面,表示已经创建过了,所以不用new了 if (i < seedNum) { mStringBlocks[i] = seed[i]; } else { //去native层拿到后后面的StringBlock mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true); } }}//frameworks/base/core/jni/android_util_AssetManager.cppstatic jint android_content_AssetManager_getStringBlockCount(JNIEnv* env, jobject clazz){ //assetManagerForJavaObject函数值得注意,马上分析 AssetManager* am = assetManagerForJavaObject(env, clazz); if (am == NULL) { return 0; } //其实就是我们已经加载的资源包的个数,后面会详细讲,这里就不细说了 return am->getResources().getTableCount();}//frameworks/base/core/jni/android_util_AssetManager.cppstatic jlong android_content_AssetManager_getNativeStringBlock(JNIEnv* env, jobject clazz, jint block){ //assetManagerForJavaObject函数值得注意,马上分析 AssetManager* am = assetManagerForJavaObject(env, clazz); if (am == NULL) { return 0; } //去我们加载的第block资源包中取出Global String Pool的地址,后面会详细讲,这里就不细说了 return reinterpret_cast<jlong>(am->getResources().getTableStringBlock(block));}
还记的java层的AssetManager
在构造的时候会保存一个native层AssetManager
的地址在mObject
成员中吗?既然保存了它,当然有用,有什么用,怎么用呢?
AssetManager* assetManagerForJavaObject(JNIEnv* env, jobject obj){ //拿到java层的mObject,它存的就是native层的AssetManager对象的地址 jlong amHandle = env->GetLongField(obj, gAssetManagerOffsets.mObject); //直接强转,得到native层的AssetManager对象 AssetManager* am = reinterpret_cast<AssetManager*>(amHandle); if (am != NULL) { return am; } jniThrowException(env, "java/lang/IllegalStateException", "AssetManager has been finalized!"); return NULL;}
看完这个函数的实现,java层AssetManager
中的mObject成员的作用一目了然。StringBlock的创建就先讲到这里了,后面还会深入将,下面我们看看Android的资源是如何加载的。这个问题得分开说:system_server和一般App中资源的加载还不太一样。system_server在起来以后,会创建ActivityThread、Context等对象:
//frameowrk/base/core/java/android/app/ActivityThread.javapublic static ActivityThread systemMain() { //...省略无关代码 ActivityThread thread = new ActivityThread(); //true表示是system_server进程,否则表示是应用进程 thread.attach(true); return thread;}private void attach(boolean system) { //...省略无关代码 ContextImpl context = ContextImpl.createAppContext( this, getSystemContext().mPackageInfo); //...省略无关代码}public ContextImpl getSystemContext() { synchronized (this) { if (mSystemContext == null) { mSystemContext = ContextImpl.createSystemContext(this); } return mSystemContext; }}
我们看到system_server起来后,会先去创建ActivityThread对象,然后创建Context对象,我们看看系统的Context对象是如何创建的:
//framework/base/core/java/android/app/ContextImpl.javastatic ContextImpl createSystemContext(ActivityThread mainThread) { //在LoadedApk对象packageInfo构造的时候,会去加载系统资源 LoadedApk packageInfo = new LoadedApk(mainThread); //创建Context对象 ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, false, null, null); //*把系统资源配置信息写入资源管理框架 context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(), context.mResourcesManager.getDisplayMetricsLocked(Display.DEFAULT_DISPLAY)); return context;}
//framework/basecore/java/android/app/LoadedApk.java LoadedApk(ActivityThread activityThread) { mActivityThread = activityThread; mApplicationInfo = new ApplicationInfo(); mApplicationInfo.packageName = "android"; mPackageName = "android"; mAppDir = null; mResDir = null; mSplitAppDirs = null; mSplitResDirs = null; mOverlayDirs = null; mSharedLibraries = null; mDataDir = null; mDataDirFile = null; mLibDir = null; mBaseClassLoader = null; mSecurityViolation = false; mIncludeCode = true; mRegisterPackage = false; mClassLoader = ClassLoader.getSystemClassLoader(); //关键在这里 mResources = Resources.getSystem(); }
我们看到,在创建Context的时候,要传入一个ActivityThread对象,同时也要传入一个LoadedApk对象,而LoadedApk里主要存储两个方面的信息:一个是包相关的信息,比如包名、路径等等;另外一个就是资源,也就是mResources对象。也就是说,对于system_server而言,一个systemContext的主要意义在于:存储system_server进程相关的信息;存储系统资源相关的信息。我们看到,system_server也会有ApplicationInfo,包名就叫android
,也就是framework-res.apk
的包名。
//framework/base/core/java/android/content/res/Resources.javapublic static Resources getSystem() { //典型的单例模式,上来就申请锁,不判断是否已经构造,差评~~ synchronized (sSync) { Resources ret = mSystem; if (ret == null) { ret = new Resources(); mSystem = ret; } return ret; }}private Resources() { //拿到系统的AssetManager对象 mAssets = AssetManager.getSystem(); //默认资源配置 mConfiguration.setToDefaults(); mMetrics.setToDefaults(); updateConfiguration(null, null); //创建java层的Global string pool mAssets.ensureStringBlocks();}
//framework/base/core/java/android/content/res/AssetManager.javapublic static AssetManager getSystem() { //这个方法我们前面讲过了,还有印象吗?~~ ensureSystemAssets(); return sSystem;}
到这里,我们已经完全看到system_server自己的资源的构造过程了,system_server已经加载了android本身的系统资源包framework-res.apk
、MTK的系统资源包mediatek-res.apk
以及手机厂商的系统资源包。
接下来我们看看应用的资源包是如何加载的。应用的启动流程,我们在这里就不作介绍了,如果大家感兴趣,可以瞧瞧frameworks/base/cmds/app_process
(也就是Zygote)以及ZygoteInit.java
和RuntimeInit.java
相关的代码。不过这里有一点到时可以说说,就是Zygote进程的preload,由于所有Android进程(包括system_server进程)都是从Zygote进程fork出来的,所以Zygote进程起来后就会预先加载许多系统相关的东西,比如我们常用的各种类,系统资源等,这样就可以避免重复加载:
//framework/base/core/java/com/android/internal/os/ZygoteInit.java static void preload() { Log.d(TAG, "begin preload"); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "BeginIcuCachePinning"); beginIcuCachePinning(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadClasses"); preloadClasses(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadResources"); //我们重点关注这里 preloadResources(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadOpenGL"); preloadOpenGL(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); preloadSharedLibraries(); preloadTextResources(); // Ask the WebViewFactory to do any initialization that must run in the zygote process, // for memory sharing purposes. WebViewFactory.prepareWebViewInZygote(); endIcuCachePinning(); warmUpJcaProviders(); Log.d(TAG, "end preload"); } private static void preloadResources() { //...省略无关代码 mResources = Resources.getSystem(); mResources.startPreloading(); //...省略无关代码 int N = preloadDrawables(ar); //更新ar N = preloadColorStateLists(ar); //更新ar N = preloadDrawables(ar); mResources.finishPreloading(); }
preload我们简单说一下就略过了哈,当我们的应用进程起来后会走到ActivityThread
的main
方法:
//frameowrk/base/core/java/android/app/ActivityThread.java//运行于App进程public static void main(String[] args) { //创建应用主线程的消息队列,用于主线程的消息循环 Looper.prepareMainLooper(); //创建ActivityThread对象 ActivityThread thread = new ActivityThread(); //false表示是应用进程,而非system_server thread.attach(false); //主线程开始进入消息循环。 Looper.loop(); //...省略无关代码}private void attach(boolean system) { //...省略无关代码 final IActivityManager mgr = ActivityManagerNative.getDefault(); try { /** * mAppThread这个参数,也是一个Binder,把这个传给system_server(AMS),AMS就可以主动调用应用进程 * 的接口方法,从而实现system_server主动和应用进程的通信 */ mgr.attachApplication(mAppThread); } catch (RemoteException ex) { } //...省略无关代码}
我们看到应用程序会在主线程里先调用prepareMainLooper创建消息队列,然后通过Binder调用attachApplication方法,调到system_server。最后,开始消息循环,也就是我们的应用程序的主线程开始干活了,它主要用来处理应用进程和system_server交互的相关东西,比如四大组件的生命周期等,这也就是我们不能在应用程序主线程进行耗时操作的原因。毕竟主线程是用来管理应用以及UI显示的,如果把它用作其它耗时操作,那轻则各种卡顿,重则生命周期迟迟不能进行,甚至出错。
//framework/base/services/core/java/com/android/server/am/ActivityManagerService.java//运行于system_server进程public final void attachApplication(IApplicationThread thread) { synchronized (this) { int callingPid = Binder.getCallingPid(); final long origId = Binder.clearCallingIdentity(); //关键一句 attachApplicationLocked(thread, callingPid); Binder.restoreCallingIdentity(origId); }}private final boolean attachApplicationLocked(IApplicationThread thread, int pid) { //...省略无关代码 thread.bindApplication(processName, appInfo, providers, app.instrumentationClass, profilerInfo, app.instrumentationArguments, app.instrumentationWatcher, app.instrumentationUiAutomationConnection, testMode, enableOpenGlTrace, isRestrictedBackupMode || !normalMode, app.persistent, new Configuration(mConfiguration), app.compat, getCommonServicesLocked(app.isolated), mCoreSettingsObserver.getCoreSettingsLocked()); //...省略无关代码}
我们看到AMS又通过thread.bindApplication方法回调到应用进程了:
//frameowrk/base/core/java/android/app/ActivityThread.java//运行于App进程public final void bindApplication(String processName, ApplicationInfo appInfo, List<ProviderInfo> providers, ComponentName instrumentationName, ProfilerInfo profilerInfo, Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, IUiAutomationConnection instrumentationUiConnection, int debugMode, boolean enableOpenGlTrace, boolean isRestrictedBackupMode, boolean persistent, Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services, Bundle coreSettings) { //...省略无关代码 AppBindData data = new AppBindData(); data.processName = processName; data.appInfo = appInfo; data.providers = providers; data.instrumentationName = instrumentationName; data.instrumentationArgs = instrumentationArgs; data.instrumentationWatcher = instrumentationWatcher; data.instrumentationUiAutomationConnection = instrumentationUiConnection; data.debugMode = debugMode; data.enableOpenGlTrace = enableOpenGlTrace; data.restrictedBackupMode = isRestrictedBackupMode; data.persistent = persistent; data.config = config; data.compatInfo = compatInfo; data.initProfilerInfo = profilerInfo; sendMessage(H.BIND_APPLICATION, data); //...省略无关代码}
方法就是封装了一下这些参数,然后发送消息,消息在主线程中循环,最后走到:
private void handleBindApplication(AppBindData data) { //frameowrk/base/core/java/android/app/ActivityThread.java //运行于App进程 final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);}
//frameworks/base/core/java/android/app/ContextImpl.java static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) { if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); return new ContextImpl(null, mainThread, packageInfo, null, null, false, null, null); } private ContextImpl(ContextImpl container, ActivityThread mainThread, LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted, Display display, Configuration overrideConfiguration) { //...省略无关代码 //对于一个Context而言,这两个成员是灵魂 mMainThread = mainThread; mPackageInfo = packageInfo; //创建mResourcesManager实例 mResourcesManager = ResourcesManager.getInstance(); //创建资源 Resources resources = packageInfo.getResources(mainThread); //...省略无关代码 mResources = resources; //...省略无关代码 }
同样,还是构造Context,对于一个Context而言,最重要的两个成员变量就是mainThread和mPackageInfo,为什么呢?个人认为,Context有三个方面的作用:一、代表应用进程和system_server交互,接受system_server的调度;二、提供应用相关的信息,比如包名、apk路径等;三、提供Android资源相关的接口。从这三个功能来讲,用来让system_server调度的接口IApplicationThread服务端的实现在mMainThread中;App的应用信息和资源均在mPackageInfo中,所以这两者对于Context来说非常非常重要。
//framework/base/core/java/android/app/LoadedApk.javapublic Resources getResources(ActivityThread mainThread) { if (mResources == null) { mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs, mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this); } return mResources;}
又回到了ActivityThread
中:
//frameowrk/base/core/java/android/app/ActivityThread.javaResources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, LoadedApk pkgInfo) { return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null);}
//frameworks/base/core/java/android/app/ResourcesManager.javapublic Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) { //先从缓存中查找,如果已经创建,则直接返回,这部分代码略过 /** * 创建AssetManager对象,根据我们前面的分析,这时候它会创建sSystem成员 * 并且assets中已经加载了android源生、MTK、手机厂商三个系统资源包 * 但是没有加载我们这个应用Apk本省 */ AssetManager assets = new AssetManager(); //resDir表示我们这个apk本身,在这里被添加到了AssetManager中! //到这里,AssetManager总算把我们应用的apk添加进去了!!! if (resDir != null) { if (assets.addAssetPath(resDir) == 0) { return null; } } //添加Runtime Resources Overlay if (overlayDirs != null) { for (String idmapPath : overlayDirs) { assets.addOverlayPath(idmapPath); } } //添加资源共享库 if (libDirs != null) { for (String libDir : libDirs) { if (assets.addAssetPath(libDir) == 0) { Slog.w(TAG, "Asset path '" + libDir + "' does not exist or contains no resources."); } } } //其它处理,放入缓存,略过}
ResourcesManager
是Android为了便于Resources复用,避免资源重复加载而设计的一个类,我们不用太关心。真正关键的是getTopLevelResources
这个方法,AssetManager
最终是在这里创建的,系统资源包和apk本身这个资源包,也是在这里加进去的。关于RRO(Runtime Resources Overlay)和资源共享库前文已经描述过,这里不再多说。
至此,system_server和应用进程资源的加载我们已经分析完成,当然,只分析到java层的AssetManager
,更深入的分析我们放到后面。
更多相关文章
- Android(安卓)Eclipse JNI 调用 .so文件加载问题
- android如何支持多屏幕
- Android(安卓)、资源分目录存放
- Android资源String中html标签的使用
- Native层HIDL服务的获取原理-Android10.0 HwBinder通信原理(七)
- Android中Fragment的使用
- Android上webview界面切换动画效果
- android屏幕旋转可能带来的问题
- Android(安卓)机器人:使用系统资源