Configuration(Android6.0)

说到Configuration,大家最熟悉的想必是Android:configChanges=[“mcc”,“mnc”, “locale”,”touchscreen”, “keyboard”,“keyboardHidden”,”navigation”,”screenLayout”, “fontScale”,“uiMode”,”orientation”, “screenSize”, “smallestScreenSize”],而这个属性了解的最多的就是在横竖屏切换的”orientation” 
Activity中定义android:configChanges=”orientation”,这样当横竖屏切换时,可以在onConfigurationChanged监听到改变,同时Activity不会被重启。不知道大家是否想过,为什么如果不定义orientation时,系统需要重启Activity呢? 
这还要从资源说起,Android应用程序在资源的定义中,会为不同的屏幕尺寸,语言,横竖屏定义不同的资源文件,存放不同的资源。以保证应用程序可以适配不同的屏幕,语言,横竖屏等等。而Android系统如何从当前应用程序中获取最合适的资源来显示呢? 

下面是官网给的Android系统从应用程序中获取资源的流程:官方链接 


图中第二步:table中包含MCC, MNC, Language18个资源获取的维度,table说的是资源配置表,有了这个资源配置表,Android系统就可以从aapt打包生成的resources.arsc中通过上面图标中的步骤,找到最合适的资源。而这个资源配置表,就是通过Configuration来设置的。 
再回到系统为什么需要重启Activity的问题上,比如当Configuration中语言(local)发生变化时,也就是说当前获取最佳资源的维度发生了变化,也就是说在Configuration变化之前已经启动的Activity上显示的资源已经不是最合适当前配置的资源了,因此系统需要更新资源配置,清除之前配置缓存的资源,然后重启已经启动的Activity以便显示更新Configuration配置的资源。 
接下来就以语言的变化流程,看系统是如何更新配置,清除缓存资源,然后重启已经启动的Activity

1 Setting应用中更换语言时:调用LocalePicker.updateLocale

public static void updateLocale(Locale locale) { 
IActivityManager am = ActivityManagerNative.getDefault(); 
Configuration config = am.getConfiguration(); 
config.setLocale(locale); 
config.userSetLocale = true; 
am.updateConfiguration(config); 
} 

这个函数主要获取AMS代理对象,然后从AMS中获取Configuration对象config,设置config中改变的对象locale也就是语言,并设置userSetLocaletrue, 最有通过AMS的代理对象调用AMSupdateConfiguration函数。

2 AMSupdateConfiguration函数主要是调用updateConfigurationLocked()函数。 
这个函数比较长,分三个部分来分析: 
1
初始化和其他设置阶段:

boolean updateConfigurationLocked(Configuration values, 
ActivityRecord starting, boolean persistent, boolean initLocale) { 
int changes = 0; 
if (values != null) { 
Configuration newConfig = new Configuration(mConfiguration); 
changes = newConfig.updateFrom(values); 
if (changes != 0) { 
if (!initLocale && values.locale != null && values.userSetLocale) { 
final String languageTag = values.locale.toLanguageTag(); 
SystemProperties.set(“persist.sys.locale”, languageTag); 
mHandler.sendMessage(mHandler.obtainMessage(SEND_LOCALE_TO_MOUNT_DAEMON_MSG, values.locale)); 
} 
mConfigurationSeq++; 
if (mConfigurationSeq <= 0) { 
mConfigurationSeq = 1; 
} 
newConfig.seq = mConfigurationSeq; 
mConfiguration = newConfig; 
mUsageStatsService.reportConfigurationChange(newConfig, mCurrentUserId);
  if (persistent && Settings.System.hasInterestingConfigurationChanges(changes)) {
                Message msg = mHandler.obtainMessage(UPDATE_CONFIGURATION_MSG);
                msg.obj = new Configuration(configCopy);
                mHandler.sendMessage(msg);

A 创建newConfig = new Configuration(mConfiguration); 
Configuration构造函数中会调用setTo函数,把mConfiguration的各个成员变量赋值到newConfig中。 
Configuration的成员变量中,包含mcc(移动国家编码) mnc(移动网络编码), locale(语言)keyboardHidden(键盘显隐),等等,每一个都是一个资源获取的维度,每一个都影响当前应用中最合适资源的匹配。 
同时在ActivityInfo中还定义了,CONFIG_MCCCONFIG_MNCCONFIG_LOCALE等静态整型变量,这些整型变量与Configuration的成员变量一一对应,并最终以位的形式标记,新旧Configuration哪些成员变量发生了变化。

B 调用Configuration. updateFrom通过改变的Configuration对象values更新newConfig,同时返回整数change,change就是以位的形式标记valuesnewConfig哪些成员变量发生了变化,在语言设置的流程中必然整数changeCONFIG_LOCALE位为”1”,标记当前语言发生了变化。

C 因为Configurationlocal成员变量发生了变化,所以change不为0,接下来, 
通过发送消息SEND_LOCALE_TO_MOUNT_DAEMON_MSG 设置localMountService, 
设置newConfig mConfigurationSeq 
提交newConfig UsageStatsService(统计服务) 
发送消息UPDATE_CONFIGURATION_MSG 把改变的Configuration 保存到设置中。 
二部分:通知各个应用进程,Configuration改变:

mSystemThread.applyConfigurationToResources(configCopy); 
for (int i=mLruProcesses.size()-1; i>=0; i–) { 
ProcessRecord app = mLruProcesses.get(i); 
try { 
if (app.thread != null) { 
app.thread.scheduleConfigurationChanged(configCopy); 
} 
} catch (Exception e) { 
} 
}

A 首先调用系统进程(及AMS所在进程)通知configuration改变:mSystemThread就是ActivityThread类对象:
ActivityThread
applyConfigurationToResources函数直接调用了ResourcesManager的函数applyConfigurationToResourcesLocked

final boolean applyConfigurationToResourcesLocked(Configuration config,
        CompatibilityInfo compat) {
    if (mResConfiguration == null) {
        mResConfiguration = new Configuration();
    }
    if (!mResConfiguration.isOtherSeqNewer(config) && compat == null) {
        return false;
    }
    int changes = mResConfiguration.updateFrom(config);
    mDisplays.clear();
    DisplayMetrics defaultDisplayMetrics = getDisplayMetricsLocked();
    if (compat != null && (mResCompatibilityInfo == null ||
            !mResCompatibilityInfo.equals(compat))) {
        mResCompatibilityInfo = compat;
        changes |= ActivityInfo.CONFIG_SCREEN_LAYOUT
                | ActivityInfo.CONFIG_SCREEN_SIZE
                | ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
    }
    if (config.locale != null) {
        Locale.setDefault(config.locale);
    }
    Resources.updateSystemConfiguration(config, defaultDisplayMetrics, compat);
    ApplicationPackageManager.configurationChanged();
    Configuration tmpConfig = null;
    for (int i = mActiveResources.size() - 1; i >= 0; i--) {
        ResourcesKey key = mActiveResources.keyAt(i);
        Resources r = mActiveResources.valueAt(i).get();
        if (r != null) {
            int displayId = key.mDisplayId;
            boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
            DisplayMetrics dm = defaultDisplayMetrics;
            final boolean hasOverrideConfiguration = key.hasOverrideConfiguration();
            if (!isDefaultDisplay || hasOverrideConfiguration) {
                if (tmpConfig == null) {
                    tmpConfig = new Configuration();
                }
                tmpConfig.setTo(config);
                if (!isDefaultDisplay) {
                    dm = getDisplayMetricsLocked(displayId);
                    applyNonDefaultDisplayMetricsToConfigurationLocked(dm, tmpConfig);
                }
                if (hasOverrideConfiguration) {
                    tmpConfig.updateFrom(key.mOverrideConfiguration);
                }
                r.updateConfiguration(tmpConfig, dm, compat);
            } else {
                r.updateConfiguration(config, dm, compat);
            }
        } else {
            mActiveResources.removeAt(i);
        }
    }
    return changes != 0;
}

A1:这个函数前面部分主要是初始化并找出新的Configuration和当前的有哪些改变了并保存在change中,如果语言改变了,设置为新的Configuration的语言。

A2: 调用Resources.updateSystemConfiguration(config, defaultDisplayMetrics,compat);函数更新系统配置在Resources类中有一个Resources的静态成员变量mSystem,主要是指向系统资源的(framework-res.apk)。因此这个函数直接调用mSystem.updateConfiguration()函数:实际上还是调用ResourcesupdateConfiguration函数:

public void updateConfiguration(Configuration config,
        DisplayMetrics metrics, CompatibilityInfo compat) {
    synchronized (mAccessLock) {
        if (compat != null) {
            mCompatibilityInfo = compat;
        }
        if (metrics != null) {
            mMetrics.setTo(metrics);
        }
        mCompatibilityInfo.applyToDisplayMetrics(mMetrics);
        final int configChanges = calcConfigChanges(config);
        if (mConfiguration.locale == null) {
            mConfiguration.locale = Locale.getDefault();
            mConfiguration.setLayoutDirection(mConfiguration.locale);
        }
        if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) {
            mMetrics.densityDpi = mConfiguration.densityDpi;
            mMetrics.density = mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
        }
        mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale;
        String locale = null;
        if (mConfiguration.locale != null) {
            locale = adjustLanguageTag(mConfiguration.locale.toLanguageTag());
        }
        final int width, height;
        if (mMetrics.widthPixels >= mMetrics.heightPixels) {
            width = mMetrics.widthPixels;
            height = mMetrics.heightPixels;
        } else {
            width = mMetrics.heightPixels;
            height = mMetrics.widthPixels;
        }
        final int keyboardHidden;
        if (mConfiguration.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO
                && mConfiguration.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) {
            keyboardHidden = Configuration.KEYBOARDHIDDEN_SOFT;
        } else {
            keyboardHidden = mConfiguration.keyboardHidden;
        }
        mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
                locale, mConfiguration.orientation,
                mConfiguration.touchscreen,
                mConfiguration.densityDpi, mConfiguration.keyboard,
                keyboardHidden, mConfiguration.navigation, width, height,
                mConfiguration.smallestScreenWidthDp,
                mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
                mConfiguration.screenLayout, mConfiguration.uiMode,
                Build.VERSION.RESOURCES_SDK_INT);
        mDrawableCache.onConfigurationChange(configChanges);
        mColorDrawableCache.onConfigurationChange(configChanges);
        mColorStateListCache.onConfigurationChange(configChanges);
        mAnimatorCache.onConfigurationChange(configChanges);
        mStateListAnimatorCache.onConfigurationChange(configChanges);
        flushLayoutCache();
    }

Resources成员变量mAssets AssetManager类对象主要用于访问资源。 
Resources
成员变量mDrawableCachemColorDrawableCachemColorStateListCachemAnimatorCachemStateListAnimatorCache,主要用于缓存资源的。 
这个函数前面主要是初始化获得用于过滤资源访问的资源表,然后调用

A3: mAssets.setConfiguration函数这个函数是一个native函数:

static void android_content_AssetManager_setConfiguration(JNIEnv* env, jobject clazz, 
jint mcc, jint mnc, jstring locale, jint orientation, jint touchscreen, jint density, 
jint keyboard, jint keyboardHidden, jint navigation, jint screenWidth, jint screenHeight, jint smallestScreenWidthDp, jint screenWidthDp, jint screenHeightDp, 
jint screenLayout, jint uiMode, jint sdkVersion){ 
AssetManager* am = assetManagerForJavaObject(env, clazz); 
ResTable_config config; 
memset(&config, 0, sizeof(config)); 
const char* locale8 = locale != NULL ? env->GetStringUTFChars(locale, NULL) : NULL; 
static const jint kScreenLayoutRoundMask = 0x300; 
static const jint kScreenLayoutRoundShift = 8; 
config.mcc = (uint16_t)mcc; 
config.mnc = (uint16_t)mnc; 
config.orientation = (uint8_t)orientation; 
config.touchscreen = (uint8_t)touchscreen; 
config.density = (uint16_t)density; 
config.keyboard = (uint8_t)keyboard; 
config.inputFlags = (uint8_t)keyboardHidden; 
config.navigation = (uint8_t)navigation; 
config.screenWidth = (uint16_t)screenWidth; 
config.screenHeight = (uint16_t)screenHeight; 
config.smallestScreenWidthDp = (uint16_t)smallestScreenWidthDp; 
config.screenWidthDp = (uint16_t)screenWidthDp; 
config.screenHeightDp = (uint16_t)screenHeightDp; 
config.screenLayout = (uint8_t)screenLayout; 
config.uiMode = (uint8_t)uiMode; 
config.sdkVersion = (uint16_t)sdkVersion; 
config.minorVersion = 0; 
config.screenLayout2 = (uint8_t)((screenLayout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift); 
am->setConfiguration(config, locale8); 
if (locale != NULL) env->ReleaseStringUTFChars(locale, locale8); 
}

A4: 调用缓存资源成员变量的onConfigurationChange函数清除缓存中不合适当前Configuration的资源这里以mDrawableCache为例来分析
mDrawableCache
DrawableCache类对象,DrawableCache继承自ThemedResourceCache 
ThemedResourceCache
成员变量: 
ArrayMap
对象mThemedEntries,主要缓存以Themekey的资源, 
LongSparseArray
对象mUnthemedEntries,主要缓存指定不是以Theme来区分的资源, 
LongSparseArray
对象 mNullThemedEntries,主要缓存以themekey但是Theme为空的资源。 
另外:onConfigurationChange的参数configChanges标记configuration的改变位,是通过前面调用calcConfigChanges得到的,这个函数会先计算出新的Configuration和旧的Configuration的改变位change, 然后调用ActivityInfo.activityInfoConfigToNativechange转换为nativeCofiguration改变为。因为native的改变位和ActivityInfo定义的位不一样。所以需要转换一下,才能匹配后面资源对应的位,检查资源是否会受到configuration的影响。 
mDrawableCache.onConfigurationChange,
就是调用ThemedResourceCacheonConfigurationChange函数,这个函数直接调用prune(int configChanges)函数:

private boolean prune(int configChanges) {
    synchronized (this) {
        if (mThemedEntries != null) {
            for (int i = mThemedEntries.size() - 1; i >= 0; i--) {
                if (pruneEntriesLocked(mThemedEntries.valueAt(i), configChanges)) {
                    mThemedEntries.removeAt(i);
                }
            }
        }
        pruneEntriesLocked(mNullThemedEntries, configChanges);
        pruneEntriesLocked(mUnthemedEntries, configChanges);
        return mThemedEntries == null && mNullThemedEntries == null
                && mUnthemedEntries == null;
    }
}

这个函数主要是遍历mThemedEntries,然后调用pruneEntriesLocked函数:

private boolean pruneEntriesLocked(@Nullable LongSparseArray<WeakReference<T>> entries, int configChanges) {
    for (int i = entries.size() - 1; i >= 0; i--) {
        final WeakReference<T> ref = entries.valueAt(i);
        if (ref == null || pruneEntryLocked(ref.get(), configChanges)) {
            entries.removeAt(i);
        }
    }
    return entries.size() == 0;
}

这个函数主要是遍历entries中每一个资源,如果资源(弱引用)还没有被销毁,然后调用pruneEntryLocked函数检查缓存的资源的敏感配置位和当前configChanges相同。如果相同则表示该资源已经过期,需要重新获取,所以把该资源从entries中移除。 
需要解释的是:pruneEntryLocked最终调用的是

public boolean shouldInvalidateEntry(Drawable.ConstantState entry, int configChanges) { 
return Configuration.needNewResources(configChanges, entry.getChangingConfigurations()); 
}

然后调用Configuration.needNewResources比较新的configChangesentry. getChangingConfigurations这两个是否有相同的位,如果有表示缓存的资源entry受当前configuration改变影响,所以需要清除。 
entry. getChangingConfigurations其实就是Drawable的成员变量mChangingConfigurations 
这个成员变量的初始化,就是在ResourcesloadDrawable函数最后一段来设置的

 if (dr != null) {
        dr.setChangingConfigurations(value.changingConfigurations);
        cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
    }

A5回到ResourcesupdateConfiguration函数中,最后调用flushLayoutCache();函数清除layout文件的缓存。

B 再回到第二部分: 
遍历mLruProcesses中的ProcessRecord调用app的成员变量thread的函数app.thread.scheduleConfigurationChanged(configCopy); 
成员变量thread,ActivityThread下的一个本地binder对象ApplicationThread的代理对象。 
app.thread.scheduleConfigurationChanged
就调用到了各个进程中。 
这个函数只是往消息队列里面发送了一条CONFIGURATION_CHANGED的消息。在处理这条消息时就调用到了函数:

final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {
    int configDiff = 0;
          ……
    synchronized (mResourcesManager) {
        mResourcesManager.applyConfigurationToResourcesLocked(config, compat);
        configDiff = mConfiguration.updateFrom(config);
        config = applyCompatConfiguration(mCurDefaultDisplayDpi);
    }
         ……
    ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(false, config);
    freeTextLayoutCachesIfNeeded(configDiff);
    if (callbacks != null) {
        final int N = callbacks.size();
        for (int i=0; i<N; i++) {
            performConfigurationChanged(callbacks.get(i), config);
        }
    }
}

这个函数主要是调用mResourcesManager.applyConfigurationToResourcesLocked函数:(这个函数比较长但因为有前面的分析,并不难,所以不贴代码了) 
B1
ResourcesManager中有一个成员变量mActiveResources因为一个进程中可能运行多个应用,也就可能包含多个Apk,每一Apk对应一个Resources对象。所以函数主要作用就是,一方面调用Resources.updateSystemConfiguration更新系统Resources的对象(及framework-res.apk)另一方面遍历mActiveResources中每一个Resources对象,便调用Resources.updateConfiguration(前面已近有分析)

B2 调用collectComponentCallbacks函数收集所有实现了ComponentCallbacks2接口的对象callbacks。这个函数也比较简单,ComponentCallbacks2接口就是包含onConfigurationChanged函数,这里收集的实际就是当前进程中运行的所有 Application ActivityServiceContentProvider实例。

B3 遍历callbacks调用performConfigurationChanged执行其onConfigurationChanged函数,前提是需要在manifest文件中定义了android:configChanges属性。以语言变化为列,需要定义android:configChanges=”local”

第三部分:重启所有Activity(条件android:configChanges没有包含语言的变化及不包含”local”

boolean kept = true;
    final ActivityStack mainStack = mStackSupervisor.getFocusedStack();
    if (mainStack != null) {
        if (changes != 0 && starting == null) {
            starting = mainStack.topRunningActivityLocked(null);
        }
        if (starting != null) {
            kept = mainStack.ensureActivityConfigurationLocked(starting, changes);
            mStackSupervisor.ensureActivitiesVisibleLocked(starting, changes);
        }
    }

A 首先调用mainStack.topRunningActivityLocked函数,获得正在启动的starting Activity然后调用mainStack.ensureActivityConfigurationLocked检查是否需要更新新的Configuration 
这个函数比较长,经过缩减代码如下:

final boolean ensureActivityConfigurationLocked(ActivityRecord r,
        int globalChanges) {
    final Configuration oldConfig = r.configuration;
    final Configuration oldStackOverride = r.stackConfigOverride;
    r.configuration = newConfig;
    r.stackConfigOverride = mOverrideConfig;
              ……
    final int changes = oldConfig.diff(newConfig) | stackChanges;

    if ((changes&(~r.info.getRealConfigChanged())) != 0 || r.forceNewConfig) {
        r.configChangeFlags |= changes;
        r.startFreezingScreenLocked(r.app, globalChanges);
        r.forceNewConfig = false;
        if (r.app == null || r.app.thread == null) {
            destroyActivityLocked(r, true, "config");
        } else if (r.state == ActivityState.PAUSING) {
            r.configDestroy = true;
            return true;
        } else if (r.state == ActivityState.RESUMED) {
            relaunchActivityLocked(r, r.configChangeFlags, true);
            r.configChangeFlags = 0;
        } else {
            relaunchActivityLocked(r, r.configChangeFlags, false);
            r.configChangeFlags = 0;
        }
        return false;
    }
    return true;
}

这个函数缩减部分主要是通过ActivityoldConfigoldStackOverridenewConfig以及mOverrideConfig合成合适当前ActivityConfiguration,最后计算出具体的Configuration的改变位changes 
然后判断if ((changes&(~r.info.getRealConfigChanged())) != 0 ||r.forceNewConfig)如果当前ActivityActivityInfo中的成员变量configChanges不包含changes,也就是说android:configChanges中不包含”local”或者Activity需要强制更新Configuration。那么接着判断: 
(r.app == null || r.app.thread == null)Activity
所运行的进程,或者所运行进程中的ApplicationThreadbinder代理对象)为空,则说明当前Activity可以销毁了,这调用destroyActivityLocked函数销毁当前Activity 
(r.state == ActivityState.PAUSING)
如果当前Activity是暂停中设置r.configDestroy = true;在后面的处理过程中如果configDestroytrue 则会先destoryActivity. 
(r.state == ActivityState.RESUMED)
如果ActivityActivityState.RESUMED状态或者其他状态,则都调用relaunchActivityLocked(); 
relaunchActivityLocked()
函数主要就是调用r.app.thread.scheduleRelaunchActivity(); 
和前面一样就是调用,ApplicationThreadscheduleRelaunchActivity函数来重启这个Activity.具体重启流程大家可以自行查看。

B 调用mStackSupervisor.ensureActivitiesVisibleLocked(starting, changes);函数:

void ensureActivitiesVisibleLocked(ActivityRecord starting, int configChanges) {
    for (int displayNdx = mActivityDisplays.size() - 1; displayNdx >= 0; --displayNdx) {
        final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
        final int topStackNdx = stacks.size() - 1;
        for (int stackNdx = topStackNdx; stackNdx >= 0; --stackNdx) {
            final ActivityStack stack = stacks.get(stackNdx);
            stack.ensureActivitiesVisibleLocked(starting, configChanges);
        }
    }
}

mActivityDisplays ArrayList< ActivityDisplay>对象,这是AMSActivity的管理方式,有时间在专门写一篇Android6.0Activity的管理以及启动流程的文章,其实这个函数就是遍历每一个ActivityDisplay,然后遍历ActivityDisplay的成员变量mStacks中的每一个ActivityStack,再遍历ActivityStack成员变量mTaskHistory每一个TaskRecord。最后再遍历TaskRecord成员变量mActivities中的每一个ActivityRecord。最后调用ensureActivityConfigurationLocked处理是否重启每一个Activity 
至此,设置中语言发生变化,然后更新Configuration,清空缓存资源,设置AssetManager的资源配置表,回调ActivityonConfigurationChange函数,重启Activity的整个过程分析完成。





































































  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Android Studio配置 Android Studio是一款由Google开发的集成开发环境(IDE),用于开发Android应用程序。在使用Android Studio之前,需要进行一些配置,以确保它能够正常工作。 首先,需要安装Java开发工具包(JDK)。Android Studio需要JDK 8或更高版本才能运行。可以从Oracle官网下载并安装JDK。 其次,需要下载并安装Android Studio。可以从官网下载最新版本的Android Studio,并按照安装向导进行安装。 安装完成后,需要配置Android Studio。首先,需要设置Android SDK的路径。Android SDK是一组开发工具,包括Android平台、SDK工具和其他组件,用于开发Android应用程序。可以在安装Android Studio时选择安装Android SDK,或者在后续安装过程中安装它。在Android Studio中,可以通过“File”菜单中的“Project Structure”选项来设置Android SDK的路径。 另外,还需要配置Android虚拟设备(AVD)。AVD是一种模拟Android设备的工具,用于在开发和测试应用程序时模拟不同的设备和操作系统版本。可以在Android Studio中通过“Tools”菜单中的“AVD Manager”选项来创建和管理AVD。 最后,还需要配置Gradle。Gradle是一种构建工具,用于构建和打包Android应用程序。在Android Studio中,默认使用Gradle构建工具。可以在“File”菜单中的“Settings”选项中配置Gradle。 以上就是Android Studio的配置过程。配置完成后,就可以开始使用Android Studio开发Android应用程序了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值