Gradle: 如何在一个带有应用程序设置标志的Android库中使用BuildConfig?

68
我的基于(gradle 1.10和gradle插件0.8)的Android项目包含一个巨大的Android库,是3个不同的Android应用程序的依赖项。
在我的库中,我希望能够使用这样的结构。
if (BuildConfig.SOME_FLAG) {
    callToBigLibraries()
}

由于SOME_FLAG的最终值,proguard可以减小生成的apk的大小。

但我无法通过gradle实现如下操作:

* the BuildConfig produced by the library doesn't have the same package name than the app
* I have to import the BuildConfig with the library package in the library
* The apk of an apps includes the BuildConfig with the package of the app but not the one with the package of the library.

我曾尝试使用BuildTypes等相关功能,但没有成功。

release {
    // packageNameSuffix "library"
    buildConfigField "boolean", "SOME_FLAG", "true"
}
debug {
    //packageNameSuffix "library"
    buildConfigField "boolean", "SOME_FLAG", "true"
}

如何构建一个共享的BuildConfig,让我的库和应用程序可以在编译时覆盖其标志?

7个回答

50

作为一种解决方法,您可以使用这种方法,它使用反射从应用程序(而不是库)获取字段值:

/**
 * Gets a field from the project's BuildConfig. This is useful when, for example, flavors
 * are used at the project level to set custom fields.
 * @param context       Used to find the correct file
 * @param fieldName     The name of the field-to-access
 * @return              The value of the field, or {@code null} if the field is not found.
 */
public static Object getBuildConfigValue(Context context, String fieldName) {
    try {
        Class<?> clazz = Class.forName(context.getPackageName() + ".BuildConfig");
        Field field = clazz.getField(fieldName);
        return field.get(null);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return null;
}

例如,要获取 DEBUG 字段,只需从您的 Activity 调用此代码:


注:本翻译保留了原文中的html标签,但为方便阅读,已添加了中文标点符号。
boolean debug = (Boolean) getBuildConfigValue(this, "DEBUG");

我也在AOSP问题跟踪器上分享了这个解决方案。


2
BuildConfig是否保证在该包中? - Vlad
3
@Vlad 我想象中有一种方法可以更改BuildConfig的输出位置——然而根据我的经验,这对于默认设置是有保证的。 - Phil
5
在你的applicationId和Java类的包名称不同时,这种方法似乎可以正常工作。 (例如,如果您正在构建如此提到的变体:http://tools.android.com/tech-docs/new-build-system/applicationid-vs-packagename)。看起来context.getPackageName()将返回applicationId,这可能不是BuildConfig.class所在的Java类包。有什么解决方法吗? - NPike
2
最近我发现了一个ClassNotFoundException问题,具体表现为:如果你在gradle的buildConfig中声明了一个后缀(例如,如果你想要同一个应用但是根据不同的构建类型(debug或release)使用不同的包名进行测试),那么context.getPackageName()将返回带有该后缀的包名,但是BuildConfig仍然会部署在没有后缀的包名上。因此,当该方法寻找com.example.app.suffix.BuildConfig类时,实际上需要寻找的是com.example.app.BuildConfig类。对此有什么想法吗? - Tito Leiva
2
当ProGuard启动时会出现中断! - ofavre
显示剩余6条评论

36

更新: 随着Android Gradle插件的新版本,publishNonDefault已经被弃用且不再起作用。现在所有的变体都会被发布。

以下解决方案/变通方法适用于我。它是由谷歌问题跟踪器中的某个人发布的:

尝试在库项目中将publishNonDefault设置为true

android {
    ...
    publishNonDefault true
    ...
}

将以下依赖项添加到正在使用该库的应用项目中:

dependencies {
    releaseCompile project(path: ':library', configuration: 'release')
    debugCompile project(path: ':library', configuration: 'debug')
}

这样,使用该库的项目将包含正确的库构建类型。


22
您无法做您想要的事情,因为BuildConfig.SOME_FLAG不会正确传播到您的库;构建类型本身不会传播到库——它们总是作为RELEASE构建。这是bug https://code.google.com/p/android/issues/detail?id=52962 解决方法:如果您可以控制所有库模块,您可以确保所有被callToBigLibraries()触及的代码都在您可以使用ProGuard进行干净分离的类和包中,然后使用反射方式访问它们,如果存在则访问,如果不存在则优雅降级。您实际上正在执行相同的操作,但您是在运行时而不是编译时进行检查,这有点难度。
如果您不知道如何执行此操作,请告诉我;如果需要,我可以提供示例。

我明白了,感谢你的回答:我会监控相关问题,并在有进展时重新提出这个问题。目前为止,为了解决这个问题,我做了一些类似于你的建议的事情,而且我不想要的代码确实被清除了。 - Lakedaemon
你可以通过 defaultPublishConfig "release"defaultPublishConfig "debug" 来覆盖库的构建变体。 - IgorGanapolsky

14

我在应用程序和库中都使用了一个静态的BuildConfigHelper类,以便我可以将包的BuildConfig设置为我的库中的final static变量。

在应用程序中,放置一个像这样的类:

package com.yourbase;

import com.your.application.BuildConfig;

public final class BuildConfigHelper {

    public static final boolean DEBUG = BuildConfig.DEBUG;
    public static final String APPLICATION_ID = BuildConfig.APPLICATION_ID;
    public static final String BUILD_TYPE = BuildConfig.BUILD_TYPE;
    public static final String FLAVOR = BuildConfig.FLAVOR;
    public static final int VERSION_CODE = BuildConfig.VERSION_CODE;
    public static final String VERSION_NAME = BuildConfig.VERSION_NAME;

}

在图书馆里:

package com.your.library;

import android.support.annotation.Nullable;

import java.lang.reflect.Field;

public class BuildConfigHelper {

    private static final String BUILD_CONFIG = "com.yourbase.BuildConfigHelper";

    public static final boolean DEBUG = getDebug();
    public static final String APPLICATION_ID = (String) getBuildConfigValue("APPLICATION_ID");
    public static final String BUILD_TYPE = (String) getBuildConfigValue("BUILD_TYPE");
    public static final String FLAVOR = (String) getBuildConfigValue("FLAVOR");
    public static final int VERSION_CODE = getVersionCode();
    public static final String VERSION_NAME = (String) getBuildConfigValue("VERSION_NAME");

    private static boolean getDebug() {
        Object o = getBuildConfigValue("DEBUG");
        if (o != null && o instanceof Boolean) {
            return (Boolean) o;
        } else {
            return false;
        }
    }

    private static int getVersionCode() {
        Object o = getBuildConfigValue("VERSION_CODE");
        if (o != null && o instanceof Integer) {
            return (Integer) o;
        } else {
            return Integer.MIN_VALUE;
        }
    }

    @Nullable
    private static Object getBuildConfigValue(String fieldName) {
        try {
            Class c = Class.forName(BUILD_CONFIG);
            Field f = c.getDeclaredField(fieldName);
            f.setAccessible(true);
            return f.get(null);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

然后,在您的库中的任何位置,您想要检查BuildConfig.DEBUG的地方,都可以检查BuildConfigHelper.DEBUG,并且无需上下文即可从任何地方访问它,其他属性也是一样。 我这样做是为了使该库适用于我所有的应用程序,无需传递上下文或以其他方式设置包名称,当将其添加到新应用程序中时,只需要更改导入行即可。

编辑:我想再强调一下,这是最简单的(也是此处列出的唯一方法),可以在所有应用程序中将值分配给库中的最终静态变量,而无需上下文或硬编码某个地方的包名称,这几乎和在默认库BuildConfig中具有值一样好,对于每个应用程序中的维护最小化,只需更改每个应用程序中的导入行即可。


2
在库中,修复类名引用(常量BUILD_CONFIG)似乎不是解决此问题的可靠方法。 - StaticBR
Helpers类名从不更改,因此固定的类名引用是可以的,并且也是Helper的全部意义所在。它的工作是从应用程序BuildConfig中获取值并将其存储在库知道名称的类中。 - Kane O'Riley
1
@KaneO'Riley先生,非常感谢您的卓越解决方案。真的帮了我很多。 - Andrew G

5

如果应用程序ID与包不同(即每个项目有多个应用程序ID)并且您想从库项目中访问:

使用Gradle将基本包存储在资源中。

在main/AndroidManifest.xml中:

android {
    applicationId "com.company.myappbase"
    // note: using ${applicationId} here will be exactly as above
    // and so NOT necessarily the applicationId of the generated APK
    resValue "string", "build_config_package", "${applicationId}"
}

在Java中:

public static boolean getDebug(Context context) {
    Object obj = getBuildConfigValue("DEBUG", context);
    if (obj instanceof Boolean) {
        return (Boolean) o;
    } else {
        return false;
    }
}

private static Object getBuildConfigValue(String fieldName, Context context) {
    int resId = context.getResources().getIdentifier("build_config_package", "string", context.getPackageName());
    // try/catch blah blah
    Class<?> clazz = Class.forName(context.getString(resId) + ".BuildConfig");
    Field field = clazz.getField(fieldName);
    return field.get(null);
}

1
使用两者
my build.gradle
// ...
productFlavors {
    internal {
        // applicationId "com.elevensein.sein.internal"
        applicationIdSuffix ".internal"
        resValue "string", "build_config_package", "com.elevensein.sein"
    }

    production {
        applicationId "com.elevensein.sein"
    }
}

我想要像下面这样调用:
Boolean isDebug = (Boolean) BuildConfigUtils.getBuildConfigValue(context, "DEBUG");

BuildConfigUtils.java

public class BuildConfigUtils
{

    public static Object getBuildConfigValue (Context context, String fieldName)
    {
        Class<?> buildConfigClass = resolveBuildConfigClass(context);
        return getStaticFieldValue(buildConfigClass, fieldName);
    }

    public static Class<?> resolveBuildConfigClass (Context context)
    {
        int resId = context.getResources().getIdentifier("build_config_package",
                                                         "string",
                                                         context.getPackageName());
        if (resId != 0)
        {
            // defined in build.gradle
            return loadClass(context.getString(resId) + ".BuildConfig");
        }

        // not defined in build.gradle
        // try packageName + ".BuildConfig"
        return loadClass(context.getPackageName() + ".BuildConfig");

    }

    private static Class<?> loadClass (String className)
    {
        Log.i("BuildConfigUtils", "try class load : " + className);
        try { 
            return Class.forName(className); 
        } catch (ClassNotFoundException e) { 
            e.printStackTrace(); 
        }

        return null;
    }

    private static Object getStaticFieldValue (Class<?> clazz, String fieldName)
    {
        try { return clazz.getField(fieldName).get(null); }
        catch (NoSuchFieldException e) { e.printStackTrace(); }
        catch (IllegalAccessException e) { e.printStackTrace(); }
        return null;
    }
}

-3

对我来说,这是唯一可接受的解决方案,用于确定 Android 应用程序的 BuildConfig.class 文件:

// base entry point 
// abstract application 
// which defines the method to obtain the desired class 
// the definition of the application is contained in the library 
// that wants to access the method or in a superior library package
public abstract class BasApp extends android.app.Application {

    /*
     * GET BUILD CONFIG CLASS 
     */
    protected Class<?> getAppBuildConfigClass();

    // HELPER METHOD TO CAST CONTEXT TO BASE APP
    public static BaseApp getAs(android.content.Context context) {
        BaseApp as = getAs(context, BaseApp.class);
        return as;
    }

    // HELPER METHOD TO CAST CONTEXT TO SPECIFIC BASEpp INHERITED CLASS TYPE 
    public static <I extends BaseApp> I getAs(android.content.Context context, Class<I> forCLass) {
        android.content.Context applicationContext = context != null ?context.getApplicationContext() : null;
        return applicationContext != null && forCLass != null && forCLass.isAssignableFrom(applicationContext.getClass())
            ? (I) applicationContext
            : null;
    }
     
    // STATIC HELPER TO GET BUILD CONFIG CLASS 
    public static Class<?> getAppBuildConfigClass(android.content.Context context) {
        BaseApp as = getAs(context);
        Class buildConfigClass = as != null
            ? as.getAppBuildConfigClass()
            : null;
        return buildConfigClass;
    }
}

// FINAL APP WITH IMPLEMENTATION 
// POINTING TO DESIRED CLASS 
public class MyApp extends BaseApp {

    @Override
    protected Class<?> getAppBuildConfigClass() {
        return somefinal.app.package.BuildConfig.class;
    }

}

在库中的使用方法:

 Class<?> buildConfigClass = BaseApp.getAppBuildConfigClass(Context);
 if(buildConfigClass !- null) {
     // do your job 
 }

*有几件事情需要注意:

  1. getApplicationContext() - 可能会返回一个不是 App ContexWrapper 实现的上下文 - 查看 Application 类继承了什么,了解上下文封装的可能性。
  2. 最终应用程序返回的类可能由不同于使用它的类加载器加载 - 这取决于加载器实现和一些典型原则(层次结构、可见性)。
  3. 一切都取决于简单 DELEGATION 的实现 - 解决方案可以更复杂 - 我只想在这里展示 DELEGATION 模式的用法 :)

**为什么我反对所有基于反射的模式,因为他们都有弱点,在某些特定条件下都会失败:

  1. Class.forName(className); - 由于未指定加载器
  2. context.getPackageName() + ".BuildConfig"

a) context.getPackageName() - “默认情况下 - 否则请参见 b)” 不返回清单中定义的包,而是应用程序 ID(有时两者相同),请查看清单包属性的使用及其流程 - 最终 apt 工具将用应用程序 ID 替换它(例如 ComponentName 类的 pkg 在那里表示什么)

b) context.getPackageName() - 将返回实现想要的内容 :P

*** 我的解决方案中需要改变什么以使其更加完美

  1. 将类替换为其名称,这将消除许多类使用不同加载器访问/或用于获取涉及类的最终结果时可能出现的问题(了解描述两个类之间相等性的内容(对于运行时编译器)- 简而言之,类相等性不定义自身类,而是由加载器和类组成的一对。 (一些家庭作业-尝试使用不同的加载器加载内部类,并通过使用不同的加载器加载的外部类访问它)- 结果会出现非法访问错误:)即使内部类与所有允许访问它的外部类具有相同的修饰符 :) 编译器/链接器“VM”将它们视为两个不相关的类...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接