Android 플러그인 - 리소스 로드

6484 단어
전언
리소스는 APK 패키지의 부피가 너무 큰 원인 중 하나입니다.플러그인화 기술은 모듈을 분리하여 플러그인의 형식으로 불러옵니다.플러그인 기술에서는 각 플러그인을 별도의 APK로 독립적으로 실행할 수 있습니다.숙주가 플러그인을 시작하는 클래스는 플러그인 클래스의 자원 문제와 관련된 것을 피하기 어렵다.
그러면 플러그인 자원을 어떻게 불러오는가가 해결해야 할 문제가 된다.
의 원리
APK 패키지 프로세스 참조: Android 플러그인 기반 - APK 패키지 프로세스
Android 프로젝트는 apk로 포장될 때, aapt를 사용하여 프로젝트의 자원 이름과 id를 R.java에서 일일이 비추습니다.
R.java
    public static final int ic_launcher=0x7f060054;
    public static final int ic_launcher_background=0x7f060055;
    public static final int ic_launcher_foreground=0x7f060056;
    public static final int notification_action_background=0x7f060057;

리소스를 로드할 때마다 Resources 를 검색합니다.다음을 통해 다음을 수행할 수 있습니다.
Drawable drawable = resources.getDrawable(resId);

해당 리소스를 가져옵니다.
따라서 우리의 핵심 사고방식은 플러그인의 Resources와 플러그인의resId를 가져오는 것이다.
실천하다
그러면 플러그인의 Resources를 어떻게 얻을 수 있을까요?
ContextThemeWrapper.java
    @Override
    public Resources getResources() {
        return getResourcesInternal();
    }

    private Resources getResourcesInternal() {
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                mResources = super.getResources();
            } else {
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }

Resources.java는 App 자원의 관리 유형이다.
    /**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

주석을 통해 Resources를 차별화하는 것은 AssetManager입니다.
우리는 이 구조 방법이 PackageParser#parseBaseApk 방법에서 호출되는 것을 알아차렸다.Apk의 설치 과정을 본떠서 설치되지 않은 Apk에 Resources를 만들 수 있지 않을까요?
따라서 계속해서 AssetManager를 살펴보겠습니다.java:
/**
 * Provides access to an application's raw asset files; see {@link Resources}
 * for the way most applications will want to retrieve their resource data.
 * This class presents a lower-level API that allows you to open and read raw
 * files that have been bundled with the application as a simple stream of
 * bytes.
 */
public final class AssetManager implements AutoCloseable {
...
}

주석을 통해 우리는 이 종류가 자원 파일에 접근하는 방식을 제공한다는 것을 알 수 있다.소스 코드를 읽으면 다음 방법을 찾을 수 있습니다.
AssetManager.java
    /**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
        return  addAssetPathInternal(path, false);
    }

주석을 통해 알 수 있듯이 이 방법은 ZIP 파일을 포장하는 방법을 제공합니다.이 주석은 Application 에 사용되지 않습니다.APK 파일이 사실 ZIP 파일인 건 알고 있습니다.
여기를 보고 우리의 생각이 떠올랐다.이 방법을 통해 플러그인 APK의 path를 가져와 AssetManager를 포장합니다.그리고 AssetManager로 Resources를 생성하면 이 Resources가 플러그인의 Resources입니다.플러그인 APK는 설치되어 있지 않지만 설치 절차를 따릅니다.
위의 분석을 통해 플러그인 Resources 를 검색할 수 있습니다.
/**
 *        Resource  
 * @param context   apk    
 * @param pluginPath   apk   , apk 
 * @return
 */
public static Resources getPluginResources(Context context, String pluginPath) {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        //       addAssetPath(String path)
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        //    Apk     AssetManager
        addAssetPath.invoke(assetManager, pluginPath);
        //     apk Resources  
        Resources superRes = context.getResources();
        //     apk Resources  
        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
        return mResources;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

이로써 플러그인에 대한 Resources를 획득했습니다. Drawable과 같은 리소스를 획득하는 방법은 다음과 같습니다.
Drawable drawable = resources.getDrawable(resId);

따라서 플러그인 R.java에 대응하는 플러그인 자원에 대한resId가 부족합니다.
우리는 반사하는 방식으로 R.java의 id를 얻을 수 있습니다:
    /**
     *   apk      id
     *
     * @param context      
     * @param pluginPath apk  
     */
    public static int getResId(Context context, String pluginPath, String apkPackageName, String resName) {
        try {
            //              app_dex     ,          
            File optimizedDirectoryFile = context.getDir("dex", Context.MODE_PRIVATE);
            //      DexClassLoader    ,  :
            // 1、  dex apk   jar     ,
            // 2、apk、jar     dex     ,
            // 3、  library   ,   null,
            // 4、 ClassLoader
            DexClassLoader dexClassLoader = new DexClassLoader(pluginPath, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
            //    apk       ,   R                   id
            Class> clazz = dexClassLoader.loadClass(apkPackageName + ".R$drawable");
            Field field = clazz.getDeclaredField(resName);//    resName       
            return field.getInt(R.id.class);//    id
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

Resources 및 resId 검색을 완료하면 플러그인에 대한 리소스를 얻을 수 있습니다.
                    int resId = getResId(MainActivity.this.getApplication(), PATH, PLUGIN_PACKAGE_NAME, "ic_launcher");
                    Resources resources = getPluginResources(MainActivity.this.getApplication(), PATH);
                    Drawable drawable = resources.getDrawable(resId);
                    mIvTest.setImageDrawable(drawable);

이로써 플러그인화된 불러오는 자원의 기본적인 사고방식과 원리다.
총결산
  • 플러그인의 Resources와resId를 가져와 자원을 불러오는 명확한 사고방식
  • APK 분석 프로세스를 따라 플러그인 Resources
  • 를 가져옵니다.
  • 플러그인의 R.java에 대한 반사로resId
  • 가져오기
  • 로드 완료
  • 좋은 웹페이지 즐겨찾기