Android性能优化:注意同步锁的影响

同步锁的影响

在我们的运行过程中,我们经常要加上同步锁,避免其他线程同时修改了数据。但是在要去获取锁的过程中,该锁被其他耗时线程占用或者其他线程占用了并等待其他线程唤醒,从而导致主线程获取不了锁等待最后发生ANR的情况。实际上,这种情况一般发生在使用了CountDownLatch的情况。CountDownLatch是一个计时器闭锁,该计数器是原子操作,同时只能有一个线程去操作该计数器。调用该类await的方法一直处于阻塞的状态,直到其他线程调用countdown使得计数器为0,每次调用countdown减1。当计数器为0之后,其他线程才能被唤醒并继续后面的操作。因此,CountDownLatch特别容易引起主线程被阻塞导致现象看起来变慢了或者发生卡顿了。CountDownLatch在操作数据库的时候会经常发生,因此要特别注意这块可能潜在的问题。既主线程想去获取数据库实例的同时,然而其他线程正在查询数据库的操作,从而其他线程先占有了锁,这种情况同步延时不大,但是存在情况就是多个数据库同事操作,而数据库部分方法使用了CountDownLatch锁,从而必须等待计数器为0才能释放相关的锁。

我们可以通过Traceview或profile查看哪个地方主线程的CPU间隔比较大,从而找到对应的方法,进而通过Threads工具进一步查看线程哪些地方出现await了,进一步确定具体是哪个方法,从而根据找到的方法做出相应的解法方案。如下使用Threads工具找到对应的线程阻塞方法:

image

SharedPreference文件操作引起的同步问题

SharedPreference文件是Android App开发中基本会用到的。然而SharedPreference在使用过程中存在引起新能的问题。如下SharedPreference初始化的时候,会进入到SharedPreferencesImpl构造函数,从磁盘中加载文件资源到内存,加载过程使用了同步锁,而在get和put的操作中同样使用到同步锁,因此,若在SP没初始化完成的情况下,我们的get和put操作将只能等待初始化获得的锁释放。另外一个,我们看到初始化的时候是从磁盘进行文件读取并且解析xml文件到内存的,因此我们应该尽量保障SP文件不要过大,这样可以尽量保证初始化能快速完成。

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    // Debugging
    if (mFile.exists() && !mFile.canRead()) {
        Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
    }

    Map map = null;
    StructStat stat = null;
    Throwable thrown = null;
    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map) XmlUtils.readMapXml(str);
            } catch (Exception e) {
                Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
            } finally {
                IoUtils.closeQuietly(str);
            }
        }
    } catch (ErrnoException e) {
        // An errno exception means the stat failed. Treat as empty/non-existing by
        // ignoring.
    } catch (Throwable t) {
        thrown = t;
    }

    synchronized (mLock) {
        mLoaded = true;
        mThrowable = thrown;

        // It's important that we always signal waiters, even if we'll make
        // them fail with an exception. The try-finally is pretty wide, but
        // better safe than sorry.
        try {
            if (thrown == null) {
                if (map != null) {
                    mMap = map;
                    mStatTimestamp = stat.st_mtim;
                    mStatSize = stat.st_size;
                } else {
                    mMap = new HashMap<>();
                }
            }
            // In case of a thrown exception, we retain the old map. That allows
            // any open editors to commit and store updates.
        } catch (Throwable t) {
            mThrowable = t;
        } finally {
            mLock.notifyAll();
        }
    }
}

SharedPreference文件有两个写入的方法,apply和commit方法。如果在主线程调用这两个方法,那么apply不会阻塞主线程,而commit会阻塞主线程。如下代码:

@Override
public boolean commit() {
    long startTime = 0;

    if (DEBUG) {
        startTime = System.currentTimeMillis();
    }

    MemoryCommitResult mcr = commitToMemory();

    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    } finally {
        if (DEBUG) {
            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                    + " committed after " + (System.currentTimeMillis() - startTime)
                    + " ms");
        }
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

@Override
public void apply() {
    final long startTime = System.currentTimeMillis();

    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            @Override
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }

                if (DEBUG && mcr.wasWritten) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " applied after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
        };

    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            @Override
            public void run() {
                awaitCommit.run();
                QueuedWork.removeFinisher(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it's hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

这两个方法的不同点在下面这块代码中:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    final Runnable writeToDiskRunnable = new Runnable() {
            @Override
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr, isFromSyncCommit);
                }
                synchronized (mLock) {
                    mDiskWritesInFlight--;
                }
                if (postWriteRunnable != null) {
                    postWriteRunnable.run();
                }
            }
        };

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

判断是否是同步保存isFromSyncCommit,在enqueueDiskWrite方法中,commit方法调用的postWriteRunnable为空,而apply不为空,因此进入if语句进行阻塞写入文件操作,等写操作执行完成之后,调用countdown方法对同步锁writtenToDiskLatch进行减1操作,这里,我们可以看出,若同事多个保存正在进行的话,线程因为要等待writtenToDiskLatch计数器计数到0才能继续执行,这就可能存在卡顿的情况发生。

那么apply是不是就一定不会导致主线程阻塞呢?答案是非也,我们查看下上面的apply方法的实现,我们看到执行了QueuedWork.addFinisher(awaitCommit);那么这个QueuedWork是干嘛的呢,我们知道,既然apply是一个不会阻塞的操作,那么为了保证Activity退出什么的能正常保存,可宁要加一个标记跟踪,而QueuedWork.addFinisher(awaitCommit)就是为了跟踪这个操作,把awaitCommit等待提交这个操作记录到QueuedWork的链表sFinishers中,然后我们看下QueuedWork的waitToFinish方法如下:


/**
 * Trigger queued work to be processed immediately. The queued work is processed on a separate
 * thread asynchronous. While doing that run and process all finishers on this thread. The
 * finishers can be implemented in a way to check weather the queued work is finished.
 *
 * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
 * after Service command handling, etc. (so async work is never lost)
 */
public static void waitToFinish() {
    long startTime = System.currentTimeMillis();
    boolean hadMessages = false;

    Handler handler = getHandler();

    synchronized (sLock) {
        if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
            // Delayed work will be processed at processPendingWork() below
            handler.removeMessages(QueuedWorkHandler.MSG_RUN);

            if (DEBUG) {
                hadMessages = true;
                Log.d(LOG_TAG, "waiting");
            }
        }

        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }

    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
        processPendingWork();
    } finally {
        StrictMode.setThreadPolicy(oldPolicy);
    }

    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }

            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }

    synchronized (sLock) {
        long waitTime = System.currentTimeMillis() - startTime;

        if (waitTime > 0 || hadMessages) {
            mWaitTimes.add(Long.valueOf(waitTime).intValue());
            mNumWaits++;

            if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
                mWaitTimes.log(LOG_TAG, "waited: ");
            }
        }
    }
}

从方法的注释知道,在Activity的onPause被调用的时候会调用打改方法,从而就有可能执行了awaitCommit,而awaitCommit的run方法中就会进行CountDownLatch锁的锁获取等待,仍然会阻塞主线程。因此,不要频繁进行保存操作是有必要的。目的是让计数器尽可能快的到0,当然,一般情况下,提倡不要再主线程进行commit和apply操作,最好通过线程池来进行SP的保存操作。

你可能感兴趣的:(Android性能优化:注意同步锁的影响)