10.源码阅读(插件式换肤-安卓Resources加载资源的过程-android api 26)

简介: 我们知道,每一个View的子类都可以设置backgroud,那么这个背景是如何加载出来的呢?找到View的构造方法public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { .

我们知道,每一个View的子类都可以设置backgroud,那么这个背景是如何加载出来的呢?

找到View的构造方法

public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    ......
    case com.android.internal.R.styleable.View_background:
                    background = a.getDrawable(attr);
                    break;
    ......
}
@Nullable
    public Drawable getDrawable(@StyleableRes int index) {
        return getDrawableForDensity(index, 0);
    }
    @Nullable
    public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
         ......
         return mResources.loadDrawable(value, value.resourceId, density, mTheme);
         ......
    }

看到这一行

return mResources.loadDrawable(value, value.resourceId, density, mTheme);

进入Resources中

@NonNull
    Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
            throws NotFoundException {
        return mResourcesImpl.loadDrawable(this, value, id, density, theme);
    }

可以看到,背景最终是被Resources中的ResourcesImpl加载得到Drawable的,ResourcesImpl在Resources构造中创建出来

@Deprecated
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

    private Resources() {
        ......
        mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
                new DisplayAdjustments());
    }

我们再来看Resources是如何创建的,平常我们获取一些资源文件的时候,会这样获取Resources

context.getResources()

我们知道Context是抽象类,所以直接到Context的子类ContextImpl中去找

@Override
    public Resources getResources() {
        return mResources;
    }

那么这个mResources是什么时候创建的,找到这个方法

void setResources(Resources r) {
        if (r instanceof CompatResources) {
            ((CompatResources) r).setContext(this);
        }
        mResources = r;
    }

然后看到很多地方调用了这个方法


c.setResources(createResources(mActivityToken, pi, null, displayId, null,
                    getDisplayAdjustments(displayId).getCompatibilityInfo()));

c.setResources(createResources(mActivityToken, pi, null, displayId, null,
                    getDisplayAdjustments(displayId).getCompatibilityInfo()));

context.setResources(packageInfo.getResources());
 
context.setResources(ResourcesManager.getInstance().getResources(
                mActivityToken,
                mPackageInfo.getResDir(),
                paths,
                mPackageInfo.getOverlayDirs(),
                mPackageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                null,
                mPackageInfo.getCompatibilityInfo(),
                classLoader));

context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));

这样的方法有好几个,而且我们找不到其他地方给mResources赋值的地方,初步可以判断,Resources就是这样创建的,顺着这几个方法去看,你会发现,尽管setResources方法有好几种形式,但最后都会进入到ResourcesManger这个类中,这个方法的注释可以看看,Resources是会被缓存的,一个Resources的生命周期和这个Activity同等,当classloader改变,Resources也会改变

/**
     * Gets or creates a new Resources object associated with the IBinder token. References returned
     * by this method live as long as the Activity, meaning they can be cached and used by the
     * Activity even after a configuration change. If any other parameter is changed
     * (resDir, splitResDirs, overrideConfig) for a given Activity, the same Resources object
     * is updated and handed back to the caller. However, changing the class loader will result in a
     * new Resources object.
     * <p/>
     * If activityToken is null, a cached Resources object will be returned if it matches the
     * input parameters. Otherwise a new Resources object that satisfies these parameters is
     * returned.
     *
     * @param activityToken Represents an Activity. If null, global resources are assumed.
     * @param resDir The base resource path. Can be null (only framework resources will be loaded).
     * @param splitResDirs An array of split resource paths. Can be null.
     * @param overlayDirs An array of overlay paths. Can be null.
     * @param libDirs An array of resource library paths. Can be null.
     * @param displayId The ID of the display for which to create the resources.
     * @param overrideConfig The configuration to apply on top of the base configuration. Can be
     * null. Mostly used with Activities that are in multi-window which may override width and
     * height properties from the base config.
     * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is
     * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}.
     * @param classLoader The class loader to use when inflating Resources. If null, the
     * {@link ClassLoader#getSystemClassLoader()} is used.
     * @return a Resources object from which to access resources.
     */
public @Nullable Resources getResources(@Nullable IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }
/**
     * Gets an existing Resources object set with a ResourcesImpl object matching the given key,
     * or creates one if it doesn't exist.
     *
     * @param activityToken The Activity this Resources object should be associated with.
     * @param key The key describing the parameters of the ResourcesImpl object.
     * @param classLoader The classloader to use for the Resources object.
     *                    If null, {@link ClassLoader#getSystemClassLoader()} is used.
     * @return A Resources object that gets updated when
     *         {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)}
     *         is called.
     */
    private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        synchronized (this) {

            ......
            //下边是分两种情况,当activityToken(IBinder)是否为null的情况下根据key获取ResourcesImpl,
            //只要这个ResourcesImpl存在,就会直接得到Resources缓存返回或者新创建一个Resources返回
            if (activityToken != null) {
                ......
                //根据key获取与之对应的ResourcesImpl缓存
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    ......
                    // 只要根据key获取到的ResourcesImpl不为null,就根据这个ResourcesImpl去获取缓存的
                    //Resources,如果又这个Resources缓存,就返回,没有就创建,具体看这个方法
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                }

                // We will create the ResourcesImpl object outside of holding this lock.

            } else {
                ......
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    ......
                    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }
            }
        
        //当程序走到这里的时候,说明ResourcesImpl没有找到,Resources也就没有得到,那么这里就是根据
        //key创建出一个ResourcesImpl来,程序第一次运行的时候肯定会首先走到这里,所以,上边的代码可以
        //不用太重点的去看,接下来我们看看ResourcesImpl是如何被创建出来的,见方法createResourcesImpl
        // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
        ResourcesImpl resourcesImpl = createResourcesImpl(key);
        if (resourcesImpl == null) {
            return null;
        }

        synchronized (this) {
            ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
            if (existingResourcesImpl != null) {
                //从缓存中获取
                ......
                resourcesImpl.getAssets().close();
                resourcesImpl = existingResourcesImpl;
            } else {
                // 将创建的ResourcesImpl缓存起来
                mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
            }

            final Resources resources;
            //在此针对activityToken是否为null分别处理,在getOrCreateResourcesForActivityLocked和getOrCreateResourcesLocked
            //这两个方法中,我们重点关注,Resources不存在缓存的情况,所以,最终会看到Resourses的创建,
            //见下边的方法
            if (activityToken != null) {
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl, key.mCompatInfo);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }
            return resources;
        }
    
        //Resources的创建,这里看到根据条件的不同有两种方式获取,一个是new CompatResources,一个是
        //new Resources,进入到CompatResources类中,我们看到这个构造最终也会调用Resources的一个构造
        //方法public Resources(@Nullable ClassLoader classLoader) 返回Resources,可见这个Resources是new
        //出来的
        Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                : new Resources(classLoader);
        //给Resources设置ResourcesImpl
        resources.setImpl(impl);
        //加入缓存
        mResourceReferences.add(new WeakReference<>(resources));

getOrCreateResourcesForActivityLocked

/**
     * Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist
     * or the class loader is different.
     */
    private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken,
            @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl,
            @NonNull CompatibilityInfo compatInfo) {
        ......
            //有缓存获取缓存
            Resources resources = weakResourceRef.get();

            ......
                return resources;
            ......
        //没有缓存创建出来然后加入缓存
        Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                : new Resources(classLoader);
        resources.setImpl(impl);
        activityResources.activityResources.add(new WeakReference<>(resources));
        ......
        return resources;
    }

createResourcesImpl,可以看到ResourcesImpl的创建依赖于这几个对象AssetManager,DisplayMetrics,Configuration,DisplayAdjustments

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        ......
        return impl;
    }

着重看AssetManager的创建,这个类很关键

/**
     * Creates an AssetManager from the paths within the ResourcesKey.
     *
     * This can be overridden in tests so as to avoid creating a real AssetManager with
     * real APK paths.
     * @param key The key containing the resource paths to add to the AssetManager.
     * @return a new AssetManager.
    */
    @VisibleForTesting
    protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();

        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (key.mResDir != null) {
            // 将app中的资源路径都加入到AssetManager对象中,下边的方法都可以不看,我们重点
            //关注这个方法,可以说,应用之所以能加载资源,就是通过AssetManager以及调用addAssetPath对他设置的
            //资源路径
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }

       .....
        return assets;
    }

getDisplayMetrics,也只是new了一个DisplayMetrics

@VisibleForTesting
    protected @NonNull DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments da) {
        DisplayMetrics dm = new DisplayMetrics();
        final Display display = getAdjustedDisplay(displayId, da);
        if (display != null) {
            display.getMetrics(dm);
        } else {
            dm.setToDefaults();
        }
        return dm;
    }

generateConfig,Configuration也是new出来的

private Configuration generateConfig(@NonNull ResourcesKey key, @NonNull DisplayMetrics dm) {
        Configuration config;
       ....
            config = new Configuration(getConfiguration());
       ....
        return config;
    }

Resources的创建中有一个个关键的类,就是ResourcesImpl,这个类的创建需要几个重要的信息,其中之一就是AssetManager,通过直接实例话一个AssetManager对象并给这个对象设置资源路径,这是#Resources可以获取到文件资源的基础,DisplayMetrics或者Configuration则相当于一些固定设置,AssetManager中设置的这个路径,其实就是我们将要设置的apk的路径,从这个apk中获取资源

如此一来,我们获取到了Resources,就可以自由的去获取资源文件了

那么我们再回到最初,看看Resources是如何loadDrawable的

Resources中

@NonNull
    Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
            throws NotFoundException {
        return mResourcesImpl.loadDrawable(this, value, id, density, theme);
    }

ResourcesImpl中

@Nullable
    Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
            int density, @Nullable Resources.Theme theme)
            throws NotFoundException {
        ......

            Drawable dr;
            boolean needsNewDrawableAfterCache = false;
            if (cs != null) {
                dr = cs.newDrawable(wrapper);
            } else if (isColorDrawable) {
                dr = new ColorDrawable(value.data);
            } else {
                dr = loadDrawableForCookie(wrapper, value, id, density, null);
            }
            ......

            return dr;
        ......
/**
     * Loads a drawable from XML or resources stream.
     */
    private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
            int id, int density, @Nullable Resources.Theme theme) {
       
        
        ......

        final Drawable dr;

        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        try {
            //如果资源是xml文件
            if (file.endsWith(".xml")) {
                final XmlResourceParser rp = loadXmlResourceParser(
                        file, id, value.assetCookie, "drawable");
                dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme);
                rp.close();
            } else {
                //如果资源是图片资源,打开它得到流,然后解析得到drawable
                final InputStream is = mAssets.openNonAsset(
                        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
                is.close();
            }
        ......
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);

        return dr;
    }

经过上面的分析,皮肤切换的思路已经有了,下载apk文件,这个文件中包含有另一个皮肤的各种资源文件,通过Resources去加载这个apk中的资源,达到换肤的效果,关键代码如下

            //点击从手机中一个apk中获取图片资源并且设置给ImageView显示
            //获取系统的两个参数
            Resources superResources = getResources();
            //创建assetManger(无法直接new因为被hide了,所以用反射)
            AssetManager assetManager = AssetManager.class.newInstance();
            //添加资源目录(addAssetPath也是一样被hide无法直接调用)
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            method.setAccessible(true);//如果是私有的,添上防止万一某一天他变成了私有的
            method.invoke(assetManager,Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"red.skin");//注意你资源的名字要一致  
            //DisplayMetrics和Configuration的对象可以直接new出来,这里使用的是从getResources得到的Resources中获取,其实也是new出来的
            Resources resources = new Resources(assetManager,superResources.getDisplayMetrics(),superResources.getConfiguration());
            //用创建好的Resources获取资源(注意着三个参数,第一个是要获取资源的名字,我们设置的是girl,不要忘了,第二个参数代表这个资源在哪个文件夹中,第三个参数表示要获取资源的apk的包名,缺一不可)
            int identifier = resources.getIdentifier("girl", "drawable", "com.example.myapplication");
            if (identifier  != 0){
                Drawable drawable = resources.getDrawable(identifier);
                mImage.setImageDrawable(drawable);
            }

下面是native端AssetManager初始化的过程,为什么我们的app可以调用系统提供好的资源,以及这些资源是如何加载的,可以在这里得到答案

AssetManager的init()

public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }
//android_util_AssetManager.cpp
//AssetManager.cpp
private native final void init(boolean isSystem);
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem) { 
    if (isSystem) { 
        verifySystemIdmaps();
     } 
    // AssetManager.cpp 
    AssetManager* am = new AssetManager(); 
    if (am == NULL) {
         jniThrowException(env, "java/lang/OutOfMemoryError", ""); 
        return; 
    } 
    am->addDefaultAssets();
     ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
     env->SetLongField(clazz, gAssetManagerOffsets.mObject,reinterpret_cast<jlong>(am));
 }


bool AssetManager::addDefaultAssets() { 
    const char* root = getenv("ANDROID_ROOT");       
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set"); 
    String8 path(root); 
    // 初始化的时候加载系统的framework-res.apk    
    path.appendPath(kSystemAssets); 
    return addAssetPath(path, NULL); 
}

AssetManager的addAssetPath(String path)方法

bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
{
    AutoMutex _l(mLock);

    asset_path ap;

    String8 realPath(path);
    if (kAppZipName) {
        realPath.appendPath(kAppZipName);
    }
    ap.type = ::getFileType(realPath.string());
    if (ap.type == kFileTypeRegular) {
        ap.path = realPath;
    } else {
        ap.path = path;
        ap.type = ::getFileType(path.string());
        if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) {
            ALOGW("Asset path %s is neither a directory nor file (type=%d).",
                 path.string(), (int)ap.type);
            return false;
        }
    }

    // Skip if we have it already.
    for (size_t i=0; i<mAssetPaths.size(); i++) {
        if (mAssetPaths[i].path == ap.path) {
            if (cookie) {
                *cookie = static_cast<int32_t>(i+1);
            }
            return true;
        }
    }

    ALOGV("In %p Asset %s path: %s", this,
         ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string());

    // Check that the path has an AndroidManifest.xml
    Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
            kAndroidManifest, Asset::ACCESS_BUFFER, ap);
    if (manifestAsset == NULL) {
        // This asset path does not contain any resources.
        delete manifestAsset;
        return false;
    }
    delete manifestAsset;

    mAssetPaths.add(ap);

    // new paths are always added at the end
    if (cookie) {
        *cookie = static_cast<int32_t>(mAssetPaths.size());
    }

#ifdef HAVE_ANDROID_OS
    // Load overlays, if any
    asset_path oap;
    for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) {
        mAssetPaths.add(oap);
    }
#endif

    if (mResources != NULL) {
        appendPathToResTable(ap);
    }

    return true;
}
bool AssetManager::appendPathToResTable(const asset_path& ap) const {
    // skip those ap's that correspond to system overlays
    if (ap.isSystemOverlay) {
        return true;
    }

    Asset* ass = NULL;
    ResTable* sharedRes = NULL;
    bool shared = true;
    bool onlyEmptyResources = true;
    MY_TRACE_BEGIN(ap.path.string());
    Asset* idmap = openIdmapLocked(ap);
    size_t nextEntryIdx = mResources->getTableCount();
    ALOGV("Looking for resource asset in '%s'\n", ap.path.string());
    if (ap.type != kFileTypeDirectory) {
        if (nextEntryIdx == 0) {
            // The first item is typically the framework resources,
            // which we want to avoid parsing every time.
            sharedRes = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTable(ap.path);
            if (sharedRes != NULL) {
                // skip ahead the number of system overlay packages preloaded
                nextEntryIdx = sharedRes->getTableCount();
            }
        }
        if (sharedRes == NULL) {
            ass = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTableAsset(ap.path);
            if (ass == NULL) {
                ALOGV("loading resource table %s\n", ap.path.string());
                ass = const_cast<AssetManager*>(this)->
                    openNonAssetInPathLocked("resources.arsc",
                                             Asset::ACCESS_BUFFER,
                                             ap);
                if (ass != NULL && ass != kExcludedAsset) {
                    ass = const_cast<AssetManager*>(this)->
                        mZipSet.setZipResourceTableAsset(ap.path, ass);
                }
            }
            
            if (nextEntryIdx == 0 && ass != NULL) {
                // If this is the first resource table in the asset
                // manager, then we are going to cache it so that we
                // can quickly copy it out for others.
                ALOGV("Creating shared resources for %s", ap.path.string());
                sharedRes = new ResTable();
                sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
#ifdef HAVE_ANDROID_OS
                const char* data = getenv("ANDROID_DATA");
                LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set");
                String8 overlaysListPath(data);
                overlaysListPath.appendPath(kResourceCache);
                overlaysListPath.appendPath("overlays.list");
                addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx);
#endif
                sharedRes = const_cast<AssetManager*>(this)->
                    mZipSet.setZipResourceTable(ap.path, sharedRes);
            }
        }
    } else {
        ALOGV("loading resource table %s\n", ap.path.string());
        ass = const_cast<AssetManager*>(this)->
            openNonAssetInPathLocked("resources.arsc",
                                     Asset::ACCESS_BUFFER,
                                     ap);
        shared = false;
    }

    if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
        ALOGV("Installing resource asset %p in to table %p\n", ass, mResources);
        if (sharedRes != NULL) {
            ALOGV("Copying existing resources for %s", ap.path.string());
            mResources->add(sharedRes);
        } else {
            ALOGV("Parsing resources for %s", ap.path.string());
            mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
        }
        onlyEmptyResources = false;

        if (!shared) {
            delete ass;
        }
    } else {
        ALOGV("Installing empty resources in to table %p\n", mResources);
        mResources->addEmpty(nextEntryIdx + 1);
    }

    if (idmap != NULL) {
        delete idmap;
    }
    MY_TRACE_END();

    return onlyEmptyResources;
}
相关文章
|
3月前
|
Linux 调度 Android开发
【系统启动】Kernel怎么跳转到Android:linux与安卓的交界
【系统启动】Kernel怎么跳转到Android:linux与安卓的交界
49 0
|
5月前
|
测试技术 数据库 Android开发
0008Java安卓程序设计-ssm基于Android平台的健康管理系统
0008Java安卓程序设计-ssm基于Android平台的健康管理系统
27 0
|
5月前
|
Java 数据库 Android开发
0007Java安卓程序设计-ssm基于Android的校园新闻管理系统
0007Java安卓程序设计-ssm基于Android的校园新闻管理系统
31 0
|
5月前
|
关系型数据库 MySQL Android开发
0006Java安卓程序设计-ssm基于Android的校园二手商品交易平台1
0006Java安卓程序设计-ssm基于Android的校园二手商品交易平台
38 0
|
5月前
|
开发工具 数据库 Android开发
0001Java安卓程序设计-基于Android多餐厅点餐桌号后厨前台服务设计与开发2
0001Java安卓程序设计-基于Android多餐厅点餐桌号后厨前台服务设计与开发
28 0
|
5月前
|
Java 数据库 Android开发
0003Java安卓程序设计-springboot基于Android的学习生活交流APP
0003Java安卓程序设计-springboot基于Android的学习生活交流APP
35 0
|
3天前
|
Android开发
Android源代码定制:Overlay目录定制|调试Overlay资源是否生效
Android源代码定制:Overlay目录定制|调试Overlay资源是否生效
11 0
|
18天前
|
监控 API Android开发
构建高效安卓应用:探究Android 12中的新特性与性能优化
【4月更文挑战第8天】 在本文中,我们将深入探讨Android 12版本引入的几项关键技术及其对安卓应用性能提升的影响。不同于通常的功能介绍,我们专注于实际应用场景下的性能调优实践,以及开发者如何利用这些新特性来提高应用的响应速度和用户体验。文章将通过分析内存管理、应用启动时间、以及新的API等方面,为读者提供具体的技术实现路径和代码示例。
|
5月前
|
机器学习/深度学习 算法 物联网
100套安卓(Android)毕业设计(带论文)、大作业、现成原创作品(Android Studio)Android毕业设计项目,源码+论文
100套安卓(Android)毕业设计(带论文)、大作业、现成原创作品(Android Studio)Android毕业设计项目,源码+论文
342 4
|
3月前
|
Android开发 容器
Android安卓gravity和layout_gravity的区别
Android安卓gravity和layout_gravity的区别
46 2