ChildProcessLauncher.java revision f2477e01787aa58f445919b809d89e252beef54f
1// Copyright 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.content.browser; 6 7import android.content.Context; 8import android.util.Log; 9import android.util.SparseIntArray; 10import android.view.Surface; 11 12import org.chromium.base.CalledByNative; 13import org.chromium.base.JNINamespace; 14import org.chromium.base.SysUtils; 15import org.chromium.base.ThreadUtils; 16import org.chromium.content.app.ChildProcessService; 17import org.chromium.content.app.Linker; 18import org.chromium.content.app.LinkerParams; 19import org.chromium.content.app.PrivilegedProcessService; 20import org.chromium.content.app.SandboxedProcessService; 21import org.chromium.content.common.IChildProcessCallback; 22import org.chromium.content.common.IChildProcessService; 23 24import java.util.ArrayList; 25import java.util.Map; 26import java.util.concurrent.ConcurrentHashMap; 27 28/** 29 * This class provides the method to start/stop ChildProcess called by native. 30 */ 31@JNINamespace("content") 32public class ChildProcessLauncher { 33 private static final String TAG = "ChildProcessLauncher"; 34 35 private static final int CALLBACK_FOR_UNKNOWN_PROCESS = 0; 36 private static final int CALLBACK_FOR_GPU_PROCESS = 1; 37 private static final int CALLBACK_FOR_RENDERER_PROCESS = 2; 38 39 private static final String SWITCH_PROCESS_TYPE = "type"; 40 private static final String SWITCH_PPAPI_BROKER_PROCESS = "ppapi-broker"; 41 private static final String SWITCH_RENDERER_PROCESS = "renderer"; 42 private static final String SWITCH_GPU_PROCESS = "gpu-process"; 43 44 // The upper limit on the number of simultaneous sandboxed and privileged child service process 45 // instances supported. Each limit must not exceed total number of SandboxedProcessServiceX 46 // classes and PrivilegedProcessServiceX classes declared in this package and defined as 47 // services in the embedding application's manifest file. 48 // (See {@link ChildProcessService} for more details on defining the services.) 49 /* package */ static final int MAX_REGISTERED_SANDBOXED_SERVICES = 13; 50 /* package */ static final int MAX_REGISTERED_PRIVILEGED_SERVICES = 3; 51 52 private static class ChildConnectionAllocator { 53 // Connections to services. Indices of the array correspond to the service numbers. 54 private final ChildProcessConnection[] mChildProcessConnections; 55 56 // The list of free (not bound) service indices. When looking for a free service, the first 57 // index in that list should be used. When a service is unbound, its index is added to the 58 // end of the list. This is so that we avoid immediately reusing the freed service (see 59 // http://crbug.com/164069): the framework might keep a service process alive when it's been 60 // unbound for a short time. If a new connection to the same service is bound at that point, 61 // the process is reused and bad things happen (mostly static variables are set when we 62 // don't expect them to). 63 // SHOULD BE ACCESSED WITH mConnectionLock. 64 private final ArrayList<Integer> mFreeConnectionIndices; 65 private final Object mConnectionLock = new Object(); 66 67 private Class<? extends ChildProcessService> mChildClass; 68 private final boolean mInSandbox; 69 70 public ChildConnectionAllocator(boolean inSandbox) { 71 int numChildServices = inSandbox ? 72 MAX_REGISTERED_SANDBOXED_SERVICES : MAX_REGISTERED_PRIVILEGED_SERVICES; 73 mChildProcessConnections = new ChildProcessConnection[numChildServices]; 74 mFreeConnectionIndices = new ArrayList<Integer>(numChildServices); 75 for (int i = 0; i < numChildServices; i++) { 76 mFreeConnectionIndices.add(i); 77 } 78 setServiceClass(inSandbox ? 79 SandboxedProcessService.class : PrivilegedProcessService.class); 80 mInSandbox = inSandbox; 81 } 82 83 public void setServiceClass(Class<? extends ChildProcessService> childClass) { 84 mChildClass = childClass; 85 } 86 87 public ChildProcessConnection allocate( 88 Context context, ChildProcessConnection.DeathCallback deathCallback, 89 LinkerParams linkerParams) { 90 synchronized (mConnectionLock) { 91 if (mFreeConnectionIndices.isEmpty()) { 92 Log.w(TAG, "Ran out of service."); 93 return null; 94 } 95 int slot = mFreeConnectionIndices.remove(0); 96 assert mChildProcessConnections[slot] == null; 97 mChildProcessConnections[slot] = new ChildProcessConnection(context, slot, 98 mInSandbox, deathCallback, mChildClass, linkerParams); 99 return mChildProcessConnections[slot]; 100 } 101 } 102 103 public void free(ChildProcessConnection connection) { 104 synchronized (mConnectionLock) { 105 int slot = connection.getServiceNumber(); 106 if (mChildProcessConnections[slot] != connection) { 107 int occupier = mChildProcessConnections[slot] == null ? 108 -1 : mChildProcessConnections[slot].getServiceNumber(); 109 Log.e(TAG, "Unable to find connection to free in slot: " + slot + 110 " already occupied by service: " + occupier); 111 assert false; 112 } else { 113 mChildProcessConnections[slot] = null; 114 assert !mFreeConnectionIndices.contains(slot); 115 mFreeConnectionIndices.add(slot); 116 } 117 } 118 } 119 } 120 121 // Service class for child process. As the default value it uses SandboxedProcessService0 and 122 // PrivilegedProcessService0. 123 private static final ChildConnectionAllocator sSandboxedChildConnectionAllocator = 124 new ChildConnectionAllocator(true); 125 private static final ChildConnectionAllocator sPrivilegedChildConnectionAllocator = 126 new ChildConnectionAllocator(false); 127 128 private static boolean sConnectionAllocated = false; 129 130 /** 131 * Sets service class for sandboxed service and privileged service. 132 */ 133 public static void setChildProcessClass( 134 Class<? extends SandboxedProcessService> sandboxedServiceClass, 135 Class<? extends PrivilegedProcessService> privilegedServiceClass) { 136 // We should guarantee this is called before allocating connection. 137 assert !sConnectionAllocated; 138 sSandboxedChildConnectionAllocator.setServiceClass(sandboxedServiceClass); 139 sPrivilegedChildConnectionAllocator.setServiceClass(privilegedServiceClass); 140 } 141 142 private static ChildConnectionAllocator getConnectionAllocator(boolean inSandbox) { 143 return inSandbox ? 144 sSandboxedChildConnectionAllocator : sPrivilegedChildConnectionAllocator; 145 } 146 147 private static ChildProcessConnection allocateConnection(Context context, 148 boolean inSandbox, LinkerParams linkerParams) { 149 ChildProcessConnection.DeathCallback deathCallback = 150 new ChildProcessConnection.DeathCallback() { 151 @Override 152 public void onChildProcessDied(int pid) { 153 stop(pid); 154 } 155 }; 156 sConnectionAllocated = true; 157 return getConnectionAllocator(inSandbox).allocate(context, deathCallback, linkerParams); 158 } 159 160 private static boolean sLinkerInitialized = false; 161 private static long sLinkerLoadAddress = 0; 162 163 private static LinkerParams getLinkerParamsForNewConnection() { 164 if (!sLinkerInitialized) { 165 if (Linker.isUsed()) { 166 sLinkerLoadAddress = Linker.getBaseLoadAddress(); 167 if (sLinkerLoadAddress == 0) { 168 Log.i(TAG, "Shared RELRO support disabled!"); 169 } 170 } 171 sLinkerInitialized = true; 172 } 173 174 if (sLinkerLoadAddress == 0) 175 return null; 176 177 // Always wait for the shared RELROs in service processes. 178 final boolean waitForSharedRelros = true; 179 return new LinkerParams(sLinkerLoadAddress, 180 waitForSharedRelros, 181 Linker.getTestRunnerClassName()); 182 } 183 184 private static ChildProcessConnection allocateBoundConnection(Context context, 185 String[] commandLine, boolean inSandbox) { 186 LinkerParams linkerParams = getLinkerParamsForNewConnection(); 187 ChildProcessConnection connection = allocateConnection(context, inSandbox, linkerParams); 188 if (connection != null) { 189 connection.start(commandLine); 190 } 191 return connection; 192 } 193 194 private static void freeConnection(ChildProcessConnection connection) { 195 if (connection == null) { 196 return; 197 } 198 getConnectionAllocator(connection.isInSandbox()).free(connection); 199 return; 200 } 201 202 // Represents an invalid process handle; same as base/process/process.h kNullProcessHandle. 203 private static final int NULL_PROCESS_HANDLE = 0; 204 205 // Map from pid to ChildService connection. 206 private static Map<Integer, ChildProcessConnection> sServiceMap = 207 new ConcurrentHashMap<Integer, ChildProcessConnection>(); 208 209 // A pre-allocated and pre-bound connection ready for connection setup, or null. 210 private static ChildProcessConnection sSpareSandboxedConnection = null; 211 212 /** 213 * Manages oom bindings used to bound child services. "Oom binding" is a binding that raises the 214 * process oom priority so that it shouldn't be killed by the OS out-of-memory killer under 215 * normal conditions (it can still be killed under drastic memory pressure). 216 * 217 * This class serves a proxy between external calls that manipulate the bindings and the 218 * connections, allowing to enforce policies such as delayed removal of the bindings. 219 */ 220 static class BindingManager { 221 // Delay of 1 second used when removing the initial oom binding of a process. 222 private static final long REMOVE_INITIAL_BINDING_DELAY_MILLIS = 1 * 1000; 223 224 // Delay of 1 second used when removing temporary strong binding of a process (only on 225 // non-low-memory devices). 226 private static final long DETACH_AS_ACTIVE_HIGH_END_DELAY_MILLIS = 1 * 1000; 227 228 // Map from pid to the count of oom bindings bound for the service. Should be accessed with 229 // mCountLock. 230 private final SparseIntArray mOomBindingCount = new SparseIntArray(); 231 232 // Pid of the renderer that was most recently oom bound. This is used on low-memory devices 233 // to drop oom bindings of a process when another one acquires them, making sure that only 234 // one renderer process at a time is oom bound. Should be accessed with mCountLock. 235 private int mLastOomPid = -1; 236 237 // Pid of the renderer that we bound with a strong binding for the background period. Equals 238 // -1 when the embedder is in foreground. 239 private int mBoundForBackgroundPeriodPid = -1; 240 241 // Should be acquired before binding or unbinding the connections and modifying state 242 // variables: mOomBindingCount and mLastOomPid. 243 private final Object mCountLock = new Object(); 244 245 /** 246 * Registers an oom binding bound for a child process. Should be called with mCountLock. 247 * @param pid handle of the process. 248 */ 249 private void incrementOomCount(int pid) { 250 mOomBindingCount.put(pid, mOomBindingCount.get(pid) + 1); 251 mLastOomPid = pid; 252 } 253 254 /** 255 * Registers an oom binding unbound for a child process. Should be called with mCountLock. 256 * @param pid handle of the process. 257 */ 258 private void decrementOomCount(int pid) { 259 int count = mOomBindingCount.get(pid, -1); 260 assert count > 0; 261 count--; 262 if (count > 0) { 263 mOomBindingCount.put(pid, count); 264 } else { 265 mOomBindingCount.delete(pid); 266 } 267 } 268 269 /** 270 * Drops all oom bindings for the given renderer. 271 * @param pid handle of the process. 272 */ 273 private void dropOomBindings(int pid) { 274 ChildProcessConnection connection = sServiceMap.get(pid); 275 if (connection == null) { 276 logPidWarning(pid, "Tried to drop oom bindings for a non-existent connection"); 277 return; 278 } 279 synchronized (mCountLock) { 280 connection.dropOomBindings(); 281 mOomBindingCount.delete(pid); 282 } 283 } 284 285 /** 286 * Registers a freshly started child process. On low-memory devices this will also drop the 287 * oom bindings of the last process that was oom-bound. We can do that, because every time a 288 * connection is created on the low-end, it is used in foreground (no prerendering, no 289 * loading of tabs opened in background). 290 * @param pid handle of the process. 291 */ 292 void addNewConnection(int pid) { 293 synchronized (mCountLock) { 294 if (SysUtils.isLowEndDevice() && mLastOomPid >= 0) { 295 dropOomBindings(mLastOomPid); 296 } 297 // This will reset the previous entry for the pid in the unlikely event of the OS 298 // reusing renderer pids. 299 mOomBindingCount.put(pid, 0); 300 // Every new connection is bound with initial oom binding. 301 incrementOomCount(pid); 302 } 303 } 304 305 /** 306 * Remove the initial binding of the child process. Child processes are bound with initial 307 * binding to protect them from getting killed before they are put to use. This method 308 * allows to remove the binding once it is no longer needed. The binding is removed after a 309 * fixed delay period so that the renderer will not be killed immediately after the call. 310 */ 311 void removeInitialBinding(final int pid) { 312 final ChildProcessConnection connection = sServiceMap.get(pid); 313 if (connection == null) { 314 logPidWarning(pid, "Tried to remove a binding for a non-existent connection"); 315 return; 316 } 317 if (!connection.isInitialBindingBound()) return; 318 ThreadUtils.postOnUiThreadDelayed(new Runnable() { 319 @Override 320 public void run() { 321 synchronized (mCountLock) { 322 if (connection.isInitialBindingBound()) { 323 decrementOomCount(pid); 324 connection.removeInitialBinding(); 325 } 326 } 327 } 328 }, REMOVE_INITIAL_BINDING_DELAY_MILLIS); 329 } 330 331 /** 332 * Bind a child process as a high priority process so that it has the same priority as the 333 * main process. This can be used for the foreground renderer process to distinguish it from 334 * the background renderer process. 335 * @param pid The process handle of the service connection. 336 */ 337 void bindAsHighPriority(final int pid) { 338 ChildProcessConnection connection = sServiceMap.get(pid); 339 if (connection == null) { 340 logPidWarning(pid, "Tried to bind a non-existent connection"); 341 return; 342 } 343 synchronized (mCountLock) { 344 connection.attachAsActive(); 345 incrementOomCount(pid); 346 } 347 } 348 349 /** 350 * Unbind a high priority process which was previous bound with bindAsHighPriority. 351 * @param pid The process handle of the service. 352 */ 353 void unbindAsHighPriority(final int pid) { 354 final ChildProcessConnection connection = sServiceMap.get(pid); 355 if (connection == null) { 356 logPidWarning(pid, "Tried to unbind non-existent connection"); 357 return; 358 } 359 if (!connection.isStrongBindingBound()) return; 360 361 // This runnable performs the actual unbinding. It will be executed synchronously when 362 // on low-end devices and posted with a delay otherwise. 363 Runnable doUnbind = new Runnable() { 364 @Override 365 public void run() { 366 synchronized (mCountLock) { 367 if (connection.isStrongBindingBound()) { 368 decrementOomCount(pid); 369 connection.detachAsActive(); 370 } 371 } 372 } 373 }; 374 375 if (SysUtils.isLowEndDevice()) { 376 doUnbind.run(); 377 } else { 378 ThreadUtils.postOnUiThreadDelayed(doUnbind, DETACH_AS_ACTIVE_HIGH_END_DELAY_MILLIS); 379 } 380 } 381 382 /** 383 * @return True iff the given service process is protected from the out-of-memory killing, 384 * or it was protected when it died (either crashed or was closed). This can be used to 385 * decide if a disconnection of a renderer was a crash or a probable out-of-memory kill. In 386 * the unlikely event of the OS reusing renderer pid, the call will refer to the most recent 387 * renderer of the given pid. The binding count is being reset in addNewConnection(). 388 */ 389 boolean isOomProtected(int pid) { 390 synchronized (mCountLock) { 391 return mOomBindingCount.get(pid) > 0; 392 } 393 } 394 395 /** 396 * Called when the embedding application is sent to background. We want to maintain a strong 397 * binding on the most recently used renderer while the embedder is in background, to 398 * indicate the relative importance of the renderer to system oom killer. 399 * 400 * The embedder needs to ensure that: 401 * - every onBroughtToForeground() is followed by onSentToBackground() 402 * - pairs of consecutive onBroughtToForeground() / onSentToBackground() calls do not 403 * overlap 404 */ 405 void onSentToBackground() { 406 assert mBoundForBackgroundPeriodPid == -1; 407 // mLastOomPid can be -1 at this point as the embedding application could be used in 408 // foreground without spawning any renderers. 409 if (mLastOomPid >= 0) { 410 bindAsHighPriority(mLastOomPid); 411 mBoundForBackgroundPeriodPid = mLastOomPid; 412 } 413 } 414 415 /** 416 * Called when the embedding application is brought to foreground. This will drop the strong 417 * binding kept on the main renderer during the background period, so the embedder should 418 * make sure that this is called after the regular strong binding is attached for the 419 * foreground session. 420 */ 421 void onBroughtToForeground() { 422 if (mBoundForBackgroundPeriodPid >= 0) { 423 unbindAsHighPriority(mBoundForBackgroundPeriodPid); 424 mBoundForBackgroundPeriodPid = -1; 425 } 426 } 427 } 428 429 private static BindingManager sBindingManager = new BindingManager(); 430 431 static BindingManager getBindingManager() { 432 return sBindingManager; 433 } 434 435 @CalledByNative 436 private static boolean isOomProtected(int pid) { 437 return sBindingManager.isOomProtected(pid); 438 } 439 440 /** 441 * Called when the embedding application is sent to background. 442 */ 443 public static void onSentToBackground() { 444 sBindingManager.onSentToBackground(); 445 } 446 447 /** 448 * Called when the embedding application is brought to foreground. 449 */ 450 public static void onBroughtToForeground() { 451 sBindingManager.onBroughtToForeground(); 452 } 453 454 /** 455 * Returns the child process service interface for the given pid. This may be called on 456 * any thread, but the caller must assume that the service can disconnect at any time. All 457 * service calls should catch and handle android.os.RemoteException. 458 * 459 * @param pid The pid (process handle) of the service obtained from {@link #start}. 460 * @return The IChildProcessService or null if the service no longer exists. 461 */ 462 public static IChildProcessService getChildService(int pid) { 463 ChildProcessConnection connection = sServiceMap.get(pid); 464 if (connection != null) { 465 return connection.getService(); 466 } 467 return null; 468 } 469 470 /** 471 * Should be called early in startup so the work needed to spawn the child process can be done 472 * in parallel to other startup work. Must not be called on the UI thread. Spare connection is 473 * created in sandboxed child process. 474 * @param context the application context used for the connection. 475 */ 476 public static void warmUp(Context context) { 477 synchronized (ChildProcessLauncher.class) { 478 assert !ThreadUtils.runningOnUiThread(); 479 if (sSpareSandboxedConnection == null) { 480 sSpareSandboxedConnection = allocateBoundConnection(context, null, true); 481 } 482 } 483 } 484 485 private static String getSwitchValue(final String[] commandLine, String switchKey) { 486 if (commandLine == null || switchKey == null) { 487 return null; 488 } 489 // This format should be matched with the one defined in command_line.h. 490 final String switchKeyPrefix = "--" + switchKey + "="; 491 for (String command : commandLine) { 492 if (command != null && command.startsWith(switchKeyPrefix)) { 493 return command.substring(switchKeyPrefix.length()); 494 } 495 } 496 return null; 497 } 498 499 /** 500 * Spawns and connects to a child process. May be called on any thread. It will not block, but 501 * will instead callback to {@link #nativeOnChildProcessStarted} when the connection is 502 * established. Note this callback will not necessarily be from the same thread (currently it 503 * always comes from the main thread). 504 * 505 * @param context Context used to obtain the application context. 506 * @param commandLine The child process command line argv. 507 * @param fileIds The ID that should be used when mapping files in the created process. 508 * @param fileFds The file descriptors that should be mapped in the created process. 509 * @param fileAutoClose Whether the file descriptors should be closed once they were passed to 510 * the created process. 511 * @param clientContext Arbitrary parameter used by the client to distinguish this connection. 512 */ 513 @CalledByNative 514 static void start( 515 Context context, 516 final String[] commandLine, 517 int[] fileIds, 518 int[] fileFds, 519 boolean[] fileAutoClose, 520 final long clientContext) { 521 assert fileIds.length == fileFds.length && fileFds.length == fileAutoClose.length; 522 FileDescriptorInfo[] filesToBeMapped = new FileDescriptorInfo[fileFds.length]; 523 for (int i = 0; i < fileFds.length; i++) { 524 filesToBeMapped[i] = 525 new FileDescriptorInfo(fileIds[i], fileFds[i], fileAutoClose[i]); 526 } 527 assert clientContext != 0; 528 529 int callbackType = CALLBACK_FOR_UNKNOWN_PROCESS; 530 boolean inSandbox = true; 531 String processType = getSwitchValue(commandLine, SWITCH_PROCESS_TYPE); 532 if (SWITCH_RENDERER_PROCESS.equals(processType)) { 533 callbackType = CALLBACK_FOR_RENDERER_PROCESS; 534 } else if (SWITCH_GPU_PROCESS.equals(processType)) { 535 callbackType = CALLBACK_FOR_GPU_PROCESS; 536 } else if (SWITCH_PPAPI_BROKER_PROCESS.equals(processType)) { 537 inSandbox = false; 538 } 539 540 ChildProcessConnection allocatedConnection = null; 541 synchronized (ChildProcessLauncher.class) { 542 if (inSandbox) { 543 allocatedConnection = sSpareSandboxedConnection; 544 sSpareSandboxedConnection = null; 545 } 546 } 547 if (allocatedConnection == null) { 548 allocatedConnection = allocateBoundConnection(context, commandLine, inSandbox); 549 if (allocatedConnection == null) { 550 // Notify the native code so it can free the heap allocated callback. 551 nativeOnChildProcessStarted(clientContext, 0); 552 return; 553 } 554 } 555 final ChildProcessConnection connection = allocatedConnection; 556 Log.d(TAG, "Setting up connection to process: slot=" + connection.getServiceNumber()); 557 558 ChildProcessConnection.ConnectionCallback connectionCallback = 559 new ChildProcessConnection.ConnectionCallback() { 560 @Override 561 public void onConnected(int pid) { 562 Log.d(TAG, "on connect callback, pid=" + pid + " context=" + clientContext); 563 if (pid != NULL_PROCESS_HANDLE) { 564 sBindingManager.addNewConnection(pid); 565 sServiceMap.put(pid, connection); 566 } else { 567 freeConnection(connection); 568 } 569 nativeOnChildProcessStarted(clientContext, pid); 570 } 571 }; 572 573 // TODO(sievers): Revisit this as it doesn't correctly handle the utility process 574 // assert callbackType != CALLBACK_FOR_UNKNOWN_PROCESS; 575 576 connection.setupConnection(commandLine, 577 filesToBeMapped, 578 createCallback(callbackType), 579 connectionCallback, 580 Linker.getSharedRelros()); 581 } 582 583 /** 584 * Terminates a child process. This may be called from any thread. 585 * 586 * @param pid The pid (process handle) of the service connection obtained from {@link #start}. 587 */ 588 @CalledByNative 589 static void stop(int pid) { 590 Log.d(TAG, "stopping child connection: pid=" + pid); 591 ChildProcessConnection connection = sServiceMap.remove(pid); 592 if (connection == null) { 593 logPidWarning(pid, "Tried to stop non-existent connection"); 594 return; 595 } 596 connection.stop(); 597 freeConnection(connection); 598 } 599 600 /** 601 * This implementation is used to receive callbacks from the remote service. 602 */ 603 private static IChildProcessCallback createCallback(final int callbackType) { 604 return new IChildProcessCallback.Stub() { 605 /** 606 * This is called by the remote service regularly to tell us about new values. Note that 607 * IPC calls are dispatched through a thread pool running in each process, so the code 608 * executing here will NOT be running in our main thread -- so, to update the UI, we 609 * need to use a Handler. 610 */ 611 @Override 612 public void establishSurfacePeer( 613 int pid, Surface surface, int primaryID, int secondaryID) { 614 // Do not allow a malicious renderer to connect to a producer. This is only used 615 // from stream textures managed by the GPU process. 616 if (callbackType != CALLBACK_FOR_GPU_PROCESS) { 617 Log.e(TAG, "Illegal callback for non-GPU process."); 618 return; 619 } 620 621 nativeEstablishSurfacePeer(pid, surface, primaryID, secondaryID); 622 } 623 624 @Override 625 public Surface getViewSurface(int surfaceId) { 626 // Do not allow a malicious renderer to get to our view surface. 627 if (callbackType != CALLBACK_FOR_GPU_PROCESS) { 628 Log.e(TAG, "Illegal callback for non-GPU process."); 629 return null; 630 } 631 632 return nativeGetViewSurface(surfaceId); 633 } 634 }; 635 } 636 637 private static void logPidWarning(int pid, String message) { 638 // This class is effectively a no-op in single process mode, so don't log warnings there. 639 if (pid > 0 && !nativeIsSingleProcess()) { 640 Log.w(TAG, message + ", pid=" + pid); 641 } 642 } 643 644 private static native void nativeOnChildProcessStarted(long clientContext, int pid); 645 private static native Surface nativeGetViewSurface(int surfaceId); 646 private static native void nativeEstablishSurfacePeer( 647 int pid, Surface surface, int primaryID, int secondaryID); 648 private static native boolean nativeIsSingleProcess(); 649} 650