Android Studio打包混淆带资源的SDK

           最近要实现一个把自己的整个应用打包成SDK接入到合作公司的应用中,刚开始是想采用插件(如360的DroidPlugin,原理解析链接:分析DroidPlugin,深入理解插件化框架)的形式来做,这样的话很方便,只要提供一个apk就行了。但是问题来了,一个完整的应用里面接入了很多第三方平台的功能,在插件app中运行的话有些第三方服务启动不了,如极光推送。因此只能放弃这种便捷途径。

           我们知道就算应用再大只要把应用代码与资源拷贝到调用工程中一起编译的方法是绝对可行的,只是这种方法看起来有点low而且繁琐麻烦;经过查找资料决定采用jar包+apk的方式来实现,这样我们中只要提供jar+apk+jniLib(如果有)给调用方即可。

          在Android应用中我们是可以访问zip文件(如apk)或者目录中的资源的,这里我们先了解一下一个apk文件的结构,用好压打开一个apk文件,如下图:

图1

          图1中dex文件表示的是代码,resources.arsc中保存了R文件中Id与资源的对应信息,因此在制作资源apk时可以用好压把除了resources.arsc、AndroidMainifest.xml(包含SDK版本信息)、res和META-INF(包含签名信息,用于安全校验)之外的文件全删除以减轻apk大小。

         那么在制作好资源apk后如何在jar中使用其中的资源呢,在类AssetManager通过addAssetPath方法可以添加资源路径,方法代码如下:

    /**
     * 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) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }
这些zip文件和目录可以作为资源路径的前提是里面必须包含resources.arsc文件,要使用apk中的资源我们只需创建一个AssetManager对象调用addAssetPath添加apk路径即可。

         通过上面额介绍相信我们已经知道如何提取apk 中的资源了,那么我们怎么把整个jar所引用的资源重定向到资源apk上呢,这里我们就得分析下Activity中Android的资源获取流程,就拿设置Activity Layout来说,设置Activity布局流程如下:

  public void setContentView(@LayoutRes int layoutResID) {
        <u>getWindow().setContentView(layoutResID);</u>
        initWindowDecorActionBar();
    }

         getWindow()获取的是Activity中mWindow变量,这个变量是在Activity的attach方法中赋值的其实就是PhoneWindow类对象,代码跳转到的PhoneWindow的setContentView方法:
  @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            <u>mLayoutInflater.inflate(layoutResID, mContentParent);</u>
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
         再进入mLayoutInflater的inflate方法:
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        <u>final Resources res = getContext().getResources();</u>
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
上面代码中,getContext()获取到的是当前Activity,并通过getResources()方法获取到一个系统给我们的Resource对象,用这个对象获取到的是应用本身的资源,因此我们只要重写jar中的所有Activity、Service(要用到资源的四大组件)的getResources()(getTheme()、getAssets()),把系统给的Resource替换成我们自定义的Resource就可以为jar指定资源了,这里只需要所有Activity集成一个Activity根类即可,整个应用代码不需要做太大改动。我的根类Activity代码如下
public class BaseSDKActivity extends FragmentActivity {

    public static final String TAG = "BaseSDKActivity";

    private AssetManager assets;

    private Resources customRes;

    private Resources.Theme customTheme;

    @Override
    public Resources getResources() {
        if(BuildConfig.IS_SDK) {
            return makeResources();
        }else{
            return super.getResources();
        }
    }

    @Override
    public Resources.Theme getTheme() {
        if(BuildConfig.IS_SDK) {
            return makeResourcesTheme();
        }else{
            return super.getTheme();
        }
    }

    @Override
    public AssetManager getAssets() {
        if(BuildConfig.IS_SDK) {
            return makeAssetmanager();
        }else{
            return super.getAssets();
        }
    }

    private Resources.Theme makeResourcesTheme(){
        if (customTheme == null) {
            customTheme = makeResources().newTheme();
            customTheme.setTo(super.getTheme());
        }
        return customTheme;
    }

    private Resources makeResources(){
        if (customRes == null) {
            customRes = new Resources(makeAssetmanager(), super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
        }
        return customRes;
    }

    private AssetManager makeAssetmanager() {
        if (assets == null) {
            try {
                assets = AssetManager.class.newInstance();
                Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
                addAssetPathMethod.setAccessible(true);
                addAssetPathMethod.invoke(assets, SmartBraApp.getApp().getBraResManager().getSDKResPath());//makeDir() + "/" + "sdk_bra.apk"
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }

        return assets;
    }
}
这里会有疑惑就是怎么保证jar包的资源Id和apk中resources.arsc中是对应的。其实简单,只要jar包和apk是同时编译出来的那么jar中的id和resources.arsc中就是对应的。

         接下来就是打包jar跟apk了,很多人都是用gradle命令打包的,但是我们要打包成的jar是要经过混淆的而且要把整个工程引用的jar包、远程引用库(类似jcenter中的库),module以及module引用的jar包和远程库经过混淆并打包到一个jar包中,对于gradle指令不熟悉的话很头疼,这些都不是问题,我发现一个很简单的方法如下:

对工程打包签名:

签名完成后会得到output中生成apk在classes-proguard/release目录下生成混淆后的jar包classes.jar(得在build.gradle中开启混淆,没开启可能不会生成)

在classes-proguard/release/classes.jar中打包的是所有引用库的代码,打包好后我们可以用好压打开classes.jar选择性的删除一些不行打包的代码,apk也是如此。

需要注意的是在jar中的Notification并不能使用apk中的资源,因为Notification是系统应用绘制的,post Notification到NotificationManager时,系统应用会根据发出post Notification的应用包名去获取该应用的资源,这个过程不可控所以Notification用到的Icon要手动拷贝到调用工程固定名字用getIdentifier()方法获取,如果用到so库也要手动拷贝,到这里就算完结了--


猜你喜欢

转载自blog.csdn.net/u010949962/article/details/52300350