Android新手,内存泄漏问题

问题描述

  • A = 主页面 fragment
  • B = 输入内容页 fragment
  • C = 检索结果页 fragment 我在 A 中进行 replace 跳转到一个 B,完成输入后 replace 跳转到 C,此时使用 home 键进入后台运行,发生内存泄漏问题

报错情况

D/HomePageFragment: BaseFragment-->onPause()
D/DiscoveryFragment: BaseFragment-->onPause()
D/TvItemsFragment: BaseFragment-->onPause()
D/MainActivity: BaseActivity-->onPause()
D/LeakCanary: Scheduling check for retained objects in 5000ms because app became invisible
D/HomePageFragment: BaseFragment-->onStop()
D/DiscoveryFragment: BaseFragment-->onStop()
D/CommendFragment: BaseFragment-->onStop()
D/TvItemsFragment: BaseFragment-->onStop()
D/MainActivity: BaseActivity-->onStop()
D/MainActivity: BaseActivity-->onSaveInstanceState()

D/LeakCanary: ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    
    53078 bytes retained by leaking objects
    Signature: b01ba777d9ce636d68e71237f54fafe95ee827f8
    ┬───
    │ GC Root: System class
    │
    ├─ android.view.inputmethod.InputMethodManager class
    │    Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking)
    │    ↓ static InputMethodManager.sInstance
    ├─ android.view.inputmethod.InputMethodManager instance
    │    Leaking: NO (ViewRootImpl↓ is not leaking and InputMethodManager is a singleton)
    │    ↓ InputMethodManager.mCurRootView
    ├─ android.view.ViewRootImpl instance
    │    Leaking: NO (ViewPager2$RecyclerViewImpl↓ is not leaking and ViewRootImpl#mView is not null)
    │    ↓ ViewRootImpl.mImeFocusController
    ├─ android.view.ImeFocusController instance
    │    Leaking: NO (ViewPager2$RecyclerViewImpl↓ is not leaking)
    │    ↓ ImeFocusController.mNextServedView
    ├─ androidx.viewpager2.widget.ViewPager2$RecyclerViewImpl instance
    │    Leaking: NO (SearchFragment↓ is not leaking and View attached)
    │    mContext instance of com.moviemore.android.ui.MainActivity with mDestroyed = false
    │    View.parent androidx.viewpager2.widget.ViewPager2 attached as well
    │    View#mParent is set
    │    View#mAttachInfo is not null (view attached)
    │    View.mID = R.id.null
    │    View.mWindowAttachCount = 1
    │    ↓ ViewPager2$RecyclerViewImpl.mAdapter
    ├─ com.moviemore.android.ui.common.ui.BaseViewPagerFragment$VpAdapter instance
    │    Leaking: NO (SearchFragment↓ is not leaking)
    │    ↓ BaseViewPagerFragment$VpAdapter.mFragmentManager
    ├─ androidx.fragment.app.FragmentManagerImpl instance
    │    Leaking: NO (SearchFragment↓ is not leaking)
    │    ↓ FragmentManagerImpl.mFragmentStore
    ├─ androidx.fragment.app.FragmentStore instance
    │    Leaking: NO (SearchFragment↓ is not leaking)
    │    ↓ FragmentStore.mActive
    ├─ java.util.HashMap instance
    │    Leaking: NO (SearchFragment↓ is not leaking)
    │    ↓ HashMap.table
    ├─ java.util.HashMap$Node[] array
    │    Leaking: NO (SearchFragment↓ is not leaking)
    │    ↓ HashMap$Node[].[6]
    ├─ java.util.HashMap$Node instance
    │    Leaking: NO (SearchFragment↓ is not leaking)
    │    ↓ HashMap$Node.value
    ├─ androidx.fragment.app.FragmentStateManager instance
    │    Leaking: NO (SearchFragment↓ is not leaking)
    │    ↓ FragmentStateManager.mFragment
    ├─ com.moviemore.android.ui.search.SearchFragment instance
    │    Leaking: NO (Fragment#mFragmentManager is not null)
    │    ↓ SearchFragment.rootView
    │                     ~~~~~~~~
    ╰→ android.widget.LinearLayout instance
    ​     Leaking: YES (ObjectWatcher was watching this because com.moviemore.android.ui.search.SearchFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
    ​     key = 888671cb-ecda-4776-b4dd-b4f2350acb32
    ​     watchDurationMillis = 7210
    ​     retainedDurationMillis = 2208
    ​     mContext instance of com.moviemore.android.ui.MainActivity with mDestroyed = false
    ​     View#mParent is null
    ​     View#mAttachInfo is null (view detached)
    ​     View.mWindowAttachCount = 1
    ====================================
    0 LIBRARY LEAKS

3 条回复    2021-07-29 16:08:15 +08:00

JellyBeanX
    1

JellyBeanX   2 天前

要么,在 A 的 onDestroyView() 中销毁 LinearLayout 的实例,要么把 replace 换成 show & hide
JellyBeanX
    2

JellyBeanX   2 天前

@JellyBeanX 说错了,是销毁持有 linearLayout 实例的对象
ukyoo
    3

ukyoo   59 分钟前

onDestroyView()后要手动把成员变量的 View 置空, 因为下一次 onCreateView()还会重新 infalte 一次布局, 这个成员变量就没什么用了, 视为泄露

Android 内存泄漏常见情况4 资源泄漏

资源未关闭或释放导致内存泄露

在使用IOFile流或者SqliteCursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。

 

1.数据库的cursor没有关闭。

操作Sqlite数据库时,Cursor是数据库表中每一行的集合,Cursor提供了很多方法,可以很方便的读取数据库中的值,
可以根据索引,列名等获取数据库中的值,通过游标的方式可以调用moveToNext()移到下一行
当我们操作完数据库后,一定要记得调用Cursor对象的close()来关闭游标,释放资源。

2,未关闭InputStream/OutputStream。

3,Bitmap对象不在使用时调用recycle()释放内存

4,BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap

 

Android 内存泄漏常见情况3 注册泄漏

android 中有很多注册和反注册,由于在注册后,上下文自身会被持久化的观察者列表所持有,如果不进行反注册,就会造成内存泄漏

内存泄漏1:Sensor Manager

代码如下:
MainActivity.java

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});

 

为什么?
通过Context调用getSystemService获取系统服务,这些服务运行在他们自己的进程执行一系列后台工作或者提供和硬件交互的接口,如果Context对象需要在一个Service内部事件发生时随时收到通知,则需要把自己作为一个监听器注册进去,这样服务就会持有一个Activity,如果开发者忘记了在Activity被销毁前注销这个监听器,这样就导致内存泄漏。

怎么解决?
在onDestroy方法里注销监听器。

内存泄漏1:未取消注册或回调导致内存泄露

比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。因此注册广播后在Activity销毁后一定要取消注册。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.registerReceiver(mReceiver, new IntentFilter());
    }

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 接收到广播需要做的逻辑
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        this.unregisterReceiver(mReceiver);
    }
}

在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用Retrofit+RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。

WebView造成内存泄露

关于WebView的内存泄露,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。

另外在查阅WebView内存泄露相关资料时看到这种情况:

Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题(Android5.1之后)。

*终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。详细分析过程请参考这篇文章:WebView内存泄漏解决方法。

@Override
protected void onDestroy() {
    super.onDestroy();
    // 先从父控件中移除WebView
    mWebViewContainer.removeView(mWebView);
    mWebView.stopLoading();
    mWebView.getSettings().setJavaScriptEnabled(false);
    mWebView.clearHistory();
    mWebView.removeAllViews();
    mWebView.destroy();
}

 

Android 内存泄漏常见情况2 内部类泄漏

线程持久化

Java中的Thread有一个特点就是她们都是直接被GC Root所引用,也就是说Dalvik虚拟机对所有被激活状态的线程都是持有强引用,导致GC永远都无法回收掉这些线程对象,除非线程被手动停止并置为null或者用户直接kill进程操作。所以当使用线程时,一定要考虑在Activity退出时,及时将线程也停止并释放掉

内存泄漏1:AsyncTask

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

 

使用LeakCanary检测到的内存泄漏:

这里写图片描述

为什么?
上面代码在activity中创建了一个匿名类AsyncTask,匿名类和非静态内部类相同,会持有外部类对象,这里也就是activity,因此如果你在Activity里声明且实例化一个匿名的AsyncTask对象,则可能会发生内存泄漏,如果这个线程在Activity销毁后还一直在后台执行,那这个线程会继续持有这个Activity的引用从而不会被GC回收,直到线程执行完成。

怎么解决?
自定义静态AsyncTask类,并且让AsyncTask的周期和Activity周期保持一致,也就是在Activity生命周期结束时要将AsyncTask cancel掉。

内存泄漏2:Handler

非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler是这样写的:

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessageDelayed(msg,1000);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // 做相应逻辑
            }
        }
    };
}

也许有人会说,mHandler并未作为静态变量持有Activity引用,生命周期可能不会比Activity长,应该不一定会导致内存泄露呢,显然不是这样的!

熟悉Handler消息机制的都知道,mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandlerActivity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueueLooper都是与线程相关联的,MessageQueueLooper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄露。

通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用静态内部类+弱引用的方式。

public class MainActivity extends AppCompatActivity {

    private Handler mHandler;

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

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private static class MyHandler extends Handler {

        private WeakReference<MainActivity> activityWeakReference;

        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 做相应逻辑
                }
            }
        }
    }
}

mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄露了。

上面的做法确实避免了Activity导致的内存泄露,发送的msg不再已经没有持有Activity的引用了,但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息给移除掉。

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}

为什么?

创建的Handler对象为匿名类,匿名类默认持有外部类activity, Handler通过发送Message与主线程交互,Message发出之后是存储在MessageQueue中的,有些Message也不是马上就被处理的。这时activity被handler持有
handler被message持有,message被messagequeue持有,message queue被loop持有,主线程的loop是全局存在的,这时就造成activity被临时性持久化,造成临时性内存泄漏

怎么解决?
可以由上面的结论看出,产生泄漏的根源在于匿名类持有Activity的引用,因此可以自定义Handler和Runnable类并声明成静态的内部类,来解除和Activity的引用。或者在activity 结束时,将发送的Message移除

内存泄漏3:Thread

代码如下:
MainActivity.java

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});

 

为什么?
Java中的Thread有一个特点就是她们都是直接被GC Root所引用,也就是说Dalvik虚拟机对所有被激活状态的线程都是持有强引用,导致GC永远都无法回收掉这些线程对象,除非线程被手动停止并置为null或者用户直接kill进程操作。看到这相信你应该也是心中有答案了吧 : 我在每一个MainActivity中都创建了一个线程,此线程会持有MainActivity的引用,即使退出Activity当前线程因为是直接被GC Root引用所以不会被回收掉,导致MainActivity也无法被GC回收

怎么解决?
当使用线程时,一定要考虑在Activity退出时,及时将线程也停止并释放掉

内存泄漏4:Timer Tasks

TimerTimerTask在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的ViewPager

public class MainActivity extends AppCompatActivity {

    private ViewPager mViewPager;
    private PagerAdapter mAdapter;
    private Timer mTimer;
    private TimerTask mTimerTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
        mTimer.schedule(mTimerTask, 3000, 3000);
    }

    private void init() {
        mViewPager = (ViewPager) findViewById(R.id.view_pager);
        mAdapter = new ViewPagerAdapter();
        mViewPager.setAdapter(mAdapter);

        mTimer = new Timer();
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                MainActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        loopViewpager();
                    }
                });
            }
        };
    }

    private void loopViewpager() {
        if (mAdapter.getCount() > 0) {
            int curPos = mViewPager.getCurrentItem();
            curPos = (++curPos) % mAdapter.getCount();
            mViewPager.setCurrentItem(curPos);
        }
    }

    private void stopLoopViewPager() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer.purge();
            mTimer = null;
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        stopLoopViewPager();
    }
}

当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancelTimerTimerTask,以避免发生内存泄漏。

为什么?
这里内存泄漏在于Timer和TimerTask没有进行Cancel,从而导致Timer和TimerTask一直引用外部类Activity。

怎么解决?
在适当的时机进行Cancel。

内存泄漏5:属性动画造成内存泄露

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

Android 内存泄漏常见情况1 静态泄漏

1,内存泄漏到本质是该释放的对象被持久化的对象引用了,造成持久化的常见情况有1,静态持久化 2,线程持久化

线程持久化

因为存活的线程是有dvk虚拟久直接持有,所以存活的线程都是持久化的

内存泄漏1:静态Activities(static Activities)

代码如下:
MainActivity.Java

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //使用LeakCanary观察是否有内存泄漏
        MyApplication.getRefWatcher().watch(this);
    }
}

 

LeakCanary检测出的内存泄漏:

这里写图片描述

为什么?
在上面代码中,我们声明了一个静态的Activity变量并且在TextView的OnClick事件里引用了当前正在运行的Activity实例,所以如果在activity的生命周期结束之前没有清除这个引用,则会引起内存泄漏。因为声明的activity是静态的,会常驻内存,如果该对象不清除,则垃圾回收器无法回收变量。

怎么解决?
*简单的方法是在onDestory方法中将静态变量activity置空,这样垃圾回收器就可以将静态变量回收。

@Override
    protected void onDestroy() {
        super.onDestroy();
        activity = null;
        //使用LeakCanary观察是否有内存泄漏
        MyApplication.getRefWatcher().watch(this);
    }

不使用静态activity,或给静态activity赋值时,考虑赋值的activity生命周期是不是全局的,或者在静态activity使用完后及时释放

内存泄漏2:静态View

代码如下:
MainActivity.java

    ...
    private static View view;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }
    void setStaticView() {
        view = findViewById(R.id.sv_view);
    }
    ...

 

LeakCanary检测到的内存泄漏

这里写图片描述

为什么?
上面代码看似没有问题,在Activity里声明一个静态变量view,然后初始化,当Activity生命周期结束了内存也释放了,但是LeakCanary却显示出现了内存泄漏,为什么?问题出在这里,View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity,所以当activity生命周期结束了,静态View没有清除掉,还持有activity的引用,因此内存泄漏了。

怎么解决?
在onDestroy方法里将静态变量置空。

@Override
protected void onDestroy() {
    super.onDestroy();
    view = null;
    MyApplication.getRefWatcher().watch(this);
} 
不使用静态view,或在activity关闭时将静态view赋值为null

内存泄漏3:静态内部类

代码如下:
MainActivity.java

private static Object inner;
void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});
 

使用LeakCanary检测到的内存泄漏:

这里写图片描述

为什么?
非静态内部类会持有外部类的引用,在上面代码中内部类持有Activity的引用,因此inner会一直持有Activity,如果Activity生命周期结束没有清除这个引用,这样就发生了内存泄漏。

怎么解决?
因为非静态内部类隐式持有外部类的强引用,所以我们将内部类声明成静态的就可以了。

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

内存泄漏3:静态Drawable

当一个Drawable附加到一个 View上时,
View会将其作为一个callback设定到Drawable上。意味着Drawable拥有一个View的引用,上面说了view会有上下文的引用

 

 

内存泄漏4:静态集合中对象没清理造成的内存泄漏

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

 

内存泄漏5:单例导致内存泄露

单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄露。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄露。

public class AppSettings {

    private static AppSettings sInstance;
    private Context mContext;

    private AppSettings(Context context) {
        this.mContext = context;
    }

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

像上面代码中这样的单例,如果我们在调用getInstance(Context context)方法的时候传入的context参数是ActivityService等上下文,就会导致内存泄露。

Activity为例,当我们启动一个Activity,并调用getInstance(Context context)方法去获取AppSettings的单例,传入Activity.this作为context,这样AppSettings类的单例sInstance就持有了Activity的引用,当我们退出Activity时,该Activity就没有用了,但是因为sIntance作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄露。

为了避免这样单例导致内存泄露,我们可以将context参数改为全局的上下文:

private AppSettings(Context context) {
    this.mContext = context.getApplicationContext();
}

全局的上下文Application Context就是应用程序的上下文,和单例的生命周期一样长,这样就避免了内存泄漏。

单例模式对应应用程序的生命周期,所以我们在构造单例的时候尽量避免使用Activity的上下文,而是使用Application的上下文。

Android开发常见的Activity中内存泄漏及解决办法

上一篇文章楼主提到由Context引发的内存泄漏,在这一篇文章里,我们来谈谈Android开发中常见的Activity内存泄漏及解决办法。本文将会以“为什么”“怎么解决”的方式来介绍这几种内存泄漏。
在开篇之前,先来了解一下什么是内存泄漏。

什么是内存泄漏?

内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗。内存泄漏并不是指物理上的内存消失,这里的内存泄漏是值由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费。

怎样会导致内存泄漏?

  • 资源对象没关闭造成的内存泄漏,如查询数据库后没有关闭游标cursor
  • 构造Adapter时,没有使用 convertView 重用
  • Bitmap对象不在使用时调用recycle()释放内存
  • 对象被生命周期长的对象引用,如activity被静态集合引用导致activity不能释放

在接下来的篇幅里,我们重点讲有关Activity常见的内存泄漏。

内存泄漏1:静态Activities(static Activities)

代码如下:
MainActivity.Java

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //使用LeakCanary观察是否有内存泄漏
        MyApplication.getRefWatcher().watch(this);
    }
}

LeakCanary检测出的内存泄漏:

这里写图片描述

为什么?
在上面代码中,我们声明了一个静态的Activity变量并且在TextView的OnClick事件里引用了当前正在运行的Activity实例,所以如果在activity的生命周期结束之前没有清除这个引用,则会引起内存泄漏。因为声明的activity是静态的,会常驻内存,如果该对象不清除,则垃圾回收器无法回收变量。

怎么解决?
*简单的方法是在onDestory方法中将静态变量activity置空,这样垃圾回收器就可以将静态变量回收。

@Override
    protected void onDestroy() {
        super.onDestroy();
        activity = null;
        //使用LeakCanary观察是否有内存泄漏
        MyApplication.getRefWatcher().watch(this);
    }

内存泄漏2:静态View

代码如下:
MainActivity.java

    ...
    private static View view;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }
    void setStaticView() {
        view = findViewById(R.id.sv_view);
    }
    ...

LeakCanary检测到的内存泄漏

这里写图片描述

为什么?
上面代码看似没有问题,在Activity里声明一个静态变量view,然后初始化,当Activity生命周期结束了内存也释放了,但是LeakCanary却显示出现了内存泄漏,为什么?问题出在这里,View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity,所以当activity生命周期结束了,静态View没有清除掉,还持有activity的引用,因此内存泄漏了。

怎么解决?
在onDestroy方法里将静态变量置空。

@Override
protected void onDestroy() {
    super.onDestroy();
    view = null;
    MyApplication.getRefWatcher().watch(this);
} 

内存泄漏3:内部类

代码如下:
MainActivity.java

private static Object inner;
void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

使用LeakCanary检测到的内存泄漏:

这里写图片描述

为什么?
非静态内部类会持有外部类的引用,在上面代码中内部类持有Activity的引用,因此inner会一直持有Activity,如果Activity生命周期结束没有清除这个引用,这样就发生了内存泄漏。

怎么解决?
因为非静态内部类隐式持有外部类的强引用,所以我们将内部类声明成静态的就可以了。

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

内存泄漏4:匿名类

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

使用LeakCanary检测到的内存泄漏:

这里写图片描述

为什么?
上面代码在activity中创建了一个匿名类AsyncTask,匿名类和非静态内部类相同,会持有外部类对象,这里也就是activity,因此如果你在Activity里声明且实例化一个匿名的AsyncTask对象,则可能会发生内存泄漏,如果这个线程在Activity销毁后还一直在后台执行,那这个线程会继续持有这个Activity的引用从而不会被GC回收,直到线程执行完成。

怎么解决?
自定义静态AsyncTask类,并且让AsyncTask的周期和Activity周期保持一致,也就是在Activity生命周期结束时要将AsyncTask cancel掉。

内存泄漏5:Handler

代码如下:
MainActivity.java

...
void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, 1000);
}

...
View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});
...

为什么?
当Android Application启动以后,framework会首先帮助我们完成UI线程的消息循环,也就是在UI线程中,Loop、MessageQueue、Message等等这些实例已经由framework帮我们实现了。所有的Application主要事件,比如Activity的生命周期方法、Button的点击事件都包含在这个Message里面,这些Message都会加入到MessageQueue中去,所以,UI线程的消息循环贯穿于整个Application生命周期,所以当你在UI线程中生成Handler的实例,就会持有Loop以及MessageQueue的引用。并且在Java中非静态内部类和匿名内持有外部类的引用,而静态内部类则不会持有外部类的引用。

怎么解决?
可以由上面的结论看出,产生泄漏的根源在于匿名类持有Activity的引用,因此可以自定义Handler和Runnable类并声明成静态的内部类,来解除和Activity的引用。

内存泄漏6:Thread

代码如下:
MainActivity.java

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});

为什么?
同AsyncTask一样,这里就不过多赘述。

怎么解决?
那我们自定义Thread并声明成static这样可以吗?其实这样的做法并不推荐,因为Thread位于GC根部,DVM会和所有的活动线程保持hard references关系,所以运行中的Thread*不会被GC无端回收了,所以正确的解决办法是在自定义静态内部类的基础上给线程加上取消机制,因此我们可以在Activity的onDestroy方法中将thread关闭掉。

内存泄漏7:Timer Tasks

代码如下:
MainActivity.java

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    },1000);
}

View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }
});

为什么?
这里内存泄漏在于Timer和TimerTask没有进行Cancel,从而导致Timer和TimerTask一直引用外部类Activity。

怎么解决?
在适当的时机进行Cancel。

内存泄漏8:Sensor Manager

代码如下:
MainActivity.java

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});

为什么?
通过Context调用getSystemService获取系统服务,这些服务运行在他们自己的进程执行一系列后台工作或者提供和硬件交互的接口,如果Context对象需要在一个Service内部事件发生时随时收到通知,则需要把自己作为一个监听器注册进去,这样服务就会持有一个Activity,如果开发者忘记了在Activity被销毁前注销这个监听器,这样就导致内存泄漏。

怎么解决?
在onDestroy方法里注销监听器。

总结

在开发中,内存泄漏*坏的情况是app耗尽内存导致崩溃,但是往往真实情况不是这样的,相反它只会耗尽大量内存但不至于闪退,可分配的内存少了,GC便会更多的工作释放内存,GC是非常耗时的操作,因此会使得页面卡顿。我们在开发中一定要注意当在Activity里实例化一个对象时看看是否有潜在的内存泄漏,一定要经常对内存泄漏进行检测。

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。

25条提高iOS App性能的技巧和诀窍

这篇文章来自iOS Tutorial Team 成员 Marcelo Fabri, 他是 Movile 的一个iOS开发者. Check out his personal website or follow him on Twitter.原文地址

     当我们开发iOS应用时,好的性能对我们的App来说是很重要的。你的用户也希望如此,但是如果你的app表现的反应迟钝或者很慢也会伤害到你的审核。

然而,由于IOS设备的限制有时很难工作得很正确。我们开发时有很多需要我们记住这些容易忘记的决定对性能的影响。

这是为什么我写这篇文章的原因。这篇文章用备忘录的形式集合了25个技巧和诀窍可以用来提高你的app性能。所以保持阅读来给你未来的App一个很不错的提高。

      Note:在优化代码之前,必须保证有个需要解决的问题!不要陷入”pre-optimizing(预优化)”你的代码。勤 用Instruments分析你的代码,发现任何一个需要提高的地方。Matt Galloway写了一个使用Instruments优化代码的的教程

   

    以下这些技巧分为三个不同那个的级别—基础,中级,高级。

   基础

   这些技巧你要总是想着实现在你开发的App中。

1. 用ARC去管理内存(Use ARC to Manage Memory)

2.适当的地方使用reuseIdentifier(Use a reuseIdentifier Where Appropriate)

3.尽可能设置视图为不透明(Set View as Opaque When Possible)

4.避免臃肿的XIBs文件(Avoid Fat XiBs)

5.不要阻塞主进程(Don’t Block the Main Thread)

6.调整图像视图中的图像尺寸(Size Images to Image Views)

7.选择正确集合(Choose the Correct Collection)

8.启用Gzip压缩(Enable GZIP Compression)

 

   中级

   这些技巧是当你遇到更复杂的情况的时候使用。

9. 重用和延迟加载视图(Reuse and Lazy Load Views)

10.缓存,缓存,缓存(Cache,Cache,Cache)

11.考虑绘图(Consider Drawing)

12.处理内存警告(Handle Memory Warnings)

13.重用大开销对象(Reuse Expensive Objects)

14.使用精灵表(Use Sprite Sheets )

15.避免重复处理数据(Avoid Re-Processing Data)

16.选择正确的数据格式(Choose the Right Data Format)

17.适当的设置背景图片(Set  Background Images Appropriately)

18.减少你的网络占用(Reduce Your Web Footprint)

19.设置阴影路径(Set the Shadow Path )

20.你的表格视图Optimize Your Table Views)

21.选择正确的数据存储方式(Choose Correct Data Storage Option)

 

高级

   这些技巧你应该只在你很积*认为它们能解决这个问题,而且你觉得用它们很舒适的时候使用。

22.加速启动时间(Speed up Launch Time )

23.使用自动释放池(Use AutoRelease Pool)

24.缓存图像(Cache Images-Or not )

25.尽可能避免日期格式化器(Avoid Date Formatters Where Possible)

没有其他的,一起去看看这些技巧吧!

 

 基础的性能提升

1)用ARC去管理内存

ARC是伴随IOS5 一起发布的,它用来消除常见的的内存泄漏。

ARC是”Automatic Reference Counting”的缩写。它自动管理你代码中的retain/release循环,这样你就不必手动做这事儿了。

下面这段代码展示了创建一个view的常用代码

查看源码

打印 ?

1 UIView *view =[[UIView alloc] init];
2 //...
3 [self.view addSubview:view];
4 [view release];

这里*其容易忘记在代码结束的地方调用release,ARC将会自动的,底层的为你做这些工作。

除了帮助你你避免内存泄漏,ARC还能保证对象不再使用时立马被回收来提高你的性能。你应该在你的工程里多用ARC。

这里是一些学习更多关于ARC的非常棒的资源

  • Apple’s official documentation 苹果的官方文档。
  • Matthijs Hollemans’s Beginning ARC in iOS Tutorial
  • Tony Dahbura’s How To Enable ARC in a Cocos2D 2.X Project
  • 如果你还是不确信ARC的好处,看看这篇文章 eight myths about ARC 说服你为什么用ARC。

值得注意的是ARC不能消除所有的内存泄漏。你依然有可能内存泄漏,这主要可能是由于blocks(块),引用循环,CoreFoundation对象管理不善(通常是C结构体,或者是确实很糟糕的代码)。

2)适当的地方使用reuseIdentifier

在app开发中的一个常见的为UITableViewCells,UICollectionViewCells,UITableViewHeaderFooterViews设置一个正确的reuseIdentifier(重用标识)。

为了*大化性能,一个tableView的数据源一般应该重用UITableViewCell对象,当它在tableView:cellForRowAtIndexPath:中分配数据给cells的时候。一个表视图维护了一个UITableViewCell对象的队列或者列表,这些对象已被数据源标记为重用。

如果你不用reuseIdentifier 会怎么样呢?

如果你用,你的tableview每显示一行将会配置一个全新的cell。这是非常费事的操作而且*对会影响你app滚动的性能。

自从引进了iOS6,你应该为header and footer 视图设置reuseIdentifiers,就像在 UICollectionView’s cells 和 supplementary views(补充视图)一样。

使用reuseIdentifiers,当你的数据源要求提供一个新的cell给tableview的时候调用这个方

查看源码

打印 ?

1 NSString *CellIdentifier = @"Cell";
2  
3 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

3)可能的时候设置视图为不透明

如果你有不透明视图(opaque views)–也就是说,没有透明度定义的视图,你应该设置他们的opaque属性为YES。

为什么? 这会允许系统以*优的方式绘制你的views。这是一个简单的属性可以在Interface Builder 和代码中设置。

苹果的文档 Apple documentation中有对这个属性的描述

这个属性提供了一个提示给图系统如何对待这个视图。如果设置为YES,绘制系统将会把这个视图视为完全不透明。这样允许系统优化一些绘制操作和提高性能。如果设置为NO,绘图系统会复合这个视图和其他的内容,这个属性的默认值是YES

在相对静态的屏幕上,设置opaque属性不会有什么大问题。尽管如此,如果你的视图是嵌入在一个scrollView,或者是一个复杂的动画的一部分,不设置这个属性*对会影响你的程序的性能。

你也可以使用Debug\Color olor Blended Layers选项 在你的模拟器中形象化的看见没有设置为不透明(opaque)的视图.你的目标应该是尽可能多的设置视图为透明。

4)  避免臃肿的XIB文件

故事板,由iOS5引进,很快的替代XIBs。尽管如此,XIBs在一下情况下依然是很有用的。如果你需要在IOS5之前版本的设备上运行或者你想自定义重用的视图,那么你确实不能避免使用它们。

如果你专注使用XIBs,那么让它们尽量的简单。尝试为一个试图控制器创建一个XIB,如果可能的话,把一个视图控制器的视图分层管理在单独的XIBs中。

注意当你加载一个XIB到内存的时候,它所有的内容都会载入内存,包括所有的图片。如果你有视图但不是要立即使用,那你就浪费了珍贵的内存。值得注意的是这不会发生在故事板中,因为故事版只会在需要的时候实例化一个视图控制器。

当你载入一个xib,所有的图像文件会被缓存,如果是开发OSX,那么音频文件也会被缓存。

Apple’s documentation 如是说:

当你载入一个包含了图和声音资源引用的nib文件时,nib加载代码读取实际的图片文件和音频文件到内存中并缓存它。在OS X中,图片和音频资源被存储在已命名的缓存 中这样你可以在之后需要的时候访问它们。在iOS中,只有图片资源被缓存,访问图片,你使用NSImage或者UIImage的imageNamed:方法来访问,具体使用取决于你 的平台。

显然这也发生在使用故事板的时候。尽管如此,我还不能找到这种说法的证据。如果你知道,请给我留言。

想学习更多关于故事板的更多内容吗?看看Matthijs Hollemans的 Beginning Storyboards in iOS 5 Part 1and Part 2.

5)不要阻塞主进程

你永远不应该在主线程中做任何繁重的工作。这是因为UIKIt的所有工作都在主线程中进行,比如绘画,管理触摸,和响应输出。

你的app的所有工作都在主线程上进行就会有阻塞主线程的风险,你的app会表现的反应迟钝。这是在App Store里获一星评论的快速途径!(作者卖萌..)

阻塞主线程*多的情况就是发生在你的app进行I/O操作,包括牵扯到任何需要读写外部资源的任务,比如读取磁盘或者网络

你可以异步的执行网络任务使用NSURLConnection中的这个方法:

查看源码

打印 ?

1 + (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler

或者使用第三方框架比如 AFNetworking.

如果你在做任何大开销的操作(比如执行一个耗时的计算,或者读写磁盘)使用Grand Central Dispatch(GCD)或者 NSOperations 和 NSOperationQueues.

使用GCD的模板如下代码所示:

查看源码

打印 ?

01 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
02  
03     // switch to a background thread and perform your expensive operation
04  
05   
06  
07     dispatch_async(dispatch_get_main_queue(), ^{
08  
09         // switch back to the main thread to update your UI
10  
11   
12  
13     });
14  
15 });

这里为什么dispatch_async 嵌套在*个的里面?这是因为任何UIKit相关的代码都必须在主线程上执行。

对NSOperation和GCD的详情感兴趣?看看Ray Wenderlich’s Multithreading and Grand Central Dispatch on iOS for Beginners 教程,和 Soheil Azarpour’s How To Use NSOperations and NSOperationQueues 教程。

6)调整图像视图中的图像尺寸

如果你用UIImageView呈现app束中的图片时,确认图片和UIImageView的尺寸相同。缩放图片会非常的耗时,特别是当你的UIImageView被嵌入UIScrollView。

如果图片是从远程服务器上下载的,有时你没法控制图片尺寸,或者你不能在服务器上在下载之前缩放它。在这些情况下你可以在图片下载完成后手动缩放一次,*好是在后台进程中。然在UIImageView中使用调整尺寸之后的图片。

7)选择正确集合

学着怎么在手头工作中使用*合适的类或对象是写出高效代码的基本。当时用集合是(collections),这个说法特别对。

可喜的是在苹果开发者文档( Collections Programming Topics)中有详细解释可用类之间的关系,还有解释各个类的适用情况。这个文档是每个使用集合的人的必读文档。

这是一个*常见的集合类型的快速简介:

  • Arrays:有序的值的列表,用index快速查找,通过值查找慢,insert/delete操作慢。
  • Dictionaries:存储键/值对.用index快速查找。
  • Sets: 无序的值列表。通过值快速查找,insert/delete快。

8)启用Gzip压缩

大量和持续增长的app依赖从远端服务器或者外部APIs获取的外部数据。某些时候你可能会开发一些需要下载XML,JSON,HTML或者其他文本格式的应用。

问题是移动设备不能保证网络环境,用户可能一分钟在边缘网络,下一分钟又是3G网络,无论什么情况下,你不想你的用户一直等待。

一个减少文件大小并加速下载的网络资源的方法是同时在你的服务器和客户端上使用GZIP压缩,对于文本数据这种有高比率压缩的数据来说非常有用。

好消息是iOS早已默认支持GZIP压缩,如果你是使用NSURLConnection或者建立在这之上的框架比如AFNetworking。更好的消息是一切云服务提供商像 Google App Engine早已发送压缩之后的响应数据。

这里有一篇文章great article about GZIP compression 介绍如何在你的Apache或IIS服务器上启用GZIP。

中级性能提升

好的,当谈到优化你的代码时,你应该很自信你已经初级的方法已经完全掌握了。但有时候有的问题的解决方法并不是那么显而易见,它由你app的结构和代码决定,尽管如此,在正确的上下文中,它们可能是没有价值的。

9)重用和延迟加载视图
越多的视图就有越多的绘图操作,*终意味着更多的CPU和内存开销。这说得特别对如果你的app嵌入很多视图在UIScrollView时。

管理这个的技巧是去模拟UITableView 和 UICollectionView的行为:不要一次创建所有的子视图,而是在需要的时候创建,然后把他们假如重用队列中。

这样,你只需要在视图浮动时配置你的视图,避免昂贵的资源分配开销。

视图创建的时机问题也同样适用于你app的其他地方。试想当你点击一个button时呈现一个视图的情景。至少有两种方法:

1.屏幕*次载入时创建视图并隐藏它。当你需要的时候,显示出来。

2.需要呈现的时候一次创建视图并显示它。

每种方法都有各自的优缺点

使用*种方法,你消耗了更多内存因为从创建开始到它释放前你都保持了它的内存,然而,当你点击button的时候,你的app会表现得响应快速因为它只需要更改视图的可视化属性。

使用第二种方法会有相反的效果,在需要的时候创建视图,消耗更少的内存,但当button被点击时应用会表现得不那么响应快速。

10)缓存,缓存,缓存

在开发应用时的一个伟大的经验是”Cache what matters”–也就是说那些不大会改变但会平凡被访问的东西。

你能缓存些什么呢?缓存的候选项有远程服务器的响应,图片,已计算过的值(比如UITableView的行高)。

NSURLConnection 根据处理的Http头缓存资源到磁盘或者内存中,你甚至可以手动创建一个NSURLRequest值加载缓存过的值。

这里有一段很棒的代码,用在任何时候你需要针对一个不大会改变的图片创建一个NSURLRequest。

查看源码

打印 ?

01 + (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
02  
03    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
04  
05  
06    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; // this will make sure the request always returns the cached image
07  
08     request.HTTPShouldHandleCookies = NO;
09  
10     request.HTTPShouldUsePipelining = YES;
11  
12     [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
13  
14     return request;
15  
16 }

如果想知道更多关于Http caching,NSURLCache,NSURLConnection等内容,请阅读the NSURLCache entry

注意,你可以通过NSURLConnection获取取一个URL请求,AFNetworking也可以。有了这个技巧这样你不用改变任何你的网络代码。

如果要缓存不牵扯到HTTP请求的其他东西,NSCache是很好的选择。

NSCache像NSDictionary,但是当系统需要回收内存的时候会自动的移除内容。

对HTTP Cache感兴趣并想学更多的内容?推荐阅读这篇文章best-practices document on HTTP caching

11)考虑绘图

在IOS中有很多方法可以制作拥有很棒外观的buttons,你可以是由全尺寸的图像,也可以使用调整尺寸之后的图像,或者你用CALayer,CoreGraphics,甚至OpenGL手动的它们。

当然,每种途径都有不同的复杂度级别和不同的性能,这篇文章非常值得一读post about iOS graphics performance here,这是Apple UIKit团队成员Andy Matuschak发表的文章,里面对各种方法有一些非常棒的见解和对性能的权衡。

使用预渲染图片更快,因为iOS不用创建一张图像和绘制图形到屏幕上(图像已经处理好了)。问题是你需要全部把这些图片放进应用束里,增加它的尺寸。那就是为什么使用可调整尺寸的图片是那么好:你通过移除”浪费了的“图片空间来节约空间。你也不需要为不同的元素生成不同的图片。(例如 buttons)

尽管如此,用图片你会失去代码调整你图片的能力,需要一次又一次的生成它们然后把它们加入到应用中。这是个缓慢的过程。另外一点如果你有动画或者很多张稍微变化的图片(例如 颜色叠加),你需要加很多的图片增加了应用束的大小。

总结一下,你需要想对你来说*重要的是什么:绘图性能还是app的大笑.通常两个都很重要,所以你会在一个工程里使用这两种方法。

12)处理内存警告

当系统内存低的时候iOS会通知所有的正在运行的app,关于低内存警告的处理苹果官方文档 official Apple documentation描述:

如果你的应用收到这个警告,它必须尽可能多的释放内存。*好的方法是移除对缓存,图像对象,和其他稍后要创建的对象的强引用。

幸运的是,UIKit提供了一些方法去接收低内存警告:

  • 实现App代理中的applicationDidReceiveMemoryWarning:方法。
  • 重载你自定义UIViewController子类中的didReceiveMemoryWarning方法。
  • 注册接收UIApplicationDidReceiveMemoryWarningNotification的通知

一旦收到这些警告,你的处理方法必须立刻响应并释放不必要的内存。

举例,如果视图当前不可见,UIViewController的默认行为是清除这些视图;子类可以通过清除额外的数据结构来补充父类的默认行为。一个应用程序维护一个图片的缓存,没有在屏幕上的图片都会被释放。

一旦收到内存警告,释放可能的全部内存是很重要的,否则你就有让你的app被系统杀死的的风险。

尽管如此,开始扑杀对象释放内存的时候要小心,因为你需要保证它们会在之后重新创建。当你开发app的时候,用你的模拟器上的模拟内存警告功能测试这种情况。

13)重用大开销对象

有的对象的初始化非常慢–NSDateFormatter 和 NSCalendar是两个例子,但是你不能避免使用它们,当你从 JSON/XML响应中解析日期时。

避免使用这些对象时的性能瓶颈,试着尽可能的重用这些对象。你可以加入你的类中成为一个属性,也可以创建为静态变量。

注意如果你选择了第二种方式,这个对象在app运行的时候会一直保持在内存里,像单例一样。

下面这段代码演示了NSDateFomatter作为一个属性的lazy加载,*次被调用然后创建它,之后就使用已创建在的实例

查看源码

打印 ?

01 // in your .h or inside a class extension
02  
03 @property (nonatomic, strong) NSDateFormatter *formatter;
04  
05   
06 // inside the implementation (.m)
07  
08 // When you need, just use self.formatter
09  
10 - (NSDateFormatter *)formatter {
11  
12     if (! _formatter) {
13  
14         _formatter = [[NSDateFormatter alloc] init];
15  
16         _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy"// twitter date format
17  
18     }
19  
20     return _formatter;
21  
22 }

同样要记住设置一个NSDateFormatter的日期格式几乎跟创建一个新的一样慢。因此,如果在你的应用中你频繁需要处理多个日期格式,你的代码应该获利于初始化创建,重用,多个NSDateFormatter对象。

14) 使用精灵表

你是一个游戏开发者吗?精灵表是你的好朋友之一.精灵表让绘制比标准屏幕绘制方法更快速,消耗更少的内存。

这里有两个很棒的精灵表使用的教程

  1. How To Use Animations and Sprite Sheets in Cocos2D
  2. How to Create and Optimize Sprite Sheets in Cocos2D with Texture Packer and Pixel Formats

第二个教程详细覆盖了像素格式,它可以对游戏性能有一个可衡量的影响。

如果对精灵表还不是很熟悉,一个很好的介绍 SpriteSheets – The Movie, Part 1and Part 2. 这些视频的作者是Andreas Löw,一个*流行的创建精灵表的工具Texture Packer的创建者。

除了使用精灵表之外,之前已经说到的内容也可以用在游戏上.举个例子,如果你的游戏有很多精灵,比如在标准的敌人或炮弹射击游戏,你可以重用精灵表额如是每次重新创建它们。

15)避免重复处理数据

很多app调用函数获取远程服务器上的数据.这些数据通常是通过JSON 或者 XML格式来传输。非常重要的是在请求和接收数据的时候努力在两端使用相同的数据结构。

理由?在内存中操纵数据以合适你的数据结构是非常昂贵的。

比如,如果你需要在表格视图中显示数据,*好请求和接收数据是数组的格式,以避免任何中间操纵数据,使其适合你在app中使用的数据结构

相似的,如果你的应用程序依赖于访问特定值的键,那么你可能会想要请求和接收一个键/值对的字典

通过*次就获取正确格式的数据,在自己的应用程序中你就会避免很多的重复处理工作,使数据符合你的选择的结构。

16)选择正确的数据格式

你可以有很多方法从web 服务中传递数据到你的app中

JSON 是一种通常比XML小且解析更快的格式,它的传输的内容也比较小。自iOS5起,内置的JSON解析很好用 built-in JSON deserialization

尽管如此,XML的一个优势当你使用SAXparsing方法时,你可以传输过程中读取它,在面的非常大的数据时,你不必像JSON一样在数据下载完之后才开始读取。

17)适当的设置背景图片

像iOS编码的其他工作一样,至少有两种不同方式去替换你视图的背景图片。

  1. 你可以设置你的视图的背景颜色为UIColor的colorWithPatternImage创建的颜色。
  2. 你可以添加一个UIImageView子试图给View

如果你有全尺寸的背景图片,你*对要用UIImageView,因为UIColor的colorWithPatternImage是重复的创建小的模式图片,在这种情况下用UIImageView方式会节约很多内存。

查看源码

打印 ?

1 // You could also achieve the same result in Interface Builder
2  
3  UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
4  
5 [self.view addSubview:backgroundView];

尽管如此,如果你计划用模式图片背景,你应该是用UIColor的colorWithPatternImage。它更快一些,而且这种情况不会使用很多内存。

查看源码

打印 ?

1 self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];

18)减少你的网络占用

UIWebView 是非常游泳的.它非常容易用来显示web内容,甚至创建你app的视窗。这些都是标准UIKit 空间很难做到的。

尽管如此,你可能注意你可以用在你的app中的UIWebView组件并没有Apple的Safari app快。这是Webkit’s的Nitro引擎的限制使用。JIT compilation.

所以为了获得*佳的性能,你需要调整你的HTML。*件事是尽可能多的避免Javascript,包括避免大的框架比如jQuery。有时使用vanilla Javascript取代依赖的框架会快很多。

随时随地遵循异步加载Javascript文件的实践。特别当它们不直接影响到页面表现的时候,比如分析脚本。

*后,总是要意识到你在用的图片,保持图片的正确尺寸。正如这个教程前面所提到的,利用精灵表的优势来节约内存和提高速度。

想要获取更多的信息,看看WWDC 2012 session #601 – Optimizing Web Content in UIWebViews and Websites on iOS.

19) 设置阴影路径

你需要给视图或者layer添加一个阴影,你应该怎么做?

大多数开发者是添加 QuartzCore框架到工程中,然后写如下代码:

查看源码

打印 ?

01 #import <QuartzCore/QuartzCore.h>
02  
03  // Somewhere later ...
04  
05 UIView *view = [[UIView alloc] init];
06  
07  // Setup the shadow ...
08  
09 view.layer.shadowOffset = CGSizeMake(-1.0f, 1.0f);
10  
11 view.layer.shadowRadius = 5.0f;
12  
13 view.layer.shadowOpacity = 0.6;

看起来非常简单,是吧?

不好的是这个方法有一个问题。核心动画必须要先做一幕动画确定视图具体形状之后才渲染阴影,这是非常费事的操作。

这里有个替代方法让系统更好的渲染,设置阴影路径:

查看源码

打印 ?

1 view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];

如果你想知道这个内容的更多技巧,Mark Pospesel 写过一篇post about shadowPath.

设置阴影路径,iOS不需要总是计算如何绘制阴影。而是用已经计算好的的路径。坏消息是它依赖与你的视图格式,你是视图可能很难计算这个路径。另一个问题是你需要在每次视图的框架改变时更新阴影路径。

20) 优化你的表格视图

表格视图需要快速的滚动,如果不能,用户能确切注意到很滞后。

为了让你的表格视图流畅的滚动,保证你实现了下列的建议。

  • 通过正确的reuseIdentifier重用cells
  • 尽量多的设置views 为不透明,包括cell本身。
  • 避免渐变,图像缩放,屏幕以外的绘制。
  • 如果行高不总是一样,缓存它们。
  • 如果cell显示的内容来自网络,确保异步和缓存。
  • 使用shadowPath来建立阴影。
  • 减少子视图的数目。
  • cellForRowAtIndexPath:中做尽量少的工作,如果需要做相同的工作,那么只做一次并缓存结果。
  • 使用适当的数据结构存储你要的信息,不同的结构有对于不同的操作有不同的代价。
  • 使用rowHeight,sectionFooterHeight,sectionHeaderHeight为常数,而不是询问代理。

21) 选择正确的数据存储方式

当要存储和读取大数据的时候你的选择是什么?

你有一些选项,包括:

  • 使用 NSUserDefaults存储它们。
  • 存储在结构化文件中,XML,JSON,Plist格式中。
  • 是用NSCoding打包?
  • 存储在本地数据库,如SQLite
  • 使用NSData

NSUserDefaults有什么问题呢?虽然说NSUserDefaults是好而且简单,它确实很好只有当你有很少的数据要存(像你的等级,或者音量是开还是关)。一旦你接触大数据,会有更好的其他选择。

保存在结构化文件中也可能有问题。一般的,在解析之前,你需要加载整个文件到内存中,这是非常耗时的操作。你可以使用SAX去处理XML文件,但是那是一个复杂的作法。同时你加载了全部的对象进内存,其中有你想要的也有不想要的。

那么NSCoding怎么样呢?不幸的是,它也同样要读写文件,跟上面说的方法有同样的问题。

你*好的解决方法是使用SQLite或者 Core Data. 通过这些技术,你可以执行特定的查询只加载需要的对象,避免强力搜索方法来检索数据。性能方面,SQLite和Core Data 非常接近。

SQLite 和 Core Data*大的不同就是它们的使用方法。Core Data呈现为一个对象图模型,但是SQLite是一个传统的DBMS(数据库管理系统).通常Apple建议你用Core Data,但是除非你有特殊的原因不让你你会想避开它,使用更低级的SQLite。

如果在你的app中使用SQLite,一个方便的库 FMDB 允许你使用SQLite而不用专研SQLite的C API。

高级性能技巧

寻找一些精英的方式去成为十足的代码忍者?这些高级性能技巧可以合适的时候使用让你的app运行得尽可能的高效。

22)加速启动时间

App的启动时间非常重要,特别是*次启动的时候。*影响意味着太多了!

*大的事情是保证你的App开始尽量的快,尽量的多的执行异步任务,不如网络请求,数据库访问,或者数据解析。

尽量避免臃肿的XIBs,因为你在主线程中加载。但是在故事板中不会有这个问题,所以尽量用它们。

Note: 监察人不会运行你的app在Xcode调试中, 所以确保测试启动性能时断开与Xcode的连接。

23)使用自动释放池

NSAutoreleasePool负责释放在代码块中的自动释放对象。通常,它是被UIKit自动调用的。但是也有一些场景我们需要手动创建NSAutoreleasePools。

举个例子,如果你创建太多的临时对象在你的代码中,你会注意到你的内存用量会增加直到对象被释放掉。问题是内存只有在UIKit排空(drains)自动释放池的时候才能被释放,这意味着内存被占用的时间超过了需要。

好消息是你可以在你的@autoreleasepool段中创建临时对象来避免上述情况。代码如下所示。

查看源码

打印 ?

01 NSArray *urls = <# An array of file URLs #>;
02 for (NSURL *url in urls) {
03  
04     @autoreleasepool {
05  
06         NSError *error;
07  
08         NSString *fileContents = [NSString stringWithContentsOfURL:url
09  
10                                          encoding:NSUTF8StringEncoding error:&error];
11  
12         /* Process the string, creating and autoreleasing more objects. */
13  
14     }
15  
16 }

在每次迭代之后会自动释放所有的对象。

 

你可以阅读更多关于NSAutoreleasePool的内容Apple’s official documentation.

24)缓存图像

这里有两种方法去加载app束中的Image,*个常见的方式是用imageNamed. 第二个是使用imageWithContentsOfFile

为什么会有两种方法,它们有效率吗?

imageNamed 在载入时有缓存的优势。文档 documentation for imageNamed是这样解释的:

这个方法看起来在系统缓存一个图像对象并指定名字,如果存在则返回对象,如果匹配图像的对象不在缓存中,这个方法会从指定的文件中加载数据,并缓存它,然后返回结果对象。

作为替代,imageWithContendsOfFile 简单的载入图像并不会缓存。

这两个方法的的演示片段如下:

查看源码

打印 ?

1 UIImage *img = [UIImage imageNamed:@"myImage"]; // caching
2  
3 // or
4 UIImage *img = [UIImage imageWithContentsOfFile:@"myImage"]; // no caching

如果你加载只使用一次大图片,那就不需要缓存。这种情况imageWithContendsOfFile会非常好,这种方式不会浪费内存来缓存图片。什么时候使用哪一种呢?

然而,imageNamed 对于要重用的图片来说是更好的选择,这种方法节约了经常的从磁盘加载图片的时间。

25) 尽可能避免日期格式化器

如果你要用NSDateFormatter来解析日期数据,你就得小心对待了。之前提到过,尽量的重用NSDateFormatters总是一个好的想法。

然而,如果你需要更快的速度,你可以使用C代替NSDateFormatter来解析日期。 Sam Soffes写了一篇 blog post about this topic来说明如何用代码来解析 ISO-8601日期串。尽管如此,你可以很容易的修改他的代码例子来适应你的特殊需求。

噢,听起来很棒,但是你相信有更好的办法吗?

如果你能控制你所处理日期的格式,尽可能的选择使用 Unix timestamps。Unix时间戳是简单的整数代表从某个起始时间点开始到现在的秒数。这个起始点通常是1970年1月1日 UTC 00:00:00。

你可以容易的把时间戳转换为NSDate,如下面所示:

查看源码

打印 ?

1 - (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
2  
3   return [NSDate dateWithTimeIntervalSince1970:timestamp];
4  
5 }

这甚至比C函数更快

注意,很多WEB APIs返回时间戳是毫秒,因为这对于javascript*终来使用和处理数据是非常常见的。只要记住将这个时间戳除以1000再传递给dateFromUnixTimestamp方法即可。