IT이야기

Gradle : 앱에서 설정되는 플래그를 사용하여 Android 라이브러리에서 BuildConfig를 사용하는 방법

cyworld 2021. 3. 26. 20:47
반응형

Gradle : 앱에서 설정되는 플래그를 사용하여 Android 라이브러리에서 BuildConfig를 사용하는 방법


내 (gradle 1.10 및 gradle 플러그인 0.8) 기반 Android 프로젝트는 3 개의 서로 다른 Android 앱에 대한 종속성 인 큰 Android 라이브러리로 구성됩니다.

제 도서관에서는 이런 구조를 사용하고 싶습니다.

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를 빌드하는 올바른 방법은 무엇입니까?


BuildConfig.SOME_FLAG라이브러리에 제대로 전파되지 않기 때문에 원하는 것을 할 수 없습니다 . 빌드 유형 자체는 라이브러리로 전파되지 않으며 항상 RELEASE로 빌드됩니다. 이것은 버그입니다 https://code.google.com/p/android/issues/detail?id=52962

해결 방법 : 모든 라이브러리 모듈을 제어 할 수있는 경우 모든 코드가 callToBigLibraries()ProGuard로 깔끔하게 분리 될 수있는 클래스 및 패키지에 있는지 확인한 다음 리플렉션을 사용하여 다음과 같은 경우 액세스 할 수 있습니다. 존재하지 않으면 우아하게 타락합니다. 기본적으로 동일한 작업을 수행하지만 컴파일 시간이 아닌 런타임에 검사를 수행하고 있으며 조금 더 어렵습니다.

이 작업을 수행하는 방법을 파악하는 데 문제가 있으면 알려주십시오. 필요한 경우 샘플을 제공 할 수 있습니다.


해결 방법으로 리플렉션을 사용하여 라이브러리가 아닌 앱에서 필드 값을 가져 오는이 메서드를 사용할 수 있습니다.

/**
 * 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 Issue Tracker 에서이 솔루션을 공유했습니다 .


다음 솔루션 / 해결 방법이 저에게 효과적입니다. Google 문제 추적기의 일부 사람이 게시했습니다.

설정 시도 publishNonDefaulttrue에서 라이브러리 프로젝트 :

android {
    ...
    publishNonDefault true
    ...
}

그리고 라이브러리를 사용하는 프로젝트에 다음 종속성을 추가합니다 .

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

이렇게하면 라이브러리를 사용하는 프로젝트에 올바른 라이브러리 빌드 유형이 포함됩니다.


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}"
}

자바 :

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);
}

앱과 라이브러리 모두에서 정적 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의 값을 갖는 좋은으로합니다.


둘 다 사용

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;
    }
}

저에게 이것은 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. 최종 앱에서 반환 된 클래스는이를 사용할 사용자와 다른 클래스 로더에 의해로드 될 수 있습니다. 로더 구현 및 로더의 일반적인 일부 원칙 (계층 구조, 가시성)에 따라 다릅니다.
    1. 모든 것은이 경우 간단한 DELEGATION !!!의 구현에 달려 있습니다. -솔루션은 더 정교 할 수 있습니다.-여기에서는 DELEGATION 패턴의 사용법 만 보여주고 싶었습니다. :)

** 모든 반사 기반 패턴이 약점을 가지고 있고 일부 특정 조건에서 모두 실패하기 때문에 왜 내가 모든 반사 기반 패턴을 다운 웨트했는지 :

  1. Class.forName (className); -지정되지 않은 로더 때문에
  2. context.getPackageName () + ".BuildConfig"

    a) context.getPackageName ()- "기본적으로-그렇지 않으면 b)"는 매니페스트에 정의 된 패키지가 아니라 응용 프로그램 ID를 반환합니다 (때때로 둘 다 동일 함). 매니페스트 패키지 속성이 사용되는 방법과 흐름을 확인합니다. apt 도구는이를 애플리케이션 ID로 대체합니다 (예를 들어 pkg가 의미하는 것은 ComponentName 클래스 참조).

    b) context.getPackageName ()-구현자가 원하는 것을 반환합니다. : P

*** 더 완벽하게 만들기 위해 내 솔루션에서 변경해야 할 사항

  1. 클래스를 액세스하는 다른 로더로로드 된 많은 클래스가 / 또는 클래스를 포함하는 최종 결과를 얻는 데 사용될 때 나타날 수있는 문제를 제거 할 이름으로 클래스를 대체합니다 (두 클래스 간의 동등성을 설명하는 것이 무엇인지 알아 두십시오 (런타임시 컴파일러의 경우). -한마디로 클래스 동등성은 자체 클래스가 아니라 로더와 클래스로 구성된 쌍을 정의합니다 . (일부 집안일-다른 로더로 내부 클래스를로드하고 다른 로더로로드 된 외부 클래스로 액세스)-it 불법 액세스 오류가 발생할 것입니다. :) 내부 클래스가 동일한 패키지에 있더라도 외부 클래스에 대한 액세스를 허용하는 모든 수정자가 있습니다. :) 컴파일러 / 링커 "VM"은 이들을 관련없는 두 클래스로 취급합니다.

여유 시간이 생기면 곧 여기서 직면 할 수있는 문제에 대해 더 많이 쓸 것입니다.

참조 URL : https://stackoverflow.com/questions/21365928/gradle-how-to-use-buildconfig-in-an-android-library-with-a-flag-that-gets-set

반응형