Android插件化——加载其他APP页面


因工作需要,整理下插件化开发的demo,方便交流使用。

1.分析

插件化开发开发时将整个app拆分,包括一个宿主和多个插件,每个插件都是一个apk(组件化每个组件是lib),最终打包的时候将宿主apk和插件apk分开或者联合打包。

使用场景就是原生APP-A在不安装原生APP-B的情况下加载其页面。

其实有很多APP都是采用这种方式,比如现在百度一下,烂大街的支付宝加载淘票票。。。那怎么判断是否为插件加载呢?
1.没有明显过程动画,不需要安装apk。如果采用Intent启动的方式,会有明显过场动画。
2.判断是否为webview加载。开启:开发者选项——显示边界布局,如果没有代表布局的彩色栅格,即为原生加载。

2.优点

  • 宿主和插件分开编译。
  • 并行开发,节约时间。
  • 动态更新插件。
  • 按需下载插件模块(第一次下载较慢)。
  • 方法分离为多个APP,避免65535.

3.详细过程

废话不多说,边看代码边解释。最后实现场景是APP-A加载APP-B的Activity

3.1 标准化加载接口

既然需要A与B能够连接(通信),需要定义一个接口。因为是加载Activity,接口当然就要符合Activity的生命周期。同时,加载布局需要Context的支持,所以也需要Context的注入。
新建一个lib,创建一个如上接口。(方便调试,所有的lib与module都在一个Project下),文件结构入下图所示:包含Interface的lib路径
参考代码如下:

package com.heima.plugs;

import android.app.Activity;
import android.os.Bundle;

/**
 * 符合加载标准(Activity生命周期 + Context注入)
 */
public interface PlugInterface {

    void onCreate(Bundle saveInstance);
    void onStart();
    void onResume();
    void onRestart();
    void onDestroy();
    void onStop();
    void onPause();

    /**
     * 注入context
     * @param context
     */
    void attachContext(Activity context);
}

3.2待加载的APP-B

新建module作为单独的被加载APP。
由于B中的Activity需要被加载,所以选择写一个基类BaseActivity,实现Interface,同时注意注入的Context对象,所以还要重写与上下文对象相关的方法。让工程中Activity继承BaseActivity,方便操作。Module结构入下图所示:在这里插入图片描述
从图中可以看出,module中除了BaseActivity外,还分别创建了3个Activity等待APP-A加载。当然,这些Activity都继承BaseActivity。
这时候我们需要注意的是AndroidManifest.xml中的Activity注册信息。AndroidManifest.xml
注意三个Activity的注册顺序,这个跟下面所讲的在A中加载有关系。这个顺序,跟在A中获得的Activity队列相关。后面会详细描述。

上代码:

package com.heima.otherapp;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;

import com.heima.plugs.PlugInterface;

public class BaseActivity extends Activity implements PlugInterface {
    public  static  final  String TAG="BaseActivity";
    protected Activity thatActivity;

    /**
     * 注入自己的上下文
     * 如果为空 使用父类
     *
     * @param layoutResID
     */
    @Override
    public void setContentView(int layoutResID) {
        if (thatActivity == null) {
            super.setContentView(layoutResID);
        } else {
            thatActivity.setContentView(layoutResID);
        }
    }

    @Override
    public void setContentView(View view) {
        if (thatActivity == null) {
            super.setContentView(view);
        } else {
            thatActivity.setContentView(view);
        }
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (thatActivity == null) {
            super.setContentView(view, params);
        } else {
            thatActivity.setContentView(view, params);
        }
    }

    @Override
    public LayoutInflater getLayoutInflater() {
        if (thatActivity == null) {
            return super.getLayoutInflater();
        } else {
            return thatActivity.getLayoutInflater();
        }
    }

    @Override
    public Window getWindow() {
        if (thatActivity == null) {
            return super.getWindow();
        } else {
            return thatActivity.getWindow();
        }
    }

    @Override
    public View findViewById(int id) {
        if (thatActivity == null) {
            return super.findViewById(id);
        } else {
            return findViewById(id);
        }
    }


    @Override
    public ClassLoader getClassLoader() {
        if (thatActivity == null) {
            return super.getClassLoader();
        } else {
            return getClassLoader();
        }

    }

    @Override
    public WindowManager getWindowManager() {
        if (thatActivity == null) {
            return super.getWindowManager();
        } else {
            return thatActivity.getWindowManager();
        }
    }


    @Override
    public ApplicationInfo getApplicationInfo() {
        if (thatActivity == null) {
            return super.getApplicationInfo();
        } else {
            return thatActivity.getApplicationInfo();
        }
    }

    @Override
    public void attachContext(Activity context) {
        thatActivity = context;
    }
    
    public void onCreate(Bundle savedInstanceState) { }
    public void onStart() { }
    public void onResume() { }
    public void onRestart() { }
    public void onPause() { }
    public void onStop() {  }
    public void onDestroy() { }
    public void onSaveInstanceState(Bundle outState) { }

    public boolean onTouchEvent(MotionEvent event) {
        return false;
    }

    public void onBackPressed() {
        if (thatActivity == null) {
            super.onBackPressed();
        } else {
            thatActivity.onBackPressed();
        }
    }

    @Override
    public void finish() {
        if (thatActivity == null) {
            super.finish();
        } else {
            thatActivity.finish();
        }
    }

    /**
     * 注意上下文对象 thatActivity
     * @param intent
     */
    @Override
    public void startActivity(Intent intent) {
        if (thatActivity == null) {
            super.startActivity(intent);
        } else {
            intent.putExtra("className", intent.getComponent().getClassName());
            thatActivity.startActivity(intent);
        }
    }
    
}

待加载页面:

/**
 * 待加载app主界面
 * 此app没有安装,仅存.apk文件在内存卡
 */
public class MainActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void start(View view) {
        //使用注入的context
        startActivity(new Intent(thatActivity, SecondActivity.class));
    }
}

其他Activity一样。
Module完成后,把其push到手机的sdcard中,等待加载。

3.3 APP-A 主加载工程

思路:

  • 1.获取读写内存卡权限。
  • 2.通过内存卡路径获取B的相关文件。
  • 3.承载页面。
    文件创建如图所示:
    在这里插入图片描述
3.3.1 加载工具类PlugManager

使用DexClassLoader+AssetManager获取外部APK资源。代码中有详细注解:

package com.heima.teststart;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.util.Log;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

/**
 * DexClassLoader加载第三方app
 */
public class PlugManager {
    public  static  final  String TAG="PlugManager";
    private static PlugManager ourInstance;
    private Context context;
    private DexClassLoader pluginDexClassLoader;
    private Resources pluginResources;
    private PackageInfo pluginPackageArchiveInfo;
    private String entryActivityName;

    private PlugManager() { }

    public static PlugManager getInstance() {
        if (ourInstance == null) {
            ourInstance = new PlugManager();
        }
        return ourInstance;
    }

    public void setContext(Context context) {
        this.context = context.getApplicationContext();
    }

    /**
     * 获取Plugin的字节码文件对象
     * 加载外部apk,重写getDexClassLoader()与getResources()
     * @param dexPath Plugin的路径
     */
    public void loadApk(String dexPath) {
        /*  optimizedDirectory   Plugin的缓存路径
         *  libraryPath          可以为null
         *  parent              为父类加载器
         */
        File dexOutFile = context.getDir("dex", Context.MODE_PRIVATE);
        pluginDexClassLoader = new DexClassLoader(dexPath, dexOutFile.getAbsolutePath(), null, context.getClassLoader());

        // 获取包名
        PackageManager packageManager = context.getPackageManager();
        pluginPackageArchiveInfo = packageManager.getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES);
        //activity集合跟App-B的Manifest中注册的activity有关 顺序也有关
        entryActivityName = pluginPackageArchiveInfo.activities[1].name;
        for (int i=0;i<pluginPackageArchiveInfo.activities.length;i++){
            Log.e(TAG, pluginPackageArchiveInfo.activities[i].name);
        }

      /*  实例化resources
        Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) */
        AssetManager assets = null;
        try {
            assets = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assets, dexPath);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        //获取Plugin的Resources
        pluginResources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
    }

    public PackageInfo getPluginPackageArchiveInfo() {
        return pluginPackageArchiveInfo;
    }

    public DexClassLoader getPluginDexClassLoader() {
        return pluginDexClassLoader;
    }

    public Resources getPluginResources() {
        return pluginResources;
    }

    public String getEntryActivityName() {
        return entryActivityName;
    }

}

这里一定要注意pluginPackageArchiveInfo.activities[i].name遍历这个数组你会发现,遍历的顺序跟B中AndroidManifest.xml的Activity注册顺序有关。

3.3.2 页面加载器 ProxyActivity

用来承载B中待加载Activity的内容。

package com.heima.teststart;

import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Bundle;

import com.heima.plugs.PlugInterface;


/**
 * 承载页---加载第三方activity页面
 */
public class ProxyActivity extends Activity {

    private PlugInterface pluginInterface;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //待启动的第三方Activity
        String className = getIntent().getStringExtra("className");
        try {
            //加载该Activity的字节码对象
            Class<?> aClass = PlugManager.getInstance().getPluginDexClassLoader().loadClass(className);
            //创建该Activity的实例
            Object newInstance = aClass.newInstance();
            //程序健壮性检查
            if (newInstance instanceof PlugInterface) {
                pluginInterface = (PlugInterface) newInstance;
                //将代理Activity的实例传递给三方Activity
                pluginInterface.attachContext(this);
                //创建bundle用来与三方apk传输数据
                Bundle bundle = new Bundle();
                //调用三方Activity的onCreate,
                pluginInterface.onCreate(bundle);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 重新,通过className拿到类名
     *
     * @return
     */
    @Override
    public ClassLoader getClassLoader() {
        return PlugManager.getInstance().getPluginDexClassLoader();
    }


    /**
     * 注意:三方调用拿到对应加载的三方Resources
     *
     * @return
     */
    @Override
    public Resources getResources() {
        return PlugManager.getInstance().getPluginResources();
    }

    @Override
    public void startActivity(Intent intent) {
        Intent intent1 = new Intent(this, ProxyActivity.class);
       String className = intent.getStringExtra("className");
        //intent1.putExtra("className", intent.getComponent().getClassName());
        intent1.putExtra("className",className);
        super.startActivity(intent1);
    }

    @Override
    public void onStart() {
        if (pluginInterface != null)
            pluginInterface.onStart();
        super.onStart();
    }

    @Override
    public void onResume() {
        if (pluginInterface != null)
            pluginInterface.onResume();
        super.onResume();
    }

    @Override
    public void onPause() {
        if (pluginInterface != null)
            pluginInterface.onPause();
        super.onPause();
    }

    @Override
    public void onRestart() {
        if (pluginInterface != null)
            pluginInterface.onRestart();
        super.onRestart();
    }

    @Override
    public void onStop() {
        if (pluginInterface != null)
            pluginInterface.onStop();
        super.onStop();
    }

    @Override
    public void onDestroy() {
        if (pluginInterface != null)
            pluginInterface.onDestroy();
        super.onDestroy();
    }

}

3.3.3 启动页面 MainActivity

通过PlugManager获取相关内容,传入ProxyActivity进行加载操作。

package com.heima.teststart;

import android.Manifest;
import android.content.Intent;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadApk();
    }

    public void loadApk() {
        //使用运行时权限
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
    }

    public void startApk(View view) {
        Intent intent = new Intent(this, ProxyActivity.class);
        String otherApkMainActivityName = PlugManager.getInstance().getEntryActivityName();
        intent.putExtra("className", otherApkMainActivityName);
        startActivity(intent);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PlugManager.getInstance().setContext(this);
        //传入APP-B的绝对路径
        PlugManager.getInstance().loadApk(Environment.getExternalStorageDirectory().getAbsolutePath()+"/otherapp-debug.apk");
    }

}

OK,完成。
附上工程连接,方便下载。GitHub传送门

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 新闻app是一款基于Android平台的小型项目应用程序,它主要用于展示各类新闻内容,为用户提供便捷的阅读体验。该项目的源码包含了应用程序的基本框架和功能实现,方便开发者进行二次开发和定制。 新闻app的源码主要包含以下几个方面的内容: 1. 用户界面设计:源码中包含了新闻app的界面布局和样式,开发者可以根据自己的需要进行修改和美。用户界面通常包括新闻列表、新闻详情、分类标签等,开发者可以自由设计并添加其他功能模块。 2. 数据获取与展示:源码中实现了与服务器进行数据交互的功能,通过网络请求获取新闻数据,并在界面上展示出来。开发者可以根据需要修改数据请求接口和解析方式,实现与自己的服务器交互。 3. 新闻分类与搜索:源码中提供了新闻分类和搜索功能的实现,用户可以根据自己的兴趣和需求选择不同的新闻分类进行浏览,也可以通过搜索关键词进行精确定位。 4. 用户交互与分享:源码中包含了用户的登录注册功能和新闻内容的分享功能,用户可以通过登录账号进行个性设置和收藏喜欢的新闻内容,也可以将新闻分享到社交媒体上与他人交流。 总之,新闻app源码是一个基础框架,开发者可以在此基础上进行二次开发和定制,根据自己的需求添加功能模块和美界面,实现自己独特的新闻应用。 ### 回答2: Android新闻App是一个基于Android平台开发的小型项目,它的主要功能是提供最新的新闻内容给用户,并且用户可以进行浏览、搜索和分享等操作。下面是这个项目的一些关键特点和所需的源码组成部分: 1. 特点: - 用户界面友好,交互性强,提供舒适的浏览体验; - 支持实时更新,提供最新的新闻内容; - 具备搜索功能,方便用户查找感兴趣的新闻; - 支持新闻分享功能,方便用户将新闻分享给朋友; - 具备图文混排的能力,可以展示新闻的文字和图片。 2. 源码组成部分: - 主界面布局代码:定义了App的整体布局结构,包括顶部导航栏、底部工具栏和新闻显示区域等。 - 数据源代码:负责获取新闻数据,可以通过API接口获取最新的新闻内容,也可以从本地数据库获取已缓存的新闻数据。 - 新闻列表适配器代码:用于将新闻数据展示在界面上,包括标题、描述和图片等。 - 新闻详情界面代码:用于显示单篇新闻的详细内容,包括标题、正文和相关图片等。 - 搜索功能代码:实现了按关键字搜索新闻的功能,可以在已有的新闻数据中进行筛选。 - 分享功能代码:集成了社交媒体的分享SDK,方便用户将新闻内容分享给朋友。 - 图片加载和缓存代码:处理了新闻中的图片加载和本地缓存,提高了图片加载速度和用户体验。 通过以上的源码组成部分,可以完成一个基本的新闻App,用户可以在界面上浏览最新的新闻内容,进行搜索和分享操作。这个小项目可以帮助开发者理解Android开发框架和开发方式,提高编码能力和UI设计能力。 ### 回答3: 新闻app是基于Android平台开发的一个小型应用程序,可以提供用户各种最新的新闻资讯。以下是关于这个项目的源码介绍。 该项目源码主要由Java语言编写,使用了Android Studio作为开发工具。代码结构清晰,包含了主要的几个模块。 1. 用户界面模块:这个模块负责显示新闻列表和新闻详情等信息,主要包含布局文件和相应的逻辑代码。列表界面使用RecyclerView控件展示新闻列表,详情界面使用WebView展示新闻内容。 2. 网络请求模块:这个模块负责与后台服务器进行数据交互,使用了Android的HttpURLConnection类来发送请求和接收响应。请求参数可以根据实际需要进行修改,例如可以根据新闻类别进行请求。 3. 数据解析模块:这个模块负责解析从服务器返回的JSON格式的数据,转换成Java对象供应用程序使用。可以使用Android提供的JSON解析库,如Gson。 4. 数据存储模块:这个模块负责缓存新闻数据,以提高应用程序的响应速度。可以使用SharedPreferences或SQLite数据库来存储新闻数据。同时也可以使用图片缓存库,如Glide或Picasso来缓存新闻图片。 5. 用户交互模块:这个模块负责处理用户的交互行为,例如点击新闻列表项跳转到新闻详情界面,下拉刷新获取最新数据等。可以使用Android提供的相关控件和事件监听器来实现用户交互。 除了以上几个主要的模块,还可以根据需要添加其他功能,例如搜索栏、分享按钮等。 总体来说,这个新闻app的源码提供了一个完整的开发框架,初学者可以通过阅读和理解源码来学习Android应用程序的开发流程和一些常用技术。同时,也可以根据实际需求进行二次开发,添加新的功能和改进用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值