android:process 的坑,你懂吗?
smgd9170
8年前
<p>许多知识知其然而不知其所以然,这也许就是大神与菜鸟的区别吧。</p> <p>最近排查问题时发现一个问题: 一个在 Application 中启动的定时任务在运行时会被调用多次,诡异的很,最后发现是一个前人留下的坑,原因就是对 android:process 不知其所以然造成的。</p> <h2>android:process 属性</h2> <p>关于 android:process 属性,相信大家都不陌生,android 官网是这样说明的 :</p> <blockquote> <p>默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。 但是,如果您发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。</p> </blockquote> <blockquote> <p>各类组件元素的清单文件条目—<activity>、<service>、<receiver> 和 <provider>—均支持 android:process 属性,此属性可以指定该组件应在哪个进程运行。您可以设置此属性,使每个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。 此外,您还可以设置 android:process,使不同应用的组件在相同的进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署。</p> </blockquote> <blockquote> <p>此外, 元素还支持 android:process 属性,以设置适用于所有组件的默认值。</p> </blockquote> <blockquote> <p>如果内存不足,而其他为用户提供更紧急服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某一进程。在被终止进程中运行的应用组件也会随之销毁。 当这些组件需要再次运行时,系统将为它们重启进程。</p> </blockquote> <blockquote> <p>决定终止哪个进程时,Android 系统将权衡它们对用户的相对重要程度。例如,相对于托管可见 Activity 的进程而言,它更有可能关闭托管屏幕上不再可见的 Activity 进程。 因此,是否终止某个进程的决定取决于该进程中所运行组件的状态。 下面,我们介绍决定终止进程所用的规则。</p> </blockquote> <p>在需要使用到新进程时,可以使用 android:process 属性,如果被设置的进程名是以一个冒号开头的,则这个新的进程对于这个应用来说是私有的,当它被需要或者这个服务需要在新进程中运行的时候,这个新进程将会被创建。如果这个进程的名字是以字符开头,并且符合 android 包名规范(如 com.roger 等),则这个服务将运行在一个以这个名字命名的全局的进程中,当然前提是它有相应的权限。若以数字开头(如 1Remote.com ),或不符合 android 包名规范(如 Remote),则在编译时将会报错 ( INSTALL_PARSE_FAILED_MANIFEST_MALFORMED )。新建进程将允许在不同应用中的各种组件可以共享一个进程,从而减少资源的占用。具体可以参考博客:<a href="/misc/goto?guid=4959671239420249064">apk,task,android:process与android:sharedUserId的区别</a></p> <p>重点来了,因为设置了 android:process 属性将组件运行到另一个进程,相当于另一个应用程序,所以在另一个线程中也将新建一个 Application 的实例。<strong>因此,每新建一个进程 Application 的 onCreate 都将被调用一次。</strong> 如果在 Application 的 onCreate 中有许多初始化工作并且需要根据进程来区分的,那就需要特别注意了。</p> <p>让我们到 Framework 中看看新建进程的逻辑,请打开老罗的博客 : <a href="/misc/goto?guid=4959671239491475883">Android系统在新进程中启动自定义服务过程(startService)的原理分析</a></p> <p>详细介绍了新进程启动的过程,其中我们重点看到 <code>Step 17. ActivityThread.handleCreateService</code>中</p> <pre> <code class="language-java">public final class ActivityThread { ...... private final void handleCreateService(CreateServiceData data) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); LoadedApk packageInfo = getPackageInfoNoCheck( data.info.applicationInfo); Service service = null; try { java.lang.ClassLoader cl = packageInfo.getClassLoader(); service = (Service) cl.loadClass(data.info.name).newInstance(); } catch (Exception e) { if (!mInstrumentation.onException(service, e)) { throw new RuntimeException( "Unable to instantiate service " + data.info.name + ": " + e.toString(), e); } } try { if (localLOGV) Slog.v(TAG, "Creating service " + data.info.name); ContextImpl context = new ContextImpl(); context.init(packageInfo, null, this); Application app = packageInfo.makeApplication(false, mInstrumentation); context.setOuterContext(service); service.attach(context, this, data.info.name, data.token, app, ActivityManagerNative.getDefault()); service.onCreate(); mServices.put(data.token, service); try { ActivityManagerNative.getDefault().serviceDoneExecuting( data.token, 0, 0, 0); } catch (RemoteException e) { // nothing to do. } } catch (Exception e) { if (!mInstrumentation.onException(service, e)) { throw new RuntimeException( "Unable to create service " + data.info.name + ": " + e.toString(), e); } } } ...... } </code></pre> <p>看到这行 <code>Application app = packageInfo.makeApplication(false, mInstrumentation);</code> 在这里创建了 Application 。</p> <h2>解决方案</h2> <p>获取当前运行进程的名称:</p> <h3>方案1</h3> <pre> <code class="language-java">public static String getProcessName(Context cxt, int pid) { ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE); List<RunningAppProcessInfo> runningApps = am.getRunningAppProcesses(); if (runningApps == null) { return null; } for (RunningAppProcessInfo procInfo : runningApps) { if (procInfo.pid == pid) { return procInfo.processName; } } return null; } </code></pre> <p>目前网上主流的方法,但效率没有方案2高,感谢由王燚同学提供的方案2</p> <h3>方案2</h3> <pre> <code class="language-java">public static String getProcessName() { try { File file = new File("/proc/" + android.os.Process.myPid() + "/" + "cmdline"); BufferedReader mBufferedReader = new BufferedReader(new FileReader(file)); String processName = mBufferedReader.readLine().trim(); mBufferedReader.close(); return processName; } catch (Exception e) { e.printStackTrace(); return null; } } </code></pre> <p>然后在 Application 的 onCreate 中获取进程名称并进行相应的判断,例如:</p> <pre> <code class="language-java">String processName = getProcessName(this, android.os.Process.myPid()); if (!TextUtils.isEmpty(processName) && processName.equals(this.getPackageName())) {//判断进程名,保证只有主进程运行 //主进程初始化逻辑 .... } </code></pre> <h2>总结</h2> <p>知其然还需知其所以然,这才是总结并提高的法宝。希望能帮到有需要的同学 :)</p> <p>Have a good day ~</p> <h3>参考</h3> <p><a href="/misc/goto?guid=4959671239578163250">http://blog.csdn.net/jason0539/article/details/45555671</a></p> <p><a href="/misc/goto?guid=4959671239491475883">Android系统在新进程中启动自定义服务过程(startService)的原理分析</a></p> <p><a href="/misc/goto?guid=4959671239420249064">apk,task,android:process与android:sharedUserId的区别</a></p> <p>来源:https://www.rogerblog.cn/2016/03/17/android-proess/</p>