月度归档: 2021 年 6 月

LeakCanary原理解析

简介

LeakCanary是一款开源的内存泄漏检查工具,在项目中,可以使用它来检测Activity是否能够被GC及时回收。github的地址为https://github.com/square/leakcanary

使用方式解析

将LeakCanary引入AS,在Application中调用如下方法,可以跟踪Activity是否被GC回收。

%title插图%num

入口函数

LeakCanary.install()方法的调用流程如下所示:

%title插图%num

install方法调用流程

Install方法如下:

%title插图%num

install方法

其中listenerServiceClass方法传入了展示分析结果的Service(DisplayLeakService);excludedRefs方法排除了开发中可以忽略的泄漏路径;buildAndInstall是主要的函数,实现了activity是否能被释放的监听。

%title插图%num

buildAndInstall

buildAndInstall会调用ActivityRefWatcher.install来监测Activity。

%title插图%num

install

*终调用了watchActivities():

%title插图%num

watchActivities

通过registerActivityLifecycleCallbacks来监听Activity的生命周期:

%title插图%num

lifecycleCallbacks

lifecycleCallbacks监听Activity的onDestroy方法,正常情况下activity在onDestroy后需要立即被回收,onActivityDestroyed方法*终会调用RefWatcher.watch方法:

%title插图%num

watch

监测机制利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收。检查方法如下:

%title插图%num

ensureGone

1、  首先通过removeWeaklyReachablereference来移除已经被回收的Activity引用

2、 通过gone(reference)判断当前弱引用对应的Activity是否已经被回收,如果已经回收说明activity能够被GC,直接返回即可。

3、  如果Activity没有被回收,调用GcTigger.runGc方法运行GC,GC完成后在运行第1步,然后运行第2步判断Activity是否被回收了,如果这时候还没有被回收,那就说明Activity可能已经泄露。

4、  如果Activity泄露了,就抓取内存dump文件(Debug.dumpHprofData)

%title插图%num

dumpHeap

5、  之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析

%title插图%num

分析dump

接着通过HeapAnalyzer(checkForLeak—findLeakingReference—findLeakTrace)来进行内存泄漏分析。

6、  *后通过DisplayLeakService进行内存泄漏的展示。

LeakCanary原理分析

导语:

提到Java语言的特点,无论是教科书还是程序员一般都会罗列出面向对象、可移植性及安全等特点。但如果你是一位刚从C/C++转到Java的程序员,对Java语言的特性除了面向对象之外,*外直接的应当是在Java虚拟机(JVM)在内存管理方面给我们变成带来的便利。JVM的这一大特性使Java程序员从繁琐的内存管理工作中得到了一定解放,但是JVM的这个特点的实现也是有代价的,并且它也并非万能。因此如果一个编程习惯不好的Java程序员如果完全将内存回收寄希望于JVM,那么OOM(Out Of Memory)就已经悄悄潜伏在了他的程序之中。

Android应用基于Java实现,因此它也将Java的优缺点继承了过来。相对来说,移动设备对于内存问题更为敏感,程序在申请一定的内存但又没有及时得到释放后就很容易发生OOM而导致crash。因此Android程序员开发过程中一般都会定时排查自己程序中可能出现的这些雷点,尽可能地避免因为crash问题而影响用户体验。

1.LeakCanary简介

目前Java程序*常用的内存分析工具应该是MAT(Memory Analyzer Tool),它是一个Eclipse插件,同时也有单独的RCP客户端,也可以通过官网的SVN下载到它的源码(具体见另一篇《compile-MAT》)并编译成jar包。LeakCanary本质上就是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,通过集成这个工具代码到自己的Android工程当中就能够在程序调试开发过程中通过一个漂亮的界面(如下图)随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,*大地方便了Android应用程序的开发。

LeakCanary_result

总的来说,LeakCanary有如下几个明显优点:

  • 针对Android Activity组件完全自动化的内存泄漏检查。
  • 可定制一些行为(dump文件和leaktrace对象的数量、自定义例外、分析结果的自定义处理等)。
  • 集成到自己工程并使用的成本很低。
  • 友好的界面展示和通知。

假如你现在想集成LeakCanary到自己的工程中,那么你只需要做以下工作:1. 导入leakcanary的jar包到自己工程(下载链接:leakcanary.zip)2. 在4.0以上,只需要在工程的Application的onCreate函数中按照如下的方式加入一行代码:

  1. public class ExampleApplication extends Application {
  2. @Override
  3. public void onCreate() {
  4. super.onCreate();
  5. LeakCanary.install(this);
  6. }
  7. }

4.0以下在需要进行内存泄漏监控的Activity的onDestroy方法中按如下加入代码:

  1. protected void onDestroy() {
  2. super.onDestroy();
  3. // start watch
  4. HeapDump.Listener heapDumpListener =
  5. new ServiceHeapDumpListener(this, listenerServiceClass);
  6. DebuggerControl debuggerControl = new AndroidDebuggerControl();
  7. AndroidHeapDumper heapDumper = new AndroidHeapDumper();
  8. heapDumper.cleanup();
  9. ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults().build();
  10. RefWatcher refWatcher = new RefWatcher(new AndroidWatchExecutor(), debuggerControl, GcTrigger.DEFAULT,
  11. heapDumper, heapDumpListener, excludedRefs);
  12. }   第二种情况下,在有多个Activity需要检测的情况看起来稍显繁琐,实际上可以用以上方法实现一个基类Activity,之后需要内存泄漏检测的Activity直接继承这个基类Activity就不需要每次都重复处理oonDestroy方法了。并且以上代码只作为示例,实际上每次watch的时候并不需要重新new一个RefWatcher对象,因为这个对象是可以重复使用的。

完成了以上两个步骤后,LeakCanary就可以为你的工程服务了,这之中需要我们自己处理的工作很少,相比较我们自己手工用MAT进行内存泄漏检测而言,确实方便了很多。## 2.LeakCanary原理分析 ##这么强大的工具,它是如何实现的呢,引用LeakCanary中文使用说明,它的基本工作原理如下:

  1. RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
  2. 然后在后台线程检查引用是否被清除,如果没有,调用GC。
  3. 如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
  4. 在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
  5. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄漏。
  6. HeapAnalyzer 计算 到 GC roots 的*短强引用路径,并确定是否是泄漏。如果是的话,建立导致泄漏的引用链。
  7. 引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。

但事实上一切并没那么简单,LeakCanary的设计者在实现的时候实际上为我们考虑了很多细节。可以通过源码分析来走一遍一次内存泄漏检查的流程。在一个Activity生命周期结束调用oonDestroy方法的时候会触发LeakCanary进行一次内存泄漏检查,LeakCanary开始进行检查的入口函数实际上是RefWatcher类的,watch方法,其源码如下:

  1. public void watch(Object watchedReference, String referenceName) {
  2. String key = UUID.randomUUID().toString();
  3. retainedKeys.add(key);
  4. final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
  5. watchExecutor.execute(new Runnable() {
  6. @Override
  7. public void run() {
  8. ensureGone(reference, watchStartNanoTime);
  9. }
  10. });
  11. } 这个函数做的主要工作就是为需要进行泄漏检查的Activity创建一个带有唯一key标志的弱引用,并将这个弱引用key保存至retainedKeys中,然后将后面的工作交给watchExecutor来执行。这个watchExecutor在LeakCanary中是AndroidWatchExecutor的实例,调用它的execute方法实际上就是向主线程的消息队列中插入了一个IdleHandler消息,这个消息只有在对应的消息队列为空的时候才会去执行,因此RefWatcher的watch方法就保证了在主线程空闲的时候才会去执行ensureGone方法,防止因为内存泄漏检查任务而严重影响应用的正常执行。ensureGone的主要源码如下:
  12. void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
  13. removeWeaklyReachableReferences();
  14. if (gone(reference) || debuggerControl.isDebuggerAttached()) {
  15. return;
  16. }
  17. gcTrigger.runGc(); // 手动执行一次gc
  18. removeWeaklyReachableReferences();
  19. if (!gone(reference)) {
  20. long startDumpHeap = System.nanoTime();
  21. long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap – gcStartNanoTime);
  22. File heapDumpFile = heapDumper.dumpHeap();
  23. if (heapDumpFile == null) {
  24. // Could not dump the heap, abort.
  25. Log.d(TAG, “Could not dump the heap, abort.”);
  26. return;
  27. }
  28. long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() – startDumpHeap);
  29. heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs,
  30. watchDurationMs, gcDurationMs, heapDumpDurationMs));
  31. }
  32. } 因为这个方法是在主线程中执行的,因此一般执行到这个方法中的时候之前被destroy的那个Activity的资源应该被JVM回收了,因此这个方法首先调用removeWeaklyReachableReferences方法来将引用队列中存在的弱引用从retainedKeys中删除掉,这样,retainedKeys中保留的就是还没有被回收对象的弱引用key。之后再用gone方法来判断我们需要检查的Activity的弱引用是否在retainedKeys中,如果没有,说明这个Activity对象已经被回收,检查结束。否则,LeakCanary主动触发一次gc,再进行以上两个步骤,如果发现这个Activity还没有被回收,就认为这个Activity很有可能泄漏了,并dump出当前的内存文件供之后进行分析。

之后的工作就是对内存文件进行分析,由于这个过程比较耗时,因此*终会把这个工作交给运行在另外一个进程中的HeapAnalyzerService来执行。HeapAnalyzerService通过调用HeapAnalyzer的checkForLeak方法进行内存分析,其源码如下:

  1. public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
  2. ISnapshot snapshot = null;
  3. try {
  4. snapshot = openSnapshot(heapDumpFile);
  5. IObject leakingRef = findLeakingReference(referenceKey, snapshot);
  6. // False alarm, weak reference was cleared in between key check and heap dump.
  7. if (leakingRef == null) {
  8. return noLeak(since(analysisStartNanoTime));
  9. }
  10. String className = leakingRef.getClazz().getName();
  11. AnalysisResult result =
  12. findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
  13. if (!result.leakFound) {
  14. result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
  15. }
  16. return result;
  17. } catch (SnapshotException e) {
  18. return failure(e, since(analysisStartNanoTime));
  19. } finally {
  20. cleanup(heapDumpFile, snapshot);
  21. }
  22. }

这个方法进行的*步就是利用HAHA将之前dump出来的内存文件解析成Snapshot对象,其中调用到的方法包括SnapshotFactory的parse和HprofIndexBuilder的fill方法。解析得到的Snapshot对象直观上和我们使用MAT进行内存分析时候罗列出内存中各个对象的结构很相似,它通过对象之间的引用链关系构成了一棵树,我们可以在这个树种查询到各个对象的信息,包括它的Class对象信息、内存地址、持有的引用及被持有的引用关系等。到了这一阶段,HAHA的任务就算完成,之后LeakCanary就需要在Snapshot中找到一条有效的到被泄漏对象之间的引用路径。首先它调用findLeakTrace方法来从Snapshot中找到被泄漏对象,源码如下:

  1. private IObject findLeakingReference(String key, ISnapshot snapshot) throws SnapshotException {
  2. Collection<IClass> refClasses =
  3. snapshot.getClassesByName(KeyedWeakReference.class.getName(), false);
  4. if (refClasses.size() != 1) {
  5. throw new IllegalStateException(
  6. “Expecting one class for “ + KeyedWeakReference.class.getName() + ” in “ + refClasses);
  7. }
  8. IClass refClass = refClasses.iterator().next();
  9. int[] weakRefInstanceIds = refClass.getObjectIds();
  10. for (int weakRefInstanceId : weakRefInstanceIds) {
  11. IObject weakRef = snapshot.getObject(weakRefInstanceId);
  12. String keyCandidate =
  13. PrettyPrinter.objectAsString((IObject) weakRef.resolveValue(“key”), 100);
  14. if (keyCandidate.equals(key)) { // 匹配key
  15. return (IObject) weakRef.resolveValue(“referent”); // 定位泄漏对象
  16. }
  17. }
  18. throw new IllegalStateException(“Could not find weak reference with key “ + key);
  19. }

为了能够准确找到被泄漏对象,LeakCanary通过被泄漏对象的弱引用来在Snapshot中定位它。因为,如果一个对象被泄漏,一定也可以在内存中找到这个对象的弱引用,再通过弱引用对象的referent就可以直接定位被泄漏对象。下一步的工作就是找到一条有效的到被泄漏对象的*短的引用,这通过findLeakTrace来实现,实际上寻找*短路径的逻辑主要是封装在PathsFromGCRootsComputerImpl这个类的getNextShortestPath和processCurrentReferrefs这两个方法当中,其源码如下:

  1. public int[] getNextShortestPath() throws SnapshotException {
  2. switch (state) {
  3. case 0: // INITIAL
  4. {
  5. }
  6. case 1: // FINAL
  7. return null;
  8. case 2: // PROCESSING GC ROOT
  9. {
  10. }
  11. case 3: // NORMAL PROCESSING
  12. {
  13. int[] res;
  14. // finish processing the current entry
  15. if (currentReferrers != null) {
  16. res = processCurrentReferrefs(lastReadReferrer + 1);
  17. if (res != null) return res;
  18. }
  19. // Continue with the FIFO
  20. while (fifo.size() > 0) {
  21. currentPath = fifo.getFirst();
  22. fifo.removeFirst();
  23. currentId = currentPath.getIndex();
  24. currentReferrers = inboundIndex.get(currentId);
  25. if (currentReferrers != null) {
  26. res = processCurrentReferrefs(0);
  27. if (res != null) return res;
  28. }
  29. }
  30. return null;
  31. }
  32. default:
  33. }
  34. }
  35. private int[] processCurrentReferrefs(int fromIndex) throws SnapshotException {
  36. GCRootInfo[] rootInfo = null;
  37. for (int i = fromIndex; i < currentReferrers.length; i++) {
  38. }
  39. for (int referrer : currentReferrers) {
  40. if (referrer >= 0 && !visited.get(referrer) && !roots.containsKey(referrer)) {
  41. if (excludeMap == null) {
  42. fifo.add(new Path(referrer, currentPath));
  43. visited.set(referrer);
  44. } else {
  45. if (!refersOnlyThroughExcluded(referrer, currentId)) {
  46. fifo.add(new Path(referrer, currentPath));
  47. visited.set(referrer);
  48. }
  49. }
  50. }
  51. }
  52. return null;
  53. }
  54. }

为了是逻辑更清晰,在这里省略了对GCRoot的处理。这个类将整个内存映像信息抽象成了一个以GCRoot为根的树,getNextShortestPath的状态3是对一般节点的处理,由于之前已经定位了被泄漏的对象在这棵树中的位置,为了找到一条到GCRoot*短的路径,PathsFromGCRootsComputerImpl采用的方法是类似于广度优先的搜索策略,在getNextShortestPath中从被泄漏的对象开始,调用一次processCurrentReferrefs将持有它引用的节点(父节点),加入到一个FIFO队列中,然后依次再调用getNextShortestPath和processCurrentReferrefs来从FIFO中取节点及将这个节点的父节点再加入FIFO队列中,一层一层向上寻找,哪条路径*先到达GCRoot就表示它应该是一条*短路径。由于FIFO保存了查询信息,因此如果要找次*短路径只需要再调用一次getNextShortestPath触发下一次查找即可,其算法原理如下图所示。

LeakCanary_result

至此,主要的工作就完成了,后面就是调用buildLeakTrace构建查询结果,这个过程相对简单,仅仅是将之前查找的*短路径转换成*后需要显示的LeakTrace对象,这个对象中包括了一个由路径上各个节点LeakTraceElement组成的链表,代表了检查到的*短泄漏路径。*后一个步骤就是将这些结果封装成AnalysisResult对象然后交给DisplayLeakService进行处理。这个service主要的工作是将检查结果写入文件,以便之后能够直接看到*近几次内存泄露的分析结果,同时以notification的方式通知用户检测到了一次内存泄漏。使用者还可以继承这个service类来并实现afterDefaultHandling来自定义对检查结果的处理,比如将结果上传刚到服务器等。

以上就是对LeakCanary源码的分析,中间省略了一些细节处理的说明,但不得不提的是LeakCanary支持自定义泄漏豁对象ExcludedRefs的集合,这些豁免对象一般都是一些已知的系统泄漏问题或者自己工程中已知但又需要被排除在检查之外的泄漏问题构成的。LeakCanary在findLeakTrace方法中如果发现这个集合中的对象存在于泄漏路径上,就会排除掉这条泄漏路径并尝试寻找下一条。

LeakCanary原理及使用

一、LeakCanary简介
LeakCanary是Square公司为Android开发者提供的一个自动检测内存泄漏的工具,LeakCanary本质上是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,我们可以通过集成LeakCanary提供的jar包到自己的工程中,一旦检测到内存泄漏,LeakCanary就会dump Memory信息,并通过另一个进程分析内存泄漏的信息并展示出来,随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,*大地方便了Android应用程序的开发。

二、LeakCanary工作机制
RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
然后在后台线程检查引用是否被清除,如果没有,调用GC。
如果引用还是未被清除,把 heap 内存 dump 到 APP对应的文件系统中的一个 .hprof 文件中。
在另外一个进程中的 HeapAnalyzerService 有一个HeapAnalyzer 使用HAHA 解析这个文件。
得益于唯一的 reference key, HeapAnalyzer 找到KeyedWeakReference,定位内存泄露。
HeapAnalyzer 计算到 GC roots的*短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
引用链传递到 APP 进程中的DisplayLeakService, 并以通知的形式展示出来。
三、LeakCanary的Android Studio集成
1. 在build.gradle中添加LeakCanary的依赖包
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:1.6.3’
releaseImplementation ‘com.squareup.leakcanary:leakcanary-android-no-op:1.6.3’

注意: debug和release版本要一致,否则会报错。

2. 在我们自定义Application的onCreate方法中注册LeakCanary
public class LeakApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) { //1
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}

注释1处的代码用来进行过滤操作,如果当前的进程是用来给LeakCanary 进行堆分析的则return,否则会执行LeakCanary的install方法。这样我们就可以使用LeakCanary了,如果检测到某个Activity 有内存泄露,LeakCanary 就会给出提示。

3. 重写Application
上述代码只能够检测Activity的内存泄漏,当然还存在其他类的内存泄漏,这时我们就需要使用RefWatcher来进行监控。重写Application,如下所示:

public class LeakApplication extends Application {
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher= setupLeakCanary();
}
private RefWatcher setupLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
}
return LeakCanary.install(this);
}
public static RefWatcher getRefWatcher(Context context) {
LeakApplication leakApplication = (LeakApplication) context.getApplicationContext();
return leakApplication.refWatcher;
}
}

install方法会返回RefWatcher用来监控对象,LeakApplication中还要提供getRefWatcher静态方法来返回全局RefWatcher。
注意: 需要在AndroidManifest.xml文件中添加android:name=”.LeakApplication”,指定Application子类,当应用启动时,这个类的实例被*个创建。这个属性是可选的,大多数APP都不需要这个属性。在没有这个属性的时候,Android会启动一个Application类的实例。

%title插图%num
4. 在activity或fragment中使用leak canary举例
public class SearchActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakThread leakThread = new LeakThread();
leakThread.start();
}
class LeakThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(6 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = LeakApplication.getRefWatcher(this);//1
refWatcher.watch(this);
}
}

SearchActivity存在内存泄漏,原因就是非静态内部类LeakThread持有外部类SearchActivity的引用,LeakThread中做了耗时操作,导致SearchActivity无法被释放。
在注释1处得到RefWatcher,并调用它的watch方法,watch方法的参数就是要监控的对象。当然,在这个例子中onDestroy方法是多余的,因为LeakCanary在调用install方法时会启动一个ActivityRefWatcher类,它用于自动监控Activity执行onDestroy方法之后是否发生内存泄露。这里只是为了方便举例,如果想要监控Fragment,在Fragment中添加如上的onDestroy方法是有用的。

public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}

运行程序,这时会在界面生成一个名为Leaks的应用图标。接下来不断的切换横竖屏,这时会闪出一个提示框,提示内容为:“Dumping memory, app will freeze.Brrrr.”。再稍等片刻,内存泄漏信息就会通过Notification展示出来,比如荣耀magic的通知栏如下图1所示。
Notification中提示了SearchActivity发生了内存泄漏。点击Notification就可以进入内存泄漏详细页,除此之外也可以通过Leaks应用的列表界面进入,列表界面如下图2所示。内存泄漏详细页如下图3所示。
点击加号就可以查看具体类所在的包名称。整个详情就是一个引用链:SearchActivity的内部类LeakThread引用了LeakThread的this$0,this$0的含义就是内部类自动保留的一个指向所在外部类的引用,而这个外部类就是详情*后一行所给出的SearchActivity的实例,这将会导致SearchActivity无法被GC(garbage collection,垃圾回收),从而产生内存泄漏。
图1:%title插图%num

图2:%title插图%num

图3:%title插图%num

解决该内存泄露的方法就是将LeakThread改为静态内部类。再次运行程序LeakThread就不会给出内存泄漏的提示了。


static class LeakThread extends Thread {

}

四、LeakCanary2使用
1. 和 LeakCanary1 相比,LeakCanary2 有以下改动
完全使用 Kotlin 重写。
使用新的Heap分析工具Shark,替换到之前的haha,按官方的说法,内存占用减少了10倍。
泄露类型分组。
2. LeakCanary2集成
只需要增加以下依赖即可:

debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.2’
1
LeakCanary2 实现了自动调用 install() 方法,实现方式是使用的 ContentProvider,相关代码位于 leakcanary-object-watcher-android 模块中的 AppWatcherInstaller.kt 中。
AppWatcherInstaller 继承 ContentProvider,重写了 onCreate() 方法,这里利用的是,注册在 Manifest 文件中的 ContentProvider,会在应用启动时,由 ActivityThread 创建并初始化。
————————————————

Android内存优化之避免内存泄漏

前言

内存泄漏向来都是内存优化的重点,它如同幽灵一般存于我们的应用当中,有时它不会现身,但一旦现身就会让你头疼不已。因此,如何避免、发现和解决内存泄漏就变得尤为重要。这一篇我们先来学习如何避免内存泄漏。

1.什么是内存泄漏

我们知道,每个应用程序都需要内存来完成工作,为了确保Android系统的每个应用都有足够的内存,Android系统需要有效地管理内存分配。当内存不足时,Android运行时就会触发GC,GC采用的垃圾标记算法为根搜索算法,
讲到根搜索算法,如下图所示:
VQx9PK.png
从上图看以看出,Obj4是可达的对象,表示它正被引用,因此不会标记为可回收的对象。Obj5、Obj6和Obj7都是不可达的对象,其中Obj5和Obj6虽然互相引用,但是因为他们到GC Roots是不可达的所以它们仍旧会标记为可回收的对象。

内存泄漏就是指没有用的对象到GC Roots是可达的(对象被引用),导致GC无法回收该对象。此时,如果Obj4是一个没有用的对象,但它仍与GC Roots是可达的,那么Obj4就会内存泄漏。
内存泄漏产生的原因,主要分为三大类:
1.由开发人员自己编码造成的泄漏。
2.第三方框架造成的泄漏。
3.由Android 系统或者第三方ROM造成的泄漏。
其中第二种和第三种有时是不可控的,但是*种是可控的,既然是可控的,我们就要尽量在编码时避免造成内存泄漏,下面就来列举出常见的内存泄漏的场景。

2.内存泄漏的场景

2.1 非静态内部类的静态实例

非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接的长期维持着外部类的引用,阻止被系统回收。

JAVA

public class SecondActivity extends AppCompatActivity {
    private static Object inner;
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.bt_next);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                createInnerClass();
                finish();
            }
        });
    }

    void createInnerClass() {
        class InnerClass {
        }
        inner = new InnerClass();//1
    }
}


当点击Button时,会在注释1处创建了非静态内部类InnerClass的静态实例inner,该实例的生命周期会和应用程序一样长,并且会一直持有SecondActivity 的引用,导致SecondActivity无法被回收。

2.2 匿名内部类的静态实例

和前面的非静态内部类一样,匿名内部类也会持有外部类实例的引用。

JAVA

public class AsyncTaskActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task);
        button = (Button) findViewById(R.id.bt_next);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startAsyncTask();
                finish();
            }
        });
    }
    void startAsyncTask() {
        new AsyncTask<Void, Void, Void>() {//1
            @Override
            protected Void doInBackground(Void... params) {
                while (true) ;
            }
        }.execute();
    }
}

在注释1处实例化了一个AsyncTask,当AsyncTask的异步任务在后台执行耗时任务期间,AsyncTaskActivity 被销毁了,被AsyncTask持有的AsyncTaskActivity实例不会被垃圾收集器回收,直到异步任务结束。
解决办法就是自定义一个静态的AsyncTask,如下所示。

JAVA

public class AsyncTaskActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task);
        button = (Button) findViewById(R.id.bt_next);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startAsyncTask();
                finish();
            }
        });
    }
    void startAsyncTask() {
        new MyAsyncTask().execute();
    }
    private static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... params) {
            while (true) ;
        }
    }
}

与AsyncTask类似的还有TimerTask,这里就不再举例。

2.3 Handler内存泄漏

Handler的Message被存储在MessageQueue中,有些Message并不能马上被处理,它们在MessageQueue中存在的时间会很长,这就会导致Handler无法被回收。如果Handler 是非静态的,则Handler也会导致引用它的Activity或者Service不能被回收。

JAVA

public class HandlerActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        button = (Button) findViewById(R.id.bt_next);
        final Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            }
        };
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mHandler.sendMessageDelayed(Message.obtain(), 60000);
                finish();
            }
        });
    }
}

Handler 是非静态的匿名内部类的实例,它会隐性引用外部类HandlerActivity 。上面的例子就是当我们点击Button时,HandlerActivity 会finish,但是Handler中的消息还没有被处理,因此HandlerActivity 无法被回收。
解决方法就是要使用一个静态的Handler内部类,Handler持有的对象要使用弱引用,并且在Activity的Destroy方法中移除MessageQueue中的消息,如下所示。

JAVA

public class HandlerActivity extends AppCompatActivity {
    private Button button;
    private MyHandler myHandler = new MyHandler(this);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        button = (Button) findViewById(R.id.bt_next);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                myHandler.sendMessageDelayed(Message.obtain(), 60000);
                finish();
            }
        });
    }
    public void show() {
    
    }
    private static class MyHandler extends Handler {
        private final WeakReference<HandlerActivity> mActivity;

        public MyHandler(HandlerActivity activity) {
            mActivity = new WeakReference<HandlerActivity2>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            if (mActivity != null && mActivity.get() == null) {
                mActivity.get().show();
            }
        }
    }
    @Override
    public void onDestroy() {
        if (myHandler != null) {
            myHandler.removeCallbacksAndMessages(null);
        }
        super.onDestroy();
    }
}

MyHandler是一个静态的内部类,它持有的 HandlerActivity对象使用了弱引用,并且在onDestroy方法中将Callbacks和Messages全部清除掉。
如果觉得麻烦,也可以使用避免内存泄漏的Handler开源库WeakHandler。

2.4 未正确使用Context

对于不是必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们可以考虑使用Application Context来代替Activity的Context,这样可以避免Activity泄露,比如如下的单例模式:

JAVA

public class AppSettings { 
 private Context mAppContext;
 private static AppSettings mAppSettings = new AppSettings();
 public static AppSettings getInstance() {
  return mAppSettings;
 }
  
 public final void setup(Context context) {
  mAppContext = context;
 }
}

mAppSettings作为静态对象,其生命周期会长于Activity。当进行屏幕旋转时,默认情况下,系统会销毁当前Activity,因为当前Activity调用了setup方法,并传入了Activity Context,使得Activity被一个单例持有,导致垃圾收集器无法回收,进而产生了内存泄露。
解决方法就是使用Application的Context:

JAVA

public final void setup(Context context) {
 mAppContext = context.getApplicationContext(); 
}

2.5 静态View

使用静态View可以避免每次启动Activity都去读取并渲染View,但是静态View会持有Activity的引用,导致Activity无法被回收,解决的办法就是在onDestory方法中将静态View置为null。

JAVA

public class SecondActivity extends AppCompatActivity {
    private static Button button;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.bt_next);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });
    }
 }   

2.6 WebView

不同的Android版本的WebView会有差异,加上不同厂商的定制ROM的WebView的差异,这就导致WebView存在着很大的兼容性问题。WebView都会存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。通常的解决办法就是为WebView单开一个进程,使用AIDL与应用的主进程进行通信。WebView进程可以根据业务需求,在合适的时机进行销毁。

2.7 资源对象未关闭

资源对象比如Cursor、File等,往往都用了缓冲,不使用的时候应该关闭它们。把他们的引用置为null,而不关闭它们,往往会造成内存泄漏。因此,在资源对象不使用时,一定要确保它已经关闭,通常在finally语句中关闭,防止出现异常时,资源未被释放的问题。

2.8 集合中对象没清理

通常把一些对象的引用加入到了集合中,当不需要该对象时,如果没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就会更加严重。

2.9 Bitmap对象

临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。
避免静态变量持有比较大的bitmap对象或者其他大的数据对象,如果已经持有,要尽快置空该静态变量。

2.10 监听器未关闭

很多系统服务(比如TelephonyMannager、SensorManager)需要register和unregister监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的Listener,要记得在合适的时候及时remove这个Listener。

Android内存泄漏的检测工具——LeakCanary

首先了解什么是内存泄露

1Leakcancary的优势
LeakCanary是一个可视化的内存泄露分析工具,他具备以下优势

· 简单:只需设置一段代码即可,打开应用运行一下就能够发现内存泄露。而MAT分析需要Heap Dump,获取文件,手动分析等多个步骤。

· 易于发现问题:在手机端即可查看问题即引用关系,而MAT则需要你分析,找到Path To GC Roots等关系。

· 人人可参与:开发人员,测试测试,产品经理基本上只要会用App就有可能发现问题。而传统的MAT方式,只有部分开发者才有精力和能力实施。

 

 

2. 使用说明
2.1 在build.gradle中添加依赖

首先,在必须在对应的app模块的gradle添加对应的库,在其他module模块添加无效

dependencies {

debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.5.4’
releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.5.4’

}

到这里,就添加了对他的依赖。这里说明一下,使用其他版本可能会因为版本问题导致报错,建议使用*新版本,也就是这个1.5.4

2.2 在application中配置

public class BaseApplication extends Application {

private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher= setupLeakCanary();//2
}
private RefWatcher setupLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
}
return LeakCanary.install(this);//1
}

public static RefWatcher getRefWatcher(Context context) {
BaseApplication leakApplication = (BaseApplication) context.getApplicationContext();
return leakApplication.refWatcher;
}
}
在注释2处,,完成对LeakCancary的安装,经过以上两个步骤,你的手机界面出现

%title插图%num

这个黄色的图标就是我们的监控工具

 

2.3 检测activity内存泄漏问题,原理是application中监控着所有activity生命周期在activity的onDestory中

@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = BaseApplication.getRefWatcher(this);//1
refWatcher.watch(this);
}

Activity生命周期结束的时候,如果你的activity发生内存泄漏,状态栏会提示你

%title插图%num

那么,黄色的应用  Leaks中

%title插图%num

会把内存泄漏的详情告诉你,像这里就是因为我的TestActivity中开了LeakThread这个线程,造成了内存泄漏

泄漏内存是115kb

 

2.4 监控的对象当我们需要对某个对象进行监控时,

BaseApplication.getRefWatcher().watch(sleaky)

其中sleaky就是我们要检测的对象

那么,哪些是需要我们检测的对象呢

默认情况下,是对Activity进行了检测。另一个需要监控的重要对象就是Fragment实例。因为它和Activity实例一样可能持有大量的视图以及视图需要的资源

其他也可以监控的对象

BroadcastReceiver ,Service, 其他有生命周期的对象,直接间接持有大内存占用的对象(即Retained Heap值比较大的对象)

何时进行监控
首先,我们需要明确什么是内存泄露,简而言之,某个对象在该释放的时候由于被其他对象持有没有被释放,因而造成了内存泄露。

因此,我们监控也需要设置在对象(很快)被释放的时候,如Activity和Fragment的onDestroy方法。

一个错误示例,比如监控一个Activity,放在onCreate就会大错特错了,那么你每次都会收到Activity的泄露通知。

 

 

 

 

以上就是LeakCancary的使用方法

3.LeakCanary的原理
Android 应用的整个生命周期由其组件的生命周期组成,如下图中所示。用户使用应用的过程中,在不同界面之间跳转,每个界面都经历着”生死“的转换,可在此建立检测点。Activity/Fragment 都有 onDestory() 回调方法, 进入此方法后,Activity/Fragment生命周期结束,应该被回收。

 

简述声明周期

然后我们需要解决:如何得到未被回收的对象。ReferenceQueue+WeakReference+手动调用 GC可实现这个需求。

WeakReference 创建时,传入一个 ReferenceQueue 对象。当被 WeakReference 引用的对象的生命周期结束,一旦被 GC 检查到,GC 将会把该对象添加到 ReferenceQueue 中,待ReferenceQueue处理。当 GC 过后对象一直不被加入 ReferenceQueue,它可能存在内存泄漏。

 

获得未被回收的 Object

找到了未被回收的对象,如何确认是否真的内存泄漏?这里可以将问题转换为:未被回收的对象,是否被其他对象引用?找出其*短引用链。VMDebug + HAHA 完成需求。

VM 会有堆内各个对象的引用情况,并能以hprof文件导出。HAHA 是一个由 square 开源的 Android 堆分析库,分析 hprof 文件生成Snapshot对象。Snapshot用以查询对象的*短引用链。

 

解析hprof

找到*短引用链后,定位问题,排查代码将会事半功倍。

解决Kali双显卡驱动不兼容导致的开机黑屏卡死

之前被这个问题困扰了很久,曾经还被劝退过一次orz,*终还是找到了解决方案

直接上解决方案

在进入引导界面启动项的那里按E,进入编辑

%title插图%num

在倒数第三行加入了nouveau.modeset=0 之后成功进入操作系统,然后发现一个问题这个办法需要每次进入操作系统之前执行一遍。

然后参照:http://blog.csdn.net/wingfox117/article/details/46278001博主的办法继续设置

1. 将nouveau添加到黑名单,防止它启动

$ cd /etc/modprobe.d$ sudo vi nvidia-graphics-drivers.conf 写入:blacklist nouveau
保存并退出: wq!检查:$ cat nvidia-graphics-drivers.conf 2. 对于:/etc/default/grub,添加到末尾。$ sudo vi /etc/default/grub末尾写入:rdblacklist=nouveau nouveau.modeset=0保存并退出: wq!检查:$ cat /etc/default/grub
3. 官网提供的操作:
$ sudo mv /boot/initramfs-$(uname -r).img /boot/initramfs-$(uname -r)-nouveau.img
然后重新生成initrd文件
$ sudo dracut /boot/initramfs-$(uname -r).img $(uname -r)$ sudo update-initramfs -u

发生一个问题,第三步里面 前两个命令无法执行,然后我只执行了第三个命令成功,以后进入操作系统再也没出过问题。

来源:https://blog.csdn.net/lyfmxy/article/details/53533948

*后!!!nvidia真是坑555,别轻易尝试安装英伟达驱动,本人水平有限已经坑两次了orz

Android开源库之使用ZBar开源库实现二维码功能

项目中本来已使用Zxing来实现二维码功能,但是在ZXing的识别效率太低,以至于在某些*端情况下,识别效果实在无法忍受,这不一定是算法问题,应该很大原因在于Zxing使用java实现。没办法只能尝试使用ZBar开源库,这个库是基于c/c++的,相比ZXing识别速度快很多是众所周知的!

下面贴出我完整的编译ZBar过程

 

一、下载源码

到ZBar的Github托管主页上下载ZBar;

在ZBar的Github托管主页上点击Android目录,看下面的说明,告知我们编译ZBar的Android SDK需要libiconv,所以我们先去下载libiconv。

二、编译libiconv

编译libiconv需要在linux环境下,我使用的是Cygwin客户端,但是死活编译的都不行,*后还是使用参考博客提供的编译好的;

三、编译zbar

把刚才编译好的libiconv放入我们项目的jni文件夹。
解压刚才下载好的Zbar,首先把Zbar的头文件所在文件夹zbar/include放入我们项目的jni文件夹下。
把Zbar对java的接口文件zbarjni.c放入我们项目的jni文件夹,zbrjni.c在zbar/java文件夹下。
把Zbar的核心库文件所在的文件夹zbar/zbar放到我们项目的jni文件夹下。
把Zbar编译时需要的Android.mk、Applicaiton.mk、config.h从zbar\android\jni下拷贝到我们项目的jni文件夹下。
此时我们项目的jni文件夹是这样的:

%title插图%num
理论上现在可以开始编译了吧,但是呢因为我们改动了zbar的文件夹结构,所以我们要对Android.mk进行改动,主要改的是文件夹路径和文件路径,修改后的Android.mk的内容如下:

MY_LOCAL_PATH := $(call my-dir)
# libiconv
include $(CLEAR_VARS)
LOCAL_PATH := $(MY_LOCAL_PATH)
LOCAL_MODULE := libiconv
LOCAL_CFLAGS := \
-Wno-multichar \
-D_ANDROID \
-DLIBDIR=”c” \
-DBUILDING_LIBICONV \
-DBUILDING_LIBCHARSET \
-DIN_LIBRARY

LOCAL_SRC_FILES := \
libiconv-1.15/lib/iconv.c \
libiconv-1.15/libcharset/lib/localcharset.c \
libiconv-1.15/lib/relocatable.c

LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/libiconv-1.15/include \
$(LOCAL_PATH)/libiconv-1.15/libcharset \
$(LOCAL_PATH)/libiconv-1.15/libcharset/include

include $(BUILD_SHARED_LIBRARY)

LOCAL_LDLIBS := -llog -lcharset
# —————————————————–
# libzbar
include $(CLEAR_VARS)
LOCAL_PATH := $(MY_LOCAL_PATH)
LOCAL_MODULE := zbar
LOCAL_SRC_FILES := \
zbarjni.c \
zbar/img_scanner.c \
zbar/decoder.c \
zbar/image.c \
zbar/symbol.c \
zbar/convert.c \
zbar/config.c \
zbar/scanner.c \
zbar/error.c \
zbar/refcnt.c \
zbar/video.c \
zbar/video/null.c \
zbar/decoder/code128.c \
zbar/decoder/code39.c \
zbar/decoder/code93.c \
zbar/decoder/codabar.c \
zbar/decoder/databar.c \
zbar/decoder/ean.c \
zbar/decoder/i25.c \
zbar/decoder/qr_finder.c \
zbar/qrcode/bch15_5.c \
zbar/qrcode/binarize.c \
zbar/qrcode/isaac.c \
zbar/qrcode/qrdec.c \
zbar/qrcode/qrdectxt.c \
zbar/qrcode/rs.c \
zbar/qrcode/util.c

LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(LOCAL_PATH)/zbar \
$(LOCAL_PATH)/libiconv-1.15/include

LOCAL_SHARED_LIBRARIES := libiconv

include $(BUILD_SHARED_LIBRARY)

然后在Application.mk中填写你要编译的平台,如果想全部编译:

APP_ABI := all
1
1
如果要指定编译某几个平台,把平台名称依次空格隔开写上即可:

APP_ABI := armeabi armeabi-v7a x86 x86_64 mips mips_64 arm64_v8a
1
1
此时我们用命令行进入项目的jni文件夹的父目录,然后此时执行ndk-build进行编译。

四、zbar中文乱码问题

1、在sourceforge下载zbar源码,修改了文件

zbar/qrcode/qrdectxt.c

,62行左右,将编码标准ISO8859-1改为GBK或者GB18030,如下:

/*latin1_cd=iconv_open(“UTF-8″,”ISO8859-1”);*/
latin1_cd=iconv_open(“UTF-8″,”GB18030”);

2、继续修改上面的文件 zbar/qrcode/qrdectxt.c,164行左右,调换解码顺序,将中文解码调到首位如下:
/*enc_list[0]=sjis_cd;
enc_list[1]=latin1_cd;*/
enc_list[0]=latin1_cd;
enc_list[1]=sjis_cd;
经过这两部调整,应该就能解决中文乱码的问题;(当然修改源码后,要记得重新执行ndk-build编译zbar)。

3、通过1、2两部基本已经解决大部分中文乱码问题,但是对于某些中文,依旧是乱码,如二维码内容是“粤8888”,“粤”字就会被识别为乱码;

该问题参考博客,只需在识别出二维码数据后,判断识别结果数据是否是日文编码“Shift_JIS”,是的话,转换为“utf-8”编码,如下:

try {
String encodeResust = new String(qrCodeString.getBytes(“Shift_JIS”), “utf-8”);
LogUtils.d(TAG + “–decodeBarcodeZbar–将‘Shift_JIS’编码格式转成‘utf-8’:” + encodeResust);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
LogUtils.e(TAG + “–decodeBarcodeZbar–识别结果不是‘Shift_JIS’编码格式”);
}

五、将zbar的java代码导入项目

把zbar/Java下在net.sourceforge.zbar包和里边的java文件拷贝到你的项目的java目录下,大概结构如下:

%title插图%num
注意对照一下so库和Image.java,ImageScanner.java,Symbol,java,SymbolSet.java四个java文件中声明静态库的名称是否需要修改,

因为我的so库名称是libzbar.so,所以静态库的声明代码需要改为:

System.loadLibrary(“zbar”);

六、Zbar调用
这里就不贴摄像头的调用和界面代码了,只贴一下调用Zbar识别二维码的代码:

/**
* 使用Zbar库识别二维码
* @param imageData 图像数据(摄像头返回的)
* @param width 图像的宽
* @param height 图像的高
* @param scanRect 扫描区域
*/
public static String decodeBarcodeZbar(byte[] imageData, int width, int height, Rect scanRect){
long start = System.currentTimeMillis();
Image barcode = new Image(width, height, “Y800”);
barcode.setData(imageData);
// 指定二维码在图片中的区域,也可以不指定,识别全图。
if(null != scanRect) barcode.setCrop(scanRect.left, scanRect.top, scanRect.width(), scanRect.height());

String qrCodeString = null;
ImageScanner mImageScanner = new ImageScanner();

int result = mImageScanner.scanImage(barcode);
if (result != 0) {
SymbolSet symSet = mImageScanner.getResults();
for (Symbol sym : symSet)
qrCodeString = sym.getData();
}

LogUtils.w(TAG + “–decodeBarcodeZbar: 总耗时:” + (System.currentTimeMillis() – start));

if (!TextUtils.isEmpty(qrCodeString)) {
// 成功识别二维码,qrCodeString就是数据。
LogUtils.d(TAG + “–decodeBarcodeZbar–识别成功:” + qrCodeString);
try {
String encodeResust = new String(qrCodeString.getBytes(“Shift_JIS”), “utf-8”);
LogUtils.d(TAG + “–decodeBarcodeZbar–将‘Shift_JIS’编码格式转成‘utf-8’:” + encodeResust);
return encodeResust;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
LogUtils.e(TAG + “–decodeBarcodeZbar–识别结果不是‘Shift_JIS’编码格式”);
return qrCodeString;
}
}else{
LogUtils.d(TAG + “–decodeBarcodeZbar–识别失败”);
return null;
}

}

————————————————

Android使用ZBar扫描二维码/条形码(实例)+常见问题汇总

写在前面 :因项目需求,需要实现二维码扫码功能,笔者测试过多种开源扫码工具,但因不跨平台、扫描速度慢等问题逐个放弃,*后选用ZBar实现功能,笔者发现ZBar扫码在跨主流手机平台、扫码速度等方面有较明显的优势,现将核心功能整理成示例代码,便于日后复用和有需要的读者参考。

 

使用方式

1.复制com.zbar.lib及其下共4个包文件到项目中。

2.在lib下添加armeabi中的libiconv.so和libzbar.so库文件。

3.添加res下的资源文件,包括drawable、layout、raw、values(包含ids.xml)等。

4.在AndroidManifest.xml清单中添加权限和Activity声明。

5.调用扫码功能,在调用处通过以下代码使用扫码功能:

[java]  view plain  copy

  1. Intent intent = new Intent();
  2. intent.setClass(MainActivity.this, CaptureActivity.class);
  3. startActivityForResult(intent, SCANNIN_GREQUEST_CODE);

6.获得扫码结果,在步骤5中代码块所在的Activity中通过以下代码获取扫码结果:

[java]  view plain  copy

  1. @Override
  2. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  3.     super.onActivityResult(requestCode, resultCode, data);
  4.     switch (requestCode) {
  5.     case SCANNIN_GREQUEST_CODE:
  6.         if (resultCode == RESULT_OK) {
  7.             String result = data.getStringExtra(“QR_CODE”);
  8.             // TODO 获取结果,做逻辑操作
  9.             tvResult.setText(result);
  10.         } else {
  11.             Toast.makeText(this“无法获取扫码结果”2000).show();
  12.         }
  13.         break;
  14.     }
  15. }

测试效果:

1.二维码(一串字符:1234567890)

%title插图%num

2.扫码前(点击“扫码”开始扫码)

%title插图%num

3.扫码时(对准二维码)

%title插图%num

4.扫码后(呈现出扫码结果)

%title插图%num

常见问题汇总

移植后,若项目本身没报错,但不能扫码,可能存在以下问题:

1.未移植armeabi文件夹下的libiconv.so和libzbar.so库文件。(缺少时一般在运行时报错)

2.未在AndroidManifest.xml清单中配置所需权限。(可以运行,但扫码时黑屏,无法开启摄像头)

[html]  view plain  copy

  1.   <!– 二维码扫码 –>
  2. <uses-permission android:name=“android.permission.VIBRATE” />
  3. <uses-permission android:name=“android.permission.CAMERA” />
  4. <uses-feature android:name=“android.hardware.camera” />
  5. <uses-feature android:name=“android.hardware.camera.autofocus” />

3.未在AndroidManifest.xml清单文件中配置Activity:CaptureActivity。(缺少时一般在运行时报错)

[html]  view plain  copy

  1. <activity
  2. android:name=“com.zbar.lib.CaptureActivity”
  3. android:configChanges=“orientation|keyboardHidden”
  4. android:screenOrientation=“portrait”
  5. android:theme=“@android:style/Theme.Black.NoTitleBar”
  6. android:windowSoftInputMode=“stateAlwaysHidden” >

记录Android端使用zbar扫描的相关问题

在此一并做一下记录。

修改Zbar有效扫描区域的关键代码,在CaptureActivity中initCamera函数中

  1. CameraManager.get().openDriver(surfaceHolder);
  2. Point point = CameraManager.get().getCameraResolution();
  3. int width = point.y;
  4. int height = point.x;
  5. int x = mCropLayout.getLeft() * width / mContainer.getWidth();
  6. int y = mCropLayout.getTop() * height / mContainer.getHeight();
  7. int cropWidth = mCropLayout.getWidth() * width / mContainer.getWidth();
  8. int cropHeight = mCropLayout.getHeight() * height / mContainer.getHeight() – (iHeight – height);// 由于扫描框的大小,比实际扫描的高度矮,故减去(iHeight – height)的高度
  9. setX(x);
  10. setY(y);
  11. setCropWidth(cropWidth);
  12. setCropHeight(cropHeight);

解码并保存图片的相关代码,在DecodeHandler中decode函数中

  1. private void decode(byte[] data, int width, int height)
  2. {
  3. byte[] rotatedData = new byte[data.length];
  4. for (int y = 0; y < height; y++)
  5. {
  6. for (int x = 0; x < width; x++)
  7. rotatedData[x * height + height y 1] = data[x + y * width];
  8. }
  9. int tmp = width;// Here we are swapping, thats the difference to #11
  10. width = height;
  11. height = tmp;
  12. ZbarManager manager = new ZbarManager();
  13. String result = manager.decode(rotatedData, width, height, true, activity.getX(), activity.getY(), activity.getCropWidth(), activity.getCropHeight());
  14. if (result != null)
  15. {
  16. if (activity.isNeedCapture())
  17. {
  18. // 生成bitmap
  19. PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(rotatedData, width, height, activity.getX(), activity.getY(), activity.getCropWidth(),
  20. activity.getCropHeight(), false);
  21. int[] pixels = source.renderThumbnail();
  22. int w = source.getThumbnailWidth();
  23. int h = source.getThumbnailHeight();
  24. Bitmap bitmap = Bitmap.createBitmap(pixels, 0, w, w, h, Bitmap.Config.ARGB_8888);
  25. try
  26. {
  27. String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() + “/Qrcode/”;
  28. File root = new File(rootPath);
  29. if (!root.exists())
  30. {
  31. root.mkdirs();
  32. }
  33. File f = new File(rootPath + “Qrcode.jpg“);
  34. if (f.exists())
  35. {
  36. f.delete();
  37. }
  38. f.createNewFile();
  39. FileOutputStream out = new FileOutputStream(f);
  40. bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
  41. out.flush();
  42. out.close();
  43. }
  44. catch (Exception e)
  45. {
  46. e.printStackTrace();
  47. }
  48. }
  49. if (null != activity.getHandler())
  50. {
  51. Message msg = new Message();
  52. msg.obj = result;
  53. msg.what = R.id.decode_succeeded;
  54. activity.getHandler().sendMessage(msg);
  55. }
  56. }
  57. else
  58. {
  59. if (null != activity.getHandler())
  60. {
  61. activity.getHandler().sendEmptyMessage(R.id.decode_failed);
  62. }
  63. }
  64. }

界面自定义扫描框,可在CaptureActivity中自定义即可。也可仿照ZXing的ViewfinderView界面编写自己的布局。

 

Android快速实现二维码扫描–Zxing

Android中二维码扫描的*常用库是zxing和zbar,zxing项目地址为https://github.com/zxing/zxing,目前还有多个人在维护。zbar主要用C来写的,对速度有要求的可使用zbar,但目前没有在维护,项目地址:https://github.com/ZBar/ZBar。

1.引入jar包

%title插图%num

2.copy Zxing包到项目

%title插图%num

这里包名不一样肯定会报错,我们暂时不管,先把资源文件copy过来,后面来做处理。

3.导入相关资源文件

copy res底下的相关资源文件,如下:
drawable、drawable-hdpi和layout

%title插图%num

raw文件和values文件

%title插图%num

4.AndroidManifest.xml加入相关权限和扫描的Activity

<uses-permission android:name=”android.permission.CAMERA” />
<uses-permission android:name=”android.permission.INTERNET” />
<uses-permission android:name=”android.permission.VIBRATE” />
<uses-permission android:name=”android.permission.FLASHLIGHT” />
<activity
android:name=”.zxing.android.CaptureActivity”
android:screenOrientation=”portrait”
android:theme=”@android:style/Theme.NoTitleBar” />
6.capture.xml的ViewfinderView改成自己包名下的

%title插图%num

7.调起扫描界面 获取扫描结果

在需要打开扫描界面的地方直接跳转到CaptureActivity就OK(使用startActivityForResult)

/**
* 跳转到扫码界面扫码
*/
private void goScan() {
Intent intent = new Intent(MainActivity.this, CaptureActivity.class);
startActivityForResult(intent, REQUEST_CODE_SCAN);
}
在onActivityResult的回调中即可获取扫描内容

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// 扫描二维码/条码回传
if (requestCode == REQUEST_CODE_SCAN && resultCode == RESULT_OK) {
if (data != null) {
//返回的文本内容
String content = data.getStringExtra(DECODED_CONTENT_KEY);
//返回的BitMap图像
Bitmap bitmap = data.getParcelableExtra(DECODED_BITMAP_KEY);
}
}
}
动态权限申请

由于扫描需要调用相机,打开摄像头属于敏感权限,所以需要进行权限的动态申请,如下

//动态权限申请
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, 1);
} else {
//扫码
goScan();
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//扫码
goScan();
} else {
Toast.makeText(this, “你拒*了权限申请,无法打开相机扫码哟!”, Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
上面的代码就是动态申请权限的流程,首先判断用户是不是已经给我们权限授权了,使用ContextCompat.checkSelfPermission()方法,*个参数是Context,第二个参数是具体的权限名称,如果等于PackageManager.PERMISSION_GRANTED表明已授权,不等于就是没有授权。
如果已授权就直接做后面的操作,如果没有授权,需要调用ActivityCompat.requestPermissions()方法申请授权,*个参数是当前Activity实例,第二个参数是权限数组,第三个是请求码。
用户的选择将会回调到onRequestPermissionsResult()方法中,授权结果封装在grantResults参数中,如果grantResults长度大于0且grantResults[0]等于PackageManager.PERMISSION_GRANTED,也就是上面权限数组中加入的*个打开摄像头的权限被授权,则可跳转至扫描界面扫码,否则提示用户未打开权限无法使用。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速