63

我基于(gradle 1.10 和 gradle plugin 0.8)的 android 项目包含一个大型 android-library,它是 3 个不同 android-apps 的依赖项

在我的图书馆中,我希望能够使用这样的结构

if (BuildConfig.SOME_FLAG) {
    callToBigLibraries()
}

因为 proguard 将能够根据 SOME_FLAG 的最终值减小生成的 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 的正确方法是什么,这些应用程序的标志将在构建时被覆盖?

4

7 回答 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:

boolean debug = (Boolean) getBuildConfigValue(this, "DEBUG");

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

于 2014-08-12T14:34:06.600 回答
36

更新:随着较新版本的 Android Gradle 插件publishNonDefault已被弃用并且不再有效。所有变体现已发布。

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

尝试在项目中设置publishNonDefault为:true

android {
    ...
    publishNonDefault true
    ...
}

并将以下依赖项添加到使用该库的应用程序项目中:

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

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

于 2015-07-16T14:46:01.303 回答
22

你不能做你想做的事,因为BuildConfig.SOME_FLAG不会被正确地传播到你的图书馆;构建类型本身不会传播到库——它们总是作为 RELEASE 构建的。这是错误https://code.google.com/p/android/issues/detail?id=52962

解决方法:如果您可以控制所有库模块,则可以确保所涉及的所有代码callToBigLibraries()都位于可以使用 ProGuard 干净利落的类和包中,然后使用反射以便在以下情况下可以访问它们如果它们不存在,它们就会存在并优雅地退化。您本质上是在做同样的事情,但是您是在运行时而不是编译时进行检查,这有点困难。

如果您无法弄清楚如何执行此操作,请告诉我;如果您需要,我可以提供样品。

于 2014-01-29T00:25:42.540 回答
8

我在应用程序和库中都使用了静态 BuildConfigHelper 类,这样我就可以将包 BuildConfig 设置为库中的最终静态变量。

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

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 中的值一样好,用于在每个应用程序中更改该导入行的最小维护。

于 2015-05-02T07:41:09.243 回答
5

对于 applicationId 与包不同的情况(即每个项目多个 applicationIds)并且您想从库项目访问:

使用 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);
}
于 2015-07-23T12:47:22.720 回答
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;
    }
}
于 2016-02-03T16:27:15.153 回答
-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 实现的上下文 - 查看 Applicatio 类扩展了什么并了解上下文包装的可能性
  2. 最终应用程序返回的类可以由不同的类加载器加载,而不是那些将使用它的类加载器 - 取决于加载器实现和加载器的一些典型原则(层次结构,可见性)
  3. 一切都取决于在这种情况下简单委托的实现!- 解决方案可能更复杂 - 我只想在这里展示 DELEGATION 模式的用法:)

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

  1. Class.forName(className); - 因为没有指定装载机
  2. context.getPackageName() + ".BuildConfig"

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

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

*** 在我的解决方案中进行哪些更改以使其更加完美

  1. 用它的名字替换类,这将减少当许多类加载不同的加载器访问/或用于获得涉及类的最终结果时可能出现的问题(了解什么描述了两个类之间的相等性(对于运行时的编译器) - 简而言之,类相等定义不是一个自我类,而是由加载器和类构成的一对。(一些家庭工作 - 尝试用不同的加载器加载内部类,并通过加载不同加载器的外部类访问它) - 它事实证明我们会得到非法访问错误:)即使内部类在同一个包中也有所有修改器允许访问它外部类:)编译器/链接器“VM”将它们视为两个不相关的类......
于 2018-06-04T19:24:06.723 回答