編輯:關於Android編程
動態加載、插件化開發很重要
當今360手機助手(DroidPlugin),個人開源(VirtualApp)、百度DL、攜程DynamicAPK都用到了該技術
本例的大概思路是:
1、apk1初始化就一個主界面MainActivity,主界面只有一個Button按鈕,點擊後,彈出Toast,然後我們把編譯好的apk1放到手機根目錄SD卡下
2、apk2有一個MainActivity界面,界面上也有一個Button,點擊按鈕後,去加載SD目錄下的apk1,調起來apk1,點擊apk1中的button,彈出Toast即可
以上就是一個簡單的邏輯?其實呢這裡面問題好多,這裡先簡單說下問題點。
1、其實點擊apk2的button啟動的不是apk1的界面,而是將apk1的界面托管給一個靜態代理類Activity,然後以靜態代理Activity去構建類似於apk1的button,繼而在靜態代理Activity的上下文環境下,彈出Toast
2、這個例子只是在靜態代理Activity類裡,進行了簡單的反射調用apk1的onCreate方法,被反射的apk1的主界面類,其實本質是一個java類,它沒有Activity裡面的邏輯,比如你拿不到裡面的layout等資源,所以這個demo也就是一個對動態加載的一個小小的理解,沒有涉及到Activity4大組建的動態代理、binder機制等
首先來看下apk的代碼把
MainActivity
package com.example.targetproject; import android.annotation.SuppressLint; import android.app.ActionBar.LayoutParams; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.Toast; public class MainActivity extends Activity { public static final String KEY_APK_PATH = "apkPath"; public static final String KEY_CLASS = "class"; public static final String DEX_PATH = android.os.Environment .getExternalStorageDirectory().getPath() + "/TargetProject.apk"; protected Activity mProxyActivity; public void setProxy(Activity proxyActivity) { mProxyActivity = proxyActivity; } @SuppressLint("NewApi") @Override protected void onCreate(Bundle savedInstanceState) { if (mProxyActivity == null) { super.onCreate(savedInstanceState); mProxyActivity = this; } Button button = new Button(mProxyActivity); button.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); button.setText("按我"); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(mProxyActivity, "你點擊了按鈕啦!", Toast.LENGTH_SHORT) .show(); } }); if (mProxyActivity == this) { super.setContentView(button); } else { mProxyActivity.setContentView(button); } } }
簡單說下裡面的邏輯關系
這個界面做了什麼呢?就是創建了一個以某個Activity為環境的Button,然後點擊button,會產生一個以某個Activity為環境的Toast,我們先拋開讓測試的apk進行通過動態加載的方式,以代理Activity調起的情況,首先它是一個可以獨立運行編譯的apk文件,所以說,我們先來分析下它需要的Activity,我們先定義一個Activity類
protected Activity mProxyActivity;
在onCreate方法中
if (mProxyActivity == null) { super.onCreate(savedInstanceState); mProxyActivity = this; }
意思就是如果沒有托管的Activity類,就使用原生的Activity,那麼如果有托管的Activity呢?我們就進行如下的設置
public void setProxy(Activity proxyActivity) { mProxyActivity = proxyActivity; }
在apk的主界面,加入一個代理類Activity,然後在代理類Activity環境下去創建button,Toast
以下就是創建Button的代碼
Button button = new Button(mProxyActivity); button.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); button.setText("按我"); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(mProxyActivity, "你點擊了按鈕啦!", Toast.LENGTH_SHORT) .show(); } });
創建Button\Toast完畢呢,就是需要設置到某個Activity環境下,這裡還得需要判斷是原生的Activity環境,還是代理Activity環境?
if (mProxyActivity == this) { super.setContentView(button); } else { mProxyActivity.setContentView(button); }
然後編譯後,運行apk沒問題,就放到手機的sd卡根目錄下,以下是我手機nexus5的路徑目錄
然後我們就來看測試apk的代碼邏輯把
MainActivity
package com.example.test; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn = (Button) this.findViewById(R.id.btn); btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String root = android.os.Environment .getExternalStorageDirectory().getPath() + "/TargetProject.apk"; Intent intent = new Intent(MainActivity.this, ProxyActivity.class); intent.putExtra(ProxyActivity.KEY_APK_PATH, root); startActivity(intent); } }); } }
先來看下主界面的代碼,這裡就是一個button,點擊後帶過去一個sd卡跟目錄下那個apk1的絕對路徑,然後調到ProxyActivity類
String root = android.os.Environment.getExternalStorageDirectory().getPath() + "/TargetProject.apk";
然後就看我們的代理Activity類
package com.example.test; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import android.app.Activity; import android.content.Intent; import android.content.pm.PackageInfo; import android.os.Bundle; import android.util.Log; import dalvik.system.DexClassLoader; /** * 代理類 * @author safly * */ public class ProxyActivity extends Activity{ public static final String KEY_APK_PATH = "apkPath"; public static final String KEY_CLASS = "class"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //獲取指定的apk文件路徑和啟動類名 String mDexPath = getIntent().getStringExtra(KEY_APK_PATH); String mClass = getIntent().getStringExtra(KEY_CLASS); if (mClass == null) { PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(mDexPath, 1); if ((packageInfo.activities != null) && (packageInfo.activities.length > 0)) { Log.e("ProxyActivity", ""+packageInfo.activities[0].name); mClass = packageInfo.activities[0].name; } } launchTargetActivity(mDexPath,mClass); } /** * 利用ClassLoader,DexClassLoader和反射將apk中的界面啟動 * @param mDexPath apk 動態加載的apk本地路徑 * @param className 要打開的動態加載類的類名 */ protected void launchTargetActivity(String mDexPath,String className) { Log.e("ProxyActivity", "launchTargetActivity"); 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[] {}); //利用反射機制獲取到設置代理Activity的方法 Method setProxy = localClass.getMethod("setProxy",new Class[] { Activity.class }); setProxy.setAccessible(true); setProxy.invoke(instance, new Object[] { this }); //利用反射機制調用onCreate方法 Method onCreate = localClass.getDeclaredMethod("onCreate",new Class[] { Bundle.class }); onCreate.setAccessible(true); onCreate.invoke(instance, new Object[] { null }); } catch (Exception e) { e.printStackTrace(); } } }
來說下上面的代碼意思
ProxyActivity類onCreate方法中獲取傳遞過來的KEY_APK_PATH(sd卡下apk1的絕對路徑),我們還需要一個類,就是apk1的主界面的全類名,因為我們需要調用裡面的onCreate方法,然後去添加button、toast控件
if (mClass == null) { PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(mDexPath, 1); if ((packageInfo.activities != null) && (packageInfo.activities.length > 0)) { Log.e("ProxyActivity", ""+packageInfo.activities[0].name); mClass = packageInfo.activities[0].name; } }
log輸出如下
ProxyActivity(23526): com.example.targetproject.MainActivity
然後就看下launchTargetActivity裡面的代碼
File dexOutputDir = this.getDir("dex", 0); dexOutputPath--/data/data/com.example.test/app_dex
以上是dex解壓釋放後的目錄 ,log輸出的目錄,以下是截圖
然後獲取一個DexClassLoader,這裡面參數為sd卡apk1的絕對路徑、app_dex路徑,然後還有一個ClassLoader.getSystemClassLoader()對象
參數如下
(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
如下方法就是獲取apk1中MainActivity的構造然後new一個實例
Class localClass = dexClassLoader.loadClass(className); Constructor localConstructor = localClass.getConstructor(new Class[] {}); Object instance = localConstructor.newInstance(new Object[] {});
以下就是反射去獲取setProxy,onCreate方法,進行設置代理Activity類,然後在代理Activity類中進行設置button\toast控件
//利用反射機制獲取到設置代理Activity的方法 Method setProxy = localClass.getMethod("setProxy",new Class[] { Activity.class }); setProxy.setAccessible(true); setProxy.invoke(instance, new Object[] { this }); //利用反射機制調用onCreate方法 Method onCreate = localClass.getDeclaredMethod("onCreate",new Class[] { Bundle.class }); onCreate.setAccessible(true); onCreate.invoke(instance, new Object[] { null });
以上就是這個小例子的邏輯,也算是對自己開啟動態加載學習的一個小入門理解把
要求 Tab 標簽可以橫向滾動,標簽可選擇,並且在選擇的時候有標線下劃線。 分析 可繼承HorizontalScrollView 實現,然後裡面標簽ITem可可以是Tex
摘要:每個有逼格的App在第一次啟動時都有一個歡迎界面,通常是幾個單頁面或者帶動畫的單頁面滑動到最後一頁有個啟動的按鈕,本文將使用Ionic2來創建,So easy!效果
繼上一篇時間和日期設置的示例之後,今天來介紹Android的布局組件中有關於時間和日期的設置的組件,希望對大家有所幫助。具體如下:時間日期設置組件:TimePicker、
直播功能現在已經是一個很熱門的功能了,很多應用都會涉及到直播模塊,比如 花椒 NOW 還有辣媽幫。。等,最近因為項目需要也加入了直播功能。直播中有一個點贊的效果 ,今天我