SegmentFault 中二病也要开发ANDROID最新的文章
2017-04-10T22:35:50+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
一种Android应用内全局获取Context实例的装置
https://segmentfault.com/a/1190000009015732
2017-04-10T22:35:50+08:00
2017-04-10T22:35:50+08:00
Kaede
https://segmentfault.com/u/kaede
7
<p>哥白尼·罗斯福·马丁路德·李开复·嫁衣曾经说过</p>
<blockquote><p>Where there is an Android App, there is an Application context.</p></blockquote>
<p>没毛病,扎心了。App运行的时候,肯定是存在至少一个Application实例的。同时,Context我们再熟悉不过了,写代码的时候经常需要使用到Context实例,它一般是通过构造方法传递进来,通过方法的形式参数传递进来,或者是通过attach方法传递进我们需要用到的类。Context实在是太重要了,以至于我经常恨不得着藏着掖着,随身带着,这样需要用到的时候就能立刻掏出来用用。但是换个角度想想,既然App运行的时候,Application实例总是存在的,那么为何不设置一个全局可以访问的静态方法用于获取Context实例,这样以来就不需要上面那些繁琐的传递方式。</p>
<p>说到这里,有的人可能说想这不是我们经常干的好事吗,有必要说的这么玄乎?少侠莫急,请听吾辈徐徐道来。</p>
<h2>获取Context实例的一般方式</h2>
<p>这再简单不过了。</p>
<pre><code class="java">public static class Foo1 {
public Foo1(Context context) {
// 1. 在构造方法带入
}
}
public static class Foo2 {
public Foo2 attach(Context context) {
// 2. 通过attach方法带入
return this;
}
}
public static class Foo2 {
public void foo(Context context) {
// 3. 调用方法的时候,通过形参带入
}
}
</code></pre>
<p>这种方式应该是最常见的获取Context实例的方式了,优点就是严格按照代码规范来,不用担心兼容性问题;缺点就是API设计严重依赖于Context这个API,如果早期接口设计不严谨,后期代码重构的时候可能很要命。此外还有一个比较有趣的问题,我们经常使用Activity或者Application类的实例作为Context的实例使用,而前者本身又实现了别的接口,比如以下代码。</p>
<pre><code class="java">public static class FooActivity extends Activity implements FooA, FooB, FooC {
Foo mFoo;
public void onCreate(Bundle bundle) {
// 禁忌·四重存在!
mFoo.foo(this, this, this, this);
}
...
}
public static class Foo {
public void foo(Context context, FooA a, FooB b, FooC c) {
...
}
}</code></pre>
<p>这段代码是我许久前看过的代码,本身不是什么厉害的东西,不过这段代码段我至今印象深刻。设想,如果Foo的接口设计可以不用依赖Context,那么这里至少可以少一个<code>this</code>不是吗。</p>
<h2>获取Context实例的二般方式</h2>
<p>现在许多开发者喜欢设计一个全局可以访问的静态方法,这样以来在设计API的时候,就不需要依赖Context了,代码看起来像是这样的。</p>
<pre><code class="java">/*
* 全局获取Context实例的静态方法。
*/
public static class Foo {
private static sContext;
public static Context getContext() {
return sContext;
}
public static void setContext(Context context) {
sContext = context;
}
}</code></pre>
<p>这样在整个项目中,都可以通过<code>Foo#getContext()</code>获取Context实例了。不过目前看起来好像还有点小缺陷,就是使用前需要调用<code>Foo#setContext(Context)</code>方法进行注册(这里暂不讨论静态Context实例带来的问题,这不是本篇幅的关注点)。好吧,以我的聪明才智,很快就想到了优化方案。</p>
<pre><code class="java">/*
* 全局获取Context实例的静态方法(改进版)。
*/
public static class FooApplication extends Application {
private static sContext;
public FooApplication() {
sContext = this;
}
public static Context getContext() {
return sContext;
}
}</code></pre>
<p>不过这样又有带来了另一个问题,一般情况下,我们是把应用的入口程序类<code>FooApplication</code>放在App模块下的,这样一来,Library模块里面代码就访问不到<code>FooApplication#getContext()</code>了。当然把<code>FooApplication</code>下移到基础库里面也是一种办法,不过以我的聪明才智又立刻想到了个好点子。</p>
<pre><code class="java">
/*
* 全局获取Context实例的静态方法(改进版之再改进)。
*/
public static class FooApplication extends BaseApplication {
...
}
/*
* 基础库里面
*/
public static class BaseApplication extends Application {
private static sContext;
public BaseApplication() {
sContext = this;
}
public static Context getContext() {
return sContext;
}
}</code></pre>
<p>这样以来,就不用把<code>FooApplication</code>下移到基础库里面,Library模块里面的代码也可以通过<code>BaseApplication#getContext()</code>访问到Context实例了。嗯,这看起来似乎是一种神奇的膜法,因吹斯听。然而,代码写完还没来得及提交,包工头打了个电话来和我说,由于项目接入了第三发SDK,需要把<code>FooApplication</code>继承<code>SdkApplication</code>。</p>
<p>…… 有没有什么办法能让<code>FooApplication</code>同时继承<code>BaseApplication</code>和<code>SdkApplication</code>啊?(场面一度很尴尬,这里省略一万字。)</p>
<p>以上谈到的,都是以前我们在获取Context实例的时候遇到的一些麻烦:</p>
<ol>
<li><p>类API设计需要依赖Context(这是一种好习惯,我可没说这不好);</p></li>
<li><p>持有静态的Context实例容易引发的内存泄露问题;</p></li>
<li><p>需要提注册Context实例(或者释放);</p></li>
<li><p>污染程序的Application类;</p></li>
</ol>
<p>那么,有没有一种方式,能够让我们在整个项目中可以全局访问到Context实例,不要提前注册,不会污染Application类,更加不会引发静态Context实例带来的内存泄露呢?</p>
<h2>一种全局获取Context实例的方式</h2>
<p>回到最开始的话,App运行的时候,肯定存在至少一个Application实例。如果我们能够在系统创建这个实例的时候,获取这个实例的应用,是不是就可以全局获取Context实例了(因为这个实例是运行时一直存在的,所以也就不用担心静态Context实例带来的问题)。那么问题来了,Application实例是什么时候创建的呢?首先先来看看我们经常用来获取Base Context实例的<code>Application#attachBaseContext(Context)</code>方法,它是继承自<code>ContextWrapper#attachBaseContext(Context)</code>的。</p>
<pre><code class="java">public class ContextWrapper extends Context {
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
}</code></pre>
<p>是谁调用了这个方法呢?可以很快定位到<code>Application#attach(Context)</code>。</p>
<pre><code class="java">public class Application extends ContextWrapper {
final void attach(Context context) {
attachBaseContext(context);
mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}
}</code></pre>
<p>又是谁调用了<code>Application#attach(Context)</code>方法呢?一路下来可以直接定位到<code>Instrumentation#newApplication(Class<?>, Context)</code>方法里(这个方法名很好懂啊,一看就知道是干啥的)。</p>
<pre><code class="java">/**
* Base class for implementing application instrumentation code. When running
* with instrumentation turned on, this class will be instantiated for you
* before any of the application code, allowing you to monitor all of the
* interaction the system has with the application. An Instrumentation
* implementation is described to the system through an AndroidManifest.xml's
* <instrumentation>.
*/
public class Instrumentation {
static public Application newApplication(Class<?> clazz, Context context)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Application app = (Application)clazz.newInstance();
app.attach(context);
return app;
}
}</code></pre>
<p>看来是在这里创建了App的入口Application类实例的,是不是想办法获取到这个实例的应用就可以了?不,还别高兴太早。我们可以把Application实例当做Context实例使用,是因为它持有了一个Context实例(base),实际上Application实例都是通过代理调用这个base实例的接口完成相应的Context工作的。在上面的代码中,可以看到系统创建了Application实例app后,通过<code>app.attach(context)</code>把context实例设置给了app。直觉告诉我们,应该进一步关注这个context实例是怎么创建的,可以定位到<code>LoadedApk#makeApplication(boolean, Instrumentation)</code>代码段里。</p>
<pre><code class="java">/**
* Local state maintained about a currently loaded .apk.
* @hide
*/
public final class LoadedApk {
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if (mApplication != null) {
return mApplication;
}
Application app = null;
String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"initializeJavaContextClassLoader");
initializeJavaContextClassLoader();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
// Context 实例创建的地方,可以看出Context实例是一个ContextImpl。
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
} catch (Exception e) {
}
...
return app;
}
}</code></pre>
<p>好了,到这里我们定位到了Application实例和Context实例创建的位置,不过距离我们的目标只成功了一半。因为如果我们要想办法获取这些实例,就得先知道这些实例被保存在什么地方。上面的代码一路逆向追踪过来,好像也没看见实例被保存给成员变量或者静态变量,所以暂时还得继续往上捋。很快就能捋到<code>ActivityThread#performLaunchActivity(ActivityClientRecord, Intent)</code>。</p>
<pre><code class="java">/**
* This manages the execution of the main thread in an
* application process, scheduling and executing activities,
* broadcasts, and other operations on it as the activity
* manager requests.
*/
public final class ActivityThread {
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
ActivityInfo aInfo = r.activityInfo;
ComponentName component = r.intent.getComponent();
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
// 创建Application实例。
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
...
}
r.paused = true;
mActivities.put(r.token, r);
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to start activity " + component
+ ": " + e.toString(), e);
}
}
return activity;
}
}</code></pre>
<p>这里是我们启动Activity的时候,Activity实例创建的具体位置,以上代码段还可以看到喜闻乐见的"Unable to start activity"异常,你们猜猜这个异常是谁抛出来的?这里就不发散了,回到我们的问题来,以上代码段获取了一个Application实例,但是并没有保持住,看起来这里的Application实例就像是一个临时变量。没办法,再看看其他地方吧。接着找到<code>ActivityThread#handleCreateService(CreateServiceData)</code>,不过这里也一样,并没有把获取的Application实例保存起来,这样我们就没有办法获取到这个实例了。</p>
<pre><code class="java">public final class ActivityThread {
private void attach(boolean system) {
sCurrentActivityThread = this;
mSystemThread = system;
if (!system) {
...
} else {
// Don't set application object here -- if the system crashes,
// we can't display an alert, we just want to die die die.
android.ddm.DdmHandleAppName.setAppName("system_process",
UserHandle.myUserId());
try {
mInstrumentation = new Instrumentation();
ContextImpl context = ContextImpl.createAppContext(
this, getSystemContext().mPackageInfo);
mInitialApplication = context.mPackageInfo.makeApplication(true, null);
mInitialApplication.onCreate();
} catch (Exception e) {
throw new RuntimeException(
"Unable to instantiate Application():" + e.toString(), e);
}
}
...
}
public static ActivityThread systemMain() {
...
ActivityThread thread = new ActivityThread();
thread.attach(true);
return thread;
}
public static void main(String[] args) {
...
ActivityThread thread = new ActivityThread();
thread.attach(false);
...
}
}</code></pre>
<p>我们可以看到,这里创建Application实例后,把实例保存在ActivityThread的成员变量<code>mInitialApplication</code>中。不过仔细一看,只有当<code>system == true</code>的时候(也就是系统应用)才会走这个逻辑,所以这里的代码也不是我们要找的。不过,这里给我们一个提示,如果能想办法获取到ActivityThread实例,或许就能直接拿到我们要的Application实例。此外,这里还把ActivityThread的实例赋值给一个静态变量<code>sCurrentActivityThread</code>,静态变量正是我们获取系统隐藏API实例的切入点,所以如果我们能确定ActivityThread的<code>mInitialApplication</code>正是我们要找的Application实例的话,那就大功告成了。继续查找到<code>ActivityThread#handleBindApplication(AppBindData)</code>,光从名字我们就能猜出这个方法是干什么的,直觉告诉我们离目标不远了~</p>
<pre><code class="java">public final class ActivityThread {
private void handleBindApplication(AppBindData data) {
...
try {
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
mInitialApplication = app;
try {
mInstrumentation.onCreate(data.instrumentationArgs);
} catch (Exception e) {
throw new RuntimeException(
"Exception thrown in onCreate() of "
+ data.instrumentationName + ": " + e.toString(), e);
}
try {
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}
}
}</code></pre>
<p>我们看到这里同样把Application实例保存在ActivityThread的成员变量<code>mInitialApplication</code>中,紧接着我们看看谁是调用了<code>handleBindApplication</code>方法,很快就能定位到<code>ActivityThread.H#handleMessage(Message)</code>里面。</p>
<pre><code class="java">public final class ActivityThread {
public final void bindApplication(String processName, ApplicationInfo appInfo,
List<ProviderInfo> providers, ComponentName instrumentationName,
ProfilerInfo profilerInfo, Bundle instrumentationArgs,
IInstrumentationWatcher instrumentationWatcher,
IUiAutomationConnection instrumentationUiConnection, int debugMode,
boolean enableBinderTracking, boolean trackAllocation,
boolean isRestrictedBackupMode, boolean persistent, Configuration config,
CompatibilityInfo compatInfo, Map<String, IBinder> services, Bundle coreSettings) {
...
sendMessage(H.BIND_APPLICATION, data);
}
private class H extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
...
case BIND_APPLICATION:
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
AppBindData data = (AppBindData)msg.obj;
handleBindApplication(data);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
case EXIT_APPLICATION:
if (mInitialApplication != null) {
mInitialApplication.onTerminate();
}
Looper.myLooper().quit();
break;
...
}
}
}
}</code></pre>
<p>Bingo!至此一切都清晰了,<code>ActivityThread#mInitialApplication</code>确实就是我们需要找的Application实例。整个流程捋顺下来,系统创建Base Context实例、Application实例,以及把Base Context实例attach到Application内部的流程大致可以归纳为以下调用顺序。</p>
<blockquote><p>ActivityThread#bindApplication (异步) --> ActivityThread#handleBindApplication --> LoadedApk#makeApplication --> Instrumentation#newApplication --> Application#attach --> ContextWrapper#attachBaseContext</p></blockquote>
<p>源码撸完了,再回到我们一开始的需求来。现在我们要获取ActivityThread的静态成员变量sCurrentActivityThread。阅读源码后我们发现可以通过<code>ActivityThread#currentActivityThread()</code>这个静态方法来获取这个静态对象,然后通过<code>ActivityThread#getApplication()</code>方法就可能直接获取我们需要的Application实例了。啊,这用反射搞起来简直再简单不过了!说搞就搞。</p>
<pre><code class="java">public class Applications {
@NonNull
public static Application context() {
return CURRENT;
}
@SuppressLint("StaticFieldLeak")
private static final Application CURRENT;
static {
try {
Object activityThread = getActivityThread();
Object app = activityThread.getClass().getMethod("getApplication").invoke(activityThread);
CURRENT = (Application) app;
} catch (Throwable e) {
throw new IllegalStateException("Can not access Application context by magic code, boom!", e);
}
}
private static Object getActivityThread() {
Object activityThread = null;
try {
Method method = Class.forName("android.app.ActivityThread").getMethod("currentActivityThread");
method.setAccessible(true);
activityThread = method.invoke(null);
} catch (final Exception e) {
Log.w(TAG, e);
}
return activityThread;
}
}
// 测试代码
@RunWith(AndroidJUnit4.class)
public class ApplicationTest {
public static final String TAG = "ApplicationTest";
@Test
public void testGetGlobalContext() {
Application context = Applications.context();
Assert.assertNotNull(context);
Log.i(TAG, String.valueOf(context));
// MyApplication是项目的自定义Application类
Assert.assertTrue(context instanceof MyApplication);
}
}</code></pre>
<p>这样以来, 无论在项目的什么地方,无论是在App模块还是Library模块,都可以通过<code>Applications#context()</code>获取Context实例,而且不需要做任何初始化工作,也不用担心静态Context实例带来的问题,测试代码跑起来没问题,接入项目后也没有发现什么异常,我们简直要上天了。不对,哪里不对。不科学,一般来说不可能这么顺利的,这一定是错觉。果然项目上线没多久后立刻原地爆炸了,在一些机型上,通过<code>Applications#context()</code>获取到的Context恒为null。</p>
<p>(╯>д<)╯⁽˙³˙⁾ 对嘛,这才科学嘛。</p>
<p>通过测试发现,在4.1.1系统的机型上,会稳定出现获取结果为null的现象,看来是系统源码的实现上有一些出入导致,总之先看看源码吧。</p>
<pre><code class="java">public final class ActivityThread {
public static ActivityThread currentActivityThread() {
return sThreadLocal.get();
}
private void attach(boolean system) {
sThreadLocal.set(this);
...
}
}</code></pre>
<p>原来是这么一个幺蛾子,在4.1.1系统上,ActivityThread是使用一个ThreadLocal实例来存放静态ActivityThread实例的。至于ThreadLocal是干什么用的这里暂不展开,简单说来,就是系统只有在UI线程使用sThreadLocal来保存静态ActivityThread实例,所以我们只能在UI线程通过sThreadLocal获取到这个保存的实例,在Worker线程sThreadLocal会直接返回空。</p>
<p>这样以来解决方案也很明朗,只需要在事先现在UI线程触发一次<code>Applications#context()</code>调用保存Application实例即可。不过项目的代码一直在变化,我们很难保证不会有谁不小心触发了一次优先的Worker线程的调用,那就GG了,所以最好在<code>Applications#context()</code>方法里处理,我们只需要确保能在Worker线程获得ActivityThread实例就Okay了。不过一时半会我想不出切确的办法,也找不到适合的切入点,只做了下简单的处理:如果是优先在Worker线程调用,就先使用UI线程的Handler提交一个任务去获取Context实例,Worker线程等待UI线程获取完Context实例,再接着返回这个实例。</p>
<p>最终完成的代码可以参考 <a href="https://link.segmentfault.com/?enc=ALntt4Ng6nfePCdeK12XRQ%3D%3D.2ls4Zp7OgcUZmSVP%2BiRtTJ%2BMIz0F%2BhvuAnzsJFiEb768x%2BZL6Sjb4Et5BoKQlxLFLtcLZjslKl7w9iJw5uERwxup5mpVpucBE2d11wiUrQNWxHWXIQkQPCKRvctE3YqHIH0uvjagUNtkMDkFlFNh7kW7y7%2F2lFC%2FdDUic7kFTr4%3D" rel="nofollow">Applications</a>。</p>
<p>(补充 2017-04-13)</p>
<p>在这里需要特别强调的时候,通过这样的方法获取Context实例,只要在<code>Application#attachBaseContext(Context)</code>执行之后才能获取到对象,在之前或者之内获取到的对象都是null,具体原因可以参考上面调用流程中的<code>ActivityThread#handleBindApplication</code>。所以,膜法什么的,还是少用为妙吧。</p>
<h2>参考链接</h2>
<p><a href="https://link.segmentfault.com/?enc=G5bPu0uEjO56yHsAQPHV6Q%3D%3D.Gs%2FPazbLuZm2yQffcT8%2FGNUSoQorW8KeOSqVp8%2BRrGwSvNwreVPFzVBcbjP7jFyhCXJQblRKCJAEpuegQba1iOx03T2CN3waat1FAkVB5WPIdnbVBNBZ6%2FTnhgKgADHVULiBaRE%2FrmUs1jZ3KEx0D1ECpc%2F8ix0tVKv31s1m0Nk%3D" rel="nofollow">ActivityThread.java</a></p>
震惊,西方的程序员跑得居然这么快
https://segmentfault.com/a/1190000008782772
2017-03-21T22:35:28+08:00
2017-03-21T22:35:28+08:00
Kaede
https://segmentfault.com/u/kaede
1
<p>昨天刚刚发表了一篇文章(<a href="https://link.segmentfault.com/?enc=U%2BD1%2F2wCjkyBKFGzmT3Yjg%3D%3D.cXtGPAipoWveDLRqrjbD8tO%2FYrhj3RHWjeLHt0VXoxYP1ACovVPPv71%2BaX5oOx2PC4M3LK%2BYgcDFyunZf534gQ%3D%3D" rel="nofollow">ProGuard又搞了个大新闻</a>),主要吐槽的是项目里面使用ProGuard工具导致的一个诡异的坑。其中根本的原因就是,ProGuard混淆Java注解类的时候,把两个方法混淆成同样的名字,导致dx工具在打包<code>.dex</code>文件的时候报错。</p>
<p>本来以为这件事情算是告一段落了,没想到自己还是太Naive了。今天早上突然收到了ProGuard开发者发来的一份邮件,Exciting!邮件里谈到了这次的坑出现的真正原因 —— Java源码和字节码(bytecode)里方法的重载(OverLoading)。</p>
<h2>被雪藏的问题真正原因</h2>
<p>在上一篇文章里,我分析到这次问题的原因是</p>
<blockquote><p>ProGuard工具在混淆注解类类<code>Route.java</code>的时候,把它的两个字段都混淆成<code>a</code>了,按道理应该是一个a和一个b,不知道是不是ProGuard的BUG,还是Route与其他库冲突了。</p></blockquote>
<p>本来我以为是ProGuard的BUG,把注解类的两个字段都混淆成一样的名字,或者是ProGuard受到别的库的影响才出现了这个BUG。显然,在Java代码里面,是不允许有两个名字相同且形参一样的方法的,哪怕是它们的返回值不同。</p>
<pre><code class="java">public static class Hello {
public String[] foo() {
return new String[]{"wor", "ld"};
}
public String foo() {
return "world";
}
}
</code></pre>
<p>这两个方法是无法重载的,IDE会提示错误并且无法编译。虽然现在不少的新编程语言支持这样返回值类型不同的方法重载,但是在Java里行不通,原因也很简单,类似下面的方法立刻就会产生歧义。</p>
<pre><code class="java">public void call() {
// 无法确定调用的是哪个方法。
foo();
}</code></pre>
<p>问题的原因虽然只是这么简单,但是其实在<code>.class</code>文件的字节码(bytecode)里,这样的重载方法是被允许的。为什么呢?简单点说,在字节码里面,对类的文件结构的描述十分严谨,方法调用必须有指定的返回类型,所以像上面那样的调用是不存在的,自然也就不存在产生歧义的问题。</p>
<p>假设现在有这样一个正常的类(上面的示例代码的正常版)。</p>
<pre><code class="java">public class Hello {
public String[] foo1() {
return new String[]{"wor", "ld"};
}
public String foo2() {
return "world";
}
public void main() {
foo1();
String s = foo2();
}
}</code></pre>
<p>这个类编译成<code>.class</code>字节码文件后,它的文件结构大概是这样的。</p>
<pre><code class="bash">+ Program class: com/bilibili/routertest/Hello
...
Interfaces (count = 0):
Constant Pool (count = 30):
...
Fields (count = 0):
Methods (count = 4):
- Method: <init>()V
Access flags: 0x1
= public Hello()
...
+ Method: foo1()[Ljava/lang/String;
Access flags: 0x1
= public java.lang.String[] foo1()
...
+ Method: foo2()Ljava/lang/String;
Access flags: 0x1
= public java.lang.String foo2()
...
+ Method: main()V
Access flags: 0x1
= public void main()
Class member attributes (count = 1):
+ Code attribute instructions (code length = 11, locals = 2, stack = 1):
[0] aload_0 v0
[1] invokevirtual #7
+ Methodref [com/bilibili/routertest/Hello.foo1 ()[Ljava/lang/String;]
[4] pop
[5] aload_0 v0
[6] invokevirtual #8
+ Methodref [com/bilibili/routertest/Hello.foo2 ()Ljava/lang/String;]
[9] astore_1 v1
[10] return
Code attribute exceptions (count = 0):
Code attribute attributes (attribute count = 1):
+ Line number table attribute (count = 3)
[0] -> line 12
[5] -> line 13
[10] -> line 14
Class file attributes (count = 1):
...</code></pre>
<p>我们重点关心其中的<code>main()V</code>方法,可以清楚的看到,上面的Java源码中,main方法调用了foo1方法,虽然没有处理返回值,但是在字节码文件结构对应的方法里明确地指明了改该方法的的返回值类型是<code>[Ljava/lang/String</code>,区别于foo2方法的<code>Ljava/lang/String</code>。也就是说,字节码里面并不会存在我们上面提到的方法调用的歧义问题,因此可以支持相同形参不同返回值的方法的重载。</p>
<p>对于这个课题感兴趣的同学可以参考这篇出自Oracle的调研文章:<a href="https://link.segmentfault.com/?enc=tGTXrlv%2F%2F6hMviUJP7qrzw%3D%3D.WgQNtzjy4J9hl8ladzAGl1ViVKPmT05vbNkK0qMFLAABEBGpOXJgzpQvXCqL03iC" rel="nofollow">Return-Type-Based Method Overloading in Java Blog</a>。</p>
<h2>总结一些人参经验</h2>
<p>关于造成该问题原因的一些阐述。</p>
<ol>
<li><p>上一篇文章提到的ProGuard构建问题其实不是ProGuard的BUG,而是Android SDK的dx工具的BUG。</p></li>
<li><p>不是只有在开启MultiDex的时候才会出现这个问题,不开启问题也会存在,这个问题与MultiDex完全没有关系。</p></li>
<li><p>ProGuard混淆的是字节码而不是Java源码,字节码支持相同形参不同返回值的方法的重载,ProGuard为了最大限度压缩代码量,对后者的重载提供了支持。</p></li>
<li><p>不仅注解类,普通的类也会出现类似的问题。</p></li>
</ol>
<p>解决该问题的一些方法。</p>
<ol>
<li><p>如果不开启ProGuard的<code>-overloadaggressively</code>功能,ProGuard不会对字节码中相同形参不同返回值的方法进行重载(这个功能默认不开启)。</p></li>
<li><p>尝试将注解类的RetentionPolicy级别降级为SOURCE级别。</p></li>
<li><p>不要让注解类出现相同形参不同返回值不同名字的方法,不然可能被混淆成重载的方法。</p></li>
<li><p>Keep住相应的注解类。</p></li>
</ol>
<p>以下是ProGuard开发者给出的建议。</p>
<pre><code>Unfortunately, dx has a bug: it crashes on this overloading. Workarounds:
- Do not use the option '-overloadaggressively' in your ProGuard configuration.
- Alternatively, keep the original annotation method names:
-keepclassmembernames @interface * { <methods>; }
The dx tool should then accept the code.
If it works, you can post this solution in your blog.
</code></pre>
<p>最后,感叹作者的反馈这么迅速。引用作者的一句原话,<code>It's a fast world!</code>,西方程序员跑的比谁都快。</p>
ProGuard 又搞了个大新闻
https://segmentfault.com/a/1190000008767781
2017-03-20T23:20:44+08:00
2017-03-20T23:20:44+08:00
Kaede
https://segmentfault.com/u/kaede
0
<p>一般情况下,Android项目经常开启ProGuard功能来混淆代码,一方面可以降低应用被反编译后代码的友善度,增加被逆向的难度,另一方面开可以通过精简Java API的名字来减少代码的总量,从而精简应用编译后的体积。</p>
<p>ProGuard有个比较坑爹的问题。在开发阶段,我们一般不启用ProGuard,只有在构建Release包的时候才开启。因此,如果有一些API被混淆了会出现BUG,那么在开发阶段我们往往无法察觉BUG,只有在构建发布包的时候才发现,甚至要等发布到线上了才能发现,这种时候解决问题的成本就很大了。</p>
<p>不过今天被ProGuard坑的不是混淆API导致的BUG,这货在之前相当长的一段时间里一直相安无事,最近突然又搞了个大新闻,而且问题排查起来相当蹊跷、诡异。</p>
<h2>新闻发生时候的背景</h2>
<p>最近在给项目的开发一个模块之间通讯用的路由框架,它需要有一些处理注解的APT功能,大概是长这个样子的。</p>
<pre><code class="java">@Route(uri = "action://sing/", desc = "念两句诗")
public static class PoemAction {
...
}</code></pre>
<p>功能大概是这样的,我先编写一个叫做 <code>PoemAction</code>,它的业务功能主要是帮你念上两句诗。然后客户只需要调用 <code>Router.open("action://sing/")</code> 就可以当场念上两句诗,这也是现在一般路由框架的功能。其中的<code>desc</code>没有别的功能,只是为了在生成路由表的时候加上一些注释,说明当前的路由地址是干什么的,看起来像是这样的。</p>
<pre><code class="java">public static class AutoGeneratedRouteTable {
public Route find(String uri) {
...
if("action://sing/".equals(uri)) {
// 念两句诗
return PoemActionRoute;
}
...
}
}</code></pre>
<p>嗯,代码很完美,单元测试和调试阶段都没有发现任何问题,好,合并进develop分支了。搞定收工,我都不禁想赞美自己的才能了,先去栖霞路玩会儿先。半个小时候突然收到了工头 <a href="https://link.segmentfault.com/?enc=6sHtunxtJwkEDLwly8TLtA%3D%3D.0O73cbyUvfk2jkBzO2gDQxp4x1s5FAb4lBLU1sNQEGo%3D" rel="nofollow">Yrom·半仙·灵魂架构师·Wang</a> 的电话,我还以为他也想来玩呢,结果他说不知道谁在项目的代码里下毒,导致构建机上有已经有几十个构建任务失败了。我了个去,我刚刚提交的代码,该不会是我的锅吧,赶紧回来。</p>
<h2>问题排查过程</h2>
<p>异常看起来是这样的。</p>
<pre><code>FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:transformClassesWithMultidexlistForRelease'.
> java.lang.UnsupportedOperationException (no error message)
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.
BUILD FAILED</code></pre>
<p>这看起来好像是MultiDex的问题啊,但是没道理Debug构建没问题,而只有Release构建出问题了,<code>transformClassesWithMultidexlistForRelease</code>任务的源码暂时也没有精力去看了,先解决阻塞同事开发的问题要紧。老规矩,使用 <strong>二分定位法</strong> 挨个回滚到develop上面的commit记录,逐个查看是那次提交导致的,结果还真是我的提交导致的。</p>
<p>难道是开了混淆,导致一些类找不到?但是类找不到只是运行时的异常而已,应该只会在运行APP的时候抛出“ClassNotFoundException”,不应该导致构建失败啊。难道是APT生成的类格式不对,导致Javac在编译该类的时候失败?于是我打开由APT工具生成的<code>AutoGeneratedRouteTable.java</code>类文件瞧瞧,发文件类的格式很完美,没有问题,甚至由于担心是中文引起的问题,我还把“念两句诗”改成“Sing two poems”,问题依旧。</p>
<p>总之一时半会无法排查出问题所在,还是赶紧解决APK的构建问题,现在因为构建失败的原因,旁边已经有一票同事正在摩拳擦掌准备把我狠狠的批判一番。所以我打算先去掉APT功能,不通过自动生成注册类的方式,而是通过手动代码注册的方式让路由工作,就当我以为事情告一段落的时候,我才发现我还是“too young”啊,构建机给了同样的错误反馈。</p>
<p>…………<br>……<br>…</p>
<p>这TM就尴尬了啊,我现在导致构建失败的提交与上一次正常构建的提交之间的差异就是给<code>PeomAction</code>加多了注解而已啊,而且这个注解现在都没有用到了,难道是注解本身的存在就会导致构建失败?</p>
<p>突然我想起来,注解类本身我是没有加入混淆的,因为代码里没有用反射的反射获取注解,而且我设计注解类本身的目的也只是为了帮我自动生成注册类而已,这些类是编译时生成的,所以不会受到混淆功能的影响。抱着死马当活马医的心态,我把注解里面的<code>desc</code>字段去掉了,万万没想到构建问题居然就解决了,而且就算我开启APT功能,问题还是没有重现,这…… 这与构建出问题的状态的差别只有一段注释的差别啊,没问题的代码看起来是这样。</p>
<pre><code class="java">public static class AutoGeneratedRouteTable {
public Route find(String uri) {
...
if("action://sing/".equals(uri)) {
(这里的注释没有了)
return PoemActionRoute;
}
...
}
}</code></pre>
<p>这难道是真实存在的某种膜法在干扰我的构建过程?突然我又想起来,因为注解类本身不需要写什么代码,所以我创建<code>Route.java</code>这个类后基本就没有对它进行过编辑了,我甚至已经忘了我对它写过什么代码,所以我决定看看是不是我写错了些什么。</p>
<pre><code class="java">@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
String[] value();
String desc() default "";
}</code></pre>
<p>这个注解类看起来再普通不过,一般写完之后也不需要再怎么修改了,而且这个类我是直接参(co)考(py)另外一个优秀的Java APT项目 <a href="https://link.segmentfault.com/?enc=Q8C4NrBo95rOS3%2BWHJDoMg%3D%3D.ZCYyv1VXjN6CfpGHpCZgG8E%2B88uIj8c%2FrqB5HySvfnN9arp7dr8l%2BU0x8tsDPhCH" rel="nofollow">DeepLinkDispatch</a> 的,想必也不会有什么大坑。目前看起来唯一有更改可能性的地方就是<code>Target</code>和<code>Retention</code>这两个属性,至于这俩的作用不属于此文章的范畴,不做展开。</p>
<p>首先,<strong>我试着把<code>Retention</code>的级别由原来的<code>CLASS</code>改成<code>SOURCE</code>级别,没想到就这么一个小改动,编译居然通过了!如果不修改<code>Retention</code>的级别,把注解里的<code>desc</code>字段移除,只保留一个<code>value</code>字段,问题也能解决,真是神奇啊</strong>,顿时我好像感受到了一股来自古老东方的神秘力量。</p>
<p>在我一直以来的认知里,<code>RetentionPolicy.SOURCE</code>是源码级别的注解,比如<code>@Override</code>、<code>@WorkerThread</code>、<code>@VisibleForTest</code>等这些注解类,这类的注解一般是配合IDE工作的,不会给代码造成任何实际影响,IDE会获取这些注解,并向你提示哪些代码可能有问题,在编译阶段这类注解加与不加没有任何实际的影响。看一下源码的解释吧。</p>
<pre><code class="java">public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}</code></pre>
<p>原来如此,<code>RetentionPolicy.CLASS</code>级别的注解会被保留到<code>.class</code>文件里,所以混淆的时候,注解类也会参与混淆,大概是混淆的时候出的问题吧。总之,先看看注解类<code>Route.java</code>被混淆后变成什么样子,查看 <code> build/output/release/mapping.txt</code> 文件。</p>
<pre><code>...
moe.studio.router.Route -> bl.buu:
java.lang.String[] value() -> a
java.lang.String desc() -> a
...</code></pre>
<p>果然不出我所料,<strong>ProGuard工具在混淆注解类类<code>Route.java</code>的时候,把它的两个字段都混淆成<code>a</code>了</strong>(按道理应该是一个a和一个b,不知道是不是ProGuard的BUG,还是Route与其他库冲突了)。</p>
<p>所以,最后的解决方案就是把<code>Retention</code>的级别由原来的<code>CLASS</code>降级成<code>SOURCE</code>,或者把注解类的字段改成一个。顺便一说,现在大多的Java APT项目用的还是<code>CLASS</code>,它们之所以没有遇到类似的问题,大多是因为他们都选择把整个注解类都KEEP住,不进行混淆了。</p>
<h2>一些姿势</h2>
<p>通过这个事件我也发现了不少问题。其一,无论单元测试写得再完美,集成进项目之前还是有必要进行一次Release构建,以确保避免一些平时开发的时候容易忽略的问题,不然小心自己打自己的脸。以下是一次打脸现场。</p>
<p><img src="/img/remote/1460000008767784?w=522&h=318" alt="" title=""></p>
<p>所以我决定,给项目的构建机加上一次 <strong>Daily Building</strong> 的功能,每天都定期构建一次,以便尽早发现问题。</p>
<p>其二,除了构建的问题之外,年轻人果然还是要多多学习,<strong>提高一下自己的知识水平</strong>。设想,如果我的Java基础够扎实的话,也就不会像这次一样,犯下<code>RetentionPolicy</code>错用这样低级的错误。如果有仔细阅读过 <code>transformClassesWithMultidexlistForRelease</code> 任务以及ProGuard工具的的源码的话,也许能很快定位到问题发生的根本原因,从而釜底抽薪一举解决问题,不像这次一样,阻塞一大半天开发进度。</p>
<p>以下放出这次定位问题的大致过程。</p>
<p>① 先定位 <code>transformClassesWithMultidexlistForRelease</code> 任务的源码。通过任务名字,可以很快地定位到 <code>MultiDexTransform.java</code> 这个类里面来,以下是这个类在执行任务时候做的工作。</p>
<pre><code class="java"> @Override
public void transform(@NonNull TransformInvocation invocation)
throws IOException, TransformException, InterruptedException {
// Re-direct the output to appropriate log levels, just like the official ProGuard task.
LoggingManager loggingManager = invocation.getContext().getLogging();
loggingManager.captureStandardOutput(LogLevel.INFO);
loggingManager.captureStandardError(LogLevel.WARN);
try {
File input = verifyInputs(invocation.getReferencedInputs());
shrinkWithProguard(input);
computeList(input);
} catch (ParseException | ProcessException e) {
throw new TransformException(e);
}
}</code></pre>
<p>可以看出,MultiDexTransform的主要工作是在<code>shrinkWithProguard</code>和<code>computeList</code>两个方法里面完成的。其中<code>shrinkWithProguard</code>的工作可以定位到ProGuard工具的<code>ProGuard#execute</code>方法里面。</p>
<pre><code class="java"> public void execute() throws IOException
{
System.out.println(VERSION);
GPL.check();
...
if (configuration.dump != null)
{
dump();
}
}</code></pre>
<p>可以定位到ProGuard最后执行的<code>dump()</code>方法里面,该方法生成了一个<code>dump.txt</code>文件,里面用文本的形式,记录了整个项目用到的所有类(混淆后的)的文件结构。查看任务的LOG信息以及<code>dump.txt</code>文件的内容,发现所有内容都正常生成,因此可以初步确定问题不是由于<code>shrinkWithProguard</code>引起的。</p>
<p>接着看看<code>computeList</code>方法,这个方法可以定位到以下代码。</p>
<pre><code class="java"> public Set<String> createMainDexList(
@NonNull File allClassesJarFile,
@NonNull File jarOfRoots,
@NonNull EnumSet<MainDexListOption> options) throws ProcessException {
BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();
ProcessInfoBuilder builder = new ProcessInfoBuilder();
String dx = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);
if (dx == null || !new File(dx).isFile()) {
throw new IllegalStateException("dx.jar is missing");
}
builder.setClasspath(dx);
builder.setMain("com.android.multidex.ClassReferenceListBuilder");
if (options.contains(MainDexListOption.DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) {
builder.addArgs("--disable-annotation-resolution-workaround");
}
builder.addArgs(jarOfRoots.getAbsolutePath());
builder.addArgs(allClassesJarFile.getAbsolutePath());
CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler();
mJavaProcessExecutor.execute(builder.createJavaProcess(), processOutputHandler)
.rethrowFailure()
.assertNormalExitValue();
LineCollector lineCollector = new LineCollector();
processOutputHandler.getProcessOutput().processStandardOutputLines(lineCollector);
return ImmutableSet.copyOf(lineCollector.getResult());
}</code></pre>
<p>从源码可以看出,这里调用了Android SDK里面的<code>dx.jar</code>工具,入口类是 <code>com.android.multidex.ClassReferenceListBuilder</code>,并传入了两个参数,分别是<code>jarOfRoots</code>文件和<code>allClassesJarFile</code>文件。</p>
<p>② 定位到<code>dx.jar</code>工具里具体出问题的地方,通过上面的分析以及构建失败输出的LOG,可以看到Gradle插件调用了<code>dx.jar</code>并传入了<code>build/intermediates/multi-dex/release/componentClasses.jar</code>和<code>build/intermediates/transforms/proguard/release/jars/3/1f/main.jar</code>两个文件。直接调用该命令试试。</p>
<pre><code class="java">Exception in thread "main" com.android.dx.cf.iface.ParseException: name already added: string{"a"}
at com.android.dx.cf.direct.AttributeListParser.parse(AttributeListParser.java:156)
at com.android.dx.cf.direct.AttributeListParser.parseIfNecessary(AttributeListParser.java:115)
at com.android.dx.cf.direct.AttributeListParser.getList(AttributeListParser.java:106)
at com.android.dx.cf.direct.DirectClassFile.parse0(DirectClassFile.java:558)
at com.android.dx.cf.direct.DirectClassFile.parse(DirectClassFile.java:406)
at com.android.dx.cf.direct.DirectClassFile.parseToEndIfNecessary(DirectClassFile.java:397)
at com.android.dx.cf.direct.DirectClassFile.getAttributes(DirectClassFile.java:311)
at com.android.multidex.MainDexListBuilder.hasRuntimeVisibleAnnotation(MainDexListBuilder.java:191)
at com.android.multidex.MainDexListBuilder.keepAnnotated(MainDexListBuilder.java:167)
at com.android.multidex.MainDexListBuilder.<init>(MainDexListBuilder.java:121)
at com.android.multidex.MainDexListBuilder.main(MainDexListBuilder.java:91)
at com.android.multidex.ClassReferenceListBuilder.main(ClassReferenceListBuilder.java:58)
Caused by: java.lang.IllegalArgumentException: name already added: string{"a"}
at com.android.dx.rop.annotation.Annotation.add(Annotation.java:208)
at com.android.dx.cf.direct.AnnotationParser.parseAnnotation(AnnotationParser.java:264)
at com.android.dx.cf.direct.AnnotationParser.parseAnnotations(AnnotationParser.java:223)
at com.android.dx.cf.direct.AnnotationParser.parseAnnotationAttribute(AnnotationParser.java:152)
at com.android.dx.cf.direct.StdAttributeFactory.runtimeInvisibleAnnotations(StdAttributeFactory.java:616)
at com.android.dx.cf.direct.StdAttributeFactory.parse0(StdAttributeFactory.java:93)
at com.android.dx.cf.direct.AttributeFactory.parse(AttributeFactory.java:96)
at com.android.dx.cf.direct.AttributeListParser.parse(AttributeListParser.java:142)
... 11 more</code></pre>
<p>从异常的堆栈可以直接看出,dx工具在执行<code>AnnotationParser#parseAnnotation</code>方法的时候出错了,原因是有两个相同的字段<code>a</code>,这也刚好印证了上面<code>mapping.txt</code>文件里面的错误信息。</p>
<p>③ 最后定位到源码里具体出问题的地方,查看dx工具里的<code>com.android.dx.rop.annotation.Annotation.java</code>的源码。</p>
<pre><code class="java"> private final TreeMap<CstString, NameValuePair> elements;
/**
* Add an element to the set of (name, value) pairs for this instance.
* It is an error to call this method if there is a preexisting element
* with the same name.
*
* @param pair {@code non-null;} the (name, value) pair to add to this instance
*/
public void add(NameValuePair pair) {
throwIfImmutable();
if (pair == null) {
throw new NullPointerException("pair == null");
}
CstString name = pair.getName();
if (elements.get(name) != null) {
throw new IllegalArgumentException("name already added: " + name);
}
elements.put(name, pair);
}</code></pre>
<p>到此,<strong>从成功定位到产生异常的具体地方</strong>。</p>
<p>④ 此外,从<code>:app:assembleRelease --debug --stacktrace</code>的异常堆栈里是无法直接看出具体出异常的地方的错误信息的,不过可以通过<code>:app:assembleRelease --full-stacktrace</code>命令输出更多的错误堆栈,从而直观地看出一些猫腻来。</p>
<pre><code>Caused by: com.android.ide.common.process.ProcessException: Error while executing java process with main class com.android.multidex.ClassReferenceListBuilder with arguments {build/intermediates/multi-dex/release/componentClasses.jar build/intermediates/transforms/proguard/release/jars/3/1f/main.jar}
at com.android.build.gradle.internal.process.GradleProcessResult.buildProcessException(GradleProcessResult.java:74)
at com.android.build.gradle.internal.process.GradleProcessResult.assertNormalExitValue(GradleProcessResult.java:49)
at com.android.builder.core.AndroidBuilder.createMainDexList(AndroidBuilder.java:1384)
at com.android.build.gradle.internal.transforms.MultiDexTransform.callDx(MultiDexTransform.java:309)
at com.android.build.gradle.internal.transforms.MultiDexTransform.computeList(MultiDexTransform.java:265)
at com.android.build.gradle.internal.transforms.MultiDexTransform.transform(MultiDexTransform.java:186)</code></pre>
<p>从上面的堆栈信息可以直接看出Gradle插件在调用dx工具的时候出现异常了(Process的返回值不是0,也就是Java程序里面调用了System.exit(0)之外的结束方法),对应的类为<code>ClassReferenceListBuilder</code>。</p>
<pre><code class="java"> public static void main(String[] args) {
int argIndex = 0;
boolean keepAnnotated = true;
while (argIndex < args.length -2) {
if (args[argIndex].equals(DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) {
keepAnnotated = false;
} else {
System.err.println("Invalid option " + args[argIndex]);
printUsage();
System.exit(STATUS_ERROR);
}
argIndex++;
}
if (args.length - argIndex != 2) {
printUsage();
System.exit(STATUS_ERROR);
}
try {
MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex],
args[argIndex + 1]);
Set<String> toKeep = builder.getMainDexList();
printList(toKeep);
} catch (IOException e) {
System.err.println("A fatal error occurred: " + e.getMessage());
System.exit(STATUS_ERROR);
return;
}
}</code></pre>
<p>由其中的 <code>MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex], args[argIndex + 1])</code> 也能进一步定位到上面的 <code>com.android.dx.rop.annotation.Annotation.java</code> 出问题的地方。</p>
<h2>参考</h2>
<p>推荐阅读 <a href="https://link.segmentfault.com/?enc=trMCL6GgNQ3xZj8SW9SUew%3D%3D.ffVe5U5eswM5R6JfF3s5Ny5mK3Mwn60AHbfyHRWVaQUsXuKbqsTXMhZFC5d7mNET" rel="nofollow">ProGuard在插件化里的应用</a>。</p>
<p>著作信息:<br>本文章出自 <a href="https://link.segmentfault.com/?enc=d0ypURixDkv%2FBZyd7dlXXA%3D%3D.gASCCoN52vZLJPQQ2byHVEPjpwOr2hIcMnn3GuDOkz4%3D" rel="nofollow"><strong>Kaede</strong></a> 的博客,原创文章若无特别说明,均遵循 <a href="https://link.segmentfault.com/?enc=Uu%2Fas5SkCbgT%2BPrySq%2BhlQ%3D%3D.rHugoubnM2lszcd7SmN5FHU8n489%2FZZqmKnpKS97sSfZ3OmhDxYsQiNLze545C0lYAS%2FjcZL1%2F%2B7JKa1dyL1Ng%3D%3D" rel="nofollow"><strong>CC BY-NC 4.0</strong></a> 知识共享许可协议4.0(署名-非商用-相同方式共享),可以随意摘抄转载,但必须标明署名及原地址。</p>
通过预安装给MultiDex加速
https://segmentfault.com/a/1190000007766471
2016-12-11T22:24:05+08:00
2016-12-11T22:24:05+08:00
Kaede
https://segmentfault.com/u/kaede
2
<p>在Android Kikat及以前的Android系统上,构建或安装Apk会出现“<strong>65535方法数超标</strong>”以及“<strong>INSTALL_FAILED_DEXOPT</strong>”问题,MultiDex是Google为了解决这个问题问题而开发的一个Support库。MultiDex出现的具体背景、使用方式可以参考<a href="https://link.segmentfault.com/?enc=DsYd%2FrO5Sxd%2BslxGyVs0Yg%3D%3D.7lAMPp%2F77zcje%2B62vxC6BhjC1AmduH4Iw3tIcXKFoSsAJCzAZNWkwtCT92G2Sw9uvee6YxqzZNAWz%2FBCXVPCYw%3D%3D" rel="nofollow">给App启用 MultiDex功能</a>,而MultiDex Support库的工作机制、源码分析可以参考<a href="https://link.segmentfault.com/?enc=437ky6grmJEuki%2BooIUGiA%3D%3D.HIYNAcF7t3eIc9GqxqIB40cZoWJXuQFRVFO9LTZ9pew4Jv7H1UaoD%2B0QLw%2FazGywedjj9d0LSFg9OoD59%2Bgd8g%3D%3D" rel="nofollow">MultiDex工作原理分析和优化方案</a>。</p>
<p>MultiDex的使用虽然很简单便捷,但是有个比较蛋疼的问题,就是在App第一次冷启动的时候会产生明显的卡顿现象。经过测试和统计,根据Apk包的大小、Android系统版本的不同,这个卡顿时间一般是2000到5000毫秒左右,极端的情况下甚至可以到20000+毫秒。通过之前的分析,我们知道具体的卡顿产生在MultiDex解压、优化dex这两个过程,而且只在第一次冷启动的时候才会触发这两个过程。那么优化的方式也很简单,在安装Apk前先对新版本的Apk做好解压和优化工作,就能在安装后第一次冷启动的时候避开这两个耗时的过程了。</p>
<h2>MultiDex是如何判断是否需要重新解压和优化dex的</h2>
<p>在之前的章节里面讲到,MultiDex在第一次做完解压和优化dex之后,会保留当前Apk的一些信息,下一次启动时候后读取这些配置信息再判断是否需要重新解压和优化dex文件。</p>
<p>这个判断主要是在MultiDexExtractor#load(Context, ApplicationInfo, File, boolean)方法里进行。</p>
<pre><code class="java"> static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
try {
...
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException ioe) {
...
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context,
getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
...
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
}
...
return files;
}</code></pre>
<p>第一次调用这个方法的时候,forceReload为false,则不需要强制重新解压dex。然后调用了<code>isModified</code>这个方法判断当前App的Apk包是否被修改过。</p>
<pre><code class="java"> private static boolean isModified(Context context, File archive, long currentCrc) {
SharedPreferences prefs = getMultiDexPreferences(context);
return (prefs.getLong(KEY_TIME_STAMP, NO_VALUE) != getTimeStamp(archive))
|| (prefs.getLong(KEY_CRC, NO_VALUE) != currentCrc);
}</code></pre>
<p><code>isModified</code>方法主要是判断当前App的Apk包的CRC值是否和上一次解压dex时记录的Apk包CRC一样(CRC值可以认为是一个稀疏的MD5算法,它的时间复杂度低很多,但是计算结果容易产生冲突),以及Apk文件的修改时间(文件的Last Modified Time)是否一致。如果这两项都一致的话就认为Apk文件没有产生变化(没有覆盖安装过),因此上一次解压和优化dex得到的缓存文件可以复用。</p>
<p>当然,光Apk包没有修改过这一项条件还不够,接下来调用了这个判断主要是在MultiDexExtractor#loadExistingExtractions(Context, File, File)。</p>
<pre><code class="java"> private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
throws IOException {
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
final List<File> files = new ArrayList<File>(totalDexNumber);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
files.add(extractedFile);
if (!verifyZipFile(extractedFile)) {
throw new IOException("Invalid ZIP file.");
}
} else {
throw new IOException("Missing extracted secondary dex file '" +
extractedFile.getPath() + "'");
}
}
return files;
}</code></pre>
<p>这里先通过SharePreference读取上一次MultiDex保存的Apk包的dex数量totalDexNumber,然后挨个加载预定的文件路径上的dex文件,加载文件的的同时还通过<code>verifyZipFile</code>方法判断dex文件的合法性。如果这个过程出现异常就认为获取上一次缓存的dex文件失败,需要重新解压。</p>
<pre><code class="java"> static boolean verifyZipFile(File file) {
try {
ZipFile zipFile = new ZipFile(file);
try {
zipFile.close();
return true;
} catch (IOException e) {
Log.w(TAG, "Failed to close zip file: " + file.getAbsolutePath());
}
} catch (ZipException ex) {
Log.w(TAG, "File " + file.getAbsolutePath() + " is not a valid zip file.", ex);
} catch (IOException ex) {
Log.w(TAG, "Got an IOException trying to open zip file: " + file.getAbsolutePath(), ex);
}
return false;
}</code></pre>
<p><code>verifyZipFile</code>这个方法非常简单,解压dex文件的时候,解压出来的文件被保存成Zip包,这个方法这是检查缓存的dex文件是否是Zip包。感觉不靠谱,虽然检查MD5值比较耗时不适合这种情景,不过好歹也像检查Apk包的CRC值和修改时间一样,检查dex缓存文件的CRC和修改时间啊。不过读取SharePreference配置是一个IO操作,如果保存的数值太多的话,也是有增加耗时和IO异常的风险的。</p>
<p>到这里我们的方案就清晰了:</p>
<ol>
<li><p>在安装新Apk前,先做好dex的解压和优化,得到dex压缩包(.zip)列表和dexopt后的odex文件(.dex)列表。</p></li>
<li><p>把dex/odex文件保存到一个内部存储路径PATH_A,同时使用SP记录新版本Apk的CRC、dex数量,以及解压出来的每一个dex的CRC值。</p></li>
<li><p>安装新版本Apk后,启动时在执行MultiDex前,把PATH_A路径上的缓存文件移动(rename)到MultiDex的缓存路径PATH_B上,同时保存当前Apk的CRC、修改时间以及dex数量到MultiDex对应的SP配置上。</p></li>
<li><p>执行原有MultiDex逻辑,让MultiDex以为之前已经做过解压和优化dex工作,从而绕开第一次MultiDex时候的耗时。</p></li>
<li><p>第一次成功启动新Apk后,对dex进行校验工作,如果校验失败则清除dex缓存,强制让App在下一次启动的时候再执行一遍MultiDex。</p></li>
</ol>
<h2>预解压(PreMultiDex)详细的流程图</h2>
<p>注:</p>
<ol>
<li><p>流程图的绿色部分为文件锁(FileLock)操作,主要是为了多进程同步。</p></li>
<li><p>红色部分为耗时的操作。</p></li>
<li><p>Dex路径为MultiDex过程中用于存储解压出来的dex文件的路径(/data/data/<package>/code_cache)。</p></li>
<li><p>PreDex路径为存储预解压得到的缓存文件的内部路径(/data/data/<package>/code_cache_pre)。</p></li>
<li><p>MultiDex从Apk包解压出来的dex文件会被压缩成Zip包(.zip),而执行dexopt操作后生成的odex文件文件名为.dex,这两个容易搞混。</p></li>
</ol>
<h3>安装新Apk前先解压和优化dex</h3>
<p>这个环节必须在升级Apk前,由旧版本的Apk进行,也就是要求App拥有<strong>自主更新</strong>的逻辑。</p>
<p><img src="/img/remote/1460000007766474?w=711&h=1147" alt="" title=""></p>
<h3>第一次运行新Apk时,移动预先安装好的dex文件</h3>
<p>从旧版的Apk覆盖安装新的Apk后,第一次运行App时MultiDex主要的耗时过程。这时需要把在旧版本Apk预安装得到的dex缓存文件移动到MultiDex使用的存储路径上。</p>
<p><img src="/img/remote/1460000007766475?w=747&h=1630" alt="" title=""></p>
<h3>第一次运行新Apk后,检查dex文件是否正确</h3>
<p>原有的MultiDex,dex文件时同步从Apk包里解压出来的,所以不存在dex文件和Apk版本对不上的问题。而<strong>PreMultiDex</strong>的方案的一个问题ui是,解压dex文件和使用dex文件这两个过程是分开的,无论版本控制做得再精确,理论上也存在版本出错的问题(比如从A版本解压得到了dex文件,而用户却选择覆盖安装了B版本,这时候由于代码逻辑的不严谨导致B版本的Apk使用了A版本解压出来的dex文件)。如果想要确保dex文件的正确性,需要对Apk包里面的dex文件和解压出来的dex文件做一下MD5值校验,而这个过程比较耗时,不适合在App启动的时候做,不然<strong>PreMultiDex</strong>就失去了意义。因此,需要在第一次运行新Apk后,启动dex的校验工作,在Worker线程对dex进行校验,如果校验失败则清除dex缓存,强制让App在下一次启动的时候再执行一遍MultiDex。</p>
<p><img src="/img/remote/1460000007766476?w=723&h=1423" alt="" title=""></p>
<h3>恢复MultiDex</h3>
<p>在MultiDex校验失败后,需要清空MultiDex的缓存文件,禁用<strong>PreMultiDex</strong>功能,并且强制让App在下一次启动的时候再执行一遍MultiDex。</p>
<p><img src="/img/remote/1460000007766477?w=497&h=780" alt="" title=""></p>
<h2>一些小细节</h2>
<h3>dex文件、odex文件?</h3>
<p>dex文件是Android虚拟机使用的可执行文件(从Java类编译得到),相当于JVM虚拟机用的class文件。但是与class文件不同,Android系统并不能直接使用dex文件,需要先使用dexopt工具对dex文件进行一次优化工作(Optimize),优化得到的odex文件才能被虚拟机加载。不同的Android设备需要不同格式的odex文件,所以这个过程只能在Android设备上进行,而不能在构建Apk的时候就处理好。</p>
<p>dex文件在Apk包里的文件后缀名是<strong>.dex</strong>,MultiDex从Apk包里解压出dex文件后会压缩成Zip包,文件后缀名是<strong>.zip</strong>。对dex文件进行dexopt操作后,会生成相同文件名的odex文件,后缀名是<strong>.dex</strong>,odex文件会比dex文件大许多,不要搞混这些文件。</p>
<p>至于为什么MultiDex解压dex文件时会进行压缩工作,可能是因为压缩后的压缩包会占用比较小的内部存储空间,因为MultiDex本来就是给旧版本的Android系统使用,一些早期的Android设备拥有的内部存储空间非常有限,而这些dex文件对于App的运行时必须的,所以才需要尽量压缩dex的体积。压缩过程会有明显的耗时,经过测试,如果不进行压缩,直接从Apk里解压dex文件,则MultiDex过程会有大约<strong>1/3</strong>的加速效果。</p>
<h3>dexopt缓存</h3>
<p>MultiDex其实并没有刻意保留dexopt后的缓存,如果只保留dex文件,而不保留odex文件,那么下一次启动执行MultiDex的时候,不需要重新解压dex文件,但是依然需要dexopt并产生odex文件,这个过程大概会占用MultiDex总耗时的一般左右。如果odex文件存在,但是已经损坏了,或者是一个非法的odex文件,依然会触发dexopt工作。也就是说,加载dex文件并创建DexFile对象的时候,Android系统会判断odex的缓存,以及缓存文件是否正确,具体过程在<a href="https://link.segmentfault.com/?enc=FT2YlfngCDQt%2FH8ezceLNA%3D%3D.sNmFZ8mH4VblTsfiTPjoP40Y376DwEhu9bb%2B0FwlzAsaG%2Fdnn3RbeMx2RtiAKyT3JguyujEYd6thGRJfK6qdfDHnzdUxx2tNBTHdjHaDt50W9BkfAo6yxZf3GsHyp9Xw" rel="nofollow">dalvik_system_DexFile.cpp</a>里实现,有兴趣的同学可以找找dex文件结构分析的文章,这里就不挖坑了。</p>
<h3>关于dex文件校验</h3>
<p>其实,如果dex文件和Apk的版本对不上的话,一般在启动App的时候就会出现ClassNotFound异常而导致App崩溃,接着再次启动由于没有重新MultiDex也会继续崩溃。而崩溃的时候,可能App崩溃上报系统还没来得及初始化,所以没有办法发现崩溃的问题。</p>
<p>为了防止这种问题,可以开发一个<strong>恢复模式</strong>或者<strong>安全模式</strong>的功能,当App出现连续的崩溃的时候,会进入恢复模式的状态,清空一些可能导致异常的数据(比如PreMultiDex的缓存),这样就能避免App因为连续崩溃而不能使用。至于怎么实现恢复,这已经是另一个领域的功能了,这里不再展开。</p>
<p>参考链接:<br><a href="https://link.segmentfault.com/?enc=iQF3H%2F1mg92fz4v10Qhl4A%3D%3D.r%2ByN4EcPdV6zREzzlh3e80dHaP2ZeHOGwcVG%2FXgX2sdEFJUqoJqnytHUefEVN%2FbWAjgZjJXbsPtO7OUs1C2Oyw%3D%3D" rel="nofollow">Google Multidex</a></p>
<p>著作信息:<br>本文章出自 <a href="https://link.segmentfault.com/?enc=xUw%2B%2BbyEbfeFEIX5vQ5wcg%3D%3D.msd7ruh3UrU7cRcUKUcGWVKS04UCHuSURZzHgCiUjUM%3D" rel="nofollow"><strong>Kaede</strong></a> 的博客,原创文章若无特别说明,均遵循 <a href="https://link.segmentfault.com/?enc=bRJxGFiwkn0IinwnjoBr0A%3D%3D.rYNvJ%2FtDnrwFfF9LEPHspSlsqfZtwTMYDxUf4uBR3uTc%2FDfvVLMYYFU2R3H7tLhq1ATQArQ90f7xszvnwlXnKw%3D%3D" rel="nofollow"><strong>CC BY-NC 4.0</strong></a> 知识共享许可协议4.0(署名-非商用-相同方式共享),可以随意摘抄转载,但必须标明署名及原地址。</p>
MultiDex工作原理分析和优化方案
https://segmentfault.com/a/1190000007764739
2016-12-11T17:54:17+08:00
2016-12-11T17:54:17+08:00
Kaede
https://segmentfault.com/u/kaede
2
<p>动态加载技术(插件化)系列已经坑了有一段时间了,不过UP主我并没有放弃治疗哈,相信在不就的未来就可以看到“系统Api Hook模式”和插件化框架Frontia的更新了。今天要讲的是动态加载技术的亲戚 —— MultiDex。他们的核心原理之一都是dex文件的加载。</p>
<p>MultiDex是Google为了解决“<strong>65535方法数超标</strong>”以及“<strong>INSTALL_FAILED_DEXOPT</strong>”问题而开发的一个Support库,具体如何使用MultiDex现在市面已经有一大堆教程(可以参考<a href="https://link.segmentfault.com/?enc=BqgsTsAOk4xqMJJIRIEY3w%3D%3D.Kxv43vA4HsV5zr3v0ykpCGGIdPATkUorcatZ34iGM25Iw9A8Chhs8KtsDANi4iuLFWvxQwdZBxKeKnqmeHODVg%3D%3D" rel="nofollow">给 App 启用 MultiDex 功能</a>),这里不再赘述。这篇日志主要是配合源码分析MultiDex的工作原理,以及提供一些MultiDex优化的方案。</p>
<h2>Dex的工作机制</h2>
<p>等等,这个章节讲的不是MultiDex吗,怎么变成Dex了?没错哈,没有Dex,哪来的MultiDex。在Android中,对Dex文件操作对应的类叫做DexFile。在<a href="https://link.segmentfault.com/?enc=wAfvTDxyN%2BPiU0Tg3I6VAQ%3D%3D.CWmtWHEp20UqFPtnoICNYnyZfjAZXhH1HB5SjBbPIDqdFsf14yzkn%2F3zMMjBEK53M3iAz%2BnW3H4HCWuksK1kRnbnv%2BpbXaC%2FaNms6PEFUtw%3D" rel="nofollow">CLASSLOADER 的工作机制</a>中,我们说到:</p>
<blockquote><p>对于 Java 程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的class文件),其中起到关键作用的就是类加载器 ClassLoader。</p></blockquote>
<p>Android程序的每一个Class都是由ClassLoader#loadClass方法加载进内存的,更准确来说,<strong>一个ClassLoader实例会有一个或者多个DexFile实例</strong>,调用了ClassLoader#loadClass之后,ClassLoader会通过类名,在自己的DexFile数组里面查找有没有那个DexFile对象里面存在这个类,如果都没有就抛ClassNotFound异常。ClassLoader通过调用DexFile的一个叫defineClass的Native方法去加载指定的类,这点与JVM略有不同,后者是直接调用ClassLoader#defineCLass方法,反正最后实际加载类的方法都叫defineClass就没错了?。</p>
<h3>创建DexFile对象</h3>
<p>首先来看看造DexFile对象的构方法。</p>
<pre><code class="java">public final class DexFile {
private int mCookie;
private final String mFileName;
...
public DexFile(File file) throws IOException {
this(file.getPath());
}
public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
}
private DexFile(String sourceName, String outputName, int flags) throws IOException {
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
}
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
}
public Class loadClass(String name, ClassLoader loader) {
String slashName = name.replace('.', '/');
return loadClassBinaryName(slashName, loader);
}
public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
native private static int openDexFile(String sourceName, String outputName,
int flags) throws IOException;
native private static int openDexFile(byte[] fileContents)
...
}</code></pre>
<p>通过以前分析过的源码,我们知道ClassLoader主要是通过DexFile.loadDex这个静态方法来创建它需要的DexFile实例的,这里创建DexFile的时候,保存了Dex文件的文件路径mFileName,<strong>同时调用了openDexFile的Native方法打开Dex文件</strong>并返回了一个mCookie的整型变量(我不知道这个干啥用的,我猜它是一个C++用的资源句柄,用于Native层访问具体的Dex文件)。在Native层的openDexFile方法里,主要做了检查当前创建来的Dex文件是否是有效的Dex文件,还是是一个带有Dex文件的压缩包,还是一个无效的Dex文件。</p>
<h3>加载Dex文件里的类</h3>
<p>加载类的时候,ClassLoader又是通过DexFile#loadClass这个方法来完成的,这个方法里调用了defineClass这个Native方法,<strong>看来DexFile才是加载Class的具体API,加载Dex文件和加载具体Class都是通过Native方法完成</strong>,ClassLoader有点名不副实啊。</p>
<h2>MultiDex的工作机制</h2>
<p>当一个Dex文件太肥的时候(方法数目太多、文件太大),在打包Apk文件的时候就会出问题,就算打包的时候不出问题,在Android 5.0以下设备上安装或运行Apk也会出问题(具体原因可以参考<a href="https://link.segmentfault.com/?enc=LJG7D2nXMTamdCNcJoDYlQ%3D%3D.BUwC8zm4fZSNM9D%2FMXsTvus5GRt6ykI5ap2r9UNiVDKYac%2FLiU2W9zpgxAgdrczgP36jbIf9LrkHtdSTUVPfvg%3D%3D" rel="nofollow">给 App 启用 MultiDex 功能</a>)。既然一个Dex文件不行的话,那就把这个硕大的Dex文件拆分成若干个小的Dex文件,刚好一个ClassLoader可以有多个DexFile,这就是MultiDex的基本设计思路。</p>
<h3>工作流程</h3>
<p>MultiDex的工作流程具体分为两个部分,一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 “multiDexEnabled true”),另一部分就是在启动Apk的时候,同时加载多个Dex文件(具体是加载Dex文件优化后的Odex文件,不过文件名还是.dex),这一部分工作从Android 5.0开始系统已经帮我们做了,但是在Android 5.0以前还是需要通过MultiDex Support库来支持(MultiDex.install(Context))。</p>
<p>所以我们需要关心的是第二部分,这个过程的简单示意流程图如下。</p>
<p><img src="/img/remote/1460000007764742" alt="jpg" title="jpg"></p>
<p>(图中红色部分为耗时比较大的地方)</p>
<h3>源码分析</h3>
<p>现在官方已经部署的MultiDex Support版本是<code>com.android.support:multidex:1.0.1</code>,但是现在仓库的master分支已经有了许多新的提交(其中最明显的区别是加入了FileLock来控制多进程同步问题),所以这里分析的源码都是最新的master分支上的。</p>
<p>MultiDex Support的入口是<code>MultiDex.install(Context)</code>,先从这里入手吧。(这次我把具体的分析都写在代码的注释了,这样看是不是更简洁明了些?)</p>
<pre><code class="java"> public static void install(Context context) {
Log.i(TAG, "install");
// 1. 判读是否需要执行MultiDex。
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}
if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) {
throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT
+ " is unsupported. Min SDK version is " + MIN_SDK_VERSION + ".");
}
try {
ApplicationInfo applicationInfo = getApplicationInfo(context);
if (applicationInfo == null) {
// Looks like running on a test Context, so just return without patching.
return;
}
// 2. 如果这个方法已经调用过一次,就不能再调用了。
synchronized (installedApk) {
String apkPath = applicationInfo.sourceDir;
if (installedApk.contains(apkPath)) {
return;
}
installedApk.add(apkPath);
// 3. 如果当前Android版本已经自身支持了MultiDex,依然可以执行MultiDex操作,
// 但是会有警告。
if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) {
Log.w(TAG, "MultiDex is not guaranteed to work in SDK version "
+ Build.VERSION.SDK_INT + ": SDK version higher than "
+ MAX_SUPPORTED_SDK_VERSION + " should be backed by "
+ "runtime with built-in multidex capabilty but it's not the "
+ "case here: java.vm.version=\""
+ System.getProperty("java.vm.version") + "\"");
}
// 4. 获取当前的ClassLoader实例,后面要做的工作,就是把其他dex文件加载后,
// 把其DexFile对象添加到这个ClassLoader实例里就完事了。
ClassLoader loader;
try {
loader = context.getClassLoader();
} catch (RuntimeException e) {
Log.w(TAG, "Failure while trying to obtain Context class loader. " +
"Must be running in test mode. Skip patching.", e);
return;
}
if (loader == null) {
Log.e(TAG,
"Context class loader is null. Must be running in test mode. "
+ "Skip patching.");
return;
}
try {
// 5. 清除旧的dex文件,注意这里不是清除上次加载的dex文件缓存。
// 获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录。
// 如果获取失败,则使用/data/data/<package>/files/code-cache目录。
// 这里清除的是第二个目录。
clearOldDexDir(context);
} catch (Throwable t) {
Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
+ "continuing without cleaning.", t);
}
// 6. 获取缓存目录(/data/data/<package>/code-cache)。
File dexDir = getDexDir(context, applicationInfo);
// 7. 加载缓存文件(如果有)。
List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
// 8. 检查缓存的dex是否安全
if (checkValidZipFiles(files)) {
// 9. 安装缓存的dex
installSecondaryDexes(loader, dexDir, files);
} else {
// 9. 从apk压缩包里面提取dex文件
Log.w(TAG, "Files were not valid zip files. Forcing a reload.");
files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);
if (checkValidZipFiles(files)) {
// 10. 安装提取的dex
installSecondaryDexes(loader, dexDir, files);
} else {
throw new RuntimeException("Zip files were not valid.");
}
}
}
} catch (Exception e) {
Log.e(TAG, "Multidex installation failure", e);
throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
}
Log.i(TAG, "install done");
}</code></pre>
<p>具体代码的分析已经在上面代码的注释里给出了,从这里我们也可以看出,整个MultiDex.install(Context)的过程中,关键的步骤就是<code>MultiDexExtractor#load</code>方法和<code>MultiDex#installSecondaryDexes</code>方法。</p>
<p>(这部分是题外话)其中有个<strong>MultiDex#clearOldDexDir(Context)</strong>方法,这个方法的作用是删除<strong>/data/data/<package>/files/code-cache</strong>,一开始我以为这个方法是删除上一次执行MultiDex后的缓存文件,不过这明显不对,不可能每次MultiDex都重新解压dex文件一边,这样每次启动会很耗时,<strong>只有第一次冷启动的时候才需要解压dex文件</strong>。后来我又想是不是以前旧版的MultiDex曾经把缓存文件放在这个目录里,现在新版本只是清除以前旧版的遗留文件?但是我找遍了整个MultiDex Repo的提交也没有见过类似的旧版本代码。后面我仔细看<strong>MultiDex#getDexDir</strong>这个方法才发现,原来MultiDex在获取dex缓存目录是,会优先获取<strong>/data/data/<package>/code-cache</strong>作为缓存目录,如果获取失败,则使用<strong>/data/data/<package>/files/code-cache</strong>目录,而后者的缓存文件会在每次App重新启动的时候被清除。感觉MultiDex获取缓存目录的逻辑不是很严谨,而获取缓存目录失败也是MultiDex工作工程中少数有重试机制的地方,看来MultiDex真的是一个临时的兼容方案,Google也许并不打算认真处理这些历史的黑锅。</p>
<p>接下来再看看MultiDexExtractor#load这个方法。</p>
<pre><code class="java">static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
boolean forceReload) throws IOException {
Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
final File sourceApk = new File(applicationInfo.sourceDir);
// 1. 获取当前Apk文件的crc值。
long currentCrc = getZipCrc(sourceApk);
// Validity check and extraction must be done only while the lock file has been taken.
File lockFile = new File(dexDir, LOCK_FILENAME);
RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
FileChannel lockChannel = null;
FileLock cacheLock = null;
List<File> files;
IOException releaseLockException = null;
try {
lockChannel = lockRaf.getChannel();
Log.i(TAG, "Blocking on lock " + lockFile.getPath());
// 2. 加上文件锁,防止多进程冲突。
cacheLock = lockChannel.lock();
Log.i(TAG, lockFile.getPath() + " locked");
// 3. 先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压。
// 此外,通过crc和文件修改时间,判断如果Apk文件已经被修改(覆盖安装),就会跳过缓存重新解压dex文件。
if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
try {
// 4. 加载缓存的dex文件
files = loadExistingExtractions(context, sourceApk, dexDir);
} catch (IOException ioe) {
Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ " falling back to fresh extraction", ioe);
// 5. 加载失败的话重新解压,并保存解压出来的dex文件的信息。
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context,
getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} else {
// 4. 重新解压,并保存解压出来的dex文件的信息。
Log.i(TAG, "Detected that extraction must be performed.");
files = performExtractions(sourceApk, dexDir);
putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
}
} finally {
if (cacheLock != null) {
try {
cacheLock.release();
} catch (IOException e) {
Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
// Exception while releasing the lock is bad, we want to report it, but not at
// the price of overriding any already pending exception.
releaseLockException = e;
}
}
if (lockChannel != null) {
closeQuietly(lockChannel);
}
closeQuietly(lockRaf);
}
if (releaseLockException != null) {
throw releaseLockException;
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
</code></pre>
<p>这个过程主要是获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。需要注意的时,如果是重新解压,这里会有明显的耗时,而且解压出来的dex文件,会被压缩成.zip压缩包,压缩的过程也会有明显的耗时(这里压缩dex文件可能是问了节省空间)。</p>
<p>如果dex文件是重新解压出来的,则会保存dex文件的信息,包括解压的apk文件的crc值、修改时间以及dex文件的数目,以便下一次启动直接使用已经解压过的dex缓存文件,而不是每一次都重新解压。</p>
<p>需要特别提到的是,里面的<strong>FileLock</strong>是最新的master分支里面新加进去的功能,现在最新的<code>1.0.1</code>版本里面是没有的。</p>
<p>无论是通过使用缓存的dex文件,还是重新从apk中解压dex文件,获取dex文件列表后,下一步就是安装(或者说加载)这些dex文件了。最后的工作在<code>MultiDex#installSecondaryDexes</code>这个方法里面。</p>
<pre><code class="java"> private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
InvocationTargetException, NoSuchMethodException, IOException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
V19.install(loader, files, dexDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(loader, files, dexDir);
} else {
V4.install(loader, files);
}
}
}</code></pre>
<p>因为在不同的SDK版本上,ClassLoader(更准确来说是DexClassLoader)加载dex文件的方式有所不同,所以这里做了V4/V14/V19的兼容(Magic Code)。</p>
<p>Build.VERSION.SDK_INT < 14</p>
<pre><code class="java"> /**
* Installer for platform versions 4 to 13.
*/
private static final class V4 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, IOException {
int extraSize = additionalClassPathEntries.size();
Field pathField = findField(loader, "path");
StringBuilder path = new StringBuilder((String) pathField.get(loader));
String[] extraPaths = new String[extraSize];
File[] extraFiles = new File[extraSize];
ZipFile[] extraZips = new ZipFile[extraSize];
DexFile[] extraDexs = new DexFile[extraSize];
for (ListIterator<File> iterator = additionalClassPathEntries.listIterator();
iterator.hasNext();) {
File additionalEntry = iterator.next();
String entryPath = additionalEntry.getAbsolutePath();
path.append(':').append(entryPath);
int index = iterator.previousIndex();
extraPaths[index] = entryPath;
extraFiles[index] = additionalEntry;
extraZips[index] = new ZipFile(additionalEntry);
extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0);
}
// 这个版本是最简单的。
// 只需要创建DexFile对象后,使用反射的方法分别扩展ClassLoader实例的以下字段即可。
pathField.set(loader, path.toString());
expandFieldArray(loader, "mPaths", extraPaths);
expandFieldArray(loader, "mFiles", extraFiles);
expandFieldArray(loader, "mZips", extraZips);
expandFieldArray(loader, "mDexs", extraDexs);
}
}</code></pre>
<p>14 <= Build.VERSION.SDK_INT < 19</p>
<pre><code class="java"> /**
* Installer for platform versions 14, 15, 16, 17 and 18.
*/
private static final class V14 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
// 扩展ClassLoader实例的"pathList"字段。
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
}
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
}
}</code></pre>
<p>从API14开始,DexClassLoader会使用一个DexpDexPathList类来封装DexFile数组。</p>
<pre><code class="java">final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final String APK_SUFFIX = ".apk";
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
try {
zip = new ZipFile(file);
} catch (IOException ex) {
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {
}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
}</code></pre>
<p>通过调用<strong>DexPathList#makeDexElements</strong>方法,可以加载我们上面解压得到的dex文件,从代码也可以看出,<strong>DexPathList#makeDexElements</strong>其实也是通过调用<strong>DexFile#loadDex</strong>来加载dex文件并创建DexFile对象的。V14中,通过反射调用<strong>DexPathList#makeDexElements</strong>方法加载我们需要的dex文件,在把加载得到的数组扩展到ClassLoader实例的"pathList"字段,从而完成dex文件的安装。</p>
<p>从DexPathList的代码中我们也可以看出,ClassLoader是支持直接加载.dex/.zip/.jar/.apk的dex文件包的(我记得以前在哪篇日志中好像提到过类似的问题…)。</p>
<p>19 <= Build.VERSION.SDK_INT</p>
<pre><code class="java"> /**
* Installer for platform versions 19.
*/
private static final class V19 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makeDexElement", e);
}
Field suppressedExceptionsField =
findField(dexPathList, "dexElementsSuppressedExceptions");
IOException[] dexElementsSuppressedExceptions =
(IOException[]) suppressedExceptionsField.get(dexPathList);
if (dexElementsSuppressedExceptions == null) {
dexElementsSuppressedExceptions =
suppressedExceptions.toArray(
new IOException[suppressedExceptions.size()]);
} else {
IOException[] combined =
new IOException[suppressedExceptions.size() +
dexElementsSuppressedExceptions.length];
suppressedExceptions.toArray(combined);
System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
dexElementsSuppressedExceptions = combined;
}
suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
}
}
private static Object[] makeDexElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method makeDexElements =
findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
ArrayList.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
suppressedExceptions);
}
}</code></pre>
<p>V19与V14差别不大,只不过<strong>DexPathList#makeDexElements</strong>方法多了一个ArrayList<IOException>参数,如果在执行<strong>DexPathList#makeDexElements</strong>方法的过程中出现异常,后面使用反射的方式把这些异常记录进DexPathList的<strong>dexElementsSuppressedExceptions</strong>字段里面。</p>
<p>无论是V4/V14还是V19,在创建DexFile对象的时候,都需要通过DexFile的Native方法<strong>openDexFile</strong>来打开dex文件,其具体细节暂不讨论(涉及到dex的文件结构,很烦,有兴趣请阅读<a href="https://link.segmentfault.com/?enc=4TUTbDzhLA7IlbvA%2FajB1Q%3D%3D.SU662zY%2BjHrW9ibS6Hc6cSRopI0BEbeFSZJOatE7AV1byu9X8Zw5b7MlwkzQOXn21pY8TyByQ6Ok8956enzmT6ji5apqR%2BYOHHSIwGqwFjexVWSOv%2Fz5F7PxQfSAgQdL" rel="nofollow">dalvik_system_DexFile.cpp</a>),这个过程的主要目的是给当前的dex文件做Optimize优化处理并生成相同文件名的odex文件,App实际加载类的时候,都是通过odex文件进行的。因为每个设备对odex格式的要求都不一样,所以这个优化的操作只能放在安装Apk的时候处理,主dex的优化我们已经在安装apk的时候搞定了,其余的dex就是在<code>MultiDex#installSecondaryDexes</code>里面优化的,而后者也是MultiDex过程中,另外一个耗时比较多的操作。(在MultiDex中,提取出来的dex文件被压缩成.zip文件,又优化后的odex文件则被保存为.dex文件。)</p>
<p>到这里,MultiDex的工作流程就结束了。怎么样,是不是觉得和以前谈到动态加载技术(插件化)的时候说的很像?没错,谁叫它们的核心都是dex文件呢。Java老师第一节课就说“<strong>类就是编程</strong>”,搞定类你就能搞定整个世界啊!</p>
<h2>优化方案</h2>
<p>MultiDex有个比较蛋疼的问题,就是会产生明显的卡顿现象,通过上面的分析,我们知道具体的卡顿产生在<strong>解压dex文件</strong>以及<strong>优化dex</strong>两个步骤。不过好在,在Application#attachBaseContext(Context)中,UI线程的阻塞是不会引发ANR的,只不过这段长时间的卡顿(白屏)还是会影响用户体验。</p>
<p>目前,优化方案能想到的有两种。</p>
<h3>PreMultiDex方案</h3>
<p>大致思路是,在安装一个新的apk的时候,先在Worker线程里做好MultiDex的解压和Optimize工作,安装apk并启动后,直接使用之前Optimize产生的odex文件,这样就可以避免第一次启动时候的Optimize工作。</p>
<p><img src="/img/remote/1460000007764743" alt="20161204148086219213560.jpg" title="20161204148086219213560.jpg"></p>
<p>安装dex的时候,核心是创建DexFile对象并使用其Native方法对dex文件进行opt处理,同时生产一个与dex文件(.zip)同名的已经opt过的dex文件(.dex)。如果安装dex的时候,这个opt过的dex文件已经存在,则跳过这个过程,这会节省许多耗时。所以优化的思路就是,下载Apk完成的时候,预先解压dex文件,并预先触发安装dex文件以生产opt过的dex文件。这样覆盖安装Apk并启动的时候,如果MultiDex能命中解压好的dex和odex文件,则能避开耗时最大的两个操作。</p>
<p>不过这个方案的缺点也是明显的,第一次安装的apk没有作用,而且事先需要使用内置的apk更新功能把新版本的apk文件下载下来后,才能做PreMultiDex工作。</p>
<h3>异步MultiDex方案</h3>
<p>这种方案也是目前比较流行的<strong>Dex手动分包方案</strong>,启动App的时候,先显示一个简单的Splash闪屏界面,然后启动Worker线程执行MultiDex#install(Context)工作,就可以避免UI线程阻塞。不过要确保启动以及启动MultiDex#install(Context)所需要的类都在主dex里面(手动分包),而且需要处理好进程同步问题。</p>
<p>参考资料:</p>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=b4lPBPjrCoLgx2xIdvg%2BVQ%3D%3D.oP5gXto1V6PnSLFjNTeht9ZDHg35l5vDJB6G4olfaQDi1yh9GS%2B4NZnZzCq1fkh8H50hPyWxnl4cInlwcvMZOA%3D%3D" rel="nofollow">Configure Apps with Over 64K Methods</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=3x1C6fXpEarCTZ4BGgZPgQ%3D%3D.JHJ4RGj%2FGj8P5ft3Hne%2B5MXKQu8fKrThArJZudNJJertnM6KopIM%2BoKiZLebd3thxW%2FPA5s3dr2R4G3vSNvCPw%3D%3D" rel="nofollow">Google Multidex</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=xT0oi%2BDZQgqgGMOc5m7pqw%3D%3D.sTV4YJA6zSYAFmDUvOydcoaoQVfjGkgwjtvp7n7lgSIoGtGGP31rKCIxmSII706Owblu%2BLbvUzpK2QL6v%2FgiRB2Tkf8rF1X5uQjx4c%2FFmlfL4WINHA3hYXV%2FNwkSRDJT" rel="nofollow">dalvik_system_DexFile.cpp</a></p></li>
</ul>
<p><br><br>著作信息:<br>本文章出自 <a href="https://link.segmentfault.com/?enc=4kSJq9x82rqzbhnZZl6how%3D%3D.FTTC1doHcNz1kE7JffOSLgvRdP9CXG8R8LfuYo4JLVE%3D" rel="nofollow"><strong>Kaede</strong></a> 的博客,原创文章若无特别说明,均遵循 <a href="https://link.segmentfault.com/?enc=z586Sel5GWbdQbkSnEnB9g%3D%3D.boipGaLvsp5i8LDuPfHtmRLV1Ai62yD9YXbPg%2FsogKfz%2BKFr%2BjOb7CNAabtuKZDSNPIF3%2FcYnIF6kq7IMa0oRA%3D%3D" rel="nofollow"><strong>CC BY-NC 4.0</strong></a> 知识共享许可协议4.0(署名-非商用-相同方式共享),可以随意摘抄转载,但必须标明署名及原地址。</p>
使用Log的一些姿势
https://segmentfault.com/a/1190000007468683
2016-11-13T18:25:00+08:00
2016-11-13T18:25:00+08:00
Kaede
https://segmentfault.com/u/kaede
1
<p><img src="/img/remote/1460000007468686" alt="20161113147902764731467.jpg" title="20161113147902764731467.jpg"></p>
<blockquote><p>LOG 是任何一种编程语言的第一个API,通常被初学者用来打印 <code>Hello, World!</code>。 有研究显示,<br>不使用 LOG 或者使用姿势错误的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情需要书写,不能是一整张白纸。</p></blockquote>
<p>LogCat是Android开发者们最熟悉不过的日志打印工具,几乎每一个Android项目里面都包含着大量的Log相关代码。不过,或许是因为Log实在是太过于普通,所以许多人在使用它的时候就显得非常随意,这些错误的使用姿势却会在不经意间给我们带来不少的大坑。</p>
<h2>Log相关的一些问题</h2>
<h3>没有关闭调试用的LOG</h3>
<p>许多同学喜欢在开发阶段用Log输出当前的一些环境数据,用于调试代码,但是在调试完成后却忘了关闭这些Log,导致发版出去的应用里面还会继续输出这些LOG,这样不仅会造成不必要的性能丢失,也会暴露一些敏感的数据,这些都是我们不愿看到的。</p>
<p>首先,我们要给Log进行分级,规定“DEBUG版本输出哪一些级别的LOG并屏蔽哪一些级别的LOG,而RELEASE版本又输出另一些级别的LOG并屏蔽另一些级别的LOG”,这样在开发阶段能够输出我们调试需要的LOG,而同时又能保证放送的版本能够屏蔽这些敏感的LOG。但是在开发阶段我们不应该特意去注意这些细节,所以必须开发一个Log工具库,在框架层级解决这个需求。</p>
<p>同时,需要注意的是,<strong>用于作为“开启/关闭Log”的开关必须是一个常量</strong>,而不能是一个变量(使用常量的话,在编译代码的时候,如果常量为false),编译器会直接把调试部分的Log代码直接去掉,而使用变量作为开关的话,这个判断逻辑会继续保留,一方面会造成性能丢失,另一方面在运行时也可以通过Hack手段强行开启这部分Log代码。</p>
<p>另外,“开启/关闭Log”的开关必须写在Log方法外部,也就是说必须先判断“开启/关闭Log”条件,再调用Log方法,因为在调用Log方法的时候已经造成了性能丢失,而且调用方法的时候,会先构造好改方法需要的参数(按照参数顺序从右往左),再调用方法,而许多人喜欢在调用Log方法的时候计算需要打印出来的内容,这里是最容易造成性能丢失的地方。因此,如果为了图方便,写一个Log工具类,在工具类内部去判断是否应该开启或关闭Log,事实上已经造成了不少的性能丢失。正确的使用姿势应该是:</p>
<pre><code class="java">public static final boolean DEBUG = true;
if (DEBUG) {
Log.v(TAG, "log something");
}</code></pre>
<h3>在循环体内部打印LOG</h3>
<p>尽管Log造成的性能损失很小,但是如果在循环体内部循环调用Log方法的话,那总体的丢失的非常可观了,所以不应该在循环体内部使用Log,正确的做法是在循环体内部拼接需要打印的内容,等跳出循环体再一次打印出来。</p>
<p>除了常见的循环体外,还要一个需要注意的场景就是Adapter。ListView/RecyclerView是Android开发中最常用的控件,因此Adapter使用的情景也很多。滚动屏幕的时候,ListView/RecyclerView会在通过Adapter频繁地绑定ItemView和数据,而且这些都是在UI线程里进行的,所以如果在绑定的过程中调用Log,可能会造成明显的卡顿。</p>
<p>至于Log到底会丢失多少性能,一般情况下,Log的性能丢失很小,毕竟是这么常见的系统Api,肯定是身经百战,早就是“best performance”了。不过我曾经有个RecyclerView在<strong>MIUI</strong>上非常卡,一开始我是RecyclerView布局没优化好,最终定位到Adapter内部的一处Log上,卡顿的地方出现在Log的Native实现。<strong>MIUI</strong>到底对用户输出的日志做了什么处理呢?非常神奇。</p>
<h3>无法获取重要LOG内容</h3>
<p>在调试代码的时候,我们经常通过LOG来定位Bug。同理,当线上的版本出现问题的时候,我们也希望能通过LOG来定位问题所在。但是问题是用户的设备上的打印出来的LOG我们根本没有方法获取,唯一的手段就是当用户设备出现问题的时候,把设备借过来连上IDE用LogCat查看输出的LOG……显然这是不可行的。</p>
<p>这种时候,我们可以在打印重要LOG(比如重要路径的触发点、或者一些异常类的信息)的时候,一并把这些信息记录到文件里。在用户反馈系统里面,一并将这些文件上传到我们的用户反馈服务器,这样在处理反馈问题的时候,就能拿到重要的参考日志了。</p>
<h2>BLog</h2>
<p>BLog 是 Android SDK 的 LOG 工具 {<a href="/u/link">@Link</a> android.util.Log} 的加强版,以方便在开发时用来<br>操作调试日志。</p>
<h3>特点</h3>
<ol>
<li><p>简单易用的API;</p></li>
<li><p>支持输出线程信息;</p></li>
<li><p>支持设置LogLevel,方便在生产环境关闭调试用的LOG;</p></li>
<li><p>支持将LOG内容写入文件,以便通过文件LOG定位用户反馈的问题;</p></li>
</ol>
<p>注意,尽管BLog支持关闭Log的输出,但是在你调用 <code>BLog.v(String)</code> 的时候,其实已经造成了性能<br>丢失,所以请尽量使用正确的姿势来使用BLog,比如</p>
<pre><code class="java">if (BuildConfig.DEBUG) {
BLog.v(TAG, "log verbose");
}</code></pre>
<h3>Getting Started</h3>
<p>GitHub : <a href="https://link.segmentfault.com/?enc=ZaoLkSeZ5PySP%2F7iTLV8nA%3D%3D.0EtFIrWkN4S7otvoCrSTWPkjeSjcnFQ%2FcQfZ58dRhXI%3D" rel="nofollow">https://github.com/kaedea/b-log</a><br>出处 : <a href="https://link.segmentfault.com/?enc=CYsvzOykUiJsA3MSiXPtbg%3D%3D.fLRQ0yh7OMh0J19iA7Fz%2Byypz%2F%2FjYouaEBkdCn%2F17G6Sejqe9%2FR6EBa2R7NQ5jkdpGnxt74fPp%2FbKzVgPPMCKw%3D%3D" rel="nofollow">使用 Log 的正确姿势</a></p>
设计一个框架化框架 Frontia
https://segmentfault.com/a/1190000005937614
2016-07-12T00:13:57+08:00
2016-07-12T00:13:57+08:00
Kaede
https://segmentfault.com/u/kaede
1
<p><img src="/img/remote/1460000006779574" alt="" title=""></p>
<h2>设计一个框架化框架 Frontia</h2>
<p>结合动态加载系列文章的分析,现在开始设计并开发一个Android的插件化框架,命名为Frontia。Frontia有“前端”的意思,寓意着Android插件能像前端开发那样动态发版,同时,这一词出自Macross动画系列,有“繁星”的意思,“我们的征途是星辰大海 KIRA!!(<ゝω·)☆”。</p>
<p>Frontia是一个Android的插件化框架(基于ClassLoader的动态加载技术),相比其他开源项目,Frontia 的特点是扩展性强,更加专注于插件的下载、更新、安装、管理,以及插件和宿主之间的交互。在深入介绍Frontia之前,我们先想想开发一个插件化框架需要考虑的问题有哪些。</p>
<h2>满足多种业务需求的插件</h2>
<p>现在的插件化需求有许多花样,首先,有的只需要将一些特定的类(或者接口的实现类)插件化,比如一些游戏的SDK,需要把登录功能和支付功能的实现插件化,这样SDK就能实现动态升级。其次,有一些业务需要将so库给做成插件化,因为一些so库需要同时内置多个CPU类型(x86/arm64等)的版本,所以会占用非常可观的体积,如果这些so库并不是核心的业务,完全可以做成插件,等到需要的时候再动态加载。再则,也有一些相对独立的业务需要独立升级,而不希望随着APP一起发版。比如“游戏广场”这样的一个业务,APP只提供一个入口启动游戏广场,启动后接下来就不管了,这样的业务可以做成插件,插件可以动态升级(游戏广场可以自由设计自己的界面,甚至增加新的页面),也可以在多个APP之间使用同一插件业务(许多APP都有游戏广场的推广业务)。</p>
<p>考虑到种种需求,我们的插件有时只需要加载一些普通的类,有时候需要加载res资源,有时候需要加载so库,有时候需要加载新的组件类(Activity、Service等)甚至调用宿主APP的某些功能(比如获取用户账号信息)。因此,我们的插件化框架在处理插件加载的具体过程时,应该能够灵活地扩展,以满足以上以及将来的各种插件需求。</p>
<h2>插件的更新策略</h2>
<p>除了处理插件的加载问题外,插件化框架还需要处理插件的更新问题,要不然插件化开发就没有意义了。加载插件前,我们需要从服务器下载插件,或者判断是否需要从服务器下载新的插件版本,下载新版本插件失败的时候,我们有又需要判断本地是否有可用的旧版本。因此,插件化框架需要提供一个完善的插件更新策略,以从服务器的插件版本列表和本地的缓存插件版本列表中,挑选出最佳的插件版本(目标插件)。</p>
<p>当我们插件的某个版本出现严重问题的时候,我们希望所有的下载过这个版本的插件的APP都要抛弃这个插件,所以插件化框架需要有“及时吊销”功能。当我们插件的最新版本更新了某些重要的功能,我们希望所有的APP都立刻升级到这个插件版本,如果下载最新版本插件失败,需要重新下载或者直接抛弃插件,而不能使用旧版本的插件,也就是说框架需要“强制升级”功能。</p>
<h2>插件的安装策略</h2>
<p>同一个版本的插件只需要下载一次就可以了,不能重复下载。插件化框架需要将下载下来的插件需要存放到指定的目录(我们可以把这个过程当作是“安装插件”),以便于知道当前APP已经安装了哪些插件,以及这些插件有哪些版本,这样我们才可以判断需不需要从服务器下载新版本的插件。</p>
<p>同时,存放在本地文件系统上的插件是不安全的,可能被其他人恶意修改,但插件被加载进宿主APP后,它就是APP程序的一部分,可以访问APP的所有内存数据,插件化框架还需要提供对本地已安装插件的安全校验功能。</p>
<h2>插件投入生产前需要解决的问题</h2>
<p>上面谈到的问题大致可以归类成插件的更新、安装以及加载问题,这些都是插件化框架应该解决的基本问题。当然除了这些问题之外,在将插件化开发引入实际生产的项目中的时候,还有一些问题不得不考虑,比如在开发插件的时候如何快捷地调试和构建插件,当插件出现BUG的时候如何快速定位问题(因为一个插件的BUG可能是由“具体的设备型号 + 具体的宿主APP版本号 + 具体的插件版本号”导致的,这也是插件化开发的诟病,尽量不要吧频繁变动的业务插件化),如何做好数据上报统计以评估插件的工作效果,当然,必不可少的,我们还需要一个可靠的服务器来托管我们插件(理想的情景是,我们调试完把代码推到构建系统,构建系统构建完把插件入库并把插件的版本信息上传到服务器,服务器更新新插件版本的可用信息,整个过程不需要手动操作)。</p>
<p>类似之前谈到的Android动态加载技术需要解决的两个主要问题,插件化开发投入生产需要解决的问题大致可归纳如下:</p>
<ul>
<li><p>插件的更新、安装、加载策略;</p></li>
<li><p>插件的安全性校验;</p></li>
<li><p>插件与宿主的通讯(互调)方式,甚至插件间互相调用的方式;</p></li>
<li><p>插件调试和构建的方法;</p></li>
<li><p>出现BUG时定位问题的方法;</p></li>
<li><p>插件数据统计;</p></li>
<li><p>插件托管的服务器(插件的持续集成);</p></li>
</ul>
<p>一言以蔽之,插件化开发不仅仅需要解决一个开发框架的问题,从整体上来看更像是需要解决一个开发平台的问题,除了解决代码的问题(粗体部分),还需解决生产工具或者效率的问题。</p>
<p>Frontia项目致力于解决以上问题,最后,放上项目的地址:<a href="https://link.segmentfault.com/?enc=dyzXpfXpinu%2BacsRHackuw%3D%3D.sMvyfpEDRICHlDM3uXGH%2B0eOkcuSssMf2mbpx86V348xtHdiG9Jf9NojIwinASrwsmbv6NxUkR%2FFtw%2FRwGrWVLhlDHyKoLJGMx4n0HtOvKuY4Rr3d7SU%2FyN8qZYLcxZn" rel="nofollow">android-frontia</a> 。</p>
ANDROID动态加载 使用SO库时要注意的一些问题
https://segmentfault.com/a/1190000005646078
2016-06-04T22:46:10+08:00
2016-06-04T22:46:10+08:00
Kaede
https://segmentfault.com/u/kaede
5
<h2>基本信息</h2>
<ul>
<li><p>作者:<a href="https://link.segmentfault.com/?enc=cRdn3ZcH3gQo9XlW%2BUtBjA%3D%3D.zX9FFmHz1XlYCFFiUwzCwCncQ4bv3CPL5hzwFgHJfiA%3D" rel="nofollow">kaedea</a></p></li>
<li><p>项目:<a href="https://link.segmentfault.com/?enc=pEoPdlQorFxAry8Dkb4f3w%3D%3D.kfX3ES9Y4UEm%2B89zEY0h1BT61raT%2FJfv0uZvLQEa%2BHAdTIFz0q4GIwVxnuZW9Y15Ta5KYI3O5i89xIpn4NxA4A%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<h2>Android项目里的SO库</h2>
<p>正好动态加载系列文章谈到了加载SO库的地方,我觉得这里可以顺便谈谈使用SO库时需要注意的一些问题。或许这些问题对于经常和SO库开发打交道的同学来说已经是老生长谈,但是既然要讨论一整个动态加载系列,我想还是有必要说说使用SO库时的一些问题。</p>
<p>在项目里使用SO库非常简单,在 <a>加载SD卡中的SO库</a> 中也有谈到,只需要把需要用到的SO库拷贝进 <strong>jniLibs(或者Eclipse项目里面的libs)</strong> 中,然后在JAVA代码中调用 <strong>System.loadLibrary("xxx")</strong> 加载对应的SO库,就可以使用JNI语句调用SO库里面的Native方法了。</p>
<p>但是有同学注意到了,SO库文件可以随便改文件名,却不能任意修改文件夹路径,而是“armeabi”、“armeabi-v7a”、“x86”等文件夹名有着严格的要求,这些文件夹名有什么意义么?</p>
<h2>SO库类型和CPU架构类型</h2>
<p>原因很简单,不同CPU架构的设备需要用不同类型SO库(从文件名也可以猜出来个大概嘛 ╮( ̄▽ ̄")╭)。</p>
<p>记得还在学校的时候,提及ARM处理器时,老师说以后移动设备的CPU基本就是ARM类型的了。老师不曾欺我,早期的Android系统几乎只支持ARM的CPU架构,不过现在至少支持以下七种不同的CPU架构:ARMv5,ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。每一种CPU类型都对应一种ABI(Application Binary Interface),“armeabi-v7a”文件夹前面的“armeabi”指的就是ARM这种类型的ABI,后面的“v7a”指的是ARMv7。这7种CPU类型对应的SO库的文件夹名是:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。</p>
<p>不同类型的移动设备在运行APP时,需要加载自己支持的类型的SO库,不然就GG了。通过 <strong>Build.SUPPORTED_ABIS</strong> 我们可以判断当前设备支持的ABI,不过一般情况下,不需要开发者自己去判断ABI,Android系统在安装APK的时候,不会安装APK里面全部的SO库文件,而是会根据当前CPU类型支持的ABI,从APK里面拷贝最合适的SO库,并保存在APP的内部存储路径的 <strong>libs</strong> 下面。(这里说一般情况,是因为有例外的情况存在,比如我们动态加载外部的SO库的时候,就需要自己判断ABI类型了。)</p>
<blockquote><p>一种CPU架构 = 一种对应的ABI参数 = 一种对应类型的SO库</p></blockquote>
<p>到这里,我们发现使用SO库的逻辑还是比较简单的,但是Android系统加载SO库的逻辑还是给我们留下了一些坑。</p>
<h2>使用SO库时要注意的一些问题</h2>
<h3>1. 别把SO库放错地方</h3>
<p>SO库其实都是APP运行时加载的,也就是说APP只有在运行的时候才知道SO库文件的存在,这就无法通过静态代码检查或者在编译APP时检查SO库文件是否正常。所以,Android开发对SO库的存放路径有严格的要求。</p>
<p>使用SO库的时候,除了“armeabi-v7a”等文件夹名需要严格按照规定的来自外,SO库要放在项目的哪个文件夹下也要按照套路来,以下是一些总结:</p>
<ul>
<li><p>Android Studio 工程放在 <strong>jniLibs/xxxabi</strong> 目录中(当然也可以通过在build.gradle文件中的设置jniLibs.srcDir属性自己指定);</p></li>
<li><p>Eclipse 工程放在 <strong>libs/xxxabi</strong> 目录中(这也是使用ndk-build命令生成SO库的默认目录);</p></li>
<li><p>aar 依赖包中位于 <strong>jni/ABI</strong> 目录中(SO库会自动包含到引用AAR压缩包到APK中);</p></li>
<li><p>最终构建出来的APK文件中,SO库存在 <strong>lib/xxxabi</strong> 目录中(也就是说无论你用什么方式构建,只要保证APK包里SO库的这个路径没错就没问题);</p></li>
<li><p>通过 PackageManager 安装后,在小于 Android 5.0 的系统中,SO库位于 APP 的 <strong>nativeLibraryPath</strong> 目录中;在大于等于 Android 5.0 的系统中,SO库位于 APP 的 <strong>nativeLibraryRootDir/CPU_ARCH</strong> 目录中;</p></li>
</ul>
<p>既然扯到了这里,顺便说一下,我在使用 Android Studio 1.5 构建APK的时候,发现 Gradle 插件只会默认打包application类型的module的jniLibs下面的SO库文件,而不会打包aar依赖包的SO库,所以会导致最终构建出来的APK里的SO库文件缺失。暂时的解决方案是把所有的SO库都放在application模块中(这显然不是很好的解决方案),不知道这是不是Studio的BUG,同事的解决方案是通过修改Gradle插件来增加对aar依赖包的SO库的打包支持(GitHub有开源的第三方Gradle插件项目,使用Java和Groovy语言开发)。</p>
<h3>2. 尽可能提供CPU支持的最优SO库</h3>
<p>当一个应用安装在设备上,只有该设备支持的CPU架构对应的SO库会被安装。但是,有时候,设备支持的SO库类型不止一种,比如大多的X86设备除了支持X86类型的SO库,还兼容ARM类型的SO库(目前应用市场上大部分的APP只适配了ARM类型的SO库,X86类型的设备如果不能兼容ARM类型的SO库的话,大概要嗝屁了吧)。</p>
<p>所以如果你的APK只适配了ARM类型的SO库的话,还是能以兼容的模式在X86类型的设备上运行(比如华硕的平板),但是这不意味着你就不用适配X86类型的SO库了,因为X86的CPU使用兼容模式运行ARM类型的SO库会异常卡顿(试着回想几年前你开始学习Android开发的时候,在PC上使用AVD模拟器的那种感觉)。</p>
<h3>3. 注意SO库的编译版本</h3>
<p>除了要注意使用了正确CPU类型的SO库,也要注意SO库的编译版本的问题。虽然现在的Android Studio支持在项目中直接编译SO库,但是更多的时候我们还是选择使用事先编译好的SO库,这时就要注意了,编译APK的时候,我们总是希望使用最新版本的build-tools来编译,因为Android SDK最新版本会帮我们做出最优的向下兼容工作。</p>
<p>但是这对于编译SO库来说就不一样了,因为NDK平台不是向下兼容的,而是向上兼容的。应该使用app的minSdkVersion对应的版本的NDK标本来编译SO库文件,如果使用了太高版本的NDK,可能会导致APP性能低下,或者引发一些SO库相关的运行时异常,比如“UnsatisfiedLinkError”,“dlopen: failed”以及其他类型的Crash。</p>
<p>一般情况下,我们都是使用编译好的SO库文件,所以当你引入一个预编译好的SO库时,你需要检查它被编译所用的平台版本。</p>
<h3>4. 尽可能为每种CPU类型都提供对应的SO库</h3>
<p>比如有时候,因为业务的需求,我们的APP不需要支持AMR64的设备,但这不意味着我们就不用编译ARM64对应的SO库。举个例子,我们的APP只支持armeabi-v7a和x86架构,然后我们的APP使用了一个第三方的Library,而这个Library提供了AMR64等更多类型CPU架构的支持,构建APK的时候,这些ARM64的SO库依然会被打包进APK里面,也就是说我们自己的SO库没有对应的ARM64的SO库,而第三方的Library却有。这时候,某些ARM64的设备安装该APK的时候,发现我们的APK里带有ARM64的SO库,会误以为我们的APP已经做好了AMR64的适配工作,所以只会选择安装APK里面ARM64类型的SO库,这样会导致我们自己项目的SO库没有被正确安装(虽然armeabi-v7a和x86类型的SO库确实存在APK包里面)。</p>
<p>这时正确的做法是,给我们自己的SO库也提供AMR64支持,或者不打包第三方Library项目的ARM64的SO库。使用第二种方案时,可以把APK里面不需要支持的ABI文件夹给删除,然后重新打包,而在Android Studio下,则可以通过以下的构建方式指定需要类型的SO库。</p>
<pre><code class="groovy">productFlavors {
flavor1 {
ndk {
abiFilters "armeabi-v7a"
abiFilters "x86"
abiFilters "armeabi"
}
}
flavor2 {
ndk {
abiFilters "armeabi-v7a"
abiFilters "x86"
abiFilters "armeabi"
abiFilters "arm64-v8a"
abiFilters "x86_64"
}
}
}
</code></pre>
<p>需要说明的是,如果我们的项目是SDK项目,我们最好提供全平台类型的SO库支持,因为APP能支持的设备CPU类型的数量,就是项目中所有SO库支持的最少CPU类型的数量(使用我们SDK的APP能支持的CPU类型只能少于等于我们SDK支持的类型)。</p>
<h3>5. 不要通过“减少其他CPU类型支持的SO库”来减少APK的体积</h3>
<p>确实,所有的x86/x86_64/armeabi-v7a/arm64-v8a设备都支持armeabi架构的SO库,因此似乎移除其他ABIs的SO库是一个减少APK大小的好办法。但事实上并不是,这不只影响到函数库的性能和兼容性。</p>
<p>X86设备能够很好的运行ARM类型函数库,但并不保证100%不发生crash,特别是对旧设备,兼容只是一种保底方案。64位设备(arm64-v8a, x86_64, mips64)能够运行32位的函数库,但是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)。</p>
<p>过减少其他CPU类型支持的SO库来减少APK的体积不是很明智的做法,如果真的需要通过减少SO库来做APK瘦身,我们也有其他办法。</p>
<h2>减少SO库体积的正确姿势</h2>
<h3>1. 构建特定ABI支持的APK</h3>
<p>我们可以构建一个APK,它支持所有的CPU类型。但是反过来,我们可以为每个CPU类型都单独构建一个APK,然后不同CPU类型的设备安装对应的APK即可,当然前提是应用市场得提供用户设备CPU类型设别的支持,就目前来说,至少PLAY市场是支持的。</p>
<p>Gradle可以通过以下配置生成不同ABI支持的APK(引用自别的文章,没实际使用过):</p>
<pre><code class="groovy">android {
...
splits {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
universalApk true //generate an additional APK that contains all the ABIs
}
}
// map for the version code
project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]
android.applicationVariants.all { variant ->
// assign different version code for each output
variant.outputs.each { output ->
output.versionCodeOverride =
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
}
}
}</code></pre>
<h3>2. 从网络下载当前设备支持的SO库</h3>
<p>说到这里,总算回到动态加载的主题了。⊙﹏⊙</p>
<p>使用Android的动态加载技术,可以加载外部的SO库,所以我们可以从网络下载SO库文件并加载了。我们可以下载所有类型的SO库文件,然后加载对应类型的SO库,也可以下载对应类型的SO库然后加载,不过无论哪种方式,我们最好都在加载SO库前,对SO库文件的类型做一下判断。</p>
<p>我个人的方案是,存储在服务器的SO库依然按照APK包的压缩方式打包,也就是,SO库存放在APK包的 <strong>libs/xxxabi</strong> 路径下面,下载完带有SO库的APK包后,我们可以遍历libs路径下的所有SO库,选择加载对应类型的SO库。</p>
<p>具体实现代码看上去像是:</p>
<pre><code class="java">/**
* 将一个SO库复制到指定路径,会先检查改SO库是否与当前CPU兼容
*
* @param sourceDir SO库所在目录
* @param so SO库名字
* @param destDir 目标根目录
* @param nativeLibName 目标SO库目录名
* @return
*/
public static boolean copySoLib(File sourceDir, String so, String destDir, String nativeLibName) throws IOException {
boolean isSuccess = false;
try {
LogUtil.d(TAG, "[copySo] 开始处理so文件");
if (Build.VERSION.SDK_INT >= 21) {
String[] abis = Build.SUPPORTED_ABIS;
if (abis != null) {
for (String abi : abis) {
LogUtil.d(TAG, "[copySo] try supported abi:" + abi);
String name = "lib" + File.separator + abi + File.separator + so;
File sourceFile = new File(sourceDir, name);
if (sourceFile.exists()) {
LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so);
//api21 64位系统的目录可能有些不同
//copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + name);
break;
}
}
} else {
LogUtil.e(TAG, "[copySo] get abis == null");
}
} else {
LogUtil.d(TAG, "[copySo] supported api:" + Build.CPU_ABI + " " + Build.CPU_ABI2);
String name = "lib" + File.separator + Build.CPU_ABI + File.separator + so;
File sourceFile = new File(sourceDir, name);
if (!sourceFile.exists() && Build.CPU_ABI2 != null) {
name = "lib" + File.separator + Build.CPU_ABI2 + File.separator + so;
sourceFile = new File(sourceDir, name);
if (!sourceFile.exists()) {
name = "lib" + File.separator + "armeabi" + File.separator + so;
sourceFile = new File(sourceDir, name);
}
}
if (sourceFile.exists()) {
LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so);
}
}
if (!isSuccess) {
LogUtil.e(TAG, "[copySo] 安装 " + so + " 失败 : NO_MATCHING_ABIS");
throw new IOException("install " + so + " fail : NO_MATCHING_ABIS");
}
} catch (IOException e) {
e.printStackTrace();
throw e;
}
return true;
}</code></pre>
<h2>总结</h2>
<ol>
<li><p>一种CPU架构 = 一种ABI = 一种对应的SO库;</p></li>
<li><p>加载SO库时,需要加载对应类型的SO库;</p></li>
<li><p>尽量提供全平台CPU类型的SO库支持;</p></li>
</ol>
<p>题外话,SO库的使用本身就是一种最纯粹的动态加载技术,SO库本身不参与APK的编译过程,使用JNI调用SO库里的Native方法的方式看上去也像是一种“硬编程”,Native方法看上去与一般的Java静态方法没什么区别,但是它的具体实现却是可以随时动态更换的(更换SO库就好),这也可以用来实现热修复的方案,与Java方法一旦加载进内存就无法再次更换不同,Native方法不需要重启APP就可以随意更换。</p>
<p>出于安全和生态控制的原因,Google Play市场不允许APP有加载外部可执行文件的行为,一旦你的APK里被检查出有额外的可执行文件时就不好玩了,所以现在许多APP都偷偷把用于动态加载的可执行文件的后缀名换成“.so”,这样被发现的几率就降低了,因为加载SO库看上去就是官方合法版本的动态加载啊(不然SO库怎么工作),虽然这么做看起来有点掩耳盗铃。</p>
<h2>参考文章</h2>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=7o5G%2BsJ2tYBAZcl7uZj3%2FA%3D%3D.CH2oGvthQdtOFdmY7464dGeWEBWV3GIYakWxA9Hcec%2F4cT0OzyUYEBjxrg37q19d" rel="nofollow">关于Android的.so文件你所需要知道的</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=UkFxYL1z%2Bm6ewhoQ3VhRmQ%3D%3D.4TYQUERL5vsJ52IguvtiE7xuRo%2FcfZGAhfxlnFAaEV00Xp2IPpDiqjdI%2B2UPTtOu00QZNzQBopUd8FkFsd3DyQ%3D%3D" rel="nofollow">Android-Plugin-Framework</a></p></li>
</ul>
Android动态加载的类型
https://segmentfault.com/a/1190000005113493
2016-05-13T00:00:19+08:00
2016-05-13T00:00:19+08:00
Kaede
https://segmentfault.com/u/kaede
0
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-5-8/68653221.jpg" alt="" title=""></p>
<h3>基本信息</h3>
<ul>
<li><p>Author:<a href="https://link.segmentfault.com/?enc=EWN3P4LXqe03kYKx1IxOMA%3D%3D.dGcGh%2B4%2BLMgftlefwIFdQC%2Fy38fapgq%2BO0oJ5Z7siMs%3D" rel="nofollow">kaedea</a></p></li>
<li><p>GitHub:<a href="https://link.segmentfault.com/?enc=EV%2BDhT8hq%2FE1pRvr9%2Fcocg%3D%3D.DHVKESS%2BKncWaUfMPgzQOUNlkmM778SdcxNBwHN%2FjmBy7o3fTVjVMqm%2B54b7QGI%2B5Ecz2kraMugo6sdlb9IZ8A%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<p>现在网络上有许多关于动态加载的介绍的文章,谈及的关键词汇有动态加载、插件化、热部署、热修复等,对于一些刚接触这方面开发技术的人来说,可能容易混淆。</p>
<p>虽然我在动态加载系列的文章中或多或少有谈到这些概念的区别,但是我觉得认识这些区别对于使用动态加载技术还是挺重要的,所以特别开这个新的文章进行分析。</p>
<h3>动态加载的类型</h3>
<p>无论是插件化、热部署还是热修复,这些技术的根源都可是说是动态加载,这也是我把“动态加载”作为这个系列文章主题的原因。</p>
<p>动态加载,就是在程序运行时,加载外部的可执行文件并运行。这里的运行时就是指应用冷启动并开始工作后;外部可以是可以是SD卡,可以是data目录,也可以是jniLib目录,这些可执行文件是没有随着应用一起编译的。</p>
<p>Android的动态加载按照工作机制的不同,可以分为虚拟机层动态加载和Native层动态加载两大类。</p>
<h4>运行在虚拟机</h4>
<p>简单来说就是只用JAVA代码搞定的类型。</p>
<p>基于虚拟机的动态加载技术的核心是类加载器ClassLoader,通过它我们能够加载一些新的类,这种方式也是目前大部分技术文章谈到的加载方式。其中,根据ClassLoader使用方式的不同,又演变出“热部署”、“插件化”、“热修复”等技术。</p>
<h5>热部署</h5>
<p>加载外部可执行文件的ClassLoader实例与原APP的ClassLoader实例是互相独立的(不在同一棵代理树上),加载进来的新的类与原APP(宿主)里存在的类互相独立,根据Java对类的定义,因为这些类的ClassLoader不同,所以他们即便包名和类名一致,或者有继承关系,他们也属于不懂的类。所以以这种方式加载进来的类与原有的类不能互通,不能污染宿主原有的类,适合用来动态加载一些独立的业务,比如一些推广的游戏,在宿主上提供一个入口,用户不需要安装游戏就能运行。因为这种方式起到不用安装就能部署游戏的作用,所以称为热部署。</p>
<h5>插件化</h5>
<p>加载外部可执行文件的ClassLoader实例与宿主的ClassLoader实例不是互相独立的,用宿主的ClassLoader加载过的类就无法从外部可执行文件中再次加载,它们可以共用一个公共库,习惯上把外部可执行文件称为插件。插件里可以存放公共库里一些借口的实现类,可以有一些新的Activity或者Service等组件,可以把一些宿主里的业务挪到插件中,插件可以自主升级,不用随着宿主APP发版。</p>
<h5>热修复</h5>
<p>在使用插件化技术的同时,也可以使用插件中的新的类来替换宿主同名的类,这样就能修复宿主中原有的类存在的BUG。相比插件化,热修复因为不需要考虑组件和res资源的问题,所以相对简单得许多,要保证插件种新的类的加载要在加载宿主中原有类的之前。</p>
<h5>拆分DEX</h5>
<p>相信大家都知道打包DEX时65536方法数超标问题,也就是一个DEX只能有65536个方法,因此有了multi-dex的解决方案,把本来只有一个的DEX,拆分成复数以上的DEX,运行时挨个加载进来,这其实也算是一种动态加载,只不过实现过程对开发者是透明的。</p>
<p>除此之外,还有另一种拆分DEX是用于减少冷启动的时间的。冷启动是指应用第一次从用户点击到完成初始化工作的全部过程。随着现在APP的体积不断增长,一些APP的DEX文件十分庞大,APP在启动的时候,单单加载所有的DEX文件就需要非常多的耗时,所以用户点击APP的时候会有一个明显的卡顿过程。因此有一种拆分DEX的方案是“拆分一个启动闪屏用的DEX,里面只存放启动闪屏界面需要用到的类,因此非常小,其他类放到其他DEX里面”,启动的时候因为只需要加载闪屏的DEX,所以非常快,APP进入闪屏后,通过异步任务去完成其他DEX的加载,就能消除卡顿的过程。</p>
<p>第一种拆分DEX是官方支持的,开发者只需要打开multi-dex功能即可;第二种拆分DEX则需要开发者自己设计。</p>
<p>基于ClassLoader的动态加载都有个共同的特点,就是新的类一旦加载进内存了,就无法再次替换了,所以无法在运行时候升级功能,需要重启APP才能生效。</p>
<h4>运行在Native</h4>
<p>有另一种动态加载方式是工作在Native层的,相比于ClassLoader,在Native层的动态加载不需要重新启动APP就能生效,这类的加载有 加载SO库 和 基于JNI HOOK 的热修复。</p>
<h5>加载SO库</h5>
<p>加载SO库是最常见的Native动态加载,我们项目经常中使用SO库,编译APP的时候,SO并不会参与编译,会原封不动被拷贝到APK包里的lib目录下,安装APK的时候,系统会扫描lib文件夹下支持当前设备CPU类型(比如arm或x86)的SO库(APK包会带有多种CPU类型对应的SO库,安装的时候只需要对应类型的)并拷贝到系统安装目录,APP在运行时可以调用 <code>System#loadLibrary</code> 方法动态加载对应的SO库,此外还可以调用 <code>System#load</code> 加载指定路径上的SO库。</p>
<p>现在的APK里面往往带有非常多的SO库,而APP运行时只需要用到对应CPU类型的SO库,因此把SO库从APK包里剥离出来也是APK瘦身的有效手段。</p>
<h5>JNI HOOK</h5>
<p>基于JNI HOOK的热修复技术的代表框架有阿里巴巴的 <strong>AndFix</strong>。Android中,修复BUG的方式就是更新类的方法,和ClassLoader通过加载新的类来更换方法的实现的想法一样,<strong>AndFix</strong> 也是通过更换方法的做法来实现热修复,不过做法比较取巧。Android中执行Native方法的时候,会去SO库中查找对应的C/C++方法,而 <strong>AndFix</strong> 先把普通Java方法用Native方法代替,再通过更换不同SO库还更换Native方法的实现。</p>
阅读Android源码的一些姿势
https://segmentfault.com/a/1190000004426348
2016-02-11T00:26:30+08:00
2016-02-11T00:26:30+08:00
Kaede
https://segmentfault.com/u/kaede
5
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-2-9/47582414.jpg" alt="" title=""></p>
<p>前面吐槽了 <a href="https://segmentfault.com/a/1190000004426339">有没有必要阅读Android源码</a>,后面觉得只吐槽不太好,还是应该多少弄点干货。</p>
<h2>日常开发中怎么阅读源码</h2>
<h3>找到正确的源码</h3>
<p>IDE是日常经常用的东西,Eclipse就不说了,直接从Android Studio(基于IntelliJ Community版本改造)开始。</p>
<p>我们平时的Android项目,都是要依赖Android SDK里对应API Level的android.jar包(而且是以Provided的形式依赖),这样才能使用Android提供的API。在IntelliJ中,当想要看具体类的源码的时候,如果Android SDK里对应API Level的Source包有下载的话,IDE会打开对应的Source包;如果还没有下载,IDE会把对应API Level的android.jar包反编译成Java代码,这个规则对于一些第三方的开源项目也一样。推荐下载Source源码,毕竟反编译的Java代码不可能完全和源码的时候一样,有时候反编译出来的代码的执行逻辑可能完全等价,但是可阅读性下降了不好,而且也少了一些重要的注释。</p>
<p>定位具体源码的时候,可以通过“Ctrl+鼠标左键”来查看,也可以通过“双击Shift”,在查找框里输入目标类的名字来定位。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-2-9/89744598.jpg" alt="" title=""></p>
<p>如上图,第一个类就是API23的NinePatchDrawable的源码,第二个就是通过android.jar反编译而来的,这里记得把“Include non-project items”勾上。</p>
<h3>关于SDK自带的源码和隐藏API</h3>
<p>Android SDK自带的Source源码包很小,并没有包括所有的Android Framework的源码,仅仅提供给应用开发参考用,一些比较少用的系统类的源码并没有给出,所以有时候你会看到如下。</p>
<pre><code class="Java">public class BaseDexClassLoader extends ClassLoader {
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
throw new RuntimeException("Stub!");
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new RuntimeException("Stub!");
}
protected URL findResource(String name) {
throw new RuntimeException("Stub!");
}
...
}</code></pre>
<p>“RuntimeException("Stub!")”表示实际运行时的逻辑会由Android ROM里面相同的类代替执行。</p>
<p>此外,在IDE里看源码的时候,有时候一些方法或者类会出现报红(找不到)的情况,如下。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-2-9/89825596.jpg" alt="" title=""></p>
<p>这是因为这些方法或者类是被Android SDK隐藏的,出于安全或者某些原因,这些API不能暴露给应用层的开发者,所以编译完成的android.jar包里会把这些API隐藏掉,而我们的Android项目是依赖android.jar的,查看源码的时候,IDE会自动去android.jar找对应的API,自然会找不到。当然,这些API在ROM中是实际存在的,有些开发者发现了一些可以修改系统行为的隐藏API,在应用层通过反射的方式强行调用这些API执行系统功能,这种手段也是一种HACK。</p>
<h3>Google的AOSP项目</h3>
<p>当你需要的源码在Android SDK Source中找不到的时候,就有必要去<a href="https://link.segmentfault.com/?enc=RpaD4XvyEIPXM%2BE%2BBcrwmw%3D%3D.L%2BwTN1YCXRmtxOdr6ZKdgC0V5vCitdUqFOjEqc2ruHogaU7d5yoCdrNJWsl2%2BBiB" rel="nofollow">AOSP</a>(Android Open Source Project)项目里面找了。</p>
<p>不过AOSP项目包括整个Android所有开源的东西,实在是太庞大了,对于一般开发者来说,我们只需要接触Framework层次的东西就够了,这里包括了base、build-tools、support包甚至Volley项目的源码。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-2-9/53056079.jpg" alt="" title=""></p>
<p>以base为例,进入base目录,能看到base项目的git仓库,左边是其所有的分支。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-2-9/70376864.jpg" alt="" title=""></p>
<p>进入“master/core/java/android/”路径就能看到熟悉的Package目录了,其他分支以及项目都类似。有必要的时候可以把整个AOSP项目Clone下来,大概20G左右,可以把项目导入到IDE里面,这样就更方便查看源码了,另外可以可是试着编译自己的Android ROM,只需要部署能够跑MakeFile命令的环境就好。</p>
<h3>一些辅助阅读的工具</h3>
<h4>Chrome扩展</h4>
<p>Android SDK Search<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-2-9/73121661.jpg" alt="" title=""></p>
<p>相信平时到 <a href="https://link.segmentfault.com/?enc=EpEvBGmiX%2FegT2ZB2a4nmg%3D%3D.4dYvLt6vozGILjk3GPB%2B6Wd5NjE8zS4Xfe4e27gEWv%2FEP93KJZvQzhjJes4R6TB9Ic8nlTdqwOtCyBgsQtw3Hw%3D%3D" rel="nofollow">Android开发者官网</a> 查看API说明的人不少,这个扩展可以在API类名旁边显示一个跳转链接,用于跳转到AOSP中对应的类的源码,方便查看源码。</p>
<h4>Source Insight</h4>
<p>在这个工具帮助下,你才可以驾驭巨大数量的Android 源码,你可以从容在Java,C++,C代码间遨游,你可以很快找到你需要的继承和调用关系。</p>
<h4>VPN梯子</h4>
<p>善用梯子是开发者基本的自我修养之一,如果你不想做“面向百度编程”的话。</p>
<p>我在一开始是使用免费的GoAgent和WallProxy,但是经常要更新,而且最近还要经常替换IP才能工作,所以后来我换成了收费的ShadowSocks,各个平台都有客户端,非常方便,我也在手机上常态用起了Google的全家桶,只不过最近老是更换服务器地址,而且部分服务器不稳定,非常担心商家是否准备捞一笔然后跑路,23333。</p>
<p>我觉得比起折腾找免费的低速、不稳定的梯子,还是用一些稳定的收费的的梯子比较划算。至于具体的用梯子的姿势,请诸位自行搜索,这里随便贴个介绍 <a href="https://link.segmentfault.com/?enc=o2t8oliIcNvZuYFiRfuYGw%3D%3D.w7ZOkRCwqhvfBIu12y%2BKcGV5%2FdYxDO2Ubk6jxeRUvChqLfsbgJB6fdjLFOG%2BhDex%2FmqJrnJJEJhdu1DkhrUVWSHePQVhU2%2FHbnDBxerLO%2B0akpGVNwVB480BopRRT0EA371l%2F0sN9ZBnRvX5kphW%2BeMd%2BzdBgug6t1DS8L1PDbHQUXLiA4Lx16zvUfLCOI8o" rel="nofollow">梯子使用总结</a>。</p>
<h2>推荐阅读的源码</h2>
<p>AOSP项目这么庞大,就算是Framework部分也有够看上一阵子的,所以推荐从常用的看起,由浅及深,同时向横向和纵向深入阅读。</p>
<h3>开始</h3>
<p><strong>Handler-Message-Looper</strong><br> Handler被称为“异步提交器”,是Android开发入门教程必定谈及的东西,这也是Activity等组件的工作机制需要用到的东西,是“数据驱动”框架的重要组成,作为阅读源码的入门最适合不过。</p>
<p><strong>Activity和Service</strong><br><br>作为经常使用到的组件,阅读其源码的花费和带来的技术提高的性价比肯定是最高的,Service可以不看,但是Activity总不能少吧。</p>
<p><strong>Fragment</strong><br><br>还在认为Fragment是一个视图吗,还在认为FragmentActivity的界面有多个Fragment组成吗,看看Fragment和FragmentManager吧,了解下生命周期的本质到底是什么。</p>
<p><strong>View</strong><br><br>想自定义高级的View类吗,那总得知道onMeasure/onLayout/onDraw这些方法是怎么被调用的,了解LayoutParams是怎么工作的,知道调用requestLayout和Invalidate的时候有什么区别。</p>
<p><strong>MotionEvent</strong><br><br>在懂的怎么自定义高级的View后,只能向用户显示界面,还得知道怎么与用户交互才能做出华丽的UI。所以必须知道TouchEvent的分发和拦截的工作机制,起码也得知道其特点,才不会一直在困扰“为什么无法监听用户的触摸事件”、“View之间的触摸事件冲突了”或者“View的滑动与点击事件冲突了”之类的问题。</p>
<p><strong>LayoutInflator</strong><br><br>布局渲染器也是开发Android UI的时候经常用到的,不过LayoutInflator实例的创建方式有好几种,你至少得知道其之间的区别。还有,LayoutInflator在渲染指定布局的时候,有container和attachToRoot等参数,阅读源码后很快能了解其区别。</p>
<p><strong>SurfaceView和TextureView</strong><br><br>阅读完View的工作机制后,就能理解为什么View在绘制复杂的UI效果时效率这么低,这时候就需要SurfaceView和TextureView了。理解双缓冲对UI更新效率的帮助,了解SurfaceView在视图叠加的时候的缺陷,了解TextureView在Android Lollipop之前的内容窜台BUG,才能用正确姿势使用这俩。</p>
<p><strong>AsyncTask</strong><br><br>异步任务也是Android开发经常遇到的问题,相比自己从Thread和Handler写起,被称为“异步任务大师”的AsyncTask类自然更受到许多小伙伴的喜欢。不过AsyncTask在早期的Android版本中差别甚大,需要做大量的适配工作,而且特别容易引起异步任务引用着组件的实例导致内存泄露从而引发OOM问题,所以不推荐直接使用AsyncTask类,不过强烈推荐阅读AsyncTask的源码学习Google优秀的异步任务设计理念。此外,如果真的要使用AsyncTask,不要直接使用系统提供的AsyncTask类,AsyncTask本身就是一个单一的Java类,没有耦合其他系统类,推荐自己从最新的Android版本中复制一份AsyncTask类的代码,自己维护,在项目中当做Support包一样使用,以规避其兼容性问题。</p>
<p><strong>Volley</strong><br><br>这个强烈推荐,是Google官方的异步任务框架,没有随Android发布,需要自己在Framework里下载代码。Volley的中文意思就是“并发”,阅读其源码能让你见识到原来异步任务框架也能写得这么低耦合和高扩扩展,其用“生产者-消费者”模式来处理异步请求的框架会让人拍案叫绝。此外,Volley框架是用于处理Http任务和Image加载任务,但是其优秀的异步控制思想也能运用与File、Sqlite等耗时任务的处理,当你能够自己写出类似Volley框架的代码时,说明你的Android技术已经有所突破。</p>
<p><strong>android.util.</strong>*<br><br>“android.util.*” 包名下有许多优秀的实用类,大多是作为Java自带类的补充,比如数据结构类的SparseArray、ArrayMap、ArraySet,用于加密的Base64,用于处理屏幕分辨率自适应的DisplayMetrics和TypedValue,用于时间换算的TimeUtils,以及用于内存缓存的LruCache,熟悉这些类对Android开发非常有帮助,也会让代码显得成熟。</p>
<h3>进阶</h3>
<p><strong>Context</strong><br><br>阅读Context源码能帮助我们了解其工作机制,了解Google是怎么在Java代码上添加Android特性的,了解Android是怎么保存和获取res资源的,了解ContextWrapper和Activity这些Context有什么区别,了解Context设计的装饰者模式(Description Pattern)。</p>
<p><strong>ClassLoader</strong><br><br>类加载器ClassLoader是Android虚拟机工作的基础,了解其“双亲代理模式”能让你更好的了解系统的类和你写的类是怎么工作的。Multi-Dex和ART模式也和ClassLoader的工作机制息息相关。</p>
<p><strong>Binder</strong><br><br>Binder是Android上RPC(Remote Procedure Call Protocol)的实现,Android系统许多功能就是居于Binder实现的,平时应用层对Binder的使用大多是在于和Service通讯的时候,不过,当我们需要使用AIDL功能的时候,就需要接触到Binder了。(推荐阅读原理即可,反正C++驱动层我是看不下去了)</p>
<p><strong>WMS,AMS,PMS,NMS,IMS等系统Service</strong><br><br>SystemServer是Android的Framework层工作的核心,Android系统启动过程包含从Linux内核加载到Home应用程序启动的整个过程。SystemServer是Zygnote孵化的第一个进程,这个进程会启动许多Framework层功能需要用到的线程,比如用于管理窗口的WindowManagerService,用于管理Activity的ActivityManagerService,用于管理APK包信息的PackageManagerService,用于管理网络的NetworkManager,用于处理用户触摸的InputManagerService等,这些系统Service提供了APP运行时需要的大多系统功能,大多使用“stub-server”的模式进行交互,而且有大量的JNI的调用。这部分的源码比较适合从事ROM开发的人阅读,应用层的开发基本不会用到,但是这方面的只是能让我们对Android Framework层的工作机制有个大抵的认识。(非常惭愧,这部分我自己看了几次,还是没能产生融会贯通的感觉,整体的认识还是比较模糊,希望继续跟着老罗的博客,捡捡肉吃)</p>
<h3>第三方开源项目</h3>
<p><strong>EventBus</strong><br><br>Android上的一个“订阅者-发布者”模式的实现框架,非常适合业务多而且经常变动的项目,能够有效预防“接口爆炸”,现在基本上中型以上的项目都会采用类似的框架。</p>
<p><strong>OTTO</strong><br><br>同上,只不过实现的具体方案不一样,而且OTTO相比EventBus来,比较小巧,代码也比较简练,非常适合处女座的开发者食用。</p>
<p><strong>RxJava</strong><br><br>相比起上面两个,RxJava可以说是把异步的思想发挥到了极致,RxJava的兴起代表了Android开发中响应式编程的崛起,同样非常适合业务多而且经常变动的项目,只不过相比传统的基于接口的开发方式,RxJava框架的开发方式会有点难以适应,特别是团队开发的时候。</p>
<p><strong>Guava</strong><br><br>这个其实也是Google自己开源的,提供了许多优秀的Java工具类,比如“one to one mapping”的Bimap,有时候一些工具类Android或Java自带的库没有提供,或许我们可以先参考Guava的。</p>
<p>以上是我自己个人推荐阅读的源码,不过每个开发者自身的兴趣和侧重点都不一样,有兴趣的参考着看就是。同时,如果有一些有趣的系统类,随时欢迎推荐给我。</p>
<h2>站在巨人的肩膀上阅读</h2>
<p>学习一个系统最好的方法就是“Read The Fucking Source Code”,坏消息是AOSP项目是在太庞大太难消化了,好消息就是现在已经有不少先驱,我们或许可以站在他们的肩膀上阅读。</p>
<p><a href="https://link.segmentfault.com/?enc=KNi3EYa2T6UnHOyZ30IECg%3D%3D.q9yY7blT9Ks9oyerg2Xq81ogGYr19Az0qihA4by70aU6%2BRDkUFmnTW6gmAmDtFIm" rel="nofollow">AOSP官方的介绍</a><br><br>项目介绍, 代码下载, 环境搭建, 刷机方法, Eclipse配置都在这里,这是一切的基础。</p>
<p><a href="https://link.segmentfault.com/?enc=SMM28AFxdO%2BQw0ckU8qiRA%3D%3D.%2BBntypxn6n7zbGpONg6QG7nR2w1Pq3xhFHfdraiWvGEeAIqZsbyx7d3ifVw624ImO1aw7o72rFHcbA3JXjgPNg%3D%3D" rel="nofollow">官方教程</a> 和 <a href="https://link.segmentfault.com/?enc=GnFkKTtXgEKcvEJa0FeOUw%3D%3D.gtU6WofnPT1janiTZX5KZRZYttZVAe%2FLIeZUSrmt0meHkcS4AEGTEhkRXP8bUBjf" rel="nofollow">官方博客</a><br><br>这个其实是给App开发者看的,但是里面也有不少关于系统机制的介绍, 值得细读。而官方博客经常有一些开发者容易疏忽的姿势的讨论,比如“Bitmap数据的回收问题”,推荐阅读。</p>
<p><a href="https://link.segmentfault.com/?enc=Zm2yLwm25YXEisL6E07eQw%3D%3D.AE90jWpjjatPb59ZmskWd7nbJf3mlomwioEKBVSGUiJx3zcGjmTI5O3PmZjRdKfV" rel="nofollow">Android Issues</a><br><br>Android官方Issue列表,记录一些系统BUG,别掉坑里了。</p>
<p><a href="https://link.segmentfault.com/?enc=rzY7l9ioRRST7nEjIKgPcg%3D%3D.OsbgLGoFjlmmxNr3Qod5X0ztkRVXKlNyZwbW8lbGpI0roz1ms1Di%2FIkKkcANT6e%2F" rel="nofollow">老罗的Android之旅</a><br><br>此老罗非彼老罗,罗升阳老师的博客非常有营养,基本可以作为指引你开始阅读AOSP源码的教程。你可以按照博客的时间顺序一篇篇挑需要的看。但这个系列的博客有些问题:早期的博客是基于旧版本的Android;<br>大量的代码流程追踪。读文章时你一定要清楚你在看的东西在整个系统处于什么样的位置。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-2-9/2827740.jpg" alt="" title=""></p>
<p>同时推荐老罗的这本书,平时看博客就可以,无聊的时候,比如在动车上可以把这本书翻翻。(非常优秀的书,不过据本人描述,这本书稿费还抵不会出版费)</p>
<p><a href="https://link.segmentfault.com/?enc=GUTno8z9DShAf5O0ynPX7w%3D%3D.OQZ2A9%2FtutllY2YtgZxuXPVb4cIoYBk8C7kuh%2FFDFXs%3D" rel="nofollow">Innost的专栏</a><br><br>邓凡平老师也是为Android大牛, 博客同样很有营养。但是不像罗升阳老师的那么系统, 更多的是一些技术点的深入探讨。</p>
<h2>阅读时的姿势</h2>
<p>现在的问题是:当你拿到一份几G的源码,该从哪里开始呢?</p>
<blockquote><p>著作权归作者所有。<br><br>商业转载请联系作者获得授权,非商业转载请注明出处。<br></p></blockquote>
<p>作者:墨小西<br><br>链接:<a href="https://link.segmentfault.com/?enc=%2FrhbwyrJVnts2Yj2uPKQew%3D%3D.4TGPmUpzCfaAKNWiUnfgJPaiGJmIX0BrgT%2BBMsAZCASRyLr1leiC%2BGUpgApsDvSORbqRfxcmy2pkiDRgPsB%2FnA%3D%3D" rel="nofollow">https://www.zhihu.com/question/19759722/answer/17019083</a> <br><br>来源:知乎</p>
<h3>宏观上看,Android源码分为功能实现上的纵向,和功能拓展上的横向。</h3>
<p>在阅读源码时需要把握好着两个思路。譬如你需要研究音频系统的实现原理,纵向:你需要从一个音乐的开始播放追踪,一路下来,你发现解码库的调用,共享内存的创建和使用,路由的切换,音频输入设备的开启,音频流的开始。譬如你要看音频系统包括哪些内容,横向:通过Framework的接口,你会发现,音频系统主要包括:放音,录音,路由切换,音效处理等。</p>
<h3>Android的功能模块绝大部分是C/S架构</h3>
<p>你心里一定需要有这个层级关系,你需要思路清晰地找到Server的位置,它才是你需要攻破的城,上面的libraries是不是很亲切的样子?看完它长成啥样后,然后你才能发现HAL和Kernel一层层地剥离。很多研究源码的同学兜兜转转,始终在JAVA层上,这是不科学的,要知道libraries才是它的精髓啊。</p>
<h3>Android的底层是Linux Kernel。</h3>
<p>在理解上面两点之后,还是需要对Kernel部分有个简单的理解,起码你要熟悉kernel的基础协议吧!你要能看懂电路图吧!你要熟悉设备的开启和关闭吧!你要熟悉调寄存器了吧!这方面的书太多了,我建议根据实例去阅读,它并不复杂,不需要一本本厚书来铺垫。在libraries和kernel间,可能还会有个HAL的东东,其实它是对kernel层的封装,方便各个硬件的接口统一。这样,如果我换个硬件,不用跑了长得很复杂的libraries里面改了,kernel调试好了后,改改HAL就好了。</p>
<p>好了,你现在是不是跃跃欲试准备去找个突破口准备进攻了,但是好像每个宝库的入口都挺难找了我大概在三个月前阅读完Android UI系统的源码,这是Android最复杂的部分,我要简单说下过程。我需要先找宝库入口,我要研究UI,首先要找什么和UI有亲戚关系吧!View大神跳出来了,沿着它往下找找看,发现它在贴图在画各种形状,但是它在哪里画呢,马良也要纸吧?很明显它就是某个宝藏,但是世人只是向我们描述了它有多美,却无人知在哪里?我们需要找一张地图罗。开发Android的同学逃不掉Activity吧!它有个setcontentview的方法,从这个名字看好像它是把view和activity结合的地方。赶紧看它的实现和被调用,然后我们就发现了Window,ViewRoot和WindowManager的身影,沿着WM和WMS我们就惊喜会发现了Surface,以及draw的函数,它居然在一个DeCorView上画东西哈。借助Source Insight, UI Java层的横向静态图呼之欲出了。完成这个静态UML,我觉得我可以开始功能实现上追踪了,这部分主要是C++的代码(这也是我坚定劝阻的放弃Eclipse的原因),我沿着draw函数,看到了各个层级的关系,SurfaceSession的控制和事务处理,SharedBuffer读写控制,彪悍的SurfaceFlinger主宰一切,OpenGL ES的神笔马良。FrameBuffer和FrameBufferDevice的图像输出,LCD设备打开后,开始接收FBD发过来的一帧帧图像,神奇吧。</p>
<h2>参考文献</h2>
<p><a href="https://link.segmentfault.com/?enc=%2BbwZeBMIGIukOD%2BpGiiMtQ%3D%3D.jDjScLlz8WmTqL80Ns2U9Kg4y9RR9WyDuYWTRzsxn%2BxEp4VPhHjaZVFCc1jJxUNz" rel="nofollow">知乎:大牛们是怎么阅读 Android 系统源码的?</a></p>
有没有必要阅读Android源码
https://segmentfault.com/a/1190000004426339
2016-02-11T00:23:54+08:00
2016-02-11T00:23:54+08:00
Kaede
https://segmentfault.com/u/kaede
1
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/16-2-8/20986738.jpg" alt="" title=""></p>
<p>或许对于许多Android开发者来说,所谓的Android工程师的工作“不过就是用XML实现设计师的美术图,用JSON解析服务器的数据,再把数据显示到界面上”就好了,源码什么的,看也好不看也罢,反正应用层的开发用不上,再加上现在优秀的轮子越来越多,拿来主义泛滥,能用就是,反正老板也不关心是不是你自己写的,用我现在老大的话来说,阅读源码似乎只是一种“锦上添花”的事,有自然好,没有也罢。</p>
<p>那么,作为Android开发者的自我修养,到底有没有必要阅读AOSP以及其他开源项目的源码呢?</p>
<h2>刚开始时候的故事</h2>
<p>对于我来说,选择编程是因为我看见了 <a href="https://link.segmentfault.com/?enc=nX2t3HO1YDMRVo7ort311Q%3D%3D.SyaL%2FpKMb%2BRfdJ8w1T6o3tkpM0gdHJU4B9D%2F4d01eKA%3D" rel="nofollow">MoeLoader</a> 这款收图应用实在是漂亮才开始写代码,我要的目的只是应用漂亮,不用在乎代码写成什么样,而且我觉得代码是我写的,这么辛苦的作品可不能白白开源给别人看。所以对于这个时候的我,那时候虽然没有考虑过类似的问题,但是很可能觉得阅读源码是没有必要的。</p>
<p>后来我开始学习Android,原因非常简单,C#根本无法找到合适的工作,而学生党的我根本无法买得起苹果三套件,此外,IE6的兼容工作让我实在是对前端敬而远之,所以选择只剩下Android了。说实在的,一开始我是不太喜欢Android开发,特别是IDE从Visual Studio切换到万恶的Eclipse,丑,卡顿,动不动就找不到依赖,甚至有时候编译一直报红Error,定位了半天找不到问题,到最后把红色的Error删除掉后居然就编译通过了!这时候的我,别说阅读源码了,我只求同一份代码在运行的时候有同样的逻辑就好。</p>
<p>再到后来,我已经有一些Android程序设计的经验了,IDE也开始换到Android Studio(Preview版本刚出来的时候,我在Android Studio和Eclipse之间切换过好几次,不得不说习惯这种东西有时很有帮助,有时候也会很可怕),换到Android Studio很大一个原因是因为Github上面许多开源项目用Android Studio来部署很方便。这个时候我接触的开源项目已经比较多了,许多时候一些开源项目总有一些BUG,我会给其提交ISSUE,不过更多时候我不能等项目所有者来解决,需要我自己解决BUG;许多时候开源项目并不能直接满足业务的需要,所以我也需要先阅读源码再改造成自己的项目能用的。</p>
<p>这里需要特别说明的是,我的第一份工作的项目是一个SDK项目,整体使用了基于ClassLoader的动态加载的框架。那时候还比较早,国外对动态加载不感兴趣,国内的话也只有零星的技术博客对这有讨论,不过大多是介绍如何实现动态加载而没有分析其工作机制。所以,当有新的技术需求,或者项目出现BUG的时候,我都需要自己阅读源码去解决问题。比如,有一次设计师需要一个全圆角的菜单背景,然而Android的点九图是X轴和Y轴都需要拉伸的,当Y轴拉伸的时候就无法实现全圆角。我能做的就是,先把点九图的原图等比缩放到Y轴填满,这样Y轴就不会被拉伸了,但是原图缩放后,点九图X轴的拉伸却出现了扭曲的样式。通过阅读NinePatchDrawable的源码,我发现点九图的原理就是一个普通的Drawable加上一个用于描述拉伸坐标的数组chunk,当我缩放Drawable的时候,也必须更新chunk,不然拉伸的坐标就对不上,后面通过阅读源码中关于chunk的描述,把对应的拉伸坐标更新后,<strong>全圆角的点九图</strong> 也就实现了。</p>
<h2>为什么要阅读源码</h2>
<p>说了这么多,到底有没有必要阅读源码?有必要,而且非常有必要!原因有三。</p>
<h3>其一,了解基层,高层才能更好地工作。</h3>
<p>比如,了解View的绘制过程,了解TouchEvent的分发和拦截过程的细节,才能写出酷炫的UI,要不然,只知道大概的原理的话,你可能要在“无法接收到触摸事件”或者“滑动事件和点击事件冲突”的这些问题上折腾半天。</p>
<p>又比如,如果哪里出现异常,你能快速定位到源码抛异常类的地方,就能快速解决BUG,对症下药,一招撂倒,有些时候,修复BUG的时间不是用在解决问题上,而是用在定位问题上。</p>
<p>这里有必要提一下,当Logcat把异常的栈信息打印出来的时候,有些异常出现的原因并不真的是Logcat的信息里描述的原因,因为Logcat里的异常的信息也只是由系统源码打印出来的,而这些源码大多时候只是普通的Java代码,和你自己写的没什么区别,如果源码抛出异常的代码的逻辑不够严谨的话,那实际的异常和Logcat里描述的异常可能对不上。比如之前搞动态加载的时候,在使用LayoutInflator渲染一个外部的XML布局时,抛了一个“Class not found”的异常,我要渲染的类可是LinearLayout啊,怎么可能没有!定位到源码里才发现,这里只要是类渲染失败就会抛这个异常,再定位到具体抛异常的地方,发现实际是Dimens资源找不到,困扰半年的问题立刻解决。</p>
<h3>其二,能够理解Android设计者的意图。</h3>
<p>这个描述可能不好,比如说,许多人都觉得Android开发其实就是Java开发,通过阅读Context类的设计,你能够理解Google是如何在Java的基础上加上Android的特性的,你能够理解Context被叫做“环境”的原因。此外,阅读Activity/Service的源码,你能理解到四大组件类明明就是普通的JAVA类,为什么他们就是组件而别的类就不是组件。阅读Handler/Message/Looper的源码,你还能理解到Handler的精髓,数据驱动比事件驱动更适合用于设计需要经常改动的框架。阅读源码,你能知道Android是怎么管理Window以及向控制View的触摸事件的,你能知道基本上所有的res资源都有等价的Java代码的实现方式,你还能知道Dalvik是怎么无缝向ART过度的,在看通的那一瞬间,保证你觉得“水可载舟,亦可赛艇”!</p>
<h3>其三,能够学习优秀开源项目的代码风格和设计理念。</h3>
<p>这也是最重要的,看多了源码之后,你会发现所谓的源码也不过是普通的的Java代码,在不知不觉中受到这些优秀设计思想的影响。相信许多人在看 <strong>Volley</strong> 源码此前,对异步任务控制的想法基本就是毫无想法,看完之后简直是醍醐灌顶,原来代码也能写得这么有魅力,再看看自己之前写的异步任务,“new Thread().start”...,简直是“too young, sometime naive”有没有。</p>
<p>看了越来越多Android的源码,自己的写应用的时候,也就能写出更加“Best Performance”的代码,见识了越来越多的开源项目,自己也能够更容易找到最符合自己应用的框架和技术方案,学习了越来越多的优秀的代码风格,自己也就更能写出漂亮以及容易扩展的代码。</p>
<p>或许对许多做Android开发来说,平时的工作就是按照设计的图写个布局,再解析后台的数据,下班了把测试用的安卓机扔进抽屉拿出自己的苹果手机…… 但有时候花点时间看看源码,或许会觉得设计代码还是挺有意思的,特别是,当你花了两天的时间构思代码,再花两天的时间写代码,这时你可能觉得你还有许多代码要写,但是突然发现只要把你写好的接口衔接一下就都完成了,而且写了两天的代码居然一次编译通过!更甚,产品突然改了个需求,你在抱怨了一顿后发现只要花10分钟把原来的接口换个实现就搞定了,这或许是程序员工作中为数不多的乐趣吧。</p>
在XML布局里给View设置点击事件
https://segmentfault.com/a/1190000004184718
2015-12-22T23:18:30+08:00
2015-12-22T23:18:30+08:00
Kaede
https://segmentfault.com/u/kaede
0
<p>给一个View设置监听点击事件是再普通不过的事情,比如</p>
<pre><code class="java"> view.setOnClickListener(onClickListener);</code></pre>
<p>另外一种做法是直接在XML布局里面指定View点击时候的回调方法,首先需要在Activity中编写用于回调的方法,比如</p>
<pre><code class="java"> public void onClickView(View view){
// do something
}</code></pre>
<p>然后在XML设置View的<code>android:onClick</code>属性</p>
<pre><code class="xml"> <View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onClick="onClickView" /></code></pre>
<p>有的时候从XML布局里直接设定点击事件会比较方便(特别是在写DEMO项目的时候),这种做法平时用的人并不多,从使用方式上大致能猜出来,View应该是在运行的时候,使用反射的方式从Activity找到“onClickView”方法并调用,因为这种做法并没有用到任何接口。</p>
<p>接下来,我们可以从源码中分析出View是怎么触发回调方法的。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-12-22/30732155.jpg" alt="" title=""><br>View有5个构造方法,第一个是内部使用的,平时在Java代码中直接创建View实例用的是第二种方法,而从XML布局渲染出来的View实例最后都是要调用第五种方法。</p>
<pre><code class="java">public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
……
// 处理onClick属性
case R.styleable.View_onClick:
if (context.isRestricted()) {
throw new IllegalStateException("The android:onClick attribute cannot "
+ "be used within a restricted context");
}
final String handlerName = a.getString(attr);
if (handlerName != null) {
// 给当前View实例设置一个DeclaredOnClickListener监听器
setOnClickListener(new DeclaredOnClickListener(this, handlerName));
}
break;
}
}
}</code></pre>
<p>处理onClick属性的时候,先判断View的Context是否isRestricted,如果是就抛出一个IllegalStateException异常。看看isRestricted方法</p>
<pre><code class="java"> /**
* Indicates whether this Context is restricted.
*
* @return {@code true} if this Context is restricted, {@code false} otherwise.
*
* @see #CONTEXT_RESTRICTED
*/
public boolean isRestricted() {
return false;
}</code></pre>
<p>isRestricted是用于判断当前的Context实例是否出于被限制的状态,按照官方的解释,处限制状态的Context,会忽略某些特点的功能,比如XML的某些属性,很明显,我们在研究的<code>android:onClick</code>属性也会被忽略。</p>
<blockquote><p>a restricted context may disable specific features. For instance, a View associated with a restricted context would ignore particular XML attributes.</p></blockquote>
<p>不过isRestricted方法是Context中为数不多的有具体实现的方法(其余基本是抽象方法),这里直接返回false,而且这个方法只有在ContextWrapper和MockContext中有重写<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-12-22/51841820.jpg" alt="" title=""></p>
<pre><code class="java">public class ContextWrapper extends Context {
Context mBase;
@Override
public boolean isRestricted() {
return mBase.isRestricted();
}
}
public class MockContext extends Context {
@Override
public boolean isRestricted() {
throw new UnsupportedOperationException();
}
}</code></pre>
<p>ContextWrapper中也只是代理调用mBase的isRestricted,而MockContext是写单元测试的时候才会用到,所以这里的isRestricted基本只会返回false,除非使用了自定义的ContextWrapper并重写了isRestricted。<br>回到View,接着的<code>final String handlerName = a.getString(attr);</code>其实就是拿到了<code>android:onClick="onClickView"</code>中的“onClickView”这个字符串,接着使用了当前View的实例和“onClickView”创建了一个DeclaredOnClickListener实例,并设置为当前View的点击监听器。</p>
<pre><code class="java">/**
* An implementation of OnClickListener that attempts to lazily load a
* named click handling method from a parent or ancestor context.
*/
private static class DeclaredOnClickListener implements OnClickListener {
private final View mHostView;
private final String mMethodName;
private Method mMethod;
public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
mHostView = hostView;
mMethodName = methodName;
}
@Override
public void onClick(@NonNull View v) {
if (mMethod == null) {
mMethod = resolveMethod(mHostView.getContext(), mMethodName);
}
try {
mMethod.invoke(mHostView.getContext(), v);
} catch (IllegalAccessException e) {
throw new IllegalStateException(
"Could not execute non-public method for android:onClick", e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(
"Could not execute method for android:onClick", e);
}
}
@NonNull
private Method resolveMethod(@Nullable Context context, @NonNull String name) {
while (context != null) {
try {
if (!context.isRestricted()) {
return context.getClass().getMethod(mMethodName, View.class);
}
} catch (NoSuchMethodException e) {
// Failed to find method, keep searching up the hierarchy.
}
if (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
} else {
// Can't search up the hierarchy, null out and fail.
context = null;
}
}
final int id = mHostView.getId();
final String idText = id == NO_ID ? "" : " with id '"
+ mHostView.getContext().getResources().getResourceEntryName(id) + "'";
throw new IllegalStateException("Could not find method " + mMethodName
+ "(View) in a parent or ancestor Context for android:onClick "
+ "attribute defined on view " + mHostView.getClass() + idText);
}
}</code></pre>
<p>到这里就清楚了,当点击View的时候,DeclaredOnClickListener实例的“onClick”方法会被调用,接着会调用“resolveMethod”方法,使用反射的方式从View的Context中找一个叫“onClickView”方法,这个方法有一个View类型的参数,最后再使用反射调用该方法。要注意的是,“onClickView”方法必须是public类型的,不然反射调用时会抛出IllegalAccessException异常。</p>
<p>同时从源码也能看出,使用<code>android:onClick</code>设置点击事件的方式是从Context里面查找回调方法的,所以如果对于在Fragment的XML里创建的View,是无法通过这种方式绑定Fragment中的回调方法的,因为Fragment自身并不是一个Context,这里的View的Context其实是FragmentActivity,这也意味着使用这种方式能够快速地从Fragment中回调到FragmentActivity。</p>
<p>此外,从DeclaredOnClickListener类的注释也能看出<code>android:onClick</code>的功能,<strong>主要是起到懒加载的作用</strong>,只有到点击View的时候,才会知道哪个方法是用于点击回调的。</p>
<p>最后,特别需要补充说明的是,使用<code>android:onClick</code>给View设置点击事件,就意味着要在Activity里添加一个非接口的public方法。现在Android的开发趋势是“不要把业务逻辑写在Activity类里面”,这样做有利于项目的维护,防止Activity爆炸,所以尽量不要在Activity里出现非接口、非生命周期的public方法。因此,贸然使用<code>android:onClick</code>可能会“污染”Activity。</p>
Android动态加载技术 系列索引
https://segmentfault.com/a/1190000004086213
2015-12-02T20:24:18+08:00
2015-12-02T20:24:18+08:00
Kaede
https://segmentfault.com/u/kaede
7
<h2>Android Dynamical Loading</h2>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-11-30/1635252.jpg" alt="android-dynamical-loading" title="android-dynamical-loading"></p>
<p>大家新年好,最近花了点时间,慢慢把这个系列的内容稍微调整了下。<br>Last Edit: 2016-2-10</p>
<h3>基本信息</h3>
<ul>
<li><p>Author:<a href="https://link.segmentfault.com/?enc=DoMcimrpNZrhXMueEd0R2w%3D%3D.kQT%2BBosLwHylf1%2FxRz7dAV8gTuuHriliBOu6xjhMhdA%3D" rel="nofollow">Kaedea</a></p></li>
<li><p>GitHub:<a href="https://link.segmentfault.com/?enc=f5ubuaWTut98SSVAVnNhVg%3D%3D.RTa8ys%2FAfJRTkLaIZOXeiGrk9tHCliV7XyWRe8SJ9FpAJoZ%2FgFLaeGBTuyIhS%2B6eVlIRdqR2N2TKoGR288RN2Q%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<h3>动态加载介绍</h3>
<p>在Android开发中采用动态加载技术,可以达到不安装新的APK就升级APP功能的目的,可以用来到达快速发版的目的,也可以用来修复一些紧急BUG。</p>
<p>现在使用得比较广泛的动态加载技术的核心一般都是使用 <strong>ClassLoader</strong> ,后者能够加载程序外部的类(已编译好的),从而达到升级代码逻辑的目的。虽然动态加载的核心原理比较简单,但是根据功能的复杂程度,实际在Android项目中使用的时候还要涉及许多其他方面的知识,这里分为几个篇幅分别进行介绍。</p>
<h5>简单易懂的介绍</h5>
<p>内容:</p>
<ol>
<li><p>动态加载技术在Android中的使用背景;</p></li>
<li><p>Android的动态的加载大致可以分为“加载SO库”和“加载DEX/JAR/APK”两种;</p></li>
<li><p>动态加载的基础是类加载器ClassLoader;</p></li>
<li><p>使用动态加载的三种模式;</p></li>
<li><p>采用动态加载的作用与代价;</p></li>
<li><p>除了ClassLoader之外的动态修改代码的技术(HotFix);</p></li>
<li><p>一些动态加载的开源项目;</p></li>
</ol>
<p>地址:<a href="http://segmentfault.com/a/1190000004062866">Android动态加载技术 简单易懂的介绍</a><br><br></p>
<h5>类加载器ClassLoader的工作机制</h5>
<p>内容:</p>
<ol>
<li><p>类加载器ClassLoader的创建过程和加载类的过程;</p></li>
<li><p>ClassLoader的双亲代理模式;</p></li>
<li><p>DexClassLoader和PathClassLoader之间的区别;</p></li>
<li><p>使用ClassLoader加载外部类需要注意的一些问题;</p></li>
<li><p>自定义ClassLoader(Hack开发)</p></li>
</ol>
<p>文章地址:<a href="http://segmentfault.com/a/1190000004062880">Android动态加载基础 ClassLoader的工作机制</a><br><br></p>
<h5>加载SD卡的SO库</h5>
<p>内容:</p>
<ol>
<li><p>如何编译和使用SO库;</p></li>
<li><p>分析Android中加载SO库相关的源码;</p></li>
<li><p>如何加载SD卡中的SO库(也是动态加载APK需要解决的问题);</p></li>
</ol>
<p>地址:<a href="http://segmentfault.com/a/1190000004062899">Android动态加载补充 加载SD卡的SO库</a><br><br></p>
<h5>简单的动态加载模式</h5>
<p>内容:</p>
<ol>
<li><p>如何创建我们需要的dex文件;</p></li>
<li><p>如何加载dex文件里面的类;</p></li>
<li><p>动态加载dex文件在ART虚拟机的兼容性问题;</p></li>
</ol>
<p>地址:<a href="http://segmentfault.com/a/1190000004062952">Android动态加载入门 简单加载模式</a><br><br></p>
<h5>代理Activity的模式</h5>
<p>内容:</p>
<ol>
<li><p>如何启动插件APK中没有注册的Activity</p></li>
<li><p>代理Activity模式开源项目“dynamic-load-apk”</p></li>
</ol>
<p>地址:<a href="http://segmentfault.com/a/1190000004062972">Android动态加载进阶 代理Activity模式</a><br><br></p>
<h5>动态创建Activity的模式</h5>
<p>内容:</p>
<ol>
<li><p>如何在运行时动态创建一个Activity;</p></li>
<li><p>自定义ClassLoader并偷梁换柱替换想要加载的类;</p></li>
<li><p>动态创建Activity模式开源项目“android-pluginmgr”</p></li>
<li><p>代理模式与动态创建类模式的区别;</p></li>
</ol>
<p>地址:<a href="http://segmentfault.com/a/1190000004077469">Android动态加载黑科技 动态创建Activity模式</a></p>
<h5>还未发布的内容</h5>
<ol>
<li><p>使用“环境注入”的模式;</p></li>
<li><p>使用动态加载技术的情形;</p></li>
<li><p>使用动态加载方式项目的项目结构调整和开发调试方式;</p></li>
<li><p>开源项目“Android-Frontia”,动态加载框架的项目,专注于“插件化”和“宿主与插件之间的通讯”;</p></li>
</ol>
Android动态加载黑科技 动态创建Activity模式
https://segmentfault.com/a/1190000004077469
2015-12-01T16:40:08+08:00
2015-12-01T16:40:08+08:00
Kaede
https://segmentfault.com/u/kaede
4
<h2>基本信息</h2>
<ul>
<li><p>Author:<a href="https://link.segmentfault.com/?enc=LcbJ9C%2BC1nHzh36%2BJnv6tw%3D%3D.m%2FDuMkSga7fCHjBwyoV0YNnP3PakFaNQy9XfGYC6OIA%3D" rel="nofollow">kaedea</a></p></li>
<li><p>GitHub:<a href="https://link.segmentfault.com/?enc=ezCWVID6YpvYAldu40NN9Q%3D%3D.mvxUQjaa2%2BozdXQJwsvkz%2BQYbNtyPVAVdNAHl0IPMOHWvBiPOAHo9bjdSB4eGGoXFIfNq0Wn8j%2FopmXQFdHSaw%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<h2>代理Activity模式的限制</h2>
<p>还记得我们在代理Activity模式里谈到启动插件APK里的Activity的两个难题吗,由于插件里的Activity没在主项目的Manifest里面注册,所以无法经历系统Framework层级的一系列初始化过程,最终导致获得的Activity实例并没有生命周期和无法使用res资源。</p>
<p>使用代理Activity能够解决这两个问题,但是有一些限制</p>
<ol>
<li><p>实际运行的Activity实例其实都是ProxyActivity,并不是真正想要启动的Activity;</p></li>
<li><p>ProxyActivity只能指定一种LaunchMode,所以插件里的Activity无法自定义LaunchMode;</p></li>
<li><p>不支持静态注册的BroadcastReceiver;</p></li>
<li><p>往往不是所有的apk都可作为插件被加载,插件项目需要依赖特定的框架,还有需要遵循一定的"开发规范";</p></li>
</ol>
<p>特别是最后一个,无法直接把一个普通的APK作为插件使用。怎么避开这些限制呢?插件的Activity不是标准的Activity对象才会有这些限制,使其成为标准的Activity是解决问题的关键,而要使其成为标准的Activity,则需要在主项目里注册这些Activity。</p>
<p>总不能把插件APK所有的Activity都事先注册到主项目里面吧,想到代理模式需要注册一个代理的ProxyActivity,那么能不能在主项目里<strong>注册一个通用的Activity</strong>(比如TargetActivity)给插件里所有的Activity用呢?解决对策就是,在需要启动插件的某一个Activity(比如PlugActivity)的时候,动态创建一个TargetActivity,新创建的TargetActivity会继承PlugActivity的所有共有行为,而这个TargetActivity的包名与类名刚好与我们事先注册的TargetActivity一致,我们就能以标准的方式启动这个Activity。</p>
<h2>动态创建Activity模式</h2>
<p>运行时动态创建并编译一个Activity类,这种想法不是天方夜谭,动态创建类的工具有<a href="https://link.segmentfault.com/?enc=anAYzhQdALuRgRGLDx621g%3D%3D.ltEO6KwWz6R8rJz4ee0f5CekNl0p9%2F9WgLBd%2BsUVHQbf1n7URnv465sWlAX0hkK5" rel="nofollow">dexmaker</a>和<a href="https://link.segmentfault.com/?enc=rWK024x7O4LLvprRaMQnvg%3D%3D.DehgpbSq0JvOioauTPUaJofT8yTYrMpEPcQOOGhreeg%3D" rel="nofollow">asmdex</a>,二者均能实现动态字节码操作,最大的区别是前者是创建dex文件,而后者是创建class文件。</p>
<h2>使用dexmaker动态创建一个类</h2>
<p>运行时创建一个编译好并能运行的类叫做“动态字节码操作(runtime bytecode manipulation)”,使用dexmaker工具能创建一个dex文件,之后我们再反编译这个dex看看创建出来的类是什么样子。</p>
<pre><code class="java">public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onMakeDex(View view){
try {
DexMaker dexMaker = new DexMaker();
// Generate a HelloWorld class.
TypeId<?> helloWorld = TypeId.get("LHelloWorld;");
dexMaker.declare(helloWorld, "HelloWorld.generated", Modifier.PUBLIC, TypeId.OBJECT);
generateHelloMethod(dexMaker, helloWorld);
// Create the dex file and load it.
File outputDir = new File(Environment.getExternalStorageDirectory() + File.separator + "dexmaker");
if (!outputDir.exists())outputDir.mkdir();
ClassLoader loader = dexMaker.generateAndLoad(this.getClassLoader(), outputDir);
Class<?> helloWorldClass = loader.loadClass("HelloWorld");
// Execute our newly-generated code in-process.
helloWorldClass.getMethod("hello").invoke(null);
} catch (Exception e) {
Log.e("MainActivity","[onMakeDex]",e);
}
}
/**
* Generates Dalvik bytecode equivalent to the following method.
* public static void hello() {
* int a = 0xabcd;
* int b = 0xaaaa;
* int c = a - b;
* String s = Integer.toHexString(c);
* System.out.println(s);
* return;
* }
*/
private static void generateHelloMethod(DexMaker dexMaker, TypeId<?> declaringType) {
// Lookup some types we'll need along the way.
TypeId<System> systemType = TypeId.get(System.class);
TypeId<PrintStream> printStreamType = TypeId.get(PrintStream.class);
// Identify the 'hello()' method on declaringType.
MethodId hello = declaringType.getMethod(TypeId.VOID, "hello");
// Declare that method on the dexMaker. Use the returned Code instance
// as a builder that we can append instructions to.
Code code = dexMaker.declare(hello, Modifier.STATIC | Modifier.PUBLIC);
// Declare all the locals we'll need up front. The API requires this.
Local<Integer> a = code.newLocal(TypeId.INT);
Local<Integer> b = code.newLocal(TypeId.INT);
Local<Integer> c = code.newLocal(TypeId.INT);
Local<String> s = code.newLocal(TypeId.STRING);
Local<PrintStream> localSystemOut = code.newLocal(printStreamType);
// int a = 0xabcd;
code.loadConstant(a, 0xabcd);
// int b = 0xaaaa;
code.loadConstant(b, 0xaaaa);
// int c = a - b;
code.op(BinaryOp.SUBTRACT, c, a, b);
// String s = Integer.toHexString(c);
MethodId<Integer, String> toHexString
= TypeId.get(Integer.class).getMethod(TypeId.STRING, "toHexString", TypeId.INT);
code.invokeStatic(toHexString, s, c);
// System.out.println(s);
FieldId<System, PrintStream> systemOutField = systemType.getField(printStreamType, "out");
code.sget(systemOutField, localSystemOut);
MethodId<PrintStream, Void> printlnMethod = printStreamType.getMethod(
TypeId.VOID, "println", TypeId.STRING);
code.invokeVirtual(printlnMethod, null, localSystemOut, s);
// return;
code.returnVoid();
}
}</code></pre>
<p>运行后在SD卡的dexmaker目录下找到刚创建的文件“Generated1532509318.jar”,把里面的“classes.dex”解压出来,然后再用“dex2jar”工具转化成jar文件,最后再用“jd-gui”工具反编译jar的源码。<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-12-1/15858284.jpg" alt="" title=""></p>
<p>至此,已经成功在运行时创建一个编译好的类。</p>
<h2>修改需要启动的目标Activity</h2>
<p>接下来的问题是如何把需要启动的、在Manifest里面没有注册的PlugActivity换成有注册的TargetActivity。</p>
<p>在Android,虚拟机加载类的时候,是通过ClassLoader的loadClass方法,而loadClass方法并不是final类型的,这意味着我们可以创建自己的类去继承ClassLoader,以重载loadClass方法并改写类的加载逻辑,在需要加载PlugActivity的时候,偷偷把其换成TargetActivity。</p>
<p>大致思路如下</p>
<pre><code class="java">public class CJClassLoader extends ClassLoader{
@override
public Class loadClass(String className){
if(当前上下文插件不为空) {
if( className 是 TargetActivity){
找到当前实际要加载的原始PlugActivity,动态创建类(TargetActivity extends PlugActivity )的dex文件
return 从dex文件中加载的TargetActivity
}else{
return 使用对应的PluginClassLoader加载普通类
}
}else{
return super.loadClass() //使用原来的类加载方法
}
}
}</code></pre>
<p>这样就能把启动插件里的PlugActivity变成启动动态创建的TargetActivity。</p>
<p>不过还有一个问题,主项目启动插件Activity的时候,我们可以替换Activity,但是如果在插件Activity(比如MainActivity)启动另一个Activity(SubActivity)的时候怎么办?插件时普通的第三方APK,我们无法更改里面跳转Activity的逻辑。其实,从主项目启动插件MainActivity的时候,其实启动的是我们动态创建的TargetActivity(extends MainActivity),而我们知道Activity启动另一个Activity的时候都是使用其“startActivityForResult”方法,所以我们可以在创建TargetActivity时,重写其“startActivityForResult”方法,让它在启动其他Activity的时候,也采用动态创建Activity的方式,这样就能解决问题。</p>
<h2>动态创建Activity开源项目 <a href="https://link.segmentfault.com/?enc=HzTUfgli0eRHjtJaHUt64Q%3D%3D.e30g36yV%2F56vAa4GtUYatHFI3l7OmCULUGej795mqRQpohTP%2BFB%2BXAcBO15p4xm8" rel="nofollow">android-pluginmgr</a>
</h2>
<p>这种脑洞大开的动态加载思路来自于<strong>houkx</strong>的开源项目<strong>android-pluginmgr</strong></p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-12-1/6260656.jpg" alt="" title=""></p>
<p><code>android-pluginmgr;</code>项目中有三种ClassLoader,一是用于替换宿主APK的Application的<code>CJClassLoader</code>,二是用于加载插件APK的<code>PluginClassLoader</code>,再来是用于加载启动插件Activity时动态生成的PlugActivity的dex包的<code>DexClassLoader</code>(存放在Map集合<code>proxyActivityLoaderMap</code>里面)。其中<code>CJClassLoader</code>是<code>PluginClassLoader</code>的Parent,而<code>PluginClassLoader</code>又是第三种<code>DexClassLoader</code>的Parent。</p>
<p>ClassLoader类加载Class的时候,会先使用Parent的<code>ClassLoader</code>,但Parent不能完成加载工作时,才会调用Child的<code>ClassLoader</code>去完成工作。</p>
<blockquote><p>java.lang.ClassLoader</p></blockquote>
<p>Loads classes and resources from a repository. One or more class loaders are installed at runtime. These are consulted whenever the runtime system needs a specific class that is not yet available in-memory. Typically, class loaders are grouped into a tree where child class loaders delegate all requests to parent class loaders. Only if the parent class loader cannot satisfy the request, the child class loader itself tries to handle it.</p>
<p>具体分析请参考 <a href="http://segmentfault.com/a/1190000004062880">Android动态加载基础 ClassLoader的工作机制</a>。</p>
<p>所以每加载一个Activity的时候都会调用到最上级的<code>CJClassLoader</code>的<code>loadClass</code>方法,从而保证启动插件Activity的时候能顺利替换成PlugActivity。当然如何控制着三种ClassLoader的加载工作,也是<code>pluginmgr</code>项目的设计难度之一。</p>
<h2>存在的问题</h2>
<p>动态类创建的方式,使得注册一个通用的Activity就能给多给Activity使用,对这种做法存在的问题也是明显的</p>
<ol>
<li><p>使用同一个注册的Activity,所以一些需要在Manifest注册的属性无法做到每个Activity都自定义配置;</p></li>
<li><p>插件中的权限,无法动态注册,插件需要的权限都得在宿主中注册,无法动态添加权限;</p></li>
<li><p>插件的Activity无法开启独立进程,因为这需要在Manifest里面注册;</p></li>
<li><p>动态字节码操作涉及到Hack开发,所以相比代理模式起来不稳定;</p></li>
</ol>
<p>其中不稳定的问题出现在对Service的支持上,使用动态创建类的方式可以搞定Activity和Broadcast Receiver,但是使用类似的方式处理Service却不行,因为“ContextImpl.getApplicationContext” 期待得到一个非ContextWrapper的context,如果不是则继续下次循环,目前的Context实例都是wrapper,所以会进入死循环。</p>
<p>据<strong>houkx</strong>称他现在有另外的思路实现“启动为安装的普通第三方APK”的目的,而且不是基于动态类创建的原理,期待他的开源项目的更新。</p>
<h2>代理Activity模式与动态创建Activity模式的区别</h2>
<p>简单地说,最大的不同是代理模式使用了一个<strong>代理的Activity</strong>,而动态创建Activity模式使用了一个<strong>通用的Activity</strong>。</p>
<p>代理模式中,使用一个代理Activity去完成本应该由插件Activity完成的工作,这个代理Activity是一个标准的Android Activity组件,具有生命周期和上下文环境(ContextWrapper和ContextCompl),但是它自身只是一个空壳,并没有承担什么业务逻辑;而插件Activity其实只是一个普通的Java对象,它没有上下文环境,但是却能正常执行业务逻辑的代码。代理Activity和不同的插件Activity配合起来,就能完成不同的业务逻辑了。所以代理模式其实还是使用常规的Android开发技术,只是在处理插件资源的时候强制调用了系统的隐藏API,因此这种模式还是可以稳定工作和升级的。</p>
<p>动态创建Activity模式,被动态创建出来的Activity类是有在主项目里面注册的,它是一个标准的Activity,它有自己的Context和生命周期,不需要代理的Activity。</p>
Android动态加载进阶 代理Activity模式
https://segmentfault.com/a/1190000004062972
2015-11-29T01:34:35+08:00
2015-11-29T01:34:35+08:00
Kaede
https://segmentfault.com/u/kaede
9
<h2>基本信息</h2>
<ul>
<li><p>作者:<a href="https://link.segmentfault.com/?enc=CRz4TeNhIg8aVwJebQ9xjA%3D%3D.fIaSfYlYEBvg0xQ9MEl7Vwf4arpbGiK2WGLLjYWv1jg%3D" rel="nofollow">kaedea</a></p></li>
<li><p>项目:<a href="https://link.segmentfault.com/?enc=48uJZAEIpBhqGUEkTrPJoQ%3D%3D.iGdTVZ41GC%2FRgyJQfNRxeASKQIT9L2dYVm%2FeSNP3Ap56Oyb1zWQWwX%2FHT0U0j5wUy%2FVLd9nM%2B0vS55PhlhKgKA%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<h2>技术背景</h2>
<p>简单模式中,使用ClassLoader加载外部的Dex或Apk文件,可以加载一些本地APP不存在的类,从而执行一些新的代码逻辑。但是使用这种方法却不能直接启动插件里的Activity。</p>
<h2>启动没有注册的Activity的两个主要问题</h2>
<p>Activity等组件是需要在Manifest中注册后才能以标准Intent的方式启动的(如果有兴趣强烈推荐你了解下Activity生命周期实现的机制及源码),通过ClassLoader加载并实例化的Activity实例只是一个普通的Java对象,能调用对象的方法,但是它没有生命周期,而且Activity等系统组件是需要Android的上下文环境的(Context等资源),没有这些东西Activity根本无法工作。</p>
<p>使用插件APK里的Activity需要解决<strong>两个问题</strong>:</p>
<ol>
<li><p>如何使插件APK里的Activity具有生命周期;</p></li>
<li><p>如何使插件APK里的Activity具有上下文环境(使用R资源);</p></li>
</ol>
<p>代理Activity模式为解决这两个问题提供了一种思路。</p>
<h2>代理Activity模式</h2>
<p>这种模式也是我们项目中,继“简单动态加载模式”之后,第二种投入实际生产项目的开发方式。</p>
<p>其主要特点是:主项目APK注册一个代理Activity(命名为ProxyActivity),ProxyActivity是一个普通的Activity,但只是一个空壳,自身并没有什么业务逻辑。每次打开插件APK里的某一个Activity的时候,都是在主项目里使用标准的方式启动ProxyActivity,再在ProxyActivity的生命周期里同步调用插件中的Activity实例的生命周期方法,从而执行插件APK的业务逻辑。</p>
<blockquote><p>ProxyActivity + 没注册的Activity = 标准的Activity</p></blockquote>
<p>下面谈谈代理模式是怎么处理上面提到的两个问题的。</p>
<h3>处理插件Activity的生命周期</h3>
<p>目前还真的没什么办法能够处理这个问题,一个Activity的启动,如果不采用标准的Intent方式,没有经历过Android系统Framework层级的一系列初始化和注册过程,它的生命周期方法是不会被系统调用的(除非你能够修改Android系统的一些代码,而这已经是另一个领域的话题了,这里不展开)。</p>
<p>那把插件APK里所有Activity都注册到主项目的Manifest里,再以标准Intent方式启动。但是事先主项目并不知道插件Activity里会新增哪些Activity,如果每次有新加的Activity都需要升级主项目的版本,那不是本末倒置了,不如把插件的逻辑直接写到主项目里来得方便。</p>
<p>那就绕绕弯吧,生命周期不就是系统对Activity一些特定方法的调用嘛,那我们可以在主项目里创建一个ProxyActivity,再由它去代理调用插件Activity的生命周期方法(这也是代理模式叫法的由来)。用ProxyActivity(一个标准的Activity实例)的生命周期同步控制插件Activity(普通类的实例)的生命周期,同步的方式可以有下面两种:</p>
<ul>
<li><p>在ProxyActivity生命周期里用反射调用插件Activity相应生命周期的方法,简单粗暴。</p></li>
<li><p>把插件Activity的生命周期抽象成接口,在ProxyActivity的生命周期里调用。另外,多了这一层接口,也方便主项目控制插件Activity。</p></li>
</ul>
<p>这里补充说明下,Fragment自带生命周期,用Fragment来代替Activity开发可以省去大部分生命周期的控制工作,但是会使得界面跳转比较麻烦,而且Honeycomb以前没有Fragment,无法在API11以前的系统使用。</p>
<h3>在插件Activity里使用R资源</h3>
<p>使用代理的方式同步调用生命周期的做法容易理解,也没什么问题,但是要使用插件里面的res资源就有点麻烦了。简单的说,res里的每一个资源都会在R.java里生成一个对应的Integer类型的id,APP启动时会先把R.java注册到当前的上下文环境,我们在代码里以R文件的方式使用资源时正是通过使用这些id访问res资源,然而插件的R.java并没有注册到当前的上下文环境,所以插件的res资源也就无法通过id使用了。</p>
<p>这个问题困扰了我们很久,一开始的项目急于投入生产,所以我们索性抛开res资源,插件里需要用到的新资源都通过纯Java代码的方式创建(包括XML布局、动画、点九图等),蛋疼但有效。知道网上出现了解决这一个问题的有效方法(一开始貌似是在手机QQ项目中出现的,但是没有开源所以不清楚,在这里真的佩服这些对技术这么有追求的开发者)。</p>
<p>记得我们平时怎么使用res资源的吗,就是“getResources().getXXX(resid)”,看看“getResources()”</p>
<pre><code class="java">@Override
public Resources getResources() {
if (mResources != null) {
return mResources;
}
if (mOverrideConfiguration == null) {
mResources = super.getResources();
return mResources;
} else {
Context resc = createConfigurationContext(mOverrideConfiguration);
mResources = resc.getResources();
return mResources;
}
}</code></pre>
<p>看起来像是通过mResources实例获取res资源的,在找找mResources实例是怎么初始化的,看看上面的代码发现是使用了super类ContextThemeWrapper里的“getResources()”方法,看进去</p>
<pre><code class="java">Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
@Override
public Resources getResources()
{
return mBase.getResources();
}</code></pre>
<p>看样子又调用了Context的“getResources()”方法,看到这里,我们知道Context只是个抽象类,其实际工作都是在ContextImpl完成的,赶紧去ContextImpl里看看“getResources()”方法吧</p>
<pre><code class="java">@Override
public Resources getResources() {
return mResources;
}</code></pre>
<p>…………<br>……<br>你TM在逗我么,还是没有mResources的创建过程啊!啊,不对,mResources是ContextImpl的成员变量,可能是在构造方法中创建的,赶紧去看看构造方法(这里只给出关键代码)。</p>
<pre><code class="JAVA">resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo);
mResources = resources;</code></pre>
<p>看样子是在ResourcesManager的“getTopLevelResources”方法中创建的,看进去</p>
<pre><code class="JAVA"> Resources getTopLevelResources(String resDir, String[] splitResDirs,
String[] overlayDirs, String[] libDirs, int displayId,
Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
Resources r;
AssetManager assets = new AssetManager();
if (libDirs != null) {
for (String libDir : libDirs) {
if (libDir.endsWith(".apk")) {
if (assets.addAssetPath(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
}
}
}
}
DisplayMetrics dm = getDisplayMetricsLocked(displayId);
Configuration config ……;
r = new Resources(assets, dm, config, compatInfo);
return r;
}</code></pre>
<p>看来这里是关键了,看样子就是通过这些代码从一个APK文件加载res资源并创建Resources实例,经过这些逻辑后就可以使用R文件访问资源了。具体过程是,获取一个AssetManager实例,使用其“addAssetPath”方法加载APK(里的资源),再使用DisplayMetrics、Configuration、CompatibilityInfo实例一起创建我们想要的Resources实例。</p>
<p>最终访问插件APK里res资源的关键代码如下</p>
<pre><code class="java"> try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration()); </code></pre>
<p>注意,有的人担心从插件APK加载进来的res资源的ID可能与主项目里现有的资源ID冲突,其实这种方式加载进来的res资源并不是融入到主项目里面来,主项目里的res资源是保存在ContextImpl里面的Resources实例,整个项目共有,而新加进来的res资源是保存在新创建的Resources实例的,也就是说ProxyActivity其实有两套res资源,并不是把新的res资源和原有的res资源合并了(所以不怕R.id重复),对两个res资源的访问都需要用对应的Resources实例,这也是开发时要处理的问题。(其实应该有3套,Android系统会加载一套framework-res.apk资源,里面存放系统默认Theme等资源)</p>
<p>额外补充下,这里你可能注意到了我们采用了反射的方法调用AssetManager的“addAssetPath”方法,而在上面ResourcesManager中调用AssetManager的“addAssetPath”方法是直接调用的,不用反射啊,而且看看SDK里AssetManager的“addAssetPath”方法的源码(这里也能看到具体APK资源的提取过程是在Native里完成的),发现它也是public类型的,外部可以直接调用,为什么还要用反射呢?</p>
<pre><code class="JAVA"> /**
* 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;
}
}</code></pre>
<p>这里有个误区,SDK的源码只是给我们参考用的,APP实际上运行的代码逻辑在android.jar里面(位于android-sdk\platforms\android-XX),反编译android.jar并找到ResourcesManager类就可以发现这些接口都是对应用层隐藏的。</p>
<pre><code class="java">public final class AssetManager{
AssetManager(){throw new RuntimeException("Stub!"); }
public void close() { throw new RuntimeException("Stub!"); }
public final InputStream open(String fileName) throws IOException { throw new RuntimeException("Stub!"); }
public final InputStream open(String fileName, int accessMode) throws IOException { throw new RuntimeException("Stub!"); }
public final AssetFileDescriptor openFd(String fileName) throws IOException { throw new RuntimeException("Stub!"); }
public final native String[] list(String paramString) throws IOException;
public final AssetFileDescriptor openNonAssetFd(String fileName) throws IOException { throw new RuntimeException("Stub!"); }
public final AssetFileDescriptor openNonAssetFd(int cookie, String fileName) throws IOException { throw new RuntimeException("Stub!"); }
public final XmlResourceParser openXmlResourceParser(String fileName) throws IOException { throw new RuntimeException("Stub!"); }
public final XmlResourceParser openXmlResourceParser(int cookie, String fileName) throws IOException { throw new RuntimeException("Stub!"); }
protected void finalize() throws Throwable { throw new RuntimeException("Stub!");
}
public final native String[] getLocales();
}</code></pre>
<p>到此,启动插件里的Activity的两大问题都有解决的方案了。</p>
<h2>代理模式的具体项目 <a href="https://link.segmentfault.com/?enc=2T%2B%2Fj%2FsN53mohohjF75EWA%3D%3D.RB1m9NaLo9tcilisw0yZU4t9yA9RY7aABGJ1ItNhsm%2Bj408vfkFIngX1kTSR56O52fHa%2BAYtIgZ0wekXmm5QxA%3D%3D" rel="nofollow">dynamic-load-apk</a>
</h2>
<p>上面只是分析了代理模式的关键技术点,如果运用到具体项目中去的话,除了两个关键的问题外,还有许多繁琐的细节需要处理,我们需要设计一个框架,规范插件APK项目的开发,也方便以后功能的扩展。这里,<strong>dynamic-load-apk</strong>向我们展示了许多优秀的处理方法,比如:</p>
<ol>
<li><p>把Activity关键的生命周期方法抽象成DLPlugin接口,ProxyActivity通过DLPlugin代理调用插件Activity的生命周期;</p></li>
<li><p>设计一个基础的BasePluginActivity类,插件项目里使用这些基类进行开发,可以以接近常规Android开发的方式开发插件项目;</p></li>
<li><p>以类似的方式处理Service的问题;</p></li>
<li><p>处理了大量常见的兼容性问题(比如使用Theme资源时出现的问题);</p></li>
<li><p>处理了插件项目里的so库的加载问题;</p></li>
<li><p>使用PluginPackage管理插件APK,从而可以方便地管理多个插件项目;</p></li>
</ol>
<h3>处理插件项目里的so库的加载</h3>
<p>这里需要把插件APK里面的SO库文件解压释放出来,在根据当前设备CPU的型号选择对应的SO库,并使用System.load方法加载到当前内存中来,具体分析请参考 <a href="http://segmentfault.com/a/1190000004062899">Android动态加载补充 加载SD卡的SO库</a>。</p>
<h3>多插件APK的管理</h3>
<p>动态加载一个插件APK需要三个对应的<code>DexClassLoader</code>、<code>AssetManager</code>、<code>Resources</code>实例,可以用组合的方式创建一个<code>PluginPackage</code>类存放这三个变量,再创建一个管理类<code>PluginManager</code>,用成员变量<code>HashMap<dexPath,pluginPackage></code>的方式保存<code>PluginPackage</code>实例。</p>
<p>具体的代码请参考原项目的文档、源码以及Sample里面的示例代码,在这里感谢<a href="https://link.segmentfault.com/?enc=X8%2FAdnHP%2BFNjImyA9%2FELew%3D%3D.JLEcBmX0MKX1v0r1Qf8IQ%2BFFoVXPeQHWpE9OjV%2Bf1mIY4TrQsp4aMfq58x3eWuRK" rel="nofollow">singwhatiwanna</a>的开源精神。</p>
<h2>实际应用中可能要处理的问题</h2>
<h3>插件APK的管理后台</h3>
<p>使用动态加载的目的,就是希望可以绕过APK的安装过程升级应用的功能,如果插件APK是打包在主项目内部的那动态加载纯粹是多次一举。更多的时候我们希望可以在线下载插件APK,并且在插件APK有新版本的时候,主项目要从服务器下载最新的插件替换本地已经存在的旧插件。为此,我们应该有一个管理后台,它大概有以下功能:</p>
<ol>
<li><p>上传不同版本的插件APK,并向APP主项目提供插件APK信息查询功能和下载功能;</p></li>
<li><p>管理在线的插件APK,并能向不同版本号的APP主项目提供最合适的插件APK;</p></li>
<li><p>万一最新的插件APK出现紧急BUG,要提供旧版本回滚功能;</p></li>
<li><p>出于安全考虑应该对APP项目的请求信息做一些安全性校验;</p></li>
</ol>
<h3>插件APK合法性校验</h3>
<p>加载外部的可执行代码,一个逃不开的问题就是要确保外部代码的安全性,我们可不希望加载一些来历不明的插件APK,因为这些插件有的时候能访问主项目的关键数据。</p>
<p>最简单可靠的做法就是校验插件APK的MD5值,如果插件APK的MD5与我们服务器预置的数值不同,就认为插件被改动过,弃用。</p>
<h2>是热部署,还是插件化?</h2>
<p>这一部分作为补充说明,如果不太熟悉动态加载的使用姿势,可能不是那么容易理解。</p>
<p>谈到动态加载的时候我们经常说到“热部署”和“插件化”这些名词,它们虽然都和动态加载有关,但是还是有一点区别,这个问题涉及到主项目与插件项目的<strong>交互方式</strong>。前面我们说到,动态加载方式,可以在“项目层级”做到代码分离,按道理我们希望是主项目和插件项目不要有任何交互行为,实际上也应该如此!这样做不仅能确保项目的安全性,也能简化开发工作,所以一般的做法是</p>
<h3>只有在用户使用到的时候才加载插件</h3>
<p>主项目还是像常规Android项目那样开发,只有用户使用插件APK的功能时才动态加载插件并运行,插件一旦运行后,与主项目没有任何交互逻辑,只有在主项目启动插件的时候才触发一次调用插件的行为。比如,我们的主项目里有几款推广的游戏,平时在用户使用主项目的功能时,可以先静默把游戏(其实就是一个插件APK)下载好,当用户点击游戏入口时,以动态加载的方式启动游戏,游戏只运行插件APK里的代码逻辑,结束后返回主项目界面。</p>
<h3>一启动主项目就加载插件</h3>
<p>另外一种完全相反的情形是,主项目只提供一个启动的入口,以及从服务器下载最新插件的更新逻辑,这两部分的代码都是长期保持不变的,应用一启动就动态加载插件,所有业务逻辑的代码都在插件里实现。比如现在一些游戏市场都要求开发者接入其SDK项目,如果SDK项目采用这种开发方式,先提供一个空壳的SDK给开发者,空壳SDK能从服务器下载最新的插件再运行插件里的逻辑,就能保证开发者开发的游戏每次启动的时候都能运行最新的代码逻辑,而不用让开发者在SDK有新版本的时候重新更换SDK并构建新的游戏APK。</p>
<h3>让插件使用主项目的功能</h3>
<p>明明,说了不要交互的,偏偏,Android开发者就是这么执着于技术。</p>
<p>有些时候,比如,主项目里有一个成熟的图片加载框架ImageLoader,而插件里也有一个ImageLoader。如果一个应用同时运行两套ImageLoader,那会有许多额外的性能开销,如果能让插件也用主项目的ImageLoader就好了。另外,如果在插件里需要用到用户登录功能,我们总不希望用户使用主项目时进行一次登录,进入插件时由来一次登录,如果能在插件里使用主项目的登录状态就好了。</p>
<p>因此,有些时候我们希望插件项目能调用主项目的功能。怎么处理好呢,由于插件项目与主项目是分开的,我们在开发插件的时候,怎么调用主项目的代码啊?这里需要稍微了解一下Android项目间的依赖方式。</p>
<p>想想一个普通的APK是怎么构建和运行的,Android SDK提供了许多系统类(如Activity、Fragment等,一般我们也喜欢在这里查看源码),我们的Android项目依赖Android SDK项目并使用这些类进行开发,那构建APK的时候会把这些类打包进来吗?不会,要是每个APK都打包一份,那得有多少冗余啊。所以Android项目至少有两种依赖的方式,一种构建时会把被依赖的项目(Library)的类打包进来,一种不会。</p>
<p>在Android Studio打开项目的Project Structure,找到具体Module的Dependencies选项卡<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-11-25/2875300.jpg" alt="" title=""></p>
<p>可以看到Library项目有个Scope属性,这里的Compile模式就是会把Library的类打包进来,而Provided模式就不会。</p>
<p>注意,使用Provided模式的Library只能是jar文件,而不能是一个Android Library项目,因为后者可能自带了一些res资源,这些资源无法一并塞进标准的jar文件里面。到这里我们明白,Android SDK的代码其实是打包进系统ROM(俗称Framework层级)里面的,我们开发Android项目的时候,只是以Provided模式引用android.jar,从这个角度也佐证了上面谈到的“为什么APP实际运行时AssetManager类的逻辑会与Android SDK里的源码不一样”。</p>
<p>现在好办了,如果要在插件里使用主项目的ImageLoader,我们可以把ImageLoader的相关代码抽离成一个Android Libary项目,主项目以Compile模式引用这个Libary,而插件项目以Provided模式引用这个Library(编译出来的jar),这样能实现两者之间的交互了,当然代价也是明显的。</p>
<ol>
<li><p>我们应该只给插件开放一些必要的接口,不然会有安全性问题;</p></li>
<li><p>作为通用模块的Library应该保持不变(起码接口不变),不然主项目与插件项目的版本同步会复杂许多;</p></li>
<li><p>因为插件项目已经严重依赖主项目了,所以插件项目不能独立运行,因为缺少必要的<strong>环境</strong>;</p></li>
</ol>
<p>最后我们再说说“热部署”和“插件化”的区别,一般我们把独立运行的插件APK叫热部署,而需要依赖主项目的环境运行的插件APK叫做插件化。</p>
Android动态加载入门 简单加载模式
https://segmentfault.com/a/1190000004062952
2015-11-29T01:29:32+08:00
2015-11-29T01:29:32+08:00
Kaede
https://segmentfault.com/u/kaede
8
<h2>基本信息</h2>
<ul>
<li><p>作者:<a href="https://link.segmentfault.com/?enc=MFSXLkTqJtFqJA1hNeuBjg%3D%3D.6XfVNbKLTdvr83sGbZqnsVQGLU4CPpu0ccgDs1c88DQ%3D" rel="nofollow">kaedea</a></p></li>
<li><p>项目:<a href="https://link.segmentfault.com/?enc=H4YU98%2ByZZiyYualo5N%2BMA%3D%3D.8A20wC4ZjOk3DmhT59qZtG7DW069vU4XIO%2BIm04HX0rP8UBzyLS3dkBfuKa3iCDQpC%2BtBYD2%2FLKpjYHOYgnEng%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<h2>初步了解Android动态加载</h2>
<p>Java程序中,JVM虚拟机是通过类加载器ClassLoader加载.jar文件里面的类的。Android也类似,不过Android用的是Dalvik/ART虚拟机,不是JVM,也不能直接加载.jar文件,而是加载dex文件。</p>
<p>先要通过Android SDK提供的DX工具把.jar文件优化成.dex文件,然后Android的虚拟机才能加载。注意,有的Android应用能直接加载.jar文件,那是因为这个.jar文件已经经过优化,只不过后缀名没改(其实已经是.dex文件)。</p>
<p>如果对ClassLoader的工作机制有兴趣,具体过程请参考 <a href="http://segmentfault.com/a/1190000004062880">Android 动态加载基础 ClassLoader工作机制</a>,这里不再赘述。</p>
<h2>如何获取能够加载的.dex文件</h2>
<p>首先我们可以通过JDK的编译命令javac把Java代码编译成.class文件,再使用jar命令把.class文件封装成.jar文件,这与编译普通Java程序的时候完全一样。</p>
<p>之后再用Android SDK的DX工具把.jar文件优化成.dex文件(在“android-sdk\build-tools\具体版本\”路径下)</p>
<blockquote><p>dx --dex --output=target.dex origin.jar // target.dex就是我们要的了</p></blockquote>
<p>此外,我们可以现把代码编译成APK文件,再把APK里面的.dex文件解压出来,或者直接把APK文件当成.dex使用(只是APK里面的静态资源文件我们暂时还用不到)。至此我们发现,无论加载.jar,还是.apk,其实都和加载.dex是等价的,Android能加载.jar和.apk,是因为它们都包含有.dex,直接加载.apk文件时,ClassLoader也会自动把.apk里的.dex解压出来。</p>
<h2>加载并调用<strong>.dex</strong>里面的方法</h2>
<p>与JVM不同,Android的虚拟机不能用ClassCload直接加载.dex,而是要用DexClassLoader或者PathClassLoader,他们都是ClassLoader的子类,这两者的区别是</p>
<ol>
<li><p>DexClassLoader:可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;</p></li>
<li><p>PathClassLoader:要传入系统中apk的存放Path,所以只能加载已经安装的apk文件;</p></li>
</ol>
<p>使用前,先看看DexClassLoader的构造方法</p>
<pre><code class="java"> public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}</code></pre>
<p>注意,我们之前提到的,DexClassLoader并不能直接加载外部存储的.dex文件,而是要先拷贝到内部存储里。这里的dexPath就是.dex的外部存储路径,而optimizedDirectory则是内部路径,libraryPath用null即可,parent则是要传入当前应用的ClassLoader,这与ClassLoader的“双亲代理模式”有关。</p>
<p>实例使用DexClassLoader的代码</p>
<pre><code class="java">File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "test_dexloader.jar");// 外部路径
File dexOutputDir = this.getDir("dex", 0);// 无法直接从外部路径加载.dex文件,需要指定APP内部路径作为缓存目录(.dex文件会被解压到此目录)
DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(), null, getClassLoader());</code></pre>
<p>到这里,我们已经成功把.dex文件给加载进来了,接下来就是如何调用.dex里面的代码,主要有两种方式。</p>
<h3>使用反射的方式</h3>
<p>使用DexClassLoader加载进来的类,我们本地并没有这些类的源码,所以无法直接调用,不过可以通过反射的方法调用,简单粗暴。</p>
<pre><code class="java">DexClassLoader dexClassLoader = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(), dexOutputDir.getAbsolutePath(), null, getClassLoader());
Class libProviderClazz = null;
try {
libProviderClazz = dexClassLoader.loadClass("me.kaede.dexclassloader.MyLoader");
// 遍历类里所有方法
Method[] methods = libProviderClazz.getDeclaredMethods();
for (int i = 0; i < methods.length; i++) {
Log.e(TAG, methods[i].toString());
}
Method start = libProviderClazz.getDeclaredMethod("func");// 获取方法
start.setAccessible(true);// 把方法设为public,让外部可以调用
String string = (String) start.invoke(libProviderClazz.newInstance());// 调用方法并获取返回值
Toast.makeText(this, string, Toast.LENGTH_LONG).show();
} catch (Exception exception) {
// Handle exception gracefully here.
exception.printStackTrace();
}</code></pre>
<h3>使用接口的方式</h3>
<p>毕竟.dex文件也是我们自己维护的,所以可以把方法抽象成公共接口,把这些接口也复制到主项目里面去,就可以通过这些接口调用动态加载得到的实例的方法了。</p>
<pre><code class="java">pulic interface IFunc{
public String func();
}
// 调用
IFunc ifunc = (IFunc)libProviderClazz;
String string = ifunc.func();
Toast.makeText(this, string, Toast.LENGTH_LONG).show();</code></pre>
<p>到这里,我们已经成功从外部路径动态加载一个.dex文件,并执行里面的代码逻辑了。通过从服务器下载最新的.dex文件并替换本地的旧文件,就能初步实现“APP的动态升级了”。</p>
<h2>如何动态更改XML布局</h2>
<p>虽然已经能动态更改代码逻辑了,但是UI界面要怎么更改啊?Android开发中大部分的情况下,UI界面都是通过XML布局实现的,放在res目录下,可是.dex库里面并没有这些静态资源啊,所以无法改变XML布局。(这里即使直接动态加载APK文件,但是通过DexClassLoader只能加载新的APK其中的.dex文件,并无法加载其中的res资源文件,所以如果在动态加载的.dex中直接使用新的APK的res资源的话会抛出异常。)</p>
<p>大家都知道,所有的XML布局在运行的时候都要通过LayoutInflator渲染成View的实例,这个实例与我们使用纯Java代码创建的View实例几乎是等价的,而且后者可能效率还更高,所有的XML布局实现的UI界面都有等价的纯代码的创建方案。由此伸展开来,res目录下所有XML资源都有等价的纯代码的实现方式,比如XML动画、XML Drawable等。</p>
<p>所以,如果想要动态更改应用的UI界面的话,可以通过用纯代码创建布局的形式来解决。此外,还可以模仿LayoutInflator的工作方式,自己写一套布局解析器来解析XML文件,这样就能在完全不依赖res资源的情况下创建UI界面了,当然这样的工作量不少,而且,完全避开res资源的话,所有的分辨率、国际化等自适应问题都要自己在应用层写代码维护了,显然脱离res资源框架不是一个很明智的做法,<strong>但是这种做法确实可行</strong>,在我们之前的实际生产中的项目中也稳定使用着,这里出于责任问题就不方便公开细节了。</p>
<p>(说实在,这种方案非常繁琐,不好维护,一方面,这是产品一句“技术可行就做呗”而产生的解决方案;另一方面,但是动态加载技术还很不成熟,也没有什么实际投入到生产的项目,所以采取了非常保守的开发方式)。</p>
<h2>使用Fragment代替Activity</h2>
<p>Activity需要在Manifest里注册,然后一标准的Intent启动才会具有生命周期,很明显,如果想要动态加载的.dex里的Activity没有注册的话,是无法启动的。</p>
<p>有一种简单粗暴的做法就是可以把.dex里所有需要用到的Activity都事先注册到原项目里,不过这样一来如果.dex里的Activity有变化,原项目就必须跟着升级。</p>
<p>另外一种方案是使用Fragment,Fragment自带生命周期,不需要在Manifest里注册,所以可以在.dex里使用Fragment来代替Activity,代价就是Fragment之间的切换会繁琐许多。</p>
<h2>ART模式的兼容性问题</h2>
<p>当初我们开始设计动态加载方案的时候,还没有ART模式。随着Kitkat的发布以及ART模式的出现,我们开始担心“用DexClassLoader加载.dex文件”的方案会不会在ART模式上面存在兼容性问题。</p>
<p>其实,ART模式相比原来的Dalvik,会在安装APK的时候,使用Android系统自带的<strong>dex2oat工具</strong>把APK里面的.dex文件转化成OAT文件,OAT文件是一种Android私有ELF文件格式,它不仅包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容。这使得我们无需重新编译原有的APK就可以让它正常地在ART里面运行,也就是我们不需要改变原来的APK编程接口。ART模式的系统里,同样存在DexClassLoader类,包名路径也没变,只不过它的具体实现与原来的有所不同,但是接口是一致的。</p>
<pre><code class="java">package dalvik.system;
import dalvik.system.BaseDexClassLoader;
import java.io.File;
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}</code></pre>
<p>也就是说,ART模式在加载.dex文件的方法上,对Dalvik做了向下兼容,所以使用DexClassLoader加载进来的.dex文件同样也会被转化成OAT文件再被执行,“以DexClassLoader为核心的动态加载方案”在ART模式上可以稳定运行。</p>
<p>关于ART模式以及OAT文件的详细分析,请参考官方的<a href="https://link.segmentfault.com/?enc=9PXDAPxsvDZIMzp6ENfyuQ%3D%3D.9wvR4Z2UZMBhPZkGbr53ozgSPJrkSjLHGoXQuTgKAcbXPRC3LClJcRkMwqCf%2FpuT" rel="nofollow">ART and Dalvik</a>,以及老罗的<a href="https://link.segmentfault.com/?enc=pElPl3QpaRna8VARU6darQ%3D%3D.cWMf03oqoH0I4cQ5nezFjPM9j5z%2BUjcq641XrorSQ2k0%2Bxm%2B67s%2BgI4DT0T6Sm%2B0uAi4dKBSR%2FdrzfM9am4A0g%3D%3D" rel="nofollow">Android ART运行时无缝替换Dalvik虚拟机的过程分析</a>。</p>
<h2>存在的问题与改进方案</h2>
<p>以上大致就是“Android动态性加载初级阶段”的解决方案,虽然现在已经能投入到具体的生产中去,但是还有一些问题无法忽略。</p>
<ol>
<li><p>无法使用res目录下的资源,特别是使用XML布局,以及无法通过res资源到达自适应</p></li>
<li><p>无法动态加载新的Activity等组件,因为这些组件需要在Manifest中注册,动态加载无法更改当前APK的Manifest</p></li>
</ol>
<p>以上问题可以通过<strong>反射调用Framework层代码</strong>以及<strong>代理Activity</strong>的方式解决,可以把这种的动态加载框架成为“代理模式”。</p>
<h2>参考日志</h2>
<p><a href="https://link.segmentfault.com/?enc=kjf%2BK%2BecoVhTUOwrtD1%2Biw%3D%3D.E7m8bRXkX%2BldbvQ99EtGI9CtcgqV81f1cECqO78ThIMuux3pCPdu%2FTPLu433dGiK" rel="nofollow">http://44289533.iteye.com/blog/1954453</a><br><a href="https://link.segmentfault.com/?enc=ibZnjsO0VbNEAE92IfJd0g%3D%3D.CVQ8INu7VJGiARU15oW7%2BS7E2yY1%2FCJDiEFn7MKjPCefUAI2hqNb7R50DmHThvZbJtkfxQdDRXK8%2BsTutHxEUA%3D%3D" rel="nofollow">http://blog.csdn.net/bboyfeiyu/article/details/11710497</a><br><a href="https://link.segmentfault.com/?enc=dqdnToxRjOUq%2FqoFXivMXQ%3D%3D.VaA0ZLpQbZFj7eYyVElrcF63ea4pLarDWSq4wWySxpvNgeM2FjMM5KqjZ2V5siA6spnmFCXSY9yfv3vIcK0gKw%3D%3D" rel="nofollow">http://www.cnblogs.com/over140/archive/2011/11/23/2259367.html</a></p>
Android动态加载补充 加载SD卡中的SO库
https://segmentfault.com/a/1190000004062899
2015-11-29T01:12:44+08:00
2015-11-29T01:12:44+08:00
Kaede
https://segmentfault.com/u/kaede
10
<h2>基本信息</h2>
<ul>
<li><p>作者:<a href="https://link.segmentfault.com/?enc=vbbWCUrz32cgfUKRXYNxxw%3D%3D.bUKiNjbl7WMgPoHYp8rp%2B%2BOrr65OHiq8K1sggapvIUM%3D" rel="nofollow">kaedea</a></p></li>
<li><p>项目:<a href="https://link.segmentfault.com/?enc=HUtGaY7kP0pa1oXd%2F1Pd3w%3D%3D.3NDHcDmXJgfS6boMZefh9sff1TArUQGREvFg%2B%2BsD%2BOVusqlxfOllT%2Bh0XgvSFblntizqx1sARVtU%2FbZDs1xEMA%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<h2>JNI与NDK</h2>
<p>Android中JNI的使用其实就包含了动态加载,APP运行时动态加载<code>.so</code>库并通过JNI调用其封装好的方法。后者一般是使用NDK工具从C/C++代码编译而成,运行在Native层,效率会比执行在虚拟机的Java代码高很多,所以Android中经常通过动态加载<code>.so</code>库来完成一些对性能比较有需求的工作(比如T9搜索、或者Bitmap的解码、图片高斯模糊处理等)。此外,由于<code>.so</code>库是由C++编译而来的,只能被反编译成汇编代码,相比Smali更难被破解,因此<code>.so</code>库也可以被用于安全领域。</p>
<p>与我们常说的基于ClassLoader的动态加载不同,SO库的加载是使用System类的(由此可见对SO库的支持也是Android的基础功能),所以这里这是作为补充说明。不过,如果使用ClassLoader加载SD卡里插件APK,而插件APK里面包含有SO库,这就涉及到了对插件APK里的SO库的加载,所以我们也要知道如何加载SD卡里面的SO库。</p>
<h2>一般的SO文件的使用姿势</h2>
<p>以一个“图片高斯模糊”的功能为例,如果使用Java代码对图像Bitmap的每一个像素点进行计算,那整体耗时将会非常大,所以可以考虑使用JNI。(详细的JNI使用教程网络上有许多,这里不赘述)</p>
<p>这里推荐一个开源的高斯模糊项目 <a href="https://link.segmentfault.com/?enc=4YhWPR7SJEytOyOJIJE5Fg%3D%3D.I8uwBAbHV5i9LEKr%2FGEFCp9%2BmMKl8ko15TqxprasOClPWUM69y0E1vwqdmR0vQMK" rel="nofollow">Android StackBlur</a></p>
<p>在命令行定位到Android.mk文件所在目录,运行NDK工具的<code>ndk-build</code>命令就能编译出我们需要SO库<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-11-26/19622075.jpg" alt="" title=""></p>
<p>再把SO库复制到Android Studio项目的<code>jniLibs</code>目录中<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-11-26/58498917.jpg" alt="" title=""><br>(Android Studio现在也支持直接编译SO库,但是有许多坑,这里我选择手动编译)</p>
<p>接着在Java中把SO库对应的模块加载进来</p>
<pre><code class="JAVA">// load so file from internal directory
try {
System.loadLibrary("stackblur");
NativeBlurProcess.isLoadLibraryOk.set(true);
Log.i("MainActivity", "loadLibrary success!");
} catch (Throwable throwable) {
Log.i("MainActivity", "loadLibrary error!" + throwable);
}</code></pre>
<p>加载成功后就可以直接使用Native方法了</p>
<pre><code class="JAVA">public class NativeBlurProcess {
public static AtomicBoolean isLoadLibraryOk = new AtomicBoolean(false);
//native method
private static native void functionToBlur(Bitmap bitmapOut, int radius, int threadCount, int threadIndex, int round);
}</code></pre>
<p>由此可见,在Android项目中,SO库的使用也是一种动态加载,在运行时把可执行文件加载进来。一般情况下,SO库都是打包在APK内部的,不允许修改。这种“动态加载”看起来不是我们熟悉的那种啊,貌似没什么卵用。不过,其实SO库也是可以存放在外部存储路径的。</p>
<h2>如何把SO文件存放在外部存储</h2>
<p>注意到上面加载SO库的时候我们用到了System类的“loadLibrary”方法,同时我们也发现System类还有一个“load”方法,看起来差不多啊,看看他们有什么区别吧!</p>
<pre><code class="JAVA">/**
* See {@link Runtime#load}.
*/
public static void load(String pathName) {
Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
}
/**
* See {@link Runtime#loadLibrary}.
*/
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}</code></pre>
<p>先看看loadLibrary,这里调用了Runtime的loadLibrary,进去一看,又是动态加载熟悉的ClassLoader了(这里也佐证了SO库的使用就是一种动态加载的说法)</p>
<pre><code class="JAVA"> /*
* Searches for and loads the given shared library using the given ClassLoader.
*/
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
String filename = loader.findLibrary(libraryName);
String error = doLoad(filename, loader);
return;
}
……
}</code></pre>
<p>看样子就像是通过库名获取一个文件路径,再调用“doLoad”方法加载这个文件,先看看“loader.findLibrary(libraryName)”</p>
<pre><code class="JAVA"> protected String findLibrary(String libName) {
return null;
}</code></pre>
<p>ClassLoader只是一个抽象类,它的大部分工作都在BaseDexClassLoader类中实现,进去看看</p>
<pre><code class="java">public class BaseDexClassLoader extends ClassLoader {
public String findLibrary(String name) {
throw new RuntimeException("Stub!");
}
}</code></pre>
<p>不对啊,这里只是抛了一个RuntimeException异常,什么都没做啊!</p>
<p><strong>其实这里有一个误区</strong>,也是刚开始开Android SDK源码的同学容易搞混的。Android SDK自带的源码其实只是给我们开发者参考的,基本只是一些常用的类,Google不会把整个Android系统的源码都放到这里来,因为整个项目非常大,ClassLoader类平时我们接触得少,所以它的具体实现的源码并没有打包进SDK里,如果需要,我们要到官方AOSP项目里面去看(顺便一提,整个AOSP5.1项目大小超过150GB,真的有需要的话推荐用一个移动硬盘存储)。</p>
<p>这里为了方便,我们可以直接看在线的代码 <a href="https://link.segmentfault.com/?enc=9t4TFqMVwHylH%2FQagIm6Ow%3D%3D.U90l5F9W9R6MfVTLMb%2BN2GbbXvYh%2Fp1aUuRmap%2FxaQ%2BfAKc40NmvFjoKlRkfcIsxjFwt0SSuBUfgWhLW7fx24WPk1utP87oJIHZOwYpH5d6S8cCvupD1d3FR3plJ%2F5InUyjv%2BQIEevkQusIpkG2QeNgTUPFJtyLt2m8HOk1z26Y%3D" rel="nofollow">BaseDexClassLoader.java</a></p>
<pre><code class="java">@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}</code></pre>
<p>再看进去DexPathList类</p>
<pre><code class="java">/**
* Finds the named native code library on any of the library
* directories pointed at by this instance. This will find the
* one in the earliest listed directory, ignoring any that are not
* readable regular files.
*
* @return the complete path to the library or {@code null} if no
* library was found
*/
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (File directory : nativeLibraryDirectories) {
File file = new File(directory, fileName);
if (file.exists() && file.isFile() && file.canRead()) {
return file.getPath();
}
}
return null;
}</code></pre>
<p>到这里已经明朗了,根据传进来的libName,扫描APK内部的nativeLibrary目录,获取并返回内部SO库文件的完整路径filename。再回到Runtime类,获取filename后调用了“doLoad”方法,看看</p>
<pre><code class="JAVA">private String doLoad(String name, ClassLoader loader) {
String ldLibraryPath = null;
String dexPath = null;
if (loader == null) {
ldLibraryPath = System.getProperty("java.library.path");
} else if (loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
ldLibraryPath = dexClassLoader.getLdLibraryPath();
}
synchronized (this) {
return nativeLoad(name, loader, ldLibraryPath);
}
}</code></pre>
<p>到这里就彻底清楚了,调用Native方法“nativeLoad”,通过完整的SO库路径filename,把目标SO库加载进来。</p>
<p>说了半天还没有进入正题呢,不过我们可以想到,如果使用loadLibrary方法,到最后还是要找到目标SO库的完整路径,再把SO库加载进来,那我们能不能一开始就给出SO库的完整路径,然后直接加载进来?我们猜想load方法就是干这个的,看看。</p>
<pre><code class="java">void load(String absolutePath, ClassLoader loader) {
if (absolutePath == null) {
throw new NullPointerException("absolutePath == null");
}
String error = doLoad(absolutePath, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}</code></pre>
<p>我勒个去,一上来就直接来到doLoad方法了,这证明我们的猜想可能是正确的,那么在实际项目中测试看看吧!</p>
<p>我们先把SO放在Asset里,然后再复制到内部存储,再使用load方法把其加载进来。</p>
<pre><code class="java">public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
File dir = this.getDir("jniLibs", Activity.MODE_PRIVATE);
File distFile = new File(dir.getAbsolutePath() + File.separator + "libstackblur.so");
if (copyFileFromAssets(this, "libstackblur.so", distFile.getAbsolutePath())){
//使用load方法加载内部储存的SO库
System.load(distFile.getAbsolutePath());
NativeBlurProcess.isLoadLibraryOk.set(true);
}
}
public void onDoBlur(View view){
ImageView imageView = (ImageView) findViewById(R.id.iv_app);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), android.R.drawable.sym_def_app_icon);
Bitmap blur = NativeBlurProcess.blur(bitmap,20,false);
imageView.setImageBitmap(blur);
}
public static boolean copyFileFromAssets(Context context, String fileName, String path) {
boolean copyIsFinish = false;
try {
InputStream is = context.getAssets().open(fileName);
File file = new File(path);
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
byte[] temp = new byte[1024];
int i = 0;
while ((i = is.read(temp)) > 0) {
fos.write(temp, 0, i);
}
fos.close();
is.close();
copyIsFinish = true;
} catch (IOException e) {
e.printStackTrace();
Log.e("MainActivity", "[copyFileFromAssets] IOException "+e.toString());
}
return copyIsFinish;
}
}</code></pre>
<p>点击onDoBlur按钮,果然加载成功了!<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-11-26/49457004.jpg" alt="" title=""></p>
<p>那能不能直接加载外部存储上面的SO库呢,把SO库拷贝到SD卡上面试试。<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-11-26/91753544.jpg" alt="" title=""></p>
<p>看起来是不可以的样子,Permission denied!</p>
<blockquote><p>java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map "/storage/emulated/0/libstackblur.so" segment 1: Permission denied</p></blockquote>
<p>看起来像是没有权限的样子,看看源码哪里抛出的异常吧</p>
<pre><code class="JAVA"> /*
* Loads the given shared library using the given ClassLoader.
*/
void load(String absolutePath, ClassLoader loader) {
if (absolutePath == null) {
throw new NullPointerException("absolutePath == null");
}
String error = doLoad(absolutePath, loader);
if (error != null) {
// 这里抛出的异常
throw new UnsatisfiedLinkError(error);
}
}</code></pre>
<p>应该是执行doLoad方法时出现了错误,但是上面也看过了,doLoad方法里调用了Native方法“nativeLoad”,那应该就是Native代码里出现的错误。平时我很少看到Native里面,上一次看的时候,是因为需要看看点九图NinePathDrawable的缩放控制信息chunk数组的具体作用是怎么样,费了好久才找到我想要的一小段代码。所以这里就暂时不跟进去了,有兴趣的同学可以告诉我关键代码的位置。</p>
<p>我在一个Google的开发者论坛上找到了一些答案</p>
<blockquote><p>The SD Card is mounted noexec, so I'm not sure this will work.</p></blockquote>
<p>Moreover, using the SD Card as a storage location is a really bad idea, since any other application can modify/delete/corrupt it easily.<br>Try downloading the library to your application's data directory instead, and load it from here.</p>
<p>这也容易理解,SD卡等外部存储路径是一种可拆卸的(mounted)不可执行(noexec)的储存媒介,不能直接用来作为可执行文件的运行目录,使用前应该把可执行文件复制到APP内部存储再运行。</p>
<p>最后,我们也可以看看官方的API文档<br><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-11-27/73725063.jpg" alt="" title=""></p>
<p>看来load方法的用途和我们理解的一致,文档里说的shared library就是指SO库(shared object),至此,我们就可以把SO文件移动到外部存储了,或者从网络下载都行。</p>
Android动态加载基础 ClassLoader工作机制
https://segmentfault.com/a/1190000004062880
2015-11-29T01:08:49+08:00
2015-11-29T01:08:49+08:00
Kaede
https://segmentfault.com/u/kaede
17
<p>Last Edit: 2016-02-10</p>
<h2>基本信息</h2>
<ul>
<li><p>作者:<a href="https://link.segmentfault.com/?enc=Amy0tUWnHek2O1AIcU9TIQ%3D%3D.q%2Bes1kzi1D7%2BZybK5%2BCoGrTyJg%2BeR47cHgwJKGSFYmo%3D" rel="nofollow">kaedea</a></p></li>
<li><p>项目:<a href="https://link.segmentfault.com/?enc=96SV1ujyaMRIsU4wcg3p0w%3D%3D.m4dqJfHrehqGNtNFHGJzs1VHfYI08xBtBOi90K4jjoSN6kpmaaL%2FPihbbad28n7dSfJMsarfBwzt1GLH9I5ECw%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<hr>
<h2>类加载器ClassLoader</h2>
<p>早期使用过Eclipse等Java编写的软件的同学可能比较熟悉,Eclipse可以加载许多第三方的插件(或者叫扩展),这就是动态加载。这些插件大多是一些Jar包,而使用插件其实就是动态加载Jar包里的Class进行工作。这其实非常好理解,Java代码都是写在Class里面的,程序运行在虚拟机上时,虚拟机需要把需要的Class加载进来才能创建实例对象并工作,而完成这一个加载工作的角色就是ClassLoader。</p>
<blockquote><p>对于Java程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的<code>class文件</code>),其中起到关键作用的就是类加载器ClassLoader。</p></blockquote>
<p>Android的Dalvik/ART虚拟机如同标准JAVA的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此,我们可以利用这一点,在程序运行时手动加载Class,从而达到代码动态加载可执行文件的目的。Android的Dalvik/ART虚拟机虽然与标准Java的JVM虚拟机不一样,ClassLoader具体的加载细节不一样,但是工作机制是类似的,也就是说在Android中同样可以采用类似的动态加载插件的功能,只是在Android应用中动态加载一个插件的工作要比Eclipse加载一个插件复杂许多(这点后面在解释说明)。</p>
<h2>有几个ClassLoader实例?</h2>
<p><strong>动态加载的基础是ClassLoader</strong>,从名字也可以看出,ClassLoader就是专门用来处理类加载工作的,所以这货也叫类加载器,而且一个运行中的APP <strong>不仅只有一个类加载器</strong>。</p>
<p>其实,在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类,我们的Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。</p>
<p>此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。下面我们在项目里验证看看</p>
<pre><code class="java"> @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ClassLoader classLoader = getClassLoader();
if (classLoader != null){
Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());
while (classLoader.getParent()!=null){
classLoader = classLoader.getParent();
Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());
}
}
}</code></pre>
<p>输出结果为</p>
<pre><code>[onCreate] classLoader 1 : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/me.kaede.anroidclassloadersample-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
[onCreate] classLoader 2 : java.lang.BootClassLoader@14af4e32</code></pre>
<p>可以看见有2个Classloader实例,一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader(应用启动时创建的,用于加载“/data/app/me.kaede.anroidclassloadersample-1/base.apk”里面的类)。由此也可以看出,<strong>一个运行的Android应用至少有2个ClassLoader</strong>。</p>
<h2>创建自己ClassLoader实例</h2>
<p>动态加载外部的dex文件的时候,我们也可以使用自己创建的ClassLoader实例来加载dex里面的Class,不过ClassLoader的创建方式有点特殊,我们先看看它的构造方法</p>
<pre><code class="java"> /*
* constructor for the BootClassLoader which needs parent to be null.
*/
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}</code></pre>
<p>创建一个ClassLoader实例的时候,需要使用一个现有的ClassLoader实例作为新创建的实例的Parent。这样一来,一个Android应用,甚至整个Android系统里所有的ClassLoader实例都会被一棵树关联起来,这也是ClassLoader的 <strong>双亲代理模型</strong>(Parent-Delegation Model)的特点。</p>
<h2>ClassLoader双亲代理模型加载类的特点和作用</h2>
<p>JVM中ClassLoader通过defineClass方法加载jar里面的Class,而Android中这个方法被弃用了。</p>
<pre><code class="java"> @Deprecated
protected final Class<?> defineClass(byte[] classRep, int offset, int length)
throws ClassFormatError {
throw new UnsupportedOperationException("can't load this type of class file");
}</code></pre>
<p>取而代之的是loadClass方法</p>
<pre><code class="java"> public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}</code></pre>
<h3>特点</h3>
<p>从源码中我们也可以看出,loadClass方法在加载一个类的实例的时候,</p>
<ol>
<li><p>会先查询当前ClassLoader实例是否加载过此类,有就返回;</p></li>
<li><p>如果没有。查询Parent是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;</p></li>
<li><p>如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作;</p></li>
</ol>
<p>这样做有个明显的特点,如果一个类被位于树根的ClassLoader加载过,那么在以后整个系统的生命周期内,这个类永远不会被重新加载。</p>
<h3>作用</h3>
<p>首先是共享功能,一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。</p>
<p>除此之外还有隔离功能,不同继承路线上的ClassLoader加载的类肯定不是同一个类,这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。这也好理解,一些系统层级的类会在系统初始化的时候被加载,比如java.lang.String,如果在一个应用里面能够简单地用自定义的String类把这个系统的String类给替换掉,那将会有严重的安全问题。</p>
<h2>使用ClassLoader一些需要注意的问题</h2>
<p>我们都知道,我们可以通过动态加载获得新的类,从而升级一些代码逻辑,这里有几个问题要注意一下。</p>
<p>如果你希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader会一直优先使用旧类。</p>
<p>如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的ClassLoader没有树的继承关系的另一个ClassLoader来加载新类,因为ClassLoader只会检查其Parent有没有加载过当前要加载的类,如果两个ClassLoader没有继承关系,那么旧类和新类都能被加载。</p>
<p>不过这样一来又有另一个问题了,在Java中,只有当两个实例的类名、包名以及加载其的ClassLoader都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。</p>
<blockquote><p>同一个Class = 相同的 ClassName + PackageName + ClassLoader</p></blockquote>
<p>以上问题在采用动态加载功能的开发中容易出现,请注意。</p>
<h2>DexClassLoader 和 PathClassLoader</h2>
<p>在Android中,ClassLoader是一个抽象类,实际开发过程中,我们一般是使用其具体的子类DexClassLoader、PathClassLoader这些类加载器来加载类的,它们的不同之处是:</p>
<ul>
<li><p>DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk;</p></li>
<li><p>PathClassLoader只能加载系统中已经安装过的apk;</p></li>
</ul>
<h2>类加载器的初始化</h2>
<p>平时开发的时候,使用DexClassLoader就够用了,但是我们不妨挖一下这两者具体细节上的区别。</p>
<pre><code class="java">// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}</code></pre>
<p>这两者只是简单的对BaseDexClassLoader做了一下封装,具体的实现还是在父类里。不过这里也可以看出,PathClassLoader的optimizedDirectory只能是null,进去BaseDexClassLoader看看这个参数是干什么的</p>
<pre><code class="java">public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}</code></pre>
<p>这里创建了一个DexPathList实例,进去看看</p>
<pre><code class="java"> public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
……
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
}
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = new ZipFile(file);
}
……
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
/**
* Converts a dex/jar file path and an output directory to an
* output file path for an associated optimized dex file.
*/
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}</code></pre>
<p>看到这里我们明白了,optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile<br>对象。</p>
<p>optimizedDirectory必须是一个内部存储路径,还记得我们之前说过的,无论哪种动态加载,加载的可执行文件一定要存放在内部存储。DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,因为这个dex会被复制到内部路径的optimizedDirectory;而PathClassLoader没有optimizedDirectory,所以它只能加载内部的dex,这些大都是存在系统中已经安装过的apk里面的。</p>
<h2>加载类的过程</h2>
<p>上面还只是创建了类加载器的实例,其中创建了一个DexFile实例,用来保存dex文件,我们猜想这个实例就是用来加载类的。</p>
<p>Android中,ClassLoader用loadClass方法来加载我们需要的类</p>
<pre><code class="java"> public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}</code></pre>
<p>loadClass方法调用了findClass方法,而BaseDexClassLoader重载了这个方法,得到BaseDexClassLoader看看</p>
<pre><code class="java">@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = pathList.findClass(name);
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}</code></pre>
<p>结果还是调用了DexPathList的findClass</p>
<pre><code class="java"> public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}</code></pre>
<p>这里遍历了之前所有的DexFile实例,其实也就是遍历了所有加载过的dex文件,再调用loadClassBinaryName方法一个个尝试能不能加载想要的类,真是简单粗暴</p>
<pre><code class="java"> public Class loadClassBinaryName(String name, ClassLoader loader) {
return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);</code></pre>
<p>看到这里想必大家都明白了,loadClassBinaryName中调用了Native方法defineClass加载类。</p>
<p>至此,ClassLoader的创建和加载类的过程的完成了。有趣的是,标准JVM中,ClassLoader是用defineClass加载类的,而Android中defineClass被弃用了,改用了loadClass方法,而且加载类的过程也挪到了DexFile中,在DexFile中加载类的具体方法也叫defineClass,不知道是Google故意写成这样的还是巧合。</p>
<h2>自定义ClassLoader</h2>
<p>平时进行动态加载开发的时候,使用DexClassLoader就够了。但我们也可以创建自己的类去继承ClassLoader,需要注意的是loadClass方法并不是final类型的,所以我们可以重载loadClass方法并改写类的加载逻辑。</p>
<p>通过前面我们分析知道,ClassLoader双亲代理的实现很大一部分就是在loadClass方法里,我们可以通过重写loadClass方法避开双亲代理的框架,这样一来就可以在重新加载已经加载过的类,也可以在加载类的时候注入一些代码。这是一种Hack的开发方式,采用这种开发方式的程序稳定性可能比较差,但是却可以实现一些“黑科技”的功能。</p>
<h2>Android程序比起一般Java程序在使用动态加载时麻烦在哪里</h2>
<p>通过上面的分析,我们知道使用ClassLoader动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的Java程序,在Android程序中使用动态加载主要有两个麻烦的问题:</p>
<ol>
<li><p>Android中许多组件类(如Activity、Service等)是需要在Manifest文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作;</p></li>
<li><p>Res资源是Android开发中经常用到的,而Android是把这些资源用对应的R.id注册好,运行时通过这些ID从Resource实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到R.id的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的Resource实例中保存的资源ID对不上;</p></li>
</ol>
<p>说到底,抛开虚拟机的差别不说,<strong>一个Android程序和标准的Java程序最大的区别就在于他们的上下文环境(Context)不同</strong>。Android中,这个环境可以给程序提供组件需要用到的功能,也可以提供一些主题、Res等资源,其实上面说到的两个问题都可以统一说是这个环境的问题,而现在的各种Android动态加载框架中,核心要解决的东西也正是“如何给外部的新类提供上下文环境”的问题。</p>
Android动态加载技术 简单易懂的介绍方式
https://segmentfault.com/a/1190000004062866
2015-11-29T01:02:06+08:00
2015-11-29T01:02:06+08:00
Kaede
https://segmentfault.com/u/kaede
19
<p>Last Edit: 2016-2-10</p>
<h2>基本信息</h2>
<ul>
<li><p>Author:<a href="https://link.segmentfault.com/?enc=OTUHHkd1XeJ7G4VIyLIjNA%3D%3D.ubcZy0zB6jBUANdLQ7Uc%2By7hgPiduI%2ByIT5i7vtGafk%3D" rel="nofollow">kaedea</a></p></li>
<li><p>GitHub:<a href="https://link.segmentfault.com/?enc=PA%2B44n9mB8TdbrAH6nbNzg%3D%3D.4Qlpz6LMq0pSH%2Fy1wNJOLWN5ytJ%2Bo9qHotc0hkkZpDOoGaZyWQrckPjQpFijxFBvT3Yrrl6RAFMjempbXBBbuQ%3D%3D" rel="nofollow">android-dynamical-loading</a></p></li>
</ul>
<hr>
<p>我们很早开始就在Android项目中采用了动态加载技术,主要目的是为了达到让用户不用重新安装APK就能升级应用的功能(特别是 SDK项目),这样一来不但可以大大提高应用新版本的覆盖率,也减少了服务器对旧版本接口兼容的压力,同时如果也可以快速修复一些线上的BUG。</p>
<p>这种技术并不是常规的Android开发方式,早期并没有完善的解决方案。从“不明觉厉”到稳定投入生产,一直以来我总想对此编写一些文档,这也是这篇日志的由来,没想到前前后后竟然拖沓着编辑了一年多,所以日志里有的地方思路可能有点衔接得不是很好,如果有修正建议请直接回复。</p>
<h2>技术背景</h2>
<p>通过服务器配置一些参数,Android APP获取这些参数再做出相应的逻辑,这是常有的事情。</p>
<p>比如现在大部分APP都有一个启动页面,如果到了一些重要的节日,APP的服务器会配置一些与时节相关的图片,APP启动时候再把原有的启动图换成这些新的图片,这样就能提高用户的体验了。</p>
<p>再则,早期个人开发者在安卓市场上发布应用的时候,如果应用里包含有广告,那么有可能会审核不通过。那么就通过在服务器配置一个开关,审核应用的时候先把开关关闭,这样应用就不会显示广告了;安卓市场审核通过后,再把服务器的广告开关给打开,以这样的手段规避市场的审核。</p>
<p><strong>道高一尺魔高一丈</strong>。安卓市场开始扫描APK里面的Manifest甚至dex文件,查看开发者的APK包里是否有广告的代码,如果有就有可能审核不通过。</p>
<p>通过服务器怕配置开关参数的方法行不通了,开发者们开始想,“既然这样,能不能先不要在APK写广告的代码,在用户运行APP的时候,再从服务器下载广告的代码,运行,再现实广告呢?”。答案是肯定的,这就是动态加载:</p>
<blockquote><p>在程序运行的时候,加载一些程序自身原本不存在的可执行文件并运行这些文件里的代码逻辑。</p></blockquote>
<p>看起来就像是应用从服务器下载了一些代码,然后再执行这些代码!</p>
<h2>传统PC软件中的动态加载技术</h2>
<p>动态加载技术在PC软件领域广泛使用,比如输入法的截图功能。刚刚安装好的输入法软件可能没有截图功能,当你第一次使用的时候,输入法会先从服务器下载并安装截图软件,然后再执行截图功能。</p>
<p>此外,许多的PC软件的安装目录里面都有大量的DLL文件(Dynamic Link Library),PC软件则是通过调用这些DLL里面的代码执行特定的功能的,这就是一种动态加载技术。</p>
<p>熟悉Java的同学应该比较清楚,Java的可执行文件是Jar,运行在虚拟机上JVM上,虚拟机通过ClassLoader加载Jar文件并执行里面的代码。所以Java程序也可以通过动态调用Jar文件达到动态加载的目的。</p>
<h2>Android应用的动态加载技术</h2>
<p>Android应用类似于Java程序,虚拟机换成了Dalvik/ART,而Jar换成了Dex。在Android APP运行的时候,我们是不是也可以通过下载新的应用,或者通过调用外部的Dex文件来实现动态加载呢?</p>
<p>然而在Android上实现起来可没那么容易,如果下载一个新的APK下来,不安装这个APK的话可不能运行。如果让用户手动安装完这个APK再启动,那可不像是动态加载,纯粹就是用户安装了一个新的应用,然后再启动这个新的应用(这种做法也叫做“静默安装”)。</p>
<p>动态调用外部的Dex文件则是完全没有问题的。在APK文件中往往有一个或者多个Dex文件,我们写的每一句代码都会被编译到这些文件里面,Android应用运行的时候就是通过执行这些Dex文件完成应用的功能的。虽然一个APK一旦构建出来,我们是无法更换里面的Dex文件的,但是我们可以通过加载外部的Dex文件来实现动态加载,这个外部文件可以放在外部存储,或者从网络下载。</p>
<h2>动态加载的定义</h2>
<p>开始正题之前,在这里可以先给动态加载技术做一个简单的定义。真正的动态加载应该是</p>
<ol>
<li><p>应用在运行的时候通过加载一些<strong>本地不存在</strong>的可执行文件实现一些特定的功能;</p></li>
<li><p>这些可执行文件是<strong>可以替换</strong>的;</p></li>
<li><p>更换静态资源(比如换启动图、换主题、或者用服务器参数开关控制广告的隐藏现实等)<strong>不属于</strong> 动态加载;</p></li>
<li><p>Android中动态加载的核心思想是动态调用外部的 <strong>dex文件</strong>,极端的情况下,Android APK自身带有的Dex文件只是一个程序的入口(或者说空壳),所有的功能都通过从服务器下载最新的Dex文件完成;</p></li>
</ol>
<h2>Android动态加载的类型</h2>
<p>Android项目中,动态加载技术按照加载的可执行文件的不同大致可以分为两种:</p>
<ol>
<li><p>动态加载<code>so库</code>;</p></li>
<li><p>动态加载<code>dex/jar/apk文件</code>(现在动态加载普遍说的是这种);</p></li>
</ol>
<p>其一,Android中NDK中其实就使用了动态加载,动态加载<code>.so库</code>并通过JNI调用其封装好的方法。后者一般是由<code>C/C++</code>编译而成,运行在Native层,效率会比执行在虚拟机层的Java代码高很多,所以Android中经常通过动态加载<code>.so库</code>来完成一些对性能比较有需求的工作(比如T9搜索、或者Bitmap的解码、图片高斯模糊处理等)。此外,由于<code>so库</code>是由<code>C/C++</code>编译而来的,只能被反编译成汇编代码,相比中<code>dex文件</code>反编译得到的Smali代码更难被破解,因此<code>so库</code>也可以被用于安全领域。<strong>这里为后面要讲的内容提前说明一下,一般情况下我们是把<code>so库</code>一并打包在APK内部的,但是<code>so库</code>其实也是可以从外部存储文件加载的。</strong></p>
<p>其二,“基于ClassLoader的动态加载<code>dex/jar/apk文件</code>”,就是我们上面提到的“在Android中动态加载由Java代码编译而来的<code>dex</code>包并执行其中的代码逻辑”,<strong>这是常规Android开发比较少用到的一种技术</strong>,目前网络上大多文章说到的动态加载指的就是这种(后面我们谈到“动态加载”如果没有特别指定,均默认是这种)。</p>
<p>Android项目中,所有Java代码都会被编译成<code>dex文件</code>,Android应用运行时,就是通过执行<code>dex文件</code>里的业务代码逻辑来工作的。使用动态加载技术可以在Android应用运行时加载外部的<code>dex文件</code>,而通过网络下载新的<code>dex文件</code>并替换原有的<code>dex文件</code>就可以达到不安装新APK文件就升级应用(改变代码逻辑)的目的。同时,使用动态加载技术,一般来说会使得Android开发工作变得更加复杂,这中开发方式不是官方推荐的,不是目前主流的Android开发方式,<strong>Github</strong> 和 <strong>StackOverflow</strong> 上面外国的开发者也对此不是很感兴趣,外国相关的教程更是少得可怜,目前只有在大天朝才有比较深入的研究和应用,特别是一些SDK组件项目和 <strong>BAT家族</strong> 的项目上,Github上的相关开源项目基本是国人在维护,偶尔有几个外国人请求更新英文文档。</p>
<h2>Android动态加载的大致过程</h2>
<p>无论上面的哪种动态加载,其实基本原理都是在程序运行时加载一些外部的可执行的文件,然后调用这些文件的某个方法执行业务逻辑。需要说明的是,因为文件是可执行的(so库或者dex包,也就是一种动态链接库),出于安全问题,Android并不允许直接加载手机外部存储这类<code>noexec</code>(不可执行)存储路径上的可执行文件。</p>
<p>对于这些外部的可执行文件,在Android应用中调用它们前,都要先把他们拷贝到<code>data/packagename/</code>内部储存文件路径,确保库不会被第三方应用恶意修改或拦截,然后再将他们加载到当前的运行环境并调用需要的方法执行相应的逻辑,从而实现动态调用。</p>
<p>动态加载的大致过程就是:</p>
<blockquote><ol>
<li><p>把可执行文件(.so/dex/jar/apk)拷贝到应用APP内部存储;</p></li>
<li><p>加载可执行文件;</p></li>
<li><p>调用具体的方法执行业务逻辑;</p></li>
</ol></blockquote>
<hr>
<p>以下分别对这两种动态加载的实现方式做比较深入的介绍。</p>
<h2>动态加载 <strong>so库</strong>
</h2>
<p>动态加载<code>so库</code>应该就是Android最早期的动态加载了,不过<code>so库</code>不仅可以存放在APK文件内部,还可以存放在外部存储。Android开发中,更换<code>so库</code>的情形并不多,但是可以通过把<code>so库</code>挪动到APK外部,减少APK的体积,毕竟许多<code>so库</code>文件的体积可是非常大的。</p>
<p>详细的应用方式请参考后续日志 <a href="http://segmentfault.com/a/1190000004062899">Android动态加载补充 加载SD卡的SO库</a>。</p>
<h2>动态加载 <strong>dex/jar/apk文件</strong>
</h2>
<p>我们经常讲到的那种Android动态加载技术就是这种,后面我们谈到“动态加载”如果没有特别指定,均默认是这个。</p>
<h3>基础知识:类加载器ClassLoader和dex文件</h3>
<p>动态加载dex/jar/apk文件的基础是类加载器ClassLoader,它的包路径是<code>java.lang</code>,由此可见其重要性,虚拟机就是通过类加载器加载其需要用的Class,这是Java程序运行的基础。</p>
<p>关于类加载器ClassLoader的工作机制,请参考 <a href="http://segmentfault.com/a/1190000004062880">Android动态加载基础 ClassLoader的工作机制</a>。</p>
<p>现在网上有多种基于ClassLoader的Android动态加载的开源项目,大部分核心思想都殊途同归,按照复杂程度以及具体实现的框架,大致可以分为以下三种形式,或者说模式 <a><sup>[1]</sup></a>。</p>
<h3>简单的动态加载模式</h3>
<p>理解ClassLoader的工作机制后,我们知道了Android应用在运行时使用ClassLoader动态加载外部的dex文件非常简单,不用覆盖安装新的APK,就可以更改APP的代码逻辑。但是Android却很难使用插件APK里的res资源,这意味着无法使用新的XML布局等资源,同时由于无法更改本地的Manifest清单文件,所以无法启动新的Activity等组件。</p>
<p>不过可以先把要用到的全部res资源都放到主APK里面,同时把所有需要的Activity先全部写进Manifest里,只通过动态加载更新代码,不更新res资源,如果需要改动UI界面,可以通过使用纯Java代码创建布局的方式绕开XML布局。同时也可以使用Fragment代替Activity,这样可以最大限度得避开“无法注册新组件的限制”。</p>
<p>某种程度上,简单的动态加载功能已经能满足部分业务需求了,特别是一些早期的Android项目,那时候Android的技术还不是很成熟,而且早期的Android设备更是有大量的兼容性问题(做过Android1.6兼容的同学可能深有体会),只有这种简单的加载方式才能稳定运行。这种模式的框架比较适用一些UI变化比较少的项目,比如游戏SDK,基本就只有登陆、注册界面,而且基本不会变动,更新的往往只有代码逻辑。</p>
<p>详细的应用方式请参考后续日志 <a href="http://segmentfault.com/a/1190000004062952">Android动态加载入门 简单加载模式</a>。</p>
<h3>代理Activity模式</h3>
<p>简单加载模式还是不够用,所以代理模式出现了。从这个阶段开始就稍微有点“黑科技”的味道了,比如我们可以通过动态加载,让现在的Android应用启动一些“新”的Activity,甚至不用安装就启动一个“新”的APK。宿主APK<a><sup>[2]</sup></a>需要先注册一个空壳的Activity用于代理执行插件APK的Activity的生命周期。</p>
<p>主要有以下特点:</p>
<ol>
<li><p>宿主APK可以启动未安装的插件APK;</p></li>
<li><p>插件APK也可以作为一个普通APK安装并且启动;</p></li>
<li><p>插件APK可以调用宿主APK里的一些功能;</p></li>
<li><p>宿主APK和插件APK都要接入一套指定的接口框架才能实现以上功能;</p></li>
</ol>
<p>同时也主要有一下几点限制:</p>
<ol>
<li><p>需要在Manifest注册的功能都无法在插件实现,比如应用权限、LaunchMode、静态广播等;</p></li>
<li><p>宿主一个代理用的Activity难以满足插件一些特殊的Activity的需求,插件Activity的开发受限于代理Activity;</p></li>
<li><p>宿主项目和插件项目的开发都要接入共同的框架,大多时候,插件需要依附宿主才能运行,无法独立运行;</p></li>
</ol>
<p>详细的应用方式请参考后续日志 <a href="http://segmentfault.com/a/1190000004062972">Android动态加载进阶 代理Activity模式</a>。</p>
<p>代理Activity模式的核心在于“使用宿主的一个代理Activity为插件所有的Activity提供组件工作需要的环境”,随着代理模式的逐渐成熟,现在还出现了“使用Hack手段给插件的Activity注入环境”的模式,这里暂时不展开,以后会继续分析。</p>
<p>我们目前有投入到生产中的开发方式只有简单模式和代理模式,在设计的前期遇到不少兼容性的问题,不过好在Android 4.0以后的机型上就比较少了。</p>
<h3>动态创建Activity模式</h3>
<p>天了噜,到了这个阶段就真的是“黑科技”的领域了,从而使其可以正常运行。可以试想“从网络下载一个Flappy Bird的APK,不用安装就直接运行游戏”,或者“同时运行两个甚至多个微信”。</p>
<p>动态创建Activity模式的核心是“运行时字节码操作”,现在宿主注册一个不存在的Activity,启动插件的某个Activity时都把想要启动的Activity替换成前面注册的Activity,从而是后者能正常启动。</p>
<p>这个模式有以下特点:</p>
<ol>
<li><p>主APK可以启动一个未安装的插件APK;</p></li>
<li><p>插件APK可以是任意第三方APK,无需接入指定的接口,理所当然也可以独立运行;</p></li>
</ol>
<p>详细的应用方式请参考后续日志 <a href="http://segmentfault.com/a/1190000004077469">Android动态加载黑科技 动态创建Activity模式</a>。</p>
<h2>为什么我们要使用动态加载技术</h2>
<p>说实话,作为开发我们也不想使用的,这是产品要求的!(警察蜀黍就是他,他只问我能不能实现,并木有问我实现起来难不难……好吧我们知道他们也没得选。)</p>
<p>Android开发中,最先使用动态加载技术的应该是SDK项目吧。现在网上有一大堆Android SDK项目,比如Google的Goole Play Service,向开发者提供支付、地图等功能,又比如一些Android游戏市场的SDK,用于向游戏开发者提供账号和支付功能。和普通Android应用一样,这些SDK项目也是要升级的,比如现在别人的Android应用里使用了我们的SDK1.0版本,然后发布到安卓市场上去。现在我们发现SDK1.0有一些紧急的BUG,所以升级了一个SDK1.1版本,没办法,只能让人家重新接入1.1版本再发布到市场。万一我们有SDK1.2、1.3等版本呢,本来让人家每个版本都重新接入也无可厚非,不过产品可关心体验啊,他就会问咯,“虽然我不懂技术,但是我想知道有没有办法,能让人家只接入一次我们的SDK,以后我们发布新的SDK版本的时候他们的项目也能跟着自动升级?”,答曰,“有,使用动态加载的技术能办到,只不过(开发工作量会剧增…)”,“那就用吧,我们要把产品的体验做到极致”。</p>
<p>好吧,我并没有黑产品的意思,现在团队的产品也不错,不过与上面类似的对话确实发生在我以前的项目里。这里提出来只是为了强调一下Android项目中采用动态加载技术的 <strong>作用</strong> 以及由此带来的 <strong>代价</strong>。</p>
<h2>作用与代价</h2>
<p>凡事都有两面性,特别是这种 <strong>非官方支持</strong> 的 <strong>非常规</strong> 开发方式,在采用前一定要权衡清楚其作用与代价。如果决定了要采用动态加载技术,个人推荐可以现在实际项目的一些比较独立的模块使用这种框架,把遇到的一些问题解决之后,再慢慢引进到项目的核心模块;如果遇到了一些无法跨越的问题,要有能够迅速投入生产的替代方案。</p>
<h3>作用</h3>
<ol>
<li><p>规避APK覆盖安装的升级过程,提高用户体验,顺便能 <strong>规避</strong> 一些安卓市场的限制;</p></li>
<li><p>动态修复应用的一些 <strong>紧急BUG</strong>,做好最后一道保障;</p></li>
<li><p>当应用体积太庞大的时候,可以把一些模块通过动态加载以插件的形式分割出去,这样可以减少主项目的体积,<strong>提高项目的编译速度</strong>,也能让主项目和插件项目并行开发;</p></li>
<li><p>插件模块可以用懒加载的方式在需要的时候才初始化,从而 <strong>提高应用的启动速度</strong>;</p></li>
<li><p>从项目管理上来看,分割插件模块的方式做到了 <strong>项目级别的代码分离</strong>,大大降低模块之间的耦合度,同一个项目能够分割出不同模块在多个开发团队之间 <strong>并行开发</strong>,如果出现BUG也容易定位问题;</p></li>
<li><p>在Android应用上 <strong>推广</strong> 其他应用的时候,可以使用动态加载技术让用户优先体验新应用的功能,而不用下载并安装全新的APK;</p></li>
<li><p>减少主项目DEX的方法数,<strong>65535问题</strong> 彻底成为历史(虽然现在在Android Studio中很容易开启MultiDex,这个问题也不难解决);</p></li>
</ol>
<h3>代价</h3>
<ol>
<li><p>开发方式可能变得比较诡异、繁琐,与常规开发方式不同;</p></li>
<li><p>随着动态加载框架复杂程度的加深,项目的构建过程也变得复杂,有可能要主项目和插件项目分别构建,再整合到一起;</p></li>
<li><p>由于插件项目是独立开发的,当主项目加载插件运行时,插件的运行环境已经完全不同,代码逻辑容易出现BUG,而且在主项目中调试插件十分繁琐;</p></li>
<li><p>非常规的开发方式,有些框架使用反射强行调用了部分Android系统Framework层的代码,部分Android ROM可能已经改动了这些代码,所以有存在兼容性问题的风险,特别是在一些古老Android设备和部分三星的手机上;</p></li>
<li><p>采用动态加载的插件在使用系统资源(特别是Theme)时经常有一些兼容性问题,特别是部分三星的手机;</p></li>
</ol>
<h2>其他动态修改代码的技术</h2>
<p>上面说到的都是基于ClassLoader的动态加载技术(除了加载SO库外),使用ClassLoader的一个特点就是,如果程序不重新启动,加载过一次的类就无法重新加载。因此,如果使用ClassLoader来动态升级APP或者动态修复BUG,都需要重新启动APP才能生效。</p>
<p>除了使用ClassLoader外,还可以使用jni hook的方式修改程序的执行代码。前者是在虚拟机上操作的,而后者做的已经是Native层级的工作了,直接修改应用运行时的内存地址,所以使用jni hook的方式时,不用重新应用就能生效。</p>
<p>目前采用jni hook方案的项目中比较热门的有阿里的dexposed和AndFix,有兴趣的同学可以参考 <a href="https://link.segmentfault.com/?enc=pfiY53cg0aLzgweX57lXoA%3D%3D.FKi%2BwGtkKWs0%2FJd9mdzk99KpG%2BkzPl6TrCFk2HK9JD8mHSvFmb6HquCGwAv5%2FluajQ1O9IMR58FoSl%2BRyC62XQ%3D%3D" rel="nofollow">各大热补丁方案分析和比较</a>。</p>
<h2>动态加载开源项目</h2>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=3Mp8p0vrlTzWuQibWf%2FNrQ%3D%3D.FJ6AUnMv3YUViGShTD1Nhhz6E0XXVQ0N7ct4hlomZTymApOu1gOyTNfbO0kfJEOd" rel="nofollow">ACDD</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=ciDLoiNiQDxw8VqC5BSRbg%3D%3D.d6%2BoBBd6WnDv84LfWJDzNguG%2F9Zs0g2u4oDJztgJeJRkzULiEnGioNkOUZ6P3fEtvNnZH%2BtaVKvNCqdvPE8gpQ%3D%3D" rel="nofollow">DL dynamic-load-apk</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=8POFpzvPYOyUXLDbbz5IeQ%3D%3D.G30lig3doWJwuBVLpUEmivoi%2Fj375RwbSX%2BYqeu5QT%2FGkzP1f3zi4oez3iENgXV%2B" rel="nofollow">android-pluginmgr</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=cKbc9D%2FELCiugQCihtxPlw%3D%3D.kVG35T6dVt6U7qL%2BsiF2DgON6D4n%2B1sKP26UcaricP9Css%2FlMOnxwRLE9lbhT2je" rel="nofollow">Direct-Load-apk</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=XL251L1YD906dNsYx1GODw%3D%3D.F%2F35sLhuJmXdmEQa8bNhMH9E%2FmAGt2Ofe8IldAeOlZ6RW%2For2PQwFraR8svK36%2Fk" rel="nofollow">360 DroidPlugin</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=tSObI2%2BhVo0BgQn1LFNr5w%3D%3D.HyAV7Cd99YMsTTD40ViND33npblhmBTRw7ittZ5O4n5X3law4ejzuZZwVQaZjL9P" rel="nofollow">携程网 DynamicAPK</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=RqjpPSjdLEtD2nHks3UHFw%3D%3D.rZqzK%2F0OizKuBuiiXDPC5Jc%2BErdSXhCFfzyg%2FLIJS1aDod%2Fo9ue1Y0GDpBWLWcd7" rel="nofollow">女娲 Nuwa</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=7TOdtBSdNxuSsMGFZPuhfA%3D%3D.xnJ6%2FMqjXrLRyWPGJCTOawojloBA06R0PCVRhWbjiIG1mddLfjw0aXQFQJ6jYKC6sRCEmfn65kF5%2B18hJ0Xcug%3D%3D" rel="nofollow">Android-Plugin-Framework</a></p></li>
</ul>
<h2>脚注</h2>
<p><a>[1]</a> 其实也说不上什么模式,这不过这些动态加载的开发方式都有自己明显的特征,所以姑且用“形式或者模式”来称呼好了。</p>
<p><a>[2]</a> 为了方便区分概念,阐述一些术语:<br>宿主:Host,主项目APK、主APK,也就是我们希望采用动态加载技术的主项目;<br>插件:Plugin,可以是dex、jar或者apk文件,从主项目分离开来,我们能通过动态加载加载到主项目里面来的模块,一个主APK可以同时加载多个插件APK;</p>
Android APP性能调优 一本正经的胡说八道的前言
https://segmentfault.com/a/1190000003966950
2015-11-08T18:34:28+08:00
2015-11-08T18:34:28+08:00
Kaede
https://segmentfault.com/u/kaede
1
<p>“一本正经地胡说八道”用日语怎么说?大概是「真面目にふざけている」吧。这篇日志大概就是这么一个意思?</p>
<p>一直以来都想对Android APP开发的性能调优做一下总结,其实性能调优涉及到多方面的工作,每次有一些心得我都会记录下来,零零散散记录了很多,最近发现许多地方重复了,感觉还是得做一下整理的,知识就是这么牢固起来的。</p>
<p>“APP卡顿”是一个问题,我们既需要知道怎么查找出哪里造成卡顿,也需要掌握规避这些卡顿的技巧,所以这个话题可以分为“如何定位APP中的性能问题”和“提高性能需要注意哪些点”这两部分,后续在陆续对这两点展开讨论吧,今天先从整体分析下问题存在的原因。</p>
<p>开始正题之前先让我吐一吐苦水吧。</p>
<p>我个人喜欢日语,所以学了很久的日语了,同样我也是因为喜欢Android,才开始跳进了Android开发这个坑。不过非常遗憾的是,就和“当初我在日语班里,周围大部分人只是因为日语专业比较轻松才选了它”一样,我周围的Android研发同事大部分用的是IOS手机,甚至有人问过我“我说你工资也不至于那么低吧,怎么每天都拿着安卓手机”。产品则是每次都说“你这个交互和IOS的不一样,我需求文档里写得清清楚楚,要保持一致的用户体验”。设计的话,我从来就没有遇见过拿安卓手机的。</p>
<p>我尝试说过Android不比IOS差,但是没人站在我这边。</p>
<p>自从Android系统诞生以来一直都有一个疑问,“为什么Android手机这么卡?”,无论Android设备的硬件再怎么升级,版本再怎么迭代,“Android比IOS卡顿”似乎成为一个板上钉钉的事实。Android手机真的卡么?</p>
<p>至少在Android Kitkat之前,许多Android开发者都会选择回避这个问题;Kitkat之后,有一部分开发者已经有底气回答这个问题了;随着Android Lollipop以及Marshmallow先后的出现,我觉得Android开发者都可以自信地回答说“Android不卡”了。</p>
<p>在一个公司内部分享会中,国内Android领域的大神罗升阳在分享他对ART模式的研究中说到,继Kitkat采用了ART模式后,Lollipop中,除了UI线程之外又提供了一条专门用于执行动画的线程,这样一来UI线程就可以专注于展示数据和响应用户的输入,这让Android的流畅度又上了一个档次。IOS是闭源的所以我们无从得知,但是有人分析说IOS之所以这么流畅很大一个原因就是它大概也采用了类似动画线程的机制。老罗也相信“Android系统不会比IOS卡,甚至已经比它还流畅”。</p>
<p>这可不是随便说说,我的Nexus5升级到Lollipop之后我觉得它已经可以匹敌同事的IPhone6了,前不久升级到Marshmallow内测版,我更是觉得它已经拉开IPhone6一个档次了。</p>
<p>那为什么许多人觉得Android用起来还是比IOS卡?注意老罗说“Android系统”比IOS流畅,而不是“Android应用”,言下之意就是Android系统本身不卡,卡的是设备开发商开发的ROM以及开发者开发的第三方APP。我个人觉得Android卡顿问题大致有以下的原因。</p>
<h2>客观原因</h2>
<h3>Android设备系统升级速度慢</h3>
<p>现在Lollipop甚至Kitkat的普及率还不算很高,更别谈Marshmallow了,这是Android碎片化的问题决定的,具体原因有兴趣的可以自己Google,网上一堆比我在这里吹的靠谱多的分析。</p>
<p>当然这里也有很大一部分是设备开发商的锅,Google开源的AOSP项目只能兼容Nexus系列手机的硬件,如果第三方设备开发商需要使用AOSP的话,最起码也要把AOSP里面的驱动部分改成能兼容自己的设备的,此外,他们还喜欢把系统UI风格做成自己家的,这起码也要自己写一个Launcher应用和自定义主题(这也是许多Android ROM被吐槽成换皮肤的原因);此外有一些特色功能,比如指纹设备,Android Marshmallow之前AOSP并没有这个功能,所以开发商就得自己出解决方案了。这一系列的工作,造成了开发商无法在Google发布Android的新版本之后迅速升级自家Android设备的系统。</p>
<h3>Android APP需要做过多旧版本的兼容</h3>
<p>Android4.0版本相比之前的版本性能上优化了许多,无奈我接触的Android产品都要求最低支持Android2.3,有一款SDK甚至要求支持到Android1.6而且不能使用Support库(今年可是2015年!你能想象一个手动写Thread去控制一个复杂的属性动画的效果有多糟糕吗?),所以Android开发者需要做一大堆向下兼容的工作,有时候为了保证在一些奇葩机型的兼容性,选择了保守的实现方式而不采用Android的新特性。向下兼容的逻辑使得APP即使在高级版本的Android系统上也要跑一堆没用的判断逻辑,如果这些逻辑出现在循环体内则更糟糕;采用保守的实现方式,使得APP无法发挥新版本Android的性能,即使用户手机升级了Android系统版本也享♂受新版本带来的体验。此外,即使许多新的APP产品都选择最低支持Android4.0,这使得许多新特性都不用Support库支持就能直接使用,但是许多手机设备还是无法升级到Android4.4版本的系统,即使有,很多ROM还是把ART模式给严格了,无法体验其脱胎换骨版的顺畅。</p>
<h3>大量采用“黑科技”</h3>
<p>最近支付宝被Google Play下架了,原因就是其“从Play市场以外的服务器下载可执行代码”,意思就是它使用了动态加载技术。Android的动态加载也不是新鲜的事物了,我的项目中也采用过,简单来说原理就是Android APP采用“APK空壳+可执行代码”的开发方式,APK只是一个空壳,用户安装过一次后就不用重新安装了,如果需要升级APP,APK空壳会从服务器下载新的可执行代码,更换本地的即可完成升级(具体实现方案现在网上一堆教程,也可以参考我Github上的相关项目)。采用这种开发方式,开发者可以迅速完成用户安装好的APP的升级,在某种意义上提高了用户的体验,但是开发者的开发方式也会变得比较“绕”,开发成本增加了不少,为了保证兼容性也会放弃使用Android的一些无法在动态加载框架上使用的功能,所以体验往往比不上“正统”的Android开发方式。</p>
<h3>常驻内存、全家桶以及互相唤醒</h3>
<p>缺少了Google Play的约束,一些Android APP就变得肆意妄为了,这一点在BAT系中显得格外明显。常驻内存就接受到服务器的推送信息的成功率就比较高了,用户明明关了一些APP,但是它们就不想退出,就算我们手动关闭了它们,一旦重新启动、网络变化等,它们就又重新启动了,占着本来就珍贵的手机内存,很快就不够用了,这也是Android卡顿的一大原因。“百度全家桶”你怕不怕?,一旦安装了百度家族启动的一个APP,就会偷偷帮你安装上全家族的APP,就算是我这种做Android研发的都经常中招,不用说普通的用户了。更可恨的是这些APP还会互相唤醒,Andriod Lollipop之后你可以彻底关闭一个APP,除非你手动启动它否则它无法自启动,但是家族APP之间互相唤醒使得这成为了可能,“绿色守护”等后台清理神器在“互相唤醒”大法之下也没辙了。</p>
<h3>H5内容、硬生生把Android应用做成IOS应用等等</h3>
<p>Android原生与H5界面交互的框架已经很成熟了,许多中大型的APP的有H5的界面,特别是淘宝、天猫、京东这种购物类的,这些APP有大量和时间相关的活动,在节日之前开发一个新版本的APP再发布到应用市场显得太蠢了,H5网页这种随时可以更新在线内容的技术最适合这种活动了。然而H5界面的性能在还是肛不过原生界面,特别是各种黑科技附身的Lollipop之后的时代,特别是当一个界面有H5的东西也有原生的东西那就更加糟糕了。此外,国内设计师都喜欢把Android的美术图设计得和IOS一样,这里不讨论这样做的原因,然而结果就是Android的开发需要使用大量的自定义View,这样一来,许多系统自带控件的加速效果(特别是当用到系统公共资源的时候)就没有什么卵用了,而且往往这些自定义View都有或多或少的Bug,可能设计得不合理,也可能有内存泄露,这些都会影响APP的性能。</p>
<h2>主观原因</h2>
<p>上面说的客观原因除了由于Android的碎片化问题之外,其实有很大部分是开发者有意为之,但是我相信这并不是Android开发的责任,我们是都是清白的,都是无奈的,错的不是我们,是世界啊!╮( ̄▽ ̄")╭。也不是产品经理的锅,而是大环境造就的。国内互联网的竞争堪比电商,不这么做,不抢占用户的手机的话根本就无法生存,就算百度不去做,阿里也会去……所以勉强可以把这些归类为客观因素。但是瞎BB了这么多,都不是我想说的主要内容,我想许多人都不感兴趣,我只是一时来兴致了敲了这么多字,感觉如果不贴出来的话不就亏了嘛。<br>…………<br>……<br>…</p>
<p>既然你都看到这了,顺便把主观原因部分也看完吧_(-ω-`_)⌒)_。</p>
<p>主观原因主要是由于开发过程中的过错导致的,总体上来说大致有以下几方面的原因</p>
<h3>没做好异步</h3>
<p>APP要流畅的话就好让它保持高度相应,CPU要能及时响应UI线程的操作。不过不可能把所有非UI的工作统统扔到异步任务里面去,有时候一些工作直接在UI线程搞会非常方便,而且太多后台线程也会造成大量的性能开销。这是一个矛盾,这时候就需要成熟的异步任务框架咯,如果这个框架没写好的话,就可能导致后台任务混乱,产生多余的线程开销,UI线程得不到及时的响应,更甚,如果有异步任务造成内存泄露,内存不够用很快就卡顿了,甚至直接OOM嗝屁了。</p>
<h3>内存泄露以及GC</h3>
<p>内存是非常宝贵的,再多也不嫌多。所以当一个对象离开他的作用域后,我们一定立刻回收它占有的内存,最理想的状态是对每一个对象都能做到这样,如果有一个对象做不到,就说明他泄露了。Android中,Activity对象以及Bitmap的像素数据往往占用非常大的内存,如果这两者发生泄漏会导致可用内存急剧减少,那卡顿则就无法避免。此外,即使没有严重的内存泄露,但是频繁创建对象和回收对象的话,会引发虚拟机频繁的GC,GC占用比较大的资源开销,同样也可能会导致卡顿。总的来说,开发过程中要对内存的使用保持敏感,知根知底。</p>
<h3>没做好界面的布局优化</h3>
<p>设计师给出的同一张美术图可以有多张的布局实现,不同方案之间的性能可能相差很远。ListView等列表控件的Adapter也有许多优化点,许多人容易疏忽。</p>
<h3>没做好代码优化</h3>
<p>这就不只是Android领域的问题啦,某些特定的业务应该采用最优算法,把时间复杂度降到最低,尽量避免指数级别的时间复杂度,尽量以空间换时间,必要的时候使用Native库来提高算法。</p>
<h3>其他</h3>
<p>一开始我也提到了,其实性能调优涉及到多方面的工作,比如一些静态的网络资源要做好缓存不要重复请求;频繁数据库操作的话最好使用异步任务,SQLite默认实在UI线程直接操作数据库的;使用反射也会比较性能,不过反射有时候确实挺方便的,特别是项目庞大的时候,这个看取舍;过度使用“设计模式”也会有额外的开销,设计意味着更多接口和多态,业务跑起来需要额外的空间和时间;尽量不要开多进程,进程之间的通讯比同一进程之间的互调消耗的性能非常多,一般项目只要一个进程就够了,有推送的可以多一个推送进程;此外,不限制与Android客户端开发,H5、服务器的优化也能提高APP的性能。</p>
<h3>性能问题的排查方法</h3>
<p>这个在后面的具体分析中,再结合实际遇到的问题一起介绍吧。</p>
<p>关于性能优化的技术点,欢迎大家到这个日志里补充:<a href="http://segmentfault.com/a/1190000003965131">[收集向] Android 性能调优的技术点</a></p>
分享一些流畅的适合开发的 Android 模拟器
https://segmentfault.com/a/1190000003966493
2015-11-08T16:22:56+08:00
2015-11-08T16:22:56+08:00
Kaede
https://segmentfault.com/u/kaede
5
<p>“工欲善其事,必先利其器。” 使用<code>Android模拟器</code>开发和调试应用肯定比使用真机方便。但相比<code>XCODE</code>的<code>IOS模拟器</code>,Android SDK自带的<code>AVD</code>实在不争气,不过一些第三方的模拟器却表现不俗!</p>
<p>12年我开始接触Android开发时候,手头上甚至连一部低端的Android手机都没有,那时候用的是Android SDK自带的AVD模拟器,相信任何Android开发者都对这货深恶痛绝。一直以来,Android开发都有以下的毛病:</p>
<ul>
<li><p>AVD模拟器奇卡无比;</p></li>
<li><p>使用USB数据线链接手机经常无法设别设备,adb容易抽风;</p></li>
<li><p>Log日志输出不全;</p></li>
</ul>
<p>一直以来都想找一款能够顺畅运行APP的Android模拟器,以下就介绍几款比较给力的。</p>
<h2>大名鼎鼎的 <a href="https://link.segmentfault.com/?enc=xT1yZn%2FYbcnoSsOoenEh6w%3D%3D.mxqcv%2BKG9sP6BZjdh2OIy2tpmE%2FrGLe6cpAhOzEBsbk%3D" rel="nofollow">Genymotion</a>
</h2>
<p><a href="https://link.segmentfault.com/?enc=vtHhRpaYzQPxxnmgOuKI2g%3D%3D.00huXs9k6EyJwSOMWG2Z087OFYfsXLVNR%2BYy4dRoTik%3D" rel="nofollow">Genymotion</a>是一款顺畅和容易(fast and easy-to-use)使用的Android模拟器,可以用来运行和调试你的APP。<a href="https://link.segmentfault.com/?enc=bMoXr2Zvzcsy8gB2yKR7Uw%3D%3D.Kg4%2FWip89YVEku2CdchbvV573%2FqLt82dt%2FBe6ZPyH7U%3D" rel="nofollow">Genymotion</a> 来自于<a href="https://link.segmentfault.com/?enc=qrdwJ2yiHkxn98pQlVyEUw%3D%3D.wzvjQNEj5%2BE6p%2FMF6QaHw9yLcGptjoEBKZ7Xn71CwHE%3D" rel="nofollow">AndroVM</a> 这个开源项目,基于 x86 和 <a href="https://link.segmentfault.com/?enc=KtRCsWWxgkSeHvuNRcq%2F6g%3D%3D.OvfO9yVdC%2FDAWLiRT8hsLQhTYjy5wtW4qddqmI%2FgPnA%3D" rel="nofollow">VirtualBox</a>,支持 OpenGL 加速,可以用于 <code>Mac/Win/Linux</code>。最近发布了新版,支持了 Android2.3/4.3,新增了拖拽安装 apk,移除了 Google 市场(后面提供解决方案)。另外增加了功能更丰富的<a href="https://link.segmentfault.com/?enc=In3pScqN9CSPKapcXY9avw%3D%3D.IlMXH4k5SUzsFLViUllldT9h22qaUlyIZLQ7u5VM1y0%2BKqelSgyawq%2FajGULiFRoqaRnYAljda70hoWcRgFuxA%3D%3D" rel="nofollow">付费版</a>,个人可以继续使用免费版。</p>
<h3>特点</h3>
<ol>
<li><p>超级流畅;</p></li>
<li><p>支持拖拽安装APK;</p></li>
<li><p>有多种Android系统版本和设备类型供选择;</p></li>
<li><p>能模拟手机的旋转、充电情况、GPS数据等物理数据;</p></li>
</ol>
<h3>如何使用</h3>
<p>简单介绍下如何获取和使用 Genymotion:</p>
<ol>
<li><p><a href="https://link.segmentfault.com/?enc=otOsufvQf5FEUwx9Roq9mg%3D%3D.K8rr33y8lJTUaK4jNOmQ%2FNOzZ9EpXF4rrUHAgXIOW0TeLNLwx1Z4nNRBQGbD39%2Fl" rel="nofollow">下载</a>并安装 VirtualBox(或者下载带有VirtualBox的Genymotion);</p></li>
<li><p><a href="https://link.segmentfault.com/?enc=Z1dANQOQMUDMGickL2EyjA%3D%3D.abrI%2B5mp%2FPObKx%2FDogsi5PFEVM7td6zED3U6eK77SbHAqf4ehaFCzCj1w5YGdafs" rel="nofollow">注册</a> Genymotion 帐号并登录;</p></li>
<li><p>根据自己的系统<a href="https://link.segmentfault.com/?enc=E1lqjNEMRA%2Fig2V0hWjvew%3D%3D.BmD38YT2%2F%2FZ5O29d89pVTjuvXhb1Lp5%2FyAdg47okh%2BKPySAGGVSSYOtiJNcZrWci" rel="nofollow">下载并安装</a> Genymotion;</p></li>
</ol>
<p>启动Genymotion</p>
<p><img src="/img/bVqOd0" alt="启动Genymotion" title="启动Genymotion"></p>
<p>添加设备</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-7/81557460.jpg" alt="添加设备" title="添加设备"></p>
<p>启动设备</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-7/44012361.jpg" alt="启动设备" title="启动设备"></p>
<p>免费版跟收费版功能的区别</p>
<p><img src="/img/bVqOd4" alt="免费版跟收费版功能的区别" title="免费版跟收费版功能的区别"></p>
<p>此外,Genymotion还提供了<code>Eclipse</code>和<code>Intellij Idea(Android Studio)</code>的插件,方便你从IDE启动模拟器,不过目前插件的功能也仅仅是用于启动模拟器。</p>
<p>当然Genymotion也不是万能的,它也有一些不足之处。</p>
<h3>Genymotion无法启动</h3>
<p><code>Window</code>版本的<code>Genymotion</code>与<code>VirtualBox</code>的链接经常出问题,Genymotion经常无法启动,并提示VirtualBox引擎出错,关于Genymotion安装以及启动过程中出现的问题,你可以参考<a href="https://link.segmentfault.com/?enc=firpWowpkmf6l5wK2Ux5Hw%3D%3D.jtzSuw1ISQiSLQTKs%2BDbOTaoO4te8tuV7i92twW9ZcVJPm%2FpxZ1S7ooS8fwm88is" rel="nofollow">官方的帮助文档</a>。</p>
<h3>Genymotion无法安装Google Play</h3>
<p>前面说过,新版 Genymotion 移除了 Google 市场。实际上,对 ARM library 的支持也一并移除了:</p>
<blockquote><p>Both the “Google apps” and the “ARM library support” features are removed.</p></blockquote>
<p>有的APP用到了ARM的SO库,安装这些 App 时,会报<code>「INSTALL_FAILED_CPU_ABI_INCOMPATIBLE」</code>错误,比如微信。<a href="https://link.segmentfault.com/?enc=JmRxcdkrCUDD%2BmdQY0b0iw%3D%3D.PqrJEOscXIe12ghB351064eKlOlGAMuFV7rLbLpWMeaSLR9hKalCw%2FSd1vAeNjjpRk99TUWZcP9r2PuPMdBsSQ%3D%3D" rel="nofollow"><code>xda 论坛</code></a>给出了一个解决方案,经验证确实好用。<br>安装 GApps(含 Google 市场)和 ARM Translation(提供 ARM 支持)的步骤:</p>
<ol>
<li><p>下载 <a href="https://link.segmentfault.com/?enc=Pn7FUbi8D8wwieqrAoUUuA%3D%3D.E6nea0gNjcb0%2BU72IGdq8V2sMHJI5v3m5JJBpePGfDqiH%2BRfOLyTs01Kwu2fmgUSHHzKkB9QKSLe2jgcsaccwZuQ9CQRJ8n8MnXa%2BZWCOyQ%3D" rel="nofollow">ARM Translation Installer v1.1</a>;</p></li>
<li><p>下载对应系统的 <a href="https://link.segmentfault.com/?enc=63a1HXXMsQ5HLAdq4bCxKA%3D%3D.vk%2BCmxgh4ktpS3Sd%2B%2F0AbdUJ5ZkgED0FXUelPNzO%2BEkweTWS1RtaKXMiC10ZI4HfYG1%2B6l3wH5qTCSaG%2BgDm4RNz0jifU5TNmDY7dga3N2c%3D" rel="nofollow">GApps</a>;</p></li>
<li><p>安装第 1步下载到的文件(直接把 zip 文件拖进虚拟机,不要解压),安装完关闭虚拟机再打开;</p></li>
<li><p>安装第 2 步下载到的文件(步骤同上);</p></li>
</ol>
<p>这样,Google Play 和其他 Google App 都有了,再安装微信等应用也不会报错了。(但是此方法并不是对所有的APP都管用, <code>Genymotion对使用了ARM的SO库的APP的支持确实不好</code>,希望以后能改进)。</p>
<h2>电脑上也可以玩Android游戏的<a href="https://link.segmentfault.com/?enc=bOV4ZT4OYsSBhfx4GzUOQA%3D%3D.Y3QF%2FNVXW%2BgsB1mMUcRSyN9q2NDsRe50WbSNDh%2Frzbk%3D" rel="nofollow">BlueStacks</a>
</h2>
<p>Android 第一個第三方的模拟器就是 <code>Bluestacks</code>,网络上也有許多介绍文章。最大优势是占用资源小,安装包用量大约是 182 MB 左右,同样有 Windows / Mac 版、内置Google Play 商店。</p>
<h3>如何使用</h3>
<p>首先,xp用户需先安装<a href="https://link.segmentfault.com/?enc=BTo6Q2IsxSen7v8ktc1A6g%3D%3D.CfJ4VXQOGVdlixbrN4OfRBEo41%2FtflI4GWgg1L8%2F39yABCD6HsRB4PryYsq%2BIc%2Bh" rel="nofollow">Windows Installer 4.5</a>和<a href="https://link.segmentfault.com/?enc=DxCqs0SZ8t2m9VMtIm81UA%3D%3D.%2Fdh%2BASf29PNwfPTW0l40ubfzogivzlpdEDM1x8WjnPBpzLccp2s2q7qB4piB9vOV" rel="nofollow">.NET Framework 2.0 SP2</a>,否则会提示出错,我们这里也提供了下载,如果电脑上已经安装过这些软件可以跳过此步。然后到官网<a href="https://link.segmentfault.com/?enc=pf304apdy88diz297zbtlg%3D%3D.5H5sN4LAumq8Gi5o2enxFAhDMrKgtJTiiDSf5hIgF3ojYsUj7CgrzwcHzChTrr%2Ft" rel="nofollow">下载</a>最新的安装包并安装。</p>
<p>安装</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-7/7996490.jpg" alt="" title=""></p>
<p>启动模拟器,搜索应用并安装</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-7/45650691.jpg" alt="" title=""></p>
<p>运行APP</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-7/66469231.jpg" alt="" title=""></p>
<h3>不足之处</h3>
<p><code>Bluestacks</code>相比<code>Genymotion</code>,不容易出现无法启动的问题,也支持ARM Library,但不足之处也是明显的:</p>
<ol>
<li><p>流畅度不如<code>Genymotion</code>;</p></li>
<li><p>没有多种Android系统以及设备型号供选择;</p></li>
<li><p>最致命的,<code>Bluestacks</code>是为了游戏而不是为了开发而设计的,所以无法竖屏,不适合开发 ;</p></li>
</ol>
<h2>最适合开发的Android模拟器<a href="https://link.segmentfault.com/?enc=6z3OY5Q5OLJrUzvuzFqdCA%3D%3D.fqjVfAkXQiUiVnjhMM8JWmquSto53TZGPIGgnODl1Bs%3D" rel="nofollow">Droid4X</a>
</h2>
<p>正如官网所介绍的,海马玩模拟器(Droid4X)是迄今为止在性能,兼容性和操控体验方面最好的安卓模拟器。通过<code>Droid4X</code>,用户可以在PC上享受百万移动应用和游戏带来的全新体验。</p>
<p>海马玩模拟器在Android内核和图形渲染方面取得了突破性的成果,在同等PC硬件配置下,整体性能超出其他同类产品50%以上。海马玩模拟器美解决了ARM程序在X86架构下的运行问题,兼容市面现有99%以上的应用和游戏。</p>
<p><code>Droid4X</code>模拟器是利用<code>VirtualBox</code>为基础,支持滑动按键,自带ROOT权限,启动速度快等等。相信很多朋友使用传统安卓模拟器都会遇到各种各样的问题导致使用体验差。而这款海马玩安卓模拟器(DROID4X)不仅支持双显卡的电脑同时系统内自带资源库,让你完完全全感受原生安卓的独特魅力。使用海马玩安卓模拟器(DROID4X)能让你轻轻松松使用电脑的安卓客户端。</p>
<h3>特点</h3>
<ol>
<li><p>速度流畅,稍微不如<code>Genymotion</code>,但是比<code>BlueStacks</code>好很多;</p></li>
<li><p>支持横竖屏切换,支持摇动以及GPS数据模拟;</p></li>
<li><p>支持ARM Library,能够运行Google Play等<code>Genymotion</code>无法运行的APP;</p></li>
<li><p>支持手柄控制;</p></li>
<li><p>未来支持在IOS运行,也就是可以用IPHONE运行Android应用了,想想就怕;</p></li>
</ol>
<h3>如何使用</h3>
<ol>
<li><p><a href="https://link.segmentfault.com/?enc=1pLEKmZ31L%2BqfL%2Fx3ir8SQ%3D%3D.TGHY%2F5TLN4zlDb1ZoxNusr4Vfa%2BBigel%2FoaGwx8nX8qSWA%2FFCmB7Tovwa0bPNlLT" rel="nofollow">下载</a>并安装 VirtualBox;</p></li>
<li><p>下载并安装<a href="https://link.segmentfault.com/?enc=h1KdRKTbEcTIS27rlEhp0g%3D%3D.Pb5NGDWIjOtf%2BB9Ht1aBdFlnxDtFNmkvOhtY3BiYKrg%3D" rel="nofollow">Droid4X</a>;</p></li>
</ol>
<p>运行模拟器</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-7/89203007.jpg" alt="" title=""></p>
<p>设置竖屏</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-7/17900798.jpg" alt="" title=""></p>
<p>运行APP</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/0000.gif" alt="enter image description here" title="enter image description here"></p>
<h3>不足之处</h3>
<p><code>Droid4X</code>可以说得上没什么可以挑剔的地方,非要说的话,就是流畅度稍微不如<code>Genymotion</code>,UI不如<code>Genymotion</code>“接地气”,更像是为了游戏而设计的。此外,也不想<code>Genymotion</code>那样有众多Android系统版本可以选择,不过这些都是无关紧要的功能,毕竟我们不会用一个模拟器去作覆盖测试,是不?</p>
<h2>总结</h2>
<p>从使用经验上来看,<code>Droid4X</code>确实是一款值得每个Android开发汪使用的模拟器,试想一下,每次完成Coding,轻轻按一下<code>Shift+F10</code>,或者使用“重大事件决策按钮”,如下图,</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-7/35383762.jpg" alt="" title=""></p>
<p>轻轻一按就将APP部署到模拟器上,再也不用为了AVD模拟器的卡顿而烦恼,再也不用担心不小心碰了一下USB数据线而导致APP部署失败,再也不用担心Logcat没有打印日志,开发过程是不是变得淋漓尽致? 其实,我一开始在寻找AVD的替代品,当找到<code>Genymotion</code>的时候是很感动的,不过为此还推荐给不少朋友使用,但是用久了,发现不支持ARM Libary就觉得不妥了,后面Genymotion启动经常失败更是觉得坑爹。</p>
<p>这次,朋友推荐我使用<code>Droid4X</code>,一开始我是拒绝的,不能说你使用我就使用是不,用过之后,才发现这货简直是加了特技的,duang~的那么一下,APP就跑起来了。</p>
想收集一下Android应用性能调优的技术点
https://segmentfault.com/a/1190000003965131
2015-11-08T00:28:35+08:00
2015-11-08T00:28:35+08:00
Kaede
https://segmentfault.com/u/kaede
1
<h2>想收集一下Android应用性能调优的技术点</h2>
<h3>信息</h3>
<p>作者:Kaede<br>链接:<a href="https://link.segmentfault.com/?enc=ZDKJi97Vu2KLWHlK4xWXBg%3D%3D.J1OHJrW2gUH8963OLzvo%2BfvXI34YuX2s7uAaXvYofHs%3D" rel="nofollow">https://github.com/kaedea</a></p>
<h3>神马情况</h3>
<p>最近一个星期居然没有产品的需求,本来打算涂几个妹子过双11,突然想到许多新人进项目组后会把项目以前踩的坑给再次踩一边,特别是一些会引发性能问题的“有坏味道”的代码,虽然一点有问题的代码暂时不管也不会有多大的影响,但是“千里之堤,毁于蚁穴”,一旦问题严重了就不好处理了。不能指望每次都做好完整的Code Review,最好的做法是把“性能优化”的技术点总结一下,输出一个文档,给那些新加入的小伙伴们看看,免得重复踩坑。</p>
<p>其实性能调优涉及到多方面的工作,一晚上也只能想到这么多,而且都是一些老生常谈、炒冷饭的东西,<strong>这个贴的目的在于想骗一些高质量的干货</strong>(我从未见过如此厚颜无耻之人+脑补诸葛孔明表情图),然后我再整合进来,接下来再把每一点都讲详细一点,配合项目中遇到的实例案例进行分析,最好再写个DEMO之类的放到Github偏偏粉之类的。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-11-8/98999837.jpg" alt="为了方便一些脑洞比较小的同学,特意找来一张" title="为了方便一些脑洞比较小的同学,特意找来一张"></p>
<p>请大家补充要点啊,我一并处理。以下只是一时想到写的笔记,详细的分析还需要一点时间。</p>
<h3>要点</h3>
<h4>使用异步</h4>
<ul>
<li><p>保持APP的高度响应,不要在UI线程做耗时操作,多使用异步任务</p></li>
<li><p>使用线程时要做好线程控制;使用队列、线程池</p></li>
<li><p>谨慎使用糟糕的AysncTask、Timer</p></li>
<li><p>警惕异步任务引起的内存泄露</p></li>
<li><p>应该异步任务分类,比如HTTP,图片下载,文件读写,每一类的异步任务维护一个任务队列,而不是每一个任务都开一个线程(Volley表示我一个可以搞定这些全部 _(:з」∠)_)</p></li>
<li><p>这些常用的任务应该做好优先级处理(一般JSON数据优先于图片等静态数据的请求)</p></li>
<li><p>一般异步任务应该开启一个SingleAsyncTask,保证一时只有一个线程在工作</p></li>
<li><p>HTTP和图片下载尽量使用同一套网络请求</p></li>
<li><p>使用MVP模式规范大型Activity类的行为,避免异步任务造成的内存泄露</p></li>
</ul>
<h4>避免内存泄露</h4>
<ul>
<li><p>了解虚拟机内存回收机制</p></li>
<li><p>频繁GC也会造成卡顿,避免不必要的内存开销</p></li>
<li><p>错误的引用姿♂势造成的内存泄露(啊~要泄了~)</p></li>
<li><p>常见的Activity泄露(单例、Application、后台线程、无限动画、静态引用)</p></li>
<li><p>Bitmap泄露(HoneyComb这个问题之前压力好大)</p></li>
<li><p>尽量使用IntentService代替Service,前者会自动StopItself</p></li>
<li><p>排查内存泄露问题的方法(我一直以来都是简单暴力的人肉dump检查大法)</p></li>
<li><p>使用LeakCanary自动检查Activity泄露问题</p></li>
<li><p>对内存负载要保持敏感(Sharp)</p></li>
</ul>
<h4>视图优化</h4>
<ul>
<li><p>布局优化、减少层次,Include Merge</p></li>
<li><p>使用ViewStub避免不必要的LayoutInflate,使用GONE代替重复LayoutInflate同一个布局</p></li>
<li><p>避免过度绘制,应该减少不必要的布局背景;布局层次太深会造成过度绘制以及Measure、Layout等方法时间复杂度的指数增长</p></li>
<li><p>使用过渡动画,比如给图片的呈现加一个轻量的淡入效果会让视觉上变得流畅许多</p></li>
<li><p>避免过度的动画,不要让一个界面同时出现多出动画,比如List滚动时Item项要停止播放动画或者GIF</p></li>
<li><p>复杂动画使用SurfaceView或TextureView</p></li>
<li><p>尽量提供多套分辨率的图片,使用矢量图</p></li>
</ul>
<h4>Adapter优化</h4>
<ul>
<li><p>复用convertView,用ViewHolder代替频繁findViewById</p></li>
<li><p>不要重复setListener,要使用v.getId来复用Listener,不然会创建一堆Listener导致频繁GC</p></li>
<li><p>多布局要采用MutilItemView,而不是使用一个大布局然后动态控制需要现实的部分</p></li>
<li><p>不要在getView方法做做耗时的操作</p></li>
<li><p>快速滚动列表的时候,可以停止加载列表项的图片,停止列表项的动画,不要在这时候改变列表项的布局</p></li>
<li><p>尽量用RecyclerView(增量Notify和RecycledViewPool带你飞)</p></li>
</ul>
<h4>代码优化</h4>
<ul>
<li><p>算法优化,减少时间复杂度,参考一些经典的优化算法</p></li>
<li><p>尽量使用int,而不是float或者double</p></li>
<li><p>尽量采用基本类型,避免无必要的自动装箱和拆箱,浪费时间和空间</p></li>
<li><p>选用合适的集合类(尽量以空间换时间)、选用Android家的SparseArray,SparseBooleanArray和LongSparseArray</p></li>
<li><p>避免创建额外的对象(StringBuilder)</p></li>
<li><p>使用SO库完成一些比较独立的功能(高斯模糊)</p></li>
<li><p>预处理(提前操作)一些比较耗时的初始化工作统一放到启动图处理</p></li>
<li><p>懒加载(延迟处理)规避Activity的敏感生命周期</p></li>
<li><p>Log工具类,要在编译时删掉调试代码,而不是在运行时通过判断条件规避</p></li>
<li><p>优先使用静态方法、公有方法还是私有方法?速度区别很大哦</p></li>
<li><p>类内部直接对成员变量进行操作,不要使用getter/setter方法,调用方法耗额外的时间</p></li>
<li><p>给内部类访问的外部类成员变量要声明称包内可访问,而不是私有,不然编译的时候还是会自动创建用于访问外部类成员变量的方法</p></li>
<li><p>遍历集合时,使用i++代替Iterator,后者需要额外的对象操作,应在循环体内避免这种情况</p></li>
<li><p>如果一个基本类型或者String的值不会改变,尽量用final static,编译时会直接用变量的值替换变量,也就不需要在查询变量的值了</p></li>
</ul>
<h4>其他优化</h4>
<ul>
<li><p>数据库优化:使用索引、使用异步线程</p></li>
<li><p>网络优化 …… 一堆优秀的轮子</p></li>
<li><p>避免过度使用依赖注入框架,大量的反射</p></li>
<li><p>不过过度设计/抽象,多态看起来很有设计感,代价就是额外的代码、空间、时间</p></li>
<li><p>尽量不要开启多进程,进程的开销很大</p></li>
</ul>
<h4>APK瘦身</h4>
<ul>
<li><p>开启混淆</p></li>
<li><p>使用zipalign工具优化APK</p></li>
<li><p>适当有损图片压缩、使用矢量图</p></li>
<li><p>删除项目中冗余的资源,之前写过一些删除没有res资源的脚本</p></li>
<li><p>动态加载模块化,项目拆分啊!</p></li>
</ul>
<h4>性能问题的排查方法</h4>
<ul>
<li><p>GPU条形图,没事开来看看淘宝</p></li>
<li><p>过度绘制颜色,嗯,不要一篇姨妈红就好</p></li>
<li><p>LeakCanary,自动检测Activity泄露,挺好用的</p></li>
<li><p>TraceView(Device Monitor),Systrace,分析哪些代码占用的CPU时间太大,屡试不爽</p></li>
<li><p>Lint,检查不合理的res资源</p></li>
<li><p>layoutopt(还是optlayout?),对当前布局提出优化建议,已被lint替代,但是还能用</p></li>
<li><p>HierarchyViewer,查看手机当前界面的布局层次,布局优化时常用(只用于模拟器,真机上用要ROOT,不想ROOT加得使用ViewServer)</p></li>
<li><p>StrictMode,UI操作、网络操作等容易出现性能问题的地方,如果出现异常情况StrictMode会报警</p></li>
</ul>
<h2><strong>欢迎各位补充</strong></h2>
Android MVP模式 简单易懂的介绍方式
https://segmentfault.com/a/1190000003927200
2015-10-29T20:31:03+08:00
2015-10-29T20:31:03+08:00
Kaede
https://segmentfault.com/u/kaede
28
<h2>Android MVP Pattern</h2>
<p>Android <strong>MVP 模式</strong><sup><a class="footnote-ref">1</a></sup> 也不是什么新鲜的东西了,我在自己的项目里也普遍地使用了这个设计模式。当项目越来越庞大、复杂,参与的研发人员越来越多的时候,<strong>MVP 模式</strong>的优势就充分显示出来了。</p>
<blockquote><p>导读:MVP模式是MVC模式在Android上的一种变体,要介绍MVP就得先介绍MVC。在MVC模式中,Activity应该是属于View这一层。而实质上,它既承担了View,同时也包含一些Controller的东西在里面。这对于开发与维护来说不太友好,耦合度大高了。把Activity的View和Controller抽离出来就变成了View和Presenter,这就是MVP模式。</p></blockquote>
<h2>基本信息</h2>
<ul>
<li><p>作者:<a href="https://link.segmentfault.com/?enc=s49d2N4OKDzwC5u8pXRd8Q%3D%3D.P4kv9KFR3wwep4eqFQyb3oHyuC%2Fmb8BxwvAwxpqrTRU%3D" rel="nofollow">Kaede</a></p></li>
<li><p>项目:<a href="https://link.segmentfault.com/?enc=qHFDn8Wzb3hjFQkOEk5O2Q%3D%3D.zAJ2whXAqRJWJjq3ZMKWVUpb5hrm7WhOnDJnKhKZ%2BoRN3%2BA4eoaH94l3TjFdMP5H" rel="nofollow">Android-MVP-Pattern</a></p></li>
<li><p>出处:<a href="https://link.segmentfault.com/?enc=aHx4k4unMyaUDRsFvsbSag%3D%3D.zzrJ1XZ4QYWSHwKQw2l9ayr%2Bwiy5Ts8Xh9a7LeE339CnPWxH6cShJgVe%2BpyHPImprTIVjwATxCZzx5zlw%2BlQgA%3D%3D" rel="nofollow">Android MVP模式 简单易懂的介绍方式</a></p></li>
</ul>
<p>MVP模式(Model-View-Presenter)可以说是MVC模式(Model-View-Controller)在Android开发上的一种变种、进化模式。后者大家可能比较熟悉,就算不熟悉也可能或多或少地在自己的项目中用到过。要介绍MVP模式,就不得不先说说MVC模式。</p>
<h2>MVC模式</h2>
<p>MVC模式的结构分为三部分,实体层的Model,视图层的View,以及控制层的Controller。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-11/13126761.jpg" alt="MVC结构" title="MVC结构"></p>
<ul>
<li><p>其中View层其实就是程序的UI界面,用于向用户展示数据以及接收用户的输入</p></li>
<li><p>而Model层就是JavaBean实体类,用于保存实例数据</p></li>
<li><p>Controller控制器用于更新UI界面和数据实例</p></li>
</ul>
<p>例如,View层接受用户的输入,然后通过Controller修改对应的Model实例;同时,当Model实例的数据发生变化的时候,需要修改UI界面,可以通过Controller更新界面。(View层也可以直接更新Model实例的数据,而不用每次都通过Controller,这样对于一些简单的数据更新工作会变得方便许多。)</p>
<p>举个简单的例子,现在要实现一个飘雪的动态壁纸,可以给雪花定义一个实体类Snow,里面存放XY轴坐标数据,View层当然就是SurfaceView(或者其他视图),为了实现雪花飘的效果,可以启动一个后台线程,在线程里不断更新Snow实例里的坐标值,这部分就是Controller的工作了,Controller里还要定时更新SurfaceView上面的雪花。进一步的话,可以在SurfaceView上监听用户的点击,如果用户点击,只通过Controller对触摸点周围的Snow的坐标值进行调整,从而实现雪花在用户点击后出现弹开等效果。具体的MVC模式请自行Google。</p>
<h2>MVP模式</h2>
<p>在Android项目中,Activity和Fragment占据了大部分的开发工作。如果有一种设计模式(或者说代码结构)专门是为优化Activity和Fragment的代码而产生的,你说这种模式重要不?这就是MVP设计模式。</p>
<p>按照MVC的分层,Activity和Fragment(后面只说Activity)应该属于View层,用于展示UI界面,以及接收用户的输入,此外还要承担一些生命周期的工作。Activity是在Android开发中充当非常重要的角色,特别是TA的生命周期的功能,所以开发的时候我们经常把一些业务逻辑直接写在Activity里面,这非常直观方便,代价就是Activity会越来越臃肿,超过1000行代码是常有的事,而且如果是一些可以通用的业务逻辑(比如用户登录),写在具体的Activity里就意味着这个逻辑不能复用了。如果有进行代码重构经验的人,看到1000+行的类肯定会有所顾虑。因此,Activity不仅承担了View的角色,还承担了一部分的Controller角色,这样一来V和C就耦合在一起了,虽然这样写方便,但是如果业务调整的话,要维护起来就难了,而且在一个臃肿的Activity类查找业务逻辑的代码也会非常蛋疼,所以看起来有必要在Activity中,把View和Controller抽离开来,而这就是MVP模式的工作了。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-11/2114527.jpg" alt="MVP结构" title="MVP结构"></p>
<p>MVP模式的核心思想:</p>
<blockquote><p><strong>MVP把Activity中的UI逻辑抽象成View接口,把业务逻辑抽象成Presenter接口,Model类还是原来的Model</strong>。</p></blockquote>
<p>这就是MVP模式,现在这样的话,Activity的工作的简单了,只用来响应生命周期,其他工作都丢到Presenter中去完成。从上图可以看出,Presenter是Model和View之间的桥梁,为了让结构变得更加简单,View并不能直接对Model进行操作,这也是MVP与MVC最大的不同之处。</p>
<h2>MVP模式的作用</h2>
<p>MVP的好处都有啥,谁说对了就给他 KIRA!!(<ゝω·)☆</p>
<ul>
<li><p>分离了视图逻辑和业务逻辑,降低了耦合</p></li>
<li><p>Activity只处理生命周期的任务,代码变得更加简洁</p></li>
<li><p>视图逻辑和业务逻辑分别抽象到了View和Presenter的接口中去,提高代码的可阅读性</p></li>
<li><p>Presenter被抽象成接口,可以有多种具体的实现,所以方便进行单元测试</p></li>
<li><p>把业务逻辑抽到Presenter中去,避免后台线程引用着Activity导致Activity的资源无法被系统回收从而引起内存泄露和OOM</p></li>
</ul>
<p>其中最重要的有三点:</p>
<h3>Activity 代码变得更加简洁</h3>
<p>相信很多人阅读代码的时候,都是从Activity开始的,对着一个1000+行代码的Activity,看了都觉得难受。</p>
<p>使用MVP之后,Activity就能瘦身许多了,基本上只有FindView、SetListener以及Init的代码。其他的就是对Presenter的调用,还有对View接口的实现。这种情形下阅读代码就容易多了,而且你只要看Presenter的接口,就能明白这个模块都有哪些业务,很快就能定位到具体代码。Activity变得容易看懂,容易维护,以后要调整业务、删减功能也就变得简单许多。</p>
<h3>方便进行单元测试</h3>
<p>一般单元测试都是用来测试某些新加的业务逻辑有没有问题,如果采用传统的代码风格(习惯性上叫做MV模式,少了P),我们可能要先在Activity里写一段测试代码,测试完了再把测试代码删掉换成正式代码,这时如果发现业务有问题又得换回测试代码,咦,测试代码已经删掉了!好吧重新写吧……</p>
<p>MVP中,由于业务逻辑都在Presenter里,我们完全可以写一个PresenterTest的实现类继承Presenter的接口,现在只要在Activity里把Presenter的创建换成PresenterTest,就能进行单元测试了,测试完再换回来即可。万一发现还得进行测试,那就再换成PresenterTest吧。</p>
<h3>避免 Activity 的内存泄露</h3>
<p>Android APP 发生OOM的最大原因就是出现内存泄露造成APP的内存不够用,而造成内存泄露的两大原因之一就是Activity泄露(Activity Leak)(另一个原因是Bitmap泄露(Bitmap Leak))。</p>
<blockquote><p>Java一个强大的功能就是其虚拟机的内存回收机制,这个功能使得Java用户在设计代码的时候,不用像C++用户那样考虑对象的回收问题。然而,Java用户总是喜欢随便写一大堆对象,然后幻想着虚拟机能帮他们处理好内存的回收工作。可是虚拟机在回收内存的时候,只会回收那些没有被引用的对象,被引用着的对象因为还可能会被调用,所以不能回收。</p></blockquote>
<p>Activity是有生命周期的,用户随时可能切换Activity,当APP的内存不够用的时候,系统会回收处于后台的Activity的资源以避免OOM。</p>
<p>采用传统的MV模式,一大堆异步任务和对UI的操作都放在Activity里面,比如你可能从网络下载一张图片,在下载成功的回调里把图片加载到 Activity 的 ImageView 里面,所以异步任务保留着对Activity的引用。这样一来,即使Activity已经被切换到后台(onDestroy已经执行),这些异步任务仍然保留着对Activity实例的引用,所以系统就无法回收这个Activity实例了,结果就是Activity Leak。Android的组件中,Activity对象往往是在堆(Java Heap)里占最多内存的,所以系统会优先回收Activity对象,如果有Activity Leak,APP很容易因为内存不够而OOM。</p>
<p>采用MVP模式,只要在当前的Activity的onDestroy里,分离异步任务对Activity的引用,就能避免 Activity Leak。</p>
<p>说了这么多,没看懂?好吧,我自己都没看懂自己写的,我们还是直接看代码吧。</p>
<h2>MVP模式的使用</h2>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-12/94032090.jpg" alt="简单MVP的UML" title="简单MVP的UML"></p>
<p>上面一张简单的MVP模式的UML图,从图中可以看出,使用MVP,至少需要经历以下步骤:</p>
<ol>
<li><p>创建IPresenter接口,把所有业务逻辑的接口都放在这里,并创建它的实现PresenterCompl(在这里可以方便地查看业务功能,由于接口可以有多种实现所以也方便写单元测试)</p></li>
<li><p>创建IView接口,把所有视图逻辑的接口都放在这里,其实现类是当前的Activity/Fragment</p></li>
<li><p>由UML图可以看出,Activity里包含了一个IPresenter,而PresenterCompl里又包含了一个IView并且依赖了Model。Activity里只保留对IPresenter的调用,其它工作全部留到PresenterCompl中实现</p></li>
<li><p>Model并不是必须有的,但是一定会有View和Presenter</p></li>
</ol>
<p>通过上面的介绍,MVP的主要特点就是把Activity里的许多逻辑都抽离到View和Presenter接口中去,并由具体的实现类来完成。这种写法多了许多IView和IPresenter的接口,在某种程度上加大了开发的工作量,刚开始使用MVP的小伙伴可能会觉得这种写法比较别扭,而且难以记住。其实一开始想太多也没有什么卵用,只要在具体项目中多写几次,就能熟悉MVP模式的写法,理解TA的意图,以及享♂受其带来的好处。</p>
<p>扯了这么多,但是好像并没有什么卵用,毕竟</p>
<blockquote><p>Talk is cheap, let me show you the code!</p></blockquote>
<p>所以还是来写一下实际的项目吧。</p>
<h2>MVP模式简单实例</h2>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-12/87960424.jpg" alt="Login" title="Login"></p>
<p>一个简单的登录界面(实在想不到别的了╮( ̄▽ ̄")╭),点击LOGIN则进行账号密码验证,点击CLEAR则重置输入。</p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-12/63555794.jpg" alt="Login代码结构" title="Login代码结构"></p>
<p>项目结构看起来像是这个样子的,MVP的分层还是很清晰的。我的习惯是先按模块分Package,在模块下面再去创建<strong>model、view、presenter</strong>的子Package,当然也可以用<strong>model、view、presenter</strong>作为顶级的Package,然后把所有的模块的model、view、presenter类都到这三个顶级Package中,就好像有人喜欢把项目里所有的Activity、Fragment、Adapter都放在一起一样。</p>
<p>首先来看看LoginActivity</p>
<pre><code class="java">public class LoginActivity extends ActionBarActivity implements ILoginView, View.OnClickListener {
private EditText editUser;
private EditText editPass;
private Button btnLogin;
private Button btnClear;
ILoginPresenter loginPresenter;
private ProgressBar progressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//find view
editUser = (EditText) this.findViewById(R.id.et_login_username);
editPass = (EditText) this.findViewById(R.id.et_login_password);
btnLogin = (Button) this.findViewById(R.id.btn_login_login);
btnClear = (Button) this.findViewById(R.id.btn_login_clear);
progressBar = (ProgressBar) this.findViewById(R.id.progress_login);
//set listener
btnLogin.setOnClickListener(this);
btnClear.setOnClickListener(this);
//init
loginPresenter = new LoginPresenterCompl(this);
loginPresenter.setProgressBarVisiblity(View.INVISIBLE);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_login_clear:
loginPresenter.clear();
break;
case R.id.btn_login_login:
loginPresenter.setProgressBarVisiblity(View.VISIBLE);
btnLogin.setEnabled(false);
btnClear.setEnabled(false);
loginPresenter.doLogin(editUser.getText().toString(), editPass.getText().toString());
break;
}
}
@Override
public void onClearText() {
editUser.setText("");
editPass.setText("");
}
@Override
public void onLoginResult(Boolean result, int code) {
loginPresenter.setProgressBarVisiblity(View.INVISIBLE);
btnLogin.setEnabled(true);
btnClear.setEnabled(true);
if (result){
Toast.makeText(this,"Login Success",Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, HomeActivity.class));
}
else
Toast.makeText(this,"Login Fail, code = " + code,Toast.LENGTH_SHORT).show();
}
@Override
public void onSetProgressBarVisibility(int visibility) {
progressBar.setVisibility(visibility);
}
}</code></pre>
<p>从代码可以看出LoginActivity只做了findView以及setListener的工作,而且包含了一个ILoginPresenter,所有业务逻辑都是通过调用ILoginPresenter的具体接口来完成。所以LoginActivity的代码看起来很舒爽,甚至有点愉♂悦呢 (/ω\*)。视力不错的你可能还看到了ILoginView接口的实现,如果不懂为什么要这样写的话,可以先往下看,这里只要记住<strong>LoginActivity实现了ILoginView接口</strong>。</p>
<p>再来看看ILoginPresenter</p>
<pre><code class="java">public interface ILoginPresenter {
void clear();
void doLogin(String name, String passwd);
void setProgressBarVisiblity(int visiblity);
}</code></pre>
<pre><code class="java">public class LoginPresenterCompl implements ILoginPresenter {
ILoginView iLoginView;
IUser user;
Handler handler;
public LoginPresenterCompl(ILoginView iLoginView) {
this.iLoginView = iLoginView;
initUser();
handler = new Handler(Looper.getMainLooper());
}
@Override
public void clear() {
iLoginView.onClearText();
}
@Override
public void doLogin(String name, String passwd) {
Boolean isLoginSuccess = true;
final int code = user.checkUserValidity(name,passwd);
if (code!=0) isLoginSuccess = false;
final Boolean result = isLoginSuccess;
handler.postDelayed(new Runnable() {
@Override
public void run() {
iLoginView.onLoginResult(result, code);
}
}, 3000);
}
@Override
public void setProgressBarVisiblity(int visiblity){
iLoginView.onSetProgressBarVisibility(visiblity);
}
private void initUser(){
user = new UserModel("mvp","mvp");
}
}</code></pre>
<p>从代码可以看出,LoginPresenterCompl保留了ILoginView的引用,因此在LoginPresenterCompl里就可以直接进行UI操作了,而不用在Activity里完成。这里使用了ILoginView引用,而不是直接使用Activity,这样一来,如果在别的Activity里也需要用到相同的业务逻辑,就可以直接复用LoginPresenterCompl类了(一个Activity可以包含一个以上的Presenter,总之,需要什么业务就new什么样的Presenter,是不是很灵活(@ ̄︶ ̄@)),这也是MVP的核心思想</p>
<blockquote><p>通过IVIew和IPresenter,把Activity的<code>UI Logic</code>和<code>Business Logic</code>分离开来,Activity just does its basic job! 至于Model嘛,还是原来MVC里的Model。</p></blockquote>
<p>再来看看ILoginView,至于ILoginView的实现类呢,翻到上面看看LoginActivity吧</p>
<pre><code class="java">public interface ILoginView {
public void onClearText();
public void onLoginResult(Boolean result, int code);
public void onSetProgressBarVisibility(int visibility);
}</code></pre>
<p>代码这种东西放在日志里讲好像除了把整个版面拉长没什么卵用,我把几种自己常用的MVP的写法写成一个Demo项目,欢迎围观和PullRequest:<a href="https://link.segmentfault.com/?enc=x4oeNJYASPYfg6F6qUzYKQ%3D%3D.d4nWxj%2By8hy%2B%2FlB870kqUJts4dXy9uh%2FwXJjjOsm5h%2FM6gez1%2FKSHIiUMTJfefcI" rel="nofollow">Android-MVP-Pattern</a>。</p>
<h2>后记</h2>
<p>以上就是我的MVP模式的一点理解,在MVVM模式还没有成熟的现在,我觉得没有比MVP模式更好的替代品了。当然今天写的只是MVP的基础使用,介绍的实例项目也非常简单,看不出MVP的优势,后面还会针对MVP模式写一些日志,就目前能想到的至少包括</p>
<ul>
<li><p>Android常规的开发模式经常被称为MV模式(Model-View),引入数据绑定后的MVVM模式(Model-View-ViewModel),与MVP模式的区别</p></li>
<li><p>目前我们写ListView的Adapter都喜欢把它写成一个内部类,如果有两个Activity里要用同一个Adapter就比较难了,通过MVP模式,能轻松地复用Adapter(你说已经不用ListView了,这不是重点不是么( ˃◡˂ ))</p></li>
<li><p>MVP模式需要多写许多新的接口,这也是其缺点所在,经过一段时间的实战,我自己已有一种优化的MVP模式,我会试着总结一下,把她拿出来说说</p></li>
</ul>
<hr>
<ol><li> 我也纠结过<strong>MVP模式</strong>或者<strong>MVP结构</strong>的说法那个跟准确一点,国外普遍的叫法是直接叫<strong>Android MVP</strong>,除此之外有叫<strong>MVP Pattern</strong>的也有叫<strong>MVP Framework/Architecture</strong>,个人认为这应该算是一种代码风格(Code Style),在分类上应该比较类似设计模式(Design Pattern),所以现在我一般称为模式,不过这不是重点,不是吗。( ˃◡˂ ) <a class="footnote-backref">↩</a>
</li></ol>
Facebook开源的Android图片加载库Fresco的Demo项目
https://segmentfault.com/a/1190000003908029
2015-10-26T11:44:52+08:00
2015-10-26T11:44:52+08:00
Kaede
https://segmentfault.com/u/kaede
0
<h3>信息</h3>
<p>GitHub : <a href="https://link.segmentfault.com/?enc=v9uNuq%2BslOWOwBJ%2F%2Fe1v6A%3D%3D.FGfsXe4n1KP5Y%2Bwy87FWyNRyd%2Ffh1O03vQqIIjrkATG%2FEaBKNF28a%2FYTbCNV1H3V" rel="nofollow">Fresco Sample Usage</a><br>作者 : <a href="https://link.segmentfault.com/?enc=Pc1eVteEg2iQ0JnxEDoNaQ%3D%3D.5XJDyVUoUjk%2FNYtsXeYNLpMHeYH1QNF2s5sZFOovpyo%3D" rel="nofollow">Kaede</a><br>参考 : <a href="https://link.segmentfault.com/?enc=H2YhBjAVS0Xts6Ag4tcmUg%3D%3D.FGWof%2BvtSfr0r1biRTwQyI9k5qHtxW63s29vnFOTtBYLP7%2BijhD4L96VPnpR7v2x" rel="nofollow">fresco</a> <a href="https://link.segmentfault.com/?enc=B%2B%2BXm3MrFSCEue3PHVb9%2BQ%3D%3D.Tqtl69FnjXypLQuk0X8wIFQMTmRvU0hF5QUOCifRQYzAm88aO81hF2fmCDXCW4kY" rel="nofollow">06peng</a> <a href="https://link.segmentfault.com/?enc=PGGUZQoGujrxSvR%2Fz%2FaMTg%3D%3D.oFRNB3xS99gjUEP54C3rbzdfrfPixM2WS0sQnjt5KWc%3D" rel="nofollow">frescolib.org</a></p>
<h3>背景</h3>
<p>关于图片加载框架,我用过许多轮子,也有自己写过。目前项目在使用的是一个我基于 Volley 修改而来的 ImageLoader ,但是由于产品天花乱坠的需求,现在已经渐渐改得面目全非了,于是打算换成一个新的轮子,在 Glide 和 Fresco 纠结一段时间后,打算先尝试 Fresco 。</p>
<p>现在市面上各种图片加载库,性能其实相差不是很大,最主要的区别还是使用方式的复杂程度。Fresco使用起来非常简单,也容易扩展,然而这并不是我喜欢Fresco的主要原因。</p>
<p>图片加载框架如果不处理好Bitmap的缓存问题就容易引发Bitmap泄露,Bitmap泄露是Android内存泄露的一大原因,而内存泄露又是OOM的主要原因。</p>
<p>Bitmap是非常占用内存的,比普通的Java对象大多了,不释放的话很快就OOM了;如果释放过早,如果有ImageView还用着被释放的Bitmap,当这个ImageView被重绘的时候就会抛IllegalState异常。OOM和IllegalState可是为我项目的崩溃率贡献了好多数据。</p>
<p>Android 3.0(Honeycomb)之前,系统把Bitmap的像素数据放在Native层,所以图片加载框架必须手动做好这些Bitmap的释放工作,Honeycomb之后这些放在虚拟机Heap里,虚拟机会帮我们回收,所以可以不用手动释放。但是由于Bitmap本身非常占用虚拟机的内存,虚拟机的内存很快就不够用了,所以会频繁触发虚拟机的GC(Garbage Collector),然而GC是非常耗性能的工作,因此也会造成APP的卡顿。好在,Android 5.0(Lollipop) 已经妥善解决这个问题了。</p>
<p>对于Honeycomb之前的Bitmap缓存问题,目前我项目的轮子使用的处理方式是Google推荐的方案,简单说一下就是:设置一个count,如果一个Bitmap被set进一个ImageView就+1,remove放就-1,定期检查这些Bitmap,如果count<=0就回收掉。这个方法能通过测试妹子“机の宝库”的考验,无奈在用户的崩溃日志上看起来还是有导致OOM和IllegalState的情况(天朝水深火日的生态啊),而且这一方法并不能解决Lollipop之前频繁GC造成的卡顿问题。</p>
<p>之所以想换Fresco最主要的原因是</p>
<blockquote><p>Fresco会在Lollipop之前,把Bitmap放在Ashmem(系统匿名共享内存),在图片不显示的时候,占用的内存会自动被释放,不需要手动释放,也不会频繁触发GC!这会使得APP更加流畅,减少因图片内存占用而引发的OOM,也能有效避免IllegalState,而且在Honeycomb之前的Android版本的表现也同样优秀(官方宣称)。</p></blockquote>
<p>总之先把源码看一下,拿来用用看,用数据说话吧。目前只写了一个 Demo 项目,后续打算把笔记整理一下,写成一篇日志。</p>
<h3>简介</h3>
<p><a href="https://link.segmentfault.com/?enc=NukYOZ1oSZre2I8JbmM%2FtQ%3D%3D.X2AuF86A%2FsXZaXKCa9NrNKZXOsDe5Z4%2F%2Fv79ot4IdktPf70nb67bT6oz1%2FQC6R%2B6" rel="nofollow">Fresco</a>是Facebook开源的一个强大的Android图片加载框架,本项目是一个Fresco用法的Demo项目。</p>
<h3>项目内容</h3>
<ul>
<li><p>简单地加载一张图片</p></li>
<li><p>自定义图片的加载,比如ScaleType, Rounded Corner, Circle, Fade Animation, Placeholder, Failure Image, Retry Image, ProgressBar, PressedState Overlay</p></li>
<li><p>加载Gif以及WebPng动态图片</p></li>
<li><p>监听图片加载的过程</p></li>
<li><p>渐进式图片加载</p></li>
<li><p>调整图片大小</p></li>
<li><p>加载图片后对图片做一些处理</p></li>
<li><p>在ListView上的使用</p></li>
<li><p>在RecyclerView上的使用</p></li>
<li><p>配合第三方图片控件的使用(PhotoView, SubsamplingSacleImageView, GifDrawable)</p></li>
<li><p>相关代码段</p></li>
</ul>
<h3>Fresco的特性</h3>
<ul>
<li><p>完善的内存缓存和释放机制</p></li>
<li><p>渐进式图片加载</p></li>
<li><p>动图支持</p></li>
<li><p>可高度自定义的UI</p></li>
<li><p>可高度自定义的图片加载过程</p></li>
</ul>
<p>详细信息可以参考<a href="https://link.segmentfault.com/?enc=hSwyZ3MgUDUKNloNLOogeQ%3D%3D.GssQ1SMZHJ4z8vr3kt57xCSiawPqq9mUr589Can0Q1w%3D" rel="nofollow">frescolib.org</a></p>
<h3>预览</h3>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-25/67535863.jpg" alt="01" title="01"></p>
<p><img src="http://7xih5c.com1.z0.glb.clouddn.com/15-10-25/90791990.jpg" alt="02" title="02"></p>