Android动态加载

Dload

一个动态加载jar包的实例

github:https://github.com/andoop/Dload


概念▪说明

动态加载:

此处的动态加载是指从服务端或者其他地方获取jar包,并在运行时期,加载jar包,并与 jar包互相调用。
本例中,为了方便演示,将要动态加载的jar放到了assets目录下,在程序运行时期,将其加载到/data/data/pkgname/files下,来模拟从服务端获取

为什么要动态加载:
  1. 减少应用安装包体积, 程序包很大时,将部分模块打成jar包,动态的从服务端获取,然后加载
  2. 方便升级维护,将主要功能放入动态jar包中,主应用(不包含动态jar包的部分)主要来维护动态jar,包括jar的加载,升级等。升级应用,可以更新动态jar包,用户在不重新安装的情况下,就能做到部分(强调部分,是因为它比较适用于部分功能升级)升级
  3. 插件化开发,动态jar包可以当做插件来开发,在应用中,需要什么功能,就下载什么插件,如一些皮肤主题类的功能,可以作为插件功能来开发,用户更换皮肤或者主题时,只需要下载和更新对应的插件就行,如:桌面系统(不同的桌面主题),锁屏(不同的锁屏界面和风格)
  4. 感觉还有好多好处,不一一列举了........

demo比较简单,但是能演示和介绍整个流程以及思想就行啦,干脆利索,效果图如下:
图片名称

图片名称
图片名称
打开插件就会进入插件中的页面(ListFragment),点击任何一个条目,进入插件中另一个页面(DetailFragment)

功能▪实现细节

首先从一个类入手,这个类是功能核心,那就是ClassLoader,这个类可以让我们实现动态加载,ClassLoader是一个抽象类,实际开发过程中,我们一般是使用其具体的子类DexClassLoader、PathClassLoader这些类加载器来加载类的,它们的不同之处是:

  1. DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;
  2. PathClassLoader只能加载系统中已经安装过的apk;

所以,因为我们要加载jar,所以我们选择DexClassLoader。


看一下整个工程目录结构

宿主工程和动态工程都依赖dloadlib,dloadlib中定义了两个工程都交互的接口,并对动态jar进行加载和调用

看一下dloadlib工程结构


看一下代码细节

宿主工程中:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化插件
        Dload.getInstance(this).init(new DloadListener() {
            
            @Override
            public void onSuccess() {
                // TODO Auto-generated method stub
                Log.e(">>>","sccess");
            }
            
            @Override
            public void onStart() {
                // TODO Auto-generated method stub
                Log.e(">>>","start");
            }
            
            @Override
            public void onProgress(int persent) {
                // TODO Auto-generated method stub
                Log.e(">>>","progress>"+persent+"%");
            }
            
            @Override
            public void onFail(String err) {
                // TODO Auto-generated method stub
                Log.e(">>>","fail>"+err);
            }
        });
    }

  
    public void showList(View view){
        //打开插件中ListFragment
        Dload.getInstance(this).showList();
    }
}

Dload类中逻辑

public class Dload {

    private Context context;
    private DloadProxy dloadProxy;
    
    public static Dload INSTANCE;
    private Dload(Context context){
        this.context = context;
    }
    //单例
    public static Dload getInstance(Context context){
        
      if(INSTANCE==null){
          synchronized (Dload.class) {
              if(INSTANCE==null){
                  INSTANCE=new Dload(context);
              }
            
        }
      }
      return INSTANCE;
    }
    /**
     * 初始化状态,例如:加载插件
     */
    public void init(DloadListener dloadListener){
        //真正去加载插件
        DexExcutor.getInstance(context).init(dloadListener);
        //初始化代理类
        dloadProxy=new DloadProxy();
    }
    /**
     * 打开列表,最终会调用插件功能
     */
    public void showList(){
        //调用代理类的方法,最终会调用插件功能
        dloadProxy.showList(context);
    }
}

看一下怎么初始化插件,DexExcutor类走起..

public class DexExcutor {

    public static DexExcutor INSTANCE;
    private Context context;
    private DloadListener dloadListener;
    private String filepath;
    private DexClassLoader dexClassLoader;
    private DexExcutor(Context context){
        this.context = context;
        filepath=context.getFilesDir().getAbsolutePath();
    }
    //单例
    public static DexExcutor getInstance(Context context){
        
      if(INSTANCE==null){
          synchronized (Dload.class) {
              if(INSTANCE==null){
                  INSTANCE=new DexExcutor(context);
              }
            
        }
      }
      return INSTANCE;
    }
    //初始化插件
    public void init(DloadListener dloadListener){
        this.dloadListener = dloadListener;
        if(dloadListener!=null)
            dloadListener.onStart();
        
        //检查插件是否存在
        File file = new File(filepath+File.separator+MConstans.jarname);
        if(file.exists()){
            //存在,加载插件
            loadPlugin();
        }else{
            //不存在,则将插件从asset或者服务端加载到本地
            downloadPlugin();
        }
    }
    
    private void downloadPlugin() {
        // TODO Auto-generated method stub
        DloadUtils.downloadPlugin(context,"plugin.jar",filepath+File.separator+MConstans.jarname,new DloadUtils.Downloadlistener() {
            
            @Override
            public void onSuccess() {
                // TODO Auto-generated method stub
                loadPlugin();
            }
            
            @Override
            public void onStart() {
                // TODO Auto-generated method stub
                
            }
            
            @Override
            public void onProgress(int progress) {
                // TODO Auto-generated method stub
                if(dloadListener!=null){
                    dloadListener.onProgress(progress);
                }
            }
            
            @Override
            public void onFail(String err) {
                // TODO Auto-generated method stub
                if(dloadListener!=null){
                    dloadListener.onFail(err);
                }
            }
        });
        
    }
    /**
     * 加载插件
     */
    private void loadPlugin() {
        
        try {
            //根据插件的路径,实例化对应的dexclassloader
            dexClassLoader = new DexClassLoader(filepath+File.separator+MConstans.jarname, filepath,null, context.getClassLoader());
            if(dloadListener!=null){
                dloadListener.onProgress(100);
                dloadListener.onSuccess();
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            if(dloadListener!=null)
                dloadListener.onFail(e.getMessage());
        }
    }
    
    /**
     * 根据类名获取一个实例
     * 
     * @param className 动态工程中类的全类名
     *           
     * @return Object
     */
    public Object newInstance(String className) {
        try {
            if (null != dexClassLoader) {
                Class<?> dynamic_class = dexClassLoader.loadClass(className);
                return dynamic_class.newInstance();
            }
        } catch (ClassNotFoundException e) {
            Log.e(">>>", "newInstance:" + e.getMessage());
        } catch (InstantiationException e) {
            Log.e(">>>", "newInstance:" + e.getMessage());
        } catch (IllegalAccessException e) {
            Log.e(">>>", "newInstance:" + e.getMessage());
        }
        return null;
    }

    
}

流程很简单,再看一下代理是怎样个代理,进入DloadProxy,

public class DloadProxy implements Idload {
  
    @Override
    public void showList(Context context) {
        //实例化动态工程中的类,这个类实现了Idload接口
        Idload newInstance = (Idload) DexExcutor.getInstance(context).newInstance("cn.andoop.android.dloadplugin.DloadImp");
        if(newInstance==null){

            Toast.makeText(context, "插件还没有加载好", Toast.LENGTH_SHORT).show();
            return;
        }
        //真正调用动态工程中的功能
        newInstance.showList(context);
    }
}

有没有很简单!(因为是demo,所以逻辑越简单越好,没有做很多容错处理)


看一下动态工程中怎样实现的吧

public class DloadImp implements Idload {

    @Override
    public void showList(Context context) {
        // TODO Auto-generated method stub
        DLoadActivity.start(context, "cn.andoop.android.dloadplugin.ui.ListFragment", null);
    }

    
}

DloadImp 是动态工程中的类,这个类就是对dloadlib中Idload的实现
DloadActivity是dloadlib中的acitvity,这个activity就是预先在宿主中注册过了activity,用它来承载动态工程中所有的页面(Fragment),这样看来,是不是动态工程中展现页面只能是Fragment?,其实不然,动态工程中其实可以有fragment,activity,service,等组件,只不过因为是动态加载进来的,说白了,最后这些类的实例最后都是反射得到的,他们的生命周期,系统可不管,所以直接使用动态工程中的fragment、activity、service等有生命周期的组件肯定是不行的,但是方法总是有地,那就是“占坑”,那就是预先在宿主中注册一些组件,如activity、service,然后让这些组件来代理动态工程中组件的生命周期,这样问题就解决了,但是也有其他问题呀,那就是动态工程中的资源怎样使用呢,能不能通过R去访问呢,这些问题留在后面解答,在我的dltest工程里寻找答案吧


看一下DLoadActivity吧,看看怎样代理fragment生命周期

public class DLoadActivity extends FragmentActivity {

    private final static String CLASS_NAME = "classname";
    private String mClassName;
    private Bundle mBundle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (null != getIntent()) {
            mClassName = getIntent().getStringExtra(CLASS_NAME);
            mBundle = getIntent().getExtras();
        }

        setContentView(getContentView());

    }

    private View getContentView() {
        // TODO Auto-generated method stub

        LinearLayout ll_content = new LinearLayout(this);
        ll_content.setOrientation(LinearLayout.VERTICAL);
        ll_content.setId(10000012);
        FragmentTransaction ft = this.getSupportFragmentManager()
                .beginTransaction();
        // 通过类名,反射获取到对应的类。既Fragment
        Fragment fragment = (Fragment) DexExcutor.getInstance(this)
                .newInstance(mClassName);
        if (null != fragment) {
            if (null != mBundle) {
                fragment.setArguments(mBundle);
            }
            ft.add(ll_content.getId(), fragment, mClassName);
            ft.commit();
        }

        return ll_content;
    }
    //调用DLoadActivity加载fragment
    public static void start(Context context, String className, Bundle bundle) {
        Intent intent = new Intent(context, DLoadActivity.class);
        if (null != bundle) {
            intent.putExtras(bundle);
        }
        intent.putExtra(CLASS_NAME, className);
        context.startActivity(intent);
    }

}

动态工程中fragment又是怎样写的呢?

public class ListFragment extends Fragment implements OnItemClickListener {

private RelativeLayout rl_content;
private MBaseAdapter mBaseAdapter;
private List<ListItem> data;

@Override
@Nullable
public View onCreateView(LayoutInflater inflater,
        @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    // TODO Auto-generated method stub
    initData();
    return getContentView();
}


private View getContentView() {
    rl_content = new RelativeLayout(getActivity());
    ListView listView = new ListView(getActivity());
    rl_content.addView(listView);
    listView.setAdapter(mBaseAdapter);
    listView.setOnItemClickListener(this);
    return rl_content;
}
   ...后面代码就不粘贴啦

在这个demo中,可不能给fragment写布局文件了,必须进行代码布局了,其实也没啥嘛,布局文件能干的,我都能在代码中实现,不就是多写几行代码而已,但是图片资源怎样使用呢?
看看下面吧


在动态工程中使用图片资源

这是动态工程中另一个fragment,在它中,使用了图片资源
public class DetailFragment extends Fragment {

    @Override
    @Nullable
    public View onCreateView(LayoutInflater inflater,
            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        return getContentView();
    }

    //照样通过代码布局
    private View getContentView() {
        LinearLayout linearLayout = new LinearLayout(getActivity());
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        TextView title = new TextView(getActivity());
        ImageView imageView=new ImageView(getActivity());
        WebView webView = new WebView(getActivity());
        
        title.setPadding(20, 20, 20, 20);
        title.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        title.setGravity(Gravity.CENTER);
        
        webView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        webView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // TODO Auto-generated method stub
                return false;
            }
        });
        webView.getSettings().setJavaScriptEnabled(true);
        
        linearLayout.addView(title);
        linearLayout.addView(imageView);
        linearLayout.addView(webView);

        //获取资源,就这样,将图片名称传入这个工具类,就可以获取bp对象
        Bitmap imageResouce = new ResourceUtils(getActivity()).getImageResouce("assets/car.jpg");
        imageView.setImageBitmap(imageResouce);

        Bundle bundle = getArguments();
        if(bundle!=null&&( bundle.getSerializable("item")!=null)){
            if(bundle.getSerializable("item") instanceof ListItem) {
                ListItem listitem = (ListItem) bundle.getSerializable("item");
                title.setText(listitem.text);
                webView.loadUrl(listitem.url);
            }
        }
        
        return linearLayout;
    }
}

ResourceUtils是一个工具类,就是读取文件中图片而已,有兴趣可以看看,也可以完善一下
代码分析到此结束,如有不懂,还是看看code吧,show you code!,但是本篇教程距离结束还早呢,还是往下看吧


怎样将动态工程打包呢?

task buildLib (type: Jar,dependsOn:'build') {
    from ('build/intermediates/classes/release')
    //包含资源目录
    from ('src/main/assets/')
    //from fileTree(dir: 'src/main',includes: ['assets/**'])

}

自定义task,打包的时候也需要用到的图片放到jar中,需要将图片放入到assets文件夹下的assets下
执行buildLib即可生成动态工程对应的jar,所在目录为build/lib


对生成的jar再次处理

android dalvik 不能直接加载变异.class文件,需要再次处理一下,编译成dex文件,通过dx命令即可,

格式如下

dx --dex --output=out.jar in.jar

次工程对应的批处理文件(在builddex中)如下:

cd E:\android_dev\sdk\sdk\build-tools\23.0.2\
e:
dx --dex --output=F:\projects\mprojects\DLoad\builddex\dex\plugin.jar F:\projects\mprojects\DLoad\builddex\dloadplugin.jar

需要处理的jar为dloadplugin.jar,生成的jar为plugin.jar,执行dx命令需要到对应文件夹下执行才行,如我的:E:\android_dev\sdk\sdk\build-tools\23.0.2\,不多解释了,很简单

最后,将生成的plugin.jar放入宿主工程的assets目录即可(本工程是这样做的,真正开发中,jar往往会放在服务端,宿主去检查时候需要更新插件),本工程只为演示而生,更多变化,以及完善,这都需要你结合实际开发而去应对。


总结一下吧

动态加载用法流程如下:

  1. 定义宿主和动态工程交互接口如:ILoad,并预先注册需要的组件
  2. 宿主或着动态工程中实现接口
  3. 生成动态jar
  4. 通过dexclassloader加载动态jar包,动态jar包可以来源于服务端或者其他地方
  5. 宿主调用动态工程中ILoad的实现,动态工程也可以调用宿主方法,宿主实现一下ILoad既可(工程中没有体现,原理比较简单,通过dloadlib,直接调用就行)

最后说一下注意点

工程中值演示了加载一个动态jar,当然也可以加载多个,但是每一个动态jar对应一个dexclassloader对象
对jar的升级维护校验,工程没有体现,真正开发,这些都要考虑。

暂时就这些吧,后期如有需求和疑问,还会再补充完善的

周一、二会不断更新内容,欢迎持续关注andoop,每周干货永不停!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容