Android内存泄漏监控

1. 内存泄漏

为什么会出现内存泄漏?因为在GC垃圾回收时会利用GC Root可达性分析算法去遍历哪些对象正在被引用。如果一个对象该销毁时却被另一个更长生命周期的对象引用,则会发生该销毁的对象无法被回收,导致内存泄漏。
在Java中,有四种对象引用:强引用可达性分析算法中此引用不会被回收;软引用可达性分析算法如果此时内存溢出时,这种引用的对象会被回收;弱引用可达性分析算法,对于这种引用对象会将不在引用链之内则会将其回收;虚引用则是一种标志作用,在回收时会调用finalize()方法。

2. 内存泄漏常见场景

2.1 单例模式中使用Context

当在一个单例对象中持有Activity的Context引用时,该引用会持有整个应用程序的生命周期,这可能会导致内存泄漏。

public class MySingleton {
    private Context mContext;
    private static MySingleton sInstance;

    private MySingleton(Context context) {
        mContext = context;
    }

    public static MySingleton getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new MySingleton(context);
        }
        return sInstance;
    }

    // ... some other methods ...
}

正确代码:

public class MySingleton {
    private Context mContext;
    private static MySingleton sInstance;

    private MySingleton(Context context) {
        mContext = context.getApplicationContext(); //使用ApplicationContext代替Activity或Application的Context
    }

    public static MySingleton getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new MySingleton(context);
        }
        return sInstance;
    }

    // ... some other methods ...
}

2.2 非静态内部类持有外部类引用

当一个非静态内部类实例化时,它会持有一个对外部类实例的引用,如果该内部类的实例长时间存在,则可能导致外部类实例的生命周期过长,从而导致内存泄漏。

示例代码:

public class MyActivity extends Activity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ... handle message ...
        }
    };

    // ... some other methods ...
}

正确代码:

public class MyActivity extends Activity {
    private static class MyHandler extends Handler {
        private WeakReference<MyActivity> mActivity;

        public MyHandler(MyActivity activity) {
            mActivity = new WeakReference<MyActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MyActivity activity = mActivity.get();
            if (activity != null) {
                // ... handle message ...
            }
        }
    }

    private MyHandler mHandler = new MyHandler(this);

    // ... some other methods ...
}

2.3 注册广播接收器未注销

当应用程序注册广播接收器时,如果不及时注销,它将一直存在,从而导致内存泄漏。

正确代码:

public class MyActivity extends Activity {
    private BroadcastReceiver mReceiver;
    private boolean mIsReceiverRegistered = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    mReceiver = new MyBroadcastReceiver();
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_SCREEN_ON);
    filter.addAction(Intent.ACTION_SCREEN_OFF);
    mIsReceiverRegistered = true;
    registerReceiver(mReceiver, filter); //注册广播接收器
}

 @Override
protected void onDestroy() {
    super.onDestroy();
    if (mIsReceiverRegistered) { //判断广播接收器是否已注册
        unregisterReceiver(mReceiver); //注销广播接收器
        mIsReceiverRegistered = false;
    }
}

// ... some other methods ...
}

2.4 匿名内部类持有外部类引用

当一个匿名内部类实例化时,它会持有一个对外部类实例的引用,如果该内部类的实例长时间存在,则可能导致外部类实例的生命周期过长,从而导致内存泄漏。

示例代码:

public class MyActivity extends Activity {
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread().start();
    }

    // ... some other methods ...
}

2.5 Handler导致的内存泄漏

当使用Handler时,如果在处理消息时,持有Activity或Fragment的引用,则可能导致内存泄漏。

示例代码:

public class MyActivity extends Activity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ... handle message ...
        }
    };

    // ... some other methods ...
}

正确代码:

public class MyActivity extends Activity {
    private static class MyHandler extends Handler {
        private WeakReference<MyActivity> mActivity;

        public MyHandler(MyActivity activity) {
            mActivity = new WeakReference<MyActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MyActivity activity = mActivity.get();
            if (activity != null) {
                // ... handle message ...
            }
        }
    }

    private MyHandler mHandler = new MyHandler(this);

    // ... some other methods ...
}

2.6 资源没有正确释放导致的内存泄漏

当使用资源(如Bitmap、Cursor等)时,如果没有正确释放,则可能导致内存泄漏。

示例代码:

public class MyActivity extends Activity {
    private Bitmap mBitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
    }

    // ... some other methods ...
}

正确代码:

public class MyActivity extends Activity {
    private Bitmap mBitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mBitmap != null) {
            mBitmap.recycle();
            mBitmap = null;
        }
    }

    // ... some other methods ...
}

3. 内存泄漏监控

虽然对常见的内存泄漏场景有认识,但是还是需要对内存泄漏进行自动监控,目前有的检测工具有:leakcanarymatrix的Resource Canary。其使用过程不再讲述。

3.1 检测内存泄漏

无论是使用Leakcanary还是Matrix工具,它们检测代码是否内存泄露都是一样的思路:
通过 registerActivityLifecycleCallbacks在每个Activity销毁onDestroy的时候,通过弱引用WeakReference去持有Activity,然后通过间隔预定的时间手动调用GC,并通过弱引用的get()方法去查看Activity在内存中是否被回收,如果没有被回收则判断为内存泄露。当然手动调用GC并一定会做回收操作,像Matrix就通过多次的GC判断,才认为内存泄露。

 class ActivityRefWatcher(val application: Application?) {

    private var handlerThread: HandlerThread? = null
    private var handler: Handler? = null
    private var lastTriggeredTime: Long = 0
    private val maxRedetectTimes = 2
    private val lock = java.lang.Object()

    private val destroyedActivityInfos: ConcurrentLinkedQueue<DestroyedActivityInfo> by lazy { ConcurrentLinkedQueue() }

    private val retryableTaskExecutor: RetryableTaskExecutor by lazy {
        RetryableTaskExecutor(GC_TIME, handlerThread)
    }

    init {
        handlerThread =
            HandlerThreadUtil.getNewHandlerThread("ActivityRefWatcher", Thread.NORM_PRIORITY)
        handler = HandlerThreadUtil.getDefaultHandler()
    }

    fun start() {
        stopDetect()
        application?.registerActivityLifecycleCallbacks(removedActivityMonitor)
        scheduleDetectProcedure()
    }

    fun stop() {
        stopDetect()
        handler?.removeCallbacksAndMessages(null)
    }

    private val removedActivityMonitor: ActivityLifecycleCallbacks =
        object : EmptyActivityLifecycleCallback() {
            override fun onActivityDestroyed(activity: Activity) {
                // 弱引用Activity,并收集相关信息
                pushDestroyedActivityInfo(activity)
                // 2s 后开始触发gc
                handler?.postDelayed({ triggerGc() }, delayTime)
            }
        }


    private val scanDestroyedActivitiesTask: RetryableTaskExecutor.RetryableTask =
        object : RetryableTaskExecutor.RetryableTask {

            override fun execute(): RetryableTaskExecutor.RetryableTask.Status {
                return checkDestroyedActivities()
            }
        }

    private fun scheduleDetectProcedure() {
        retryableTaskExecutor.executeInBackground(scanDestroyedActivitiesTask)
    }

    private fun stopDetect() {
        application?.unregisterActivityLifecycleCallbacks(removedActivityMonitor)
        unscheduleDetectProcedure()
    }

    private fun unscheduleDetectProcedure() {
        retryableTaskExecutor.clearTasks()
        destroyedActivityInfos.clear()
    }


    private fun pushDestroyedActivityInfo(activity: Activity) {
        val activityName = activity.javaClass.name
        val uuid = UUID.randomUUID()
        val keyBuilder = java.lang.StringBuilder()
        keyBuilder.append(ACTIVITY_REFKEY_PREFIX)
            .append(activityName)
            .append("_")
            .append(java.lang.Long.toHexString(uuid.mostSignificantBits))
            .append(java.lang.Long.toHexString(uuid.leastSignificantBits))
        val key = keyBuilder.toString()
        val destroyedActivityInfo = DestroyedActivityInfo(key, activity, activityName)
        destroyedActivityInfos.add(destroyedActivityInfo)
        synchronized(lock) {
            lock.notifyAll()
        }
    }

    /**
     * 调用GC
     */
    private fun triggerGc() {
        val currentTime = System.currentTimeMillis()
        if (currentTime - lastTriggeredTime < GC_TIME / 2 - 100) {
            Log.d(TAG, "skip triggering gc for frequency")
            return
        }
        lastTriggeredTime = currentTime
        Log.d(TAG, "triggering gc...")
        Runtime.getRuntime().gc()
        try {
            Thread.sleep(100)
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
        Runtime.getRuntime().runFinalization()
        Log.d(TAG, "gc was triggered.")
    }

    private fun checkDestroyedActivities(): RetryableTaskExecutor.RetryableTask.Status {
        if (destroyedActivityInfos.isEmpty()) {
            synchronized(lock) {
                try {
                    while (destroyedActivityInfos.isEmpty())
                        lock.wait()
                } catch (ignored: Throwable) {
                    // Ignored.
                }
            }
            return RetryableTaskExecutor.RetryableTask.Status.RETRY
        }

        triggerGc()

        val infoIt = destroyedActivityInfos.iterator()
        while (infoIt.hasNext()) {
            val destroyedActivityInfo = infoIt.next()
            triggerGc()
            if (destroyedActivityInfo.activityRef.get() == null) {
                infoIt.remove()
                continue
            }
            ++destroyedActivityInfo.detectedCount
            if (destroyedActivityInfo.detectedCount < maxRedetectTimes) {
                triggerGc()
                continue
            }
            Log.i(
                TAG,
                "the leaked activity ${destroyedActivityInfo.activityName} with key ${destroyedActivityInfo.key} has been processed. stop polling",
            )
            // 内存泄漏
            infoIt.remove()
        }
        return RetryableTaskExecutor.RetryableTask.Status.RETRY
    }

    companion object {

        private const val TAG = "ActivityRefWatcher"

        private val delayTime: Long = 2_000

        private const val ACTIVITY_REFKEY_PREFIX = "ACTIVITY_RESCANARY_REFKEY_"

        private val GC_TIME = TimeUnit.MINUTES.toMillis(1)

    }


}

3.2 dump与分析hprof

当知道有Activity内存泄漏之后,就要去分析内存泄漏的引用链。这里可以通过内存快照来分析泄漏链,hprof文件就是虚拟机在某个时刻上所有对象的内存快照,记录了对象的类名,大小和引用关系等。所以,这里关于hprof有两个操作,一是dump hprof文件,二是分析hprof文件。这两个操作都是耗时的,一定要在子线程或通过fork一个子进程,进行dump和分析hprof。

dump hprof
dump内存快照可以通过Debug.dumpHprofData方法,dump文件大小可能有几百兆,一些优化是边dump边裁剪文件,这需要涉及到native hook的技术。
下面分别列出在子线程中dump和在子进程dump的实例代码:

  • 子线程dump

    public void dumpHprofData() {
        final String hprofPath = "/sdcard/myapp.hprof";

        // 在异步线程中执行dumpHprofData()操作
        new Thread(new Runnable() {
            @Override
            public void run() {
                Debug.dumpHprofData(hprofPath);

                // 在主线程中提示用户操作完成
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(MyActivity.this, "hprof file generated", Toast.LENGTH_SHORT).show();
                    }
                });
            }
        }).start();
    }

            val storageDirectory = File(application?.cacheDir, "leakactivity")
            if (!storageDirectory.exists()) {
                storageDirectory.mkdir()
            }
            val fileName =
                SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(Date())
            val file = File(storageDirectory, fileName)
            // dump 出堆转储文件
            Debug.dumpHprofData(file.absolutePath)
            Log.i(TAG, "dumpHeap: ${file.absolutePath}")
  • 子进程dump
private fun dumpHprof() {
    Thread {
        // 创建一个子进程
        val process = Runtime.getRuntime().exec(arrayOf("sh"))

        // 获取输出流和输入流
        val outputStream = process.outputStream
        val inputStream = process.inputStream

        // 向子进程写入命令
        outputStream.write("am dumpheap com.package.name /sdcard/leak.hprof\n".toByteArray())
        outputStream.flush()

        // 读取子进程输出的结果
        val reader = BufferedReader(InputStreamReader(inputStream))
        var line: String?
        while (reader.readLine().also { line = it } != null) {
            Log.d("DumpHprof", line!!)
        }

        // 关闭输出流和输入流
        outputStream.close()
        inputStream.close()

        // 等待子进程结束
        process.waitFor()

        // 处理hprof文件
        // ...
    }.start()
}

这段代码创建了一个子进程,并向子进程发送命令来执行Debug.dumpHprofData操作,这样就可以让主线程不卡顿了。

分析 hprof
分析hprof也是一种相对耗时的操作,分析hprof可以在本地也可以放到服务器上,如果之前的hprof文件没有裁剪,可以裁剪之后才分析或上传。https://blog.yorek.xyz/android/3rd-library/hprof-shrink/ 一文中讲了几种方案,这里不展开分析。提供几种开源方案,一种是shark_leakcanary库,一种是haha库,当然也可以自己实现分析hprof文件,其主要结构是header和多个record组成。
如下是使用shark_leakcanary库实现的hprof文件的分析。

  private fun dumpHeap() {
        handler?.post {
            val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener { step ->
                Log.i(TAG, "Analysis in progress, working on: ${step.name}")
            })
            val heapAnalysis = heapAnalyzer.analyze(
                heapDumpFile = file,
                leakingObjectFinder = FilteringLeakingObjectFinder(
                    AndroidObjectInspectors.appLeakingObjectFilters
                ),
                referenceMatchers = AndroidReferenceMatchers.appDefaults,
                computeRetainedHeapSize = true,
                objectInspectors = AndroidObjectInspectors.appDefaults.toMutableList(),
                proguardMapping = null,
                metadataExtractor = AndroidMetadataExtractor
            )
            Log.i(TAG, "dumpHeap: \n$heapAnalysis")
        }
    }

4. Bitmap优化

4.1 常规Bitmap优化

图片占用的内存大小 = 图片宽度 × 图片高度 × 每个像素占用的字节数
例如,如果有一张 1000 × 1000 像素的 ARGB_8888 格式的图片,每个像素占用 4 个字节,则该图片占用的内存大小为:1000 × 1000 × 4 = 4,000,000 字节 = 3.81 MB

由于 Bitmap 对象可能占用大量的内存,因此在使用 Bitmap 时需要注意其优化,以避免内存问题和性能问题。以下是一些 Android Bitmap 的优化方案:

  1. 使用 inSampleSize 属性来减少 Bitmap 对象的内存使用,它指定了加载图片时缩小的倍数。例如,如果将 inSampleSize 设为 2,则图片将被缩小为原始大小的 1/2。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
  1. 使用 RGB_565 格式来减少 Bitmap 对象的内存使用。默认情况下,Android 使用 ARGB_8888 格式来表示 Bitmap 对象。而使用 RGB_565 格式可以将每个像素的内存使用减少到一半。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image, options);
  1. 使用 BitmapRegionDecoder 来只加载图片的一部分,而不是整个图片。例如,如果只需要加载图片的顶部一部分,可以使用以下代码:
InputStream inputStream = getResources().openRawResource(R.drawable.image);
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream, false);
Bitmap bitmap = decoder.decodeRegion(new Rect(0, 0, decoder.getWidth(), decoder.getHeight() / 2), null);
  1. 缓存 Bitmap 对象
    使用 LruCache 来缓存 Bitmap 对象,以避免频繁地创建和销毁 Bitmap 对象。LruCache 是一个内存缓存类,可以在内存达到一定限制时自动删除最近最少使用的对象。例如,以下代码演示了如何使用 LruCache 来缓存 Bitmap 对象:
private LruCache<String, Bitmap> mBitmapCache;

public void initBitmapCache() {
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    int cacheSize = maxMemory / 8;

    mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount() / 1024;
        }
    };
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    if (getBitmapFromCache(key) == null) {
        mBitmapCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromCache(String key) {
    return mBitmapCache.get(key);
}
  1. 合理释放 Bitmap 对象
    当 Bitmap 对象不再使用时,需要手动调用 recycle() 方法来释放内存。例如,以下代码演示了如何在 ImageView 中加载 Bitmap 并在不再需要时释放 Bitmap:
ImageView imageView = findViewById(R.id.image_view);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image);
imageView.setImageBitmap(bitmap);

// 释放 Bitmap 对象
imageView.setImageBitmap(null);
bitmap.recycle();

4.2 大图监控

可以Plugin、Transform、ASM技术来在ImageView的方法进行插桩技术。

  1. 创建一个Gradle插件,使用Transform技术修改字节码。
  2. 使用ASM技术在ImageView的setImageDrawable()方法中插入代码,用于监控图片的宽高。
    具体代码不表。

5. 内存常见优化

最后在来总结下,内存优化的有哪些常用方案:

  1. 使用 SparseArray 和 ArrayMap 替代 HashMap
    HashMap 存储大量的对象时会消耗很大的内存,尤其是在处理大量数据时。在 Android 开发中,我们可以使用 SparseArray 和 ArrayMap 来替代 HashMap。SparseArray 是 Android 提供的一个优化版的 Map,专门用来处理键为 int 类型的情况;而 ArrayMap 则是优化版的 Map,专门用来处理小数据集合的情况,它比 HashMap 更加高效。

  2. 使用 Bitmap 配置的 ARGB_8888
    Bitmap 是 Android 开发中经常使用的对象,它占用大量的内存。在使用 Bitmap 时,我们可以通过设置 Bitmap 的 Config 来减少内存消耗。ARGB_8888 是一种高质量的 Bitmap 配置,虽然会占用更多的内存,但是可以保证图片的清晰度。

  3. 使用 BitmapFactory.Options 来压缩图片
    在 Android 应用中,我们经常需要加载大量的图片,而这些图片的分辨率往往很高,导致内存消耗过大。为了减少内存消耗,我们可以使用 BitmapFactory.Options 来对图片进行压缩。可以通过设置 BitmapFactory.Options 中的 inSampleSize 属性来控制压缩比例。

  4. 使用 LruCache 来缓存对象
    LruCache 是 Android 提供的一种缓存对象的方式,它可以帮助我们减少内存消耗。LruCache 可以按照最近最少使用的原则来缓存对象,并且可以根据缓存对象的大小来自动调整缓存容量。

  5. 及时释放资源
    在 Android 开发中,我们需要及时释放无用的资源,以避免内存泄漏和内存溢出。例如,关闭 Cursor 对象、释放 Bitmap 对象、及时取消异步任务等。

  6. 使用工具检查内存泄漏问题
    使用内存分析工具,可以帮助我们检查内存问题,包括内存泄漏和内存溢出。我们可以使用这些工具来找出应用中的内存问题,并及时进行优化。

  7. 优化布局和控件
    在布局中,我们可以使用 FrameLayout 代替 RelativeLayout,因为 RelativeLayout 对内存消耗较大。在使用控件时,我们可以避免使用过多的控件和嵌套控件,尽量使用简单的布局方式和控件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • 前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。 本篇是...
    zhx喜籽阅读 822评论 0 4
  • 前言 这篇文章的内容是我回顾和再学习 Android 内存优化的过程中整理出来的,整理的目的是让我自己对 Andr...
    灯不利多阅读 1,574评论 0 8
  • GC算法 当内存不足时,系统就会触发GC,GC采用垃圾标记算法为跟搜索算法 从图中可以看书obj4是科大的对象,标...
    月影路西法阅读 635评论 0 0
  • Android UI优化[https://www.jianshu.com/p/50b48f45e8ab]Andro...
    海_3efc阅读 1,438评论 0 2
  • 一、什么是内存泄露? 当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而就导致...
    小村医阅读 314评论 0 0