插件化在Android开发上现在已经使用的很广泛了,各种插件化框架一代又一代的更迭,使我们开发者有了很多的选择。我们开发中经常遇到的有两个问题:一个问题,我们app开发有一个方法数上限,那就是65536;另一个问题,我们发布后的app如果进行功能添加和bug修改,就需要发版,而频繁的发版又会让用户很烦。而插件化就可以解决这两个问题。虽然我们可以用分包和热更新来解决这两个问题,但是插件化的存在,是基于模块化和组件化开发而来的,可以更好的实现我们代码的综合治理,也和契合我们现在的开发模式。
插件化框架很多,尤其是shadow,避开了我们插件化无法避免的使用反射问题,高版本支持更加流畅。不过这里我不说框架,也不说原理,我只是不引入第三方做一个插件化实践。
首先是宿主app
主页代码MainActivity.java
package com.hao.hostandroid;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import android.Manifest;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private Button bt_click;
protected static final String WRITE_EXTERNAL_STORAGE = Manifest.permission.WRITE_EXTERNAL_STORAGE;//写SD卡权限
protected static final String READ_EXTERNAL_STORAGE = Manifest.permission.READ_EXTERNAL_STORAGE;//读SD卡权限
protected static final int WRITE_EXTERNAL_STORAGE_RC = 0x01;//写SD卡权限
protected static final int READ_EXTERNAL_STORAGE_RC = 0x02;//读SD卡权限
private boolean isGetPermiss;//是否获取相关权限
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bt_click = findViewById(R.id.bt_click);
bt_click.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isGetPermiss) {
Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
intent.putExtra(ProxyActivity.EXTRA_DEX_PATH, "/mnt/sdcard/DynamicLoadHost/plugin.apk");
startActivity(intent);
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//6.0以上才需要进行动态权限的请求
ActivityCompat.requestPermissions(this, new String[]{WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE_RC);
}else{
//6.0以下不需要获取动态权限
isGetPermiss = true;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case WRITE_EXTERNAL_STORAGE_RC:
//获取到写入权限
ActivityCompat.requestPermissions(this, new String[]{READ_EXTERNAL_STORAGE}, READ_EXTERNAL_STORAGE_RC);
break;
case READ_EXTERNAL_STORAGE_RC:
//获取到读取权限
isGetPermiss = true;
break;
}
}
}
主页布局activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="host"
android:layout_centerInParent="true"/>
<Button
android:id="@+id/bt_click"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="点击跳转"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
代理Activity ProxyActivity.java
package com.hao.hostandroid;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class ProxyActivity extends AppCompatActivity {
private static final String TAG = "ProxyActivity";
public static final String FROM = "extra.from";
public static final int FROM_EXTERNAL = 0;
public static final int FROM_INTERNAL = 1;
public static final String EXTRA_DEX_PATH = "extra.dex.path";
public static final String EXTRA_CLASS = "extra.class";
private String mClass;
private String mDexPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH);
mClass = getIntent().getStringExtra(EXTRA_CLASS);
Log.d(TAG, "mClass=" + mClass + " mDexPath=" + mDexPath);
if (mClass == null) {
launchTargetActivity();
} else {
launchTargetActivity(mClass);
}
}
@SuppressLint("NewApi")
protected void launchTargetActivity() {
PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(
mDexPath, 1);
if ((packageInfo.activities != null)
&& (packageInfo.activities.length > 0)) {
String activityName = packageInfo.activities[0].name;
mClass = activityName;
launchTargetActivity(mClass);
}
}
@SuppressLint("NewApi")
protected void launchTargetActivity(final String className) {
Log.d(TAG, "start launchTargetActivity, className=" + className);
File dexOutputDir = this.getDir("dex", 0);
final String dexOutputPath = dexOutputDir.getAbsolutePath();
ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,
dexOutputPath, null, localClassLoader);
try {
Class<?> localClass = dexClassLoader.loadClass(className);
Constructor<?> localConstructor = localClass
.getConstructor(new Class[] {});
Object instance = localConstructor.newInstance(new Object[] {});
Log.d(TAG, "instance = " + instance);
Method setProxy = localClass.getMethod("setProxy",
new Class[] { Activity.class });
setProxy.setAccessible(true);
setProxy.invoke(instance, new Object[] { this });
Method onCreate = localClass.getDeclaredMethod("onCreate",
new Class[] { Bundle.class });
onCreate.setAccessible(true);
Bundle bundle = new Bundle();
bundle.putInt(FROM, FROM_EXTERNAL);
onCreate.invoke(instance, new Object[] { bundle });
} catch (Exception e) {
e.printStackTrace();
}
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hao.hostandroid">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HostAndroid">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ProxyActivity">
<intent-filter>
<action android:name="com.hao.hostandroid.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
这里我们可以看到,首先我们有一个首页,页面上是一个按钮,这个按钮在点击的时候会跳转到插件内部。
在这个类中,我们进行了读写动态权限的申请,并且要求申请权限成功后才可以触发点击事件,即访问插件工作。
接着是一个ProxyActivity,这个类当中使用DexClassLoader类,利用反射机制对插件中的代码进行了调用,模拟出和插件中页面一模一样的页面。
最后是清单文件,清单文件中一块是我们的权限申请,这个不多说。另一块是ProxyActivity的配置
<activity android:name=".ProxyActivity">
<intent-filter>
<action android:name="com.hao.hostandroid.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
一定要按照这个进行配置,否则会影响插件中页面的跳转。
接着是插件APP
首先是一个BaseActivity
package com.hao.pluginandroid;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
public class BaseActivity extends Activity {
private static final String TAG = "Client-BaseActivity";
public static final String FROM = "extra.from";
public static final int FROM_EXTERNAL = 0;
public static final int FROM_INTERNAL = 1;
public static final String EXTRA_DEX_PATH = "extra.dex.path";
public static final String EXTRA_CLASS = "extra.class";
public static final String PROXY_VIEW_ACTION = "com.hao.hostandroid.VIEW";
public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";
protected Activity mProxyActivity;
protected int mFrom = FROM_INTERNAL;
public void setProxy(Activity proxyActivity) {
Log.d(TAG, "setProxy: proxyActivity= " + proxyActivity);
mProxyActivity = proxyActivity;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);
}
if (mFrom == FROM_INTERNAL) {
super.onCreate(savedInstanceState);
mProxyActivity = this;
}
Log.d(TAG, "onCreate: from= " + mFrom);
}
protected void startActivityByProxy(String className) {
if (mProxyActivity == this) {
Intent intent = new Intent();
intent.setClassName(this, className);
this.startActivity(intent);
} else {
Intent intent = new Intent(PROXY_VIEW_ACTION);
intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);
intent.putExtra(EXTRA_CLASS, className);
mProxyActivity.startActivity(intent);
}
}
@Override
public void setContentView(View view) {
if (mProxyActivity == this) {
super.setContentView(view);
} else {
mProxyActivity.setContentView(view);
}
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mProxyActivity == this) {
super.setContentView(view, params);
} else {
mProxyActivity.setContentView(view, params);
}
}
@Deprecated
@Override
public void setContentView(int layoutResID) {
if (mProxyActivity == this) {
super.setContentView(layoutResID);
} else {
mProxyActivity.setContentView(layoutResID);
}
}
@Override
public void addContentView(View view, ViewGroup.LayoutParams params) {
if (mProxyActivity == this) {
super.addContentView(view, params);
} else {
mProxyActivity.addContentView(view, params);
}
}
}
这个类要注意两个地方,一个是
public static final String PROXY_VIEW_ACTION = "com.hao.hostandroid.VIEW";
这个要和我们宿主APP清单文件中配置的一样
<activity android:name=".ProxyActivity">
<intent-filter>
<action android:name="com.hao.hostandroid.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
另一个是
public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";
这个是我们插件放置的位置,宿主APP中首页跳转那里也用到了这个位置
if (isGetPermiss) {
Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
intent.putExtra(ProxyActivity.EXTRA_DEX_PATH, "/mnt/sdcard/DynamicLoadHost/plugin.apk");
startActivity(intent);
}
这里的两个地址一定要一致,我们要注意。
接着我们看插件首页
package com.hao.pluginandroid;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;
public class MainActivity extends BaseActivity {
private static final String TAG = "Client-MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView(savedInstanceState);
}
private void initView(Bundle savedInstanceState) {
mProxyActivity.setContentView(generateContentView(mProxyActivity));
}
private View generateContentView(final Context context) {
LinearLayout layout = new LinearLayout(context);
layout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
layout.setBackgroundColor(Color.parseColor("#F79AB5"));
Button button = new Button(context);
button.setText("button");
layout.addView(button, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "you clicked button",
Toast.LENGTH_SHORT).show();
startActivityByProxy("com.hao.pluginandroid.TestActivity");
}
});
return layout;
}
}
这个首页有两个地方要注意,首先是它的布局。插件是不能使用资源文件的,所以我们的界面都是用java代码写出来的。图片则需要用下载到sd卡的本地文件,不能使用资源图片。
另一个需要注意的是它的跳转,使用的是对应Activity的全名进行跳转。
最后是我们跳转后的页面
package com.hao.pluginandroid;
import android.graphics.Color;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.Button;
public class TestActivity extends BaseActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Button button = new Button(mProxyActivity);
button.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
button.setBackgroundColor(Color.YELLOW);
button.setText("这是测试页面");
setContentView(button);
}
}
这两个Activity都要继承BaseActivity。
经过这两步,我们就实现了一个简单的插件化开发。后续如果有机会我会整一个shadow的学习笔记。