如何能在最快的时间内,实现一个最新版本 android app 的 pad 化呢?从拿到一个大型手机 app 代码开始开发到第一个其全新 pad 版本的发布,我们用了不到3个月时间给出了一份满意的答案。
项目背景
采用最新版本手机 APP(之后称为 MyApp)代码,实现其 Pad 化,为平板和大屏手机用户提供更好的体验。为实现 MyApp 的 Pad 化工作,需要我们首先来了解一下 MyApp 项目经典页面的构成以及 Pad 化后的页面结构的变化。
1.MyApp 页面经典构成
现在主流手机 APP 主页通常采用标签栏加标签内容方式显示。而通过主页进入的二级页面全部采用全屏方式展示。比如手机 QQ,微信,支付宝等等都是采用 Tab 栏方式为主,进入一个具体功能后,全屏打开。我们项目也是如此。下面看一下 MyApp 项目手机端的页面构成图。
左侧是一个 Tab 栏(区域1)加 Tab Content(区域2)构成的页面,右侧是在 TabContent 中点击具体功能后进入的一个功能详情页面(全屏区域3)。
查看代码,发现除 TabContent 区域2,从主页开始到其他全屏显示的页面全部采用 Android Activity 组件实现。经统计得出大概有几百个 Activity。这些 Activity还包含比如Web进程,peak 进程(图片选择查看)等其他非主进程 Activity。
1.MyApp pad 化的设计图
了解了手机 MyApp 页面构成后,还要来看 Pad 化后 UI 结构的变化,通过对比来探索 Pad 化最佳的实现方案。下面是我们的 PAD 版本页面结构图。
由于 Pad 平板的空间要远大于手机空间,所以,在主页中 Pad 所展示的内容要比手机更多。通过观察设计图发现,整个页面分为了3块区域,与手机端页面的1,2,3区域一一对应。Tab 栏被移到了左侧1区,Tab Content 被移到了中间2区,而在2区打开的 Details 页面则要求在3区展示,而不再是像手机 APP 一样全屏展示。
手机 APP Pad 化道路的探索过程
通过了解 MyApp 项目经典页面构成和 pad 版页面结构的变化,以及快速Pad 化的原则,我们开始了对手机 APP pad 化实现方案的漫漫探索。
首先想到的是,既然手机APP页面主要是由Activity构成,那么我们能不能把 Activity 缩小,让多个 Activity 在同一屏幕显示呢,很快我们的方案1出来了。
方案1,如果把设计图的整个页面称为主 Activity,主 Activity 全屏显示不变,在主 Activiy 中打开的新 Activity (称为A)缩小显示在设计图3区,我们就可以实现 Pad 设计的要求。那么我们具体实现步骤为:
1,A类 Activity 继承 Base Activity
2,修改 Base Activity 的 window 的起始坐标x和宽度 width,让其刚好位于3区。
3,A类 Activity 背景改为透明
4,让在A类 Activity 继续打开的 Activity,重复1,2,3,4步骤。
但是很快就发现了问题。
在当前 Tab 打开 Activity A,切换 Tab 后 A Activity 仍然显示。
每个 Tab 打开的 Activity,都处于同一个 Activity 栈中,按打开先后顺序添加,点击返回键也是顺序退出的。这样每个 Tab 中打开的Activity 都混在一起了,而不是彼此独立。导致 back 键出现问题。
既然直接显示 Actvity 有问题,想想反正都是显示UI布局,能不能把 A 类 Activity 的根布局拉出来挂载在主 Activity 右侧?从而我们推导出了方案2。
方案2:在主 Activity 启动 A 类 Activity 时,获取 A 的根布局,添加到主 Activity 在右侧3区预留的一个空布局中。具体实现步骤为:
1,重写主 Activity 的 startActivity 方法。
2,使用 LocalActivityManager 启动 A 类 Aactivity 返回 window 对象。
3,通过 Window 对象 window.getDecorView()返回打开 Activity 的布局并 Add 到主 Actvity 上。
4,重写主 Activity 的 Back 逻辑,在点击返回键时 remove 掉挂载的 decorView。
但是在 Demo 上一测试,就发现了很多问题,
用 mat 查看到 A 类 Activity 是怎么也释放不掉的,因为 LocalActivityManager 已经抓住了 A 类 Activity 的 parent。
直接拿出 A 类 Activity 的 decorView,已经让A类 Activity 丧失了 Activity 的一切特性,包括生命周期,返回逻辑,ActivityResult,以及启动其他 Activity 等功能。会导致之前正常运行的A类 Activity 出现大量问题。
既然直接拿到根视图没有用,那该怎么做才好呢?怎么做才能使 A 类 Activity 的页面挂载在主 Activity 右侧,又能保证 A 的生命周期和 Activity 行为呢?经过大家一番思考讨论后,能不能利用插件的思想,把 A 类 Activity 中的生命周期方法以及继承自 Activity 类的方法都拿出来了,在适当时候自己调用呢?这样就保证了原来 A 中的代码不会出现问题,不需要修改 A 中的任何代码。但是用什么做容器(代理)比较好了?之后我们想到了用 Fragment,,因为 Fragment 可以作为所属 Activity 的一个块存在于任何位置,并且 Fragment 有自己的生命周期,并受所属 Activity 的生命周期影响,它就像一个子 Activity 一样。简直是绝佳容器。而且 Fragment 比较轻量,本身由 Activity 来管理(而不像 Activity 由 Android 系统服务管理),在不同的布局结构中重用 Fragment 可以优化屏幕空间和用户体验。
注意,下面所说的把 Activity 转换为 Fragment 并不是直接把 Activity 变为 Fragment,这会付出巨大代价,而是以一个空的 Fragment 为容器来承载 Activity。
终于方案3顺利出炉。
方案3,把 Activity 转换为 Fragment,使用 Fragment 模拟 Activity 的方法。然后把 Fragment 直接添加到主 Activity 的右侧布局中。实现的具体步骤为:
1,新增 BasePadActivity,让所有 Activity 继承 BasePadActivity,重写 StartActivity 方法,在该方法中手动 New 出A类 Activity,并把主 Activity 的上下文对象 Context 传递给它。
2,创建 MyFragment,持有A类 Activity 实例引用,在 MyFragment 生命周期中直接调用A类 Activity 生命周期方法,并把A类 Activity 的视图传递给 Fragment 使用。
3,A类 Activity 中继承自 Activity 的方法全部重写,具体实现由步骤1中的得到的主 Activity 上下文 context 处理。这样A类 Activity 已经成为一个普通实例化对象,不再由 Android 系统管理。
该方案有较多优点,由于继承 base,不仅能迅速将大量 Activity 快换为 Fragment,而且转换后,使原来A类 Activity 的功能逻辑维持正常。并且由于A类 Activity 的上下文其实使用了主 Activity 的上下文对象,需要在A类 Activity 获取 Resouce,Window,Asset 对象等都能通过主 Activity 的 context 进行获取。
该方案实现后,最初测试好像一切正常,但是不就后也发现了若干问题:
原 Activity 自定义 TitleBar 出现问题。
每个 Tab 标签中打开的 Fragment,由于都属于一个主 Activity,导致它们只有一个 Fragment 栈,Back 返回时会出现与方案1类似的问题。
尽管 Activity 转换为 Fragment 后,大部分行为都进行了模拟,但是还有一些重要行为没有做处理,比如说 Activity 的启动模式,Back 键,onActivityResult 等等,这些还要进行完善。
对于声明为多进程的 Activity,转换为 Fragment 后失去了多进程的特性。因为这些 Fragment 属于主 Activity,主 Activity 是属于手Q进程的。
该方案虽然也有诸多问题,但是经过调研和测试,发现基本都是能解决的。出于该方案的优点,以及对其出现的问题的解决难易评估,最终决定在该方案基础上进行优化和完善。那么对上述出现的若干问题该如何解决呢?
问题1,Activity 替换成 Fragment 之后,如何实现自定义 TitleBar?
设置自定义 TitleBar,是 Activity 所提供的接口,查看手机APP代码,大部分 Activity 都继承了 TitleBarActivity,通过
getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,
R.layout.custom_commen_title);
来定义 TitleBar 的样式。所以改为 Fragment 之后导致大量 Activity titlebar 显示不出,甚至 crash。那么能不能实现一个自定义的 window 对象继承 android.view.Window,通过 getWindow()得到的是我们自定义的 Window 对象,它可以处理自定义 Titlebar 的使用。
问题2,如何确保每个标签页中 Fragment 的操作互不干扰?
Pad 版本主页也是分为多个 Tab 标签栏的,每个标签栏中对 Fragment 的操作应该是相互独立的。Android中Fragment都是由 FragmentManager 来管理的。Fragment 的添加,替换,移除等操作都是由 FragmentManager 中对象 FragmentTransaction 来记录和执行。每个 Activity 中只有一个 FragmentManager 实例,通过代码
Activity.java
final FragmentManagerImpl mFragments = new FragmentManagerImpl();
public FragmentManager getFragmentManager() {
return mFragments;
}
获取。如果把设计图中的整个页面称为主 Activity,用主 Activity 中一个 FragmentManager 来管理所有标签栏的 Fragment 显然会引起混乱,那么能否实现每个标签页中都有一个 FragmentManager 的实例来管理当前标签中所有 Activity 转换的 Fragment?
问题3:Activity 替换成 Fragment 之后,完善 Fragment 模拟 Activity 的行为。
完善 Fragment 的 Activity 行为,比如还需要模拟 Activity 的启动模式、Activity result、startActivity、finish、onBackPressed 等等。
问题4,如何处理多进程 Activity 的显示?
在回答这个问题之前,要先问一个问题,为什么不都转换为 Fragment 呢?
之前研究手机 APP 项目代码发现,许多Activity都是设计成属于其他进程,比如 Web 进程。这样设计的原因:
其一是这类 Activity 功能都是属于同一模块,出现 crash 也不会让整个QQ崩溃。
另外一个重要原因是,Android 平台对每个进程都有内存限制,使用多进程就可以使APP所使用的内存加大几倍。其他进程可以分担主进程的内存压力,大大降低内存溢出导致的 crash。
所以不能轻易把这些 Activity 转换为 Fragment,因为转换为 Fragment,就失去了 Activity 多进程的特性,违背了之前设计初衷,大大增加了APP的内存压力。那么这种情况下是否能让多个 Activity 在同一屏幕显示,能不能让从主 Activity 打开的新 Activity 变为透明,并且让其大小和位置刚好覆盖设计图的区域3,同时让属于主 Activity 的区域1,和区域2接收事件。这样既让 Acitvity 拥有多进程的特性,又让他们看起来就像是在同一个 Activity 中操作。咦,这不是我们的方案1吗? 对的,由于以上种种原因,对于多进程的 Activity,我们还是要按照方案1来处理。
那么如何解决解决方案1中的问题。
问题5,多进程的 Activity 在切换标签后如何处理?Back 键如何处理?
在每个标签页打开的多进程 Activity,应该只与本标签页有关联,在切换到其他标签后,这些 Activity 应该隐藏起来,重新再切换 Tab 回到该标签时,之前在该标签打开的这些 Activity 应该重新显示。而且每个 Tab 的 Activitys 都应该有一个 Activity 栈来管理。
这该如何实现呢?通过阅读http://developer.android.com/guide/components/tasks-and-back-stack.html了解到
一个 app 中通常包含若干个 Activitys,我们可以把这些 Activity 分为若干类,让每一类都属于同一个 Task,以多任务的方式把这些 Activity 分为若干组。比如把在 Tab1栏内打开的多进程 Activity 放入一个Task中,把Tab2中打开的多进程 Activity 放入另外一个 Task 中,切换 tab 时,只需要让两个 task 交替移到前台显示或后台隐藏即可,而且每个Task中都维护着一个 Activity 栈。该想法似乎能解决这个问题。
那么看到这里大家又会有另外一个疑问了?既然能解决方案1中的问题,为什么不直接全部使用方案1呢?还要把Activity转为Fragment干嘛?
1,实现的问题,使用多Task的实现方式,在Android中需要声明Activity的TaskAffinity,而 TaskAffinity 不能在代码中动态声明,而只能写在配置文件中,导致不同Tab打开的同一个Activity可能需要在配置文件中声明两次,因为它们的 TaskAffinity 要不一样,而同一个Activity是不能声明两次的,所以只有写一个空的 Activity 继承它,导致大量空Activity产生,而且在代码中启动 Activity 前还要重定向到继承的Activity,比较麻烦。多进程 Activity 毕竟还是少数,所以可以这么做。但全部这样实现明显不太可取。
2,体验的问题,当切换 Tab,把 Task 移入前台,会有一个延时,而且这个延时并不确定,导致切回 tab,会先显示底部的页面,然后 task 中Activity 才覆盖上来。
3,机型的问题,极少数机型可能是由于厂家定制的原因,在多个 Activity 显示在同一屏幕时会有一个问题,在接收左侧主 Activity 的事件时,A类 Activity 会消失。经过原因查找,发现A类 Activity 的 task 自动回到了后台。应该是系统源码被修改了。这样的话基本没法用了。
经过大家若干分析讨论,我们基本理清楚了方案3所遇到问题的大致解决版本,经过均衡考虑,使用以下解决方案是目前 Pad 化最好的解决方案。
APP 快速 Pad 化实现架构方案
对 MyApp pad 化的整个流程以及会遇到的问题理清楚之后,经过思考和大家的讨论确定了我们手机端 APP Pad 化的架构方案:
1,转换 APP 主进程的 Activity 为 Fragment,转换过程尽量不修改原来 Activity 的任何代码。
2,让转换后的 Fragment 模拟 Activity 的行为,保持 Fragment 和原来 Activity 的行为一致。
3,使用 LocalActivityManager 实现每个 Tab 标签 Fragments 操作的独立性。
4,多进程 Activity(包括插件 Activity)不转换为 Fragment,实现多任务分屏显示。
1,巧妙转化 Activity 为 Fragment
实现 BasePadActivity 为所有 Activity 基类,对 Activity 转换为 Fragment 的操作在 BasePadActivity 中实现,利用类似插件的想法,以Fragment为壳,把真正具体的实现从 Activity 中搬入 Fragment,而不需要修改原 Activity 的任何代码。通过重写 StartActivity 方法,让原来去 startActivity 的动作变为添加一个新的 Fragment 动作,并调用 addToBackStatck()方法添加到 Fragment 栈中。
下面给出部分伪代码的实现 ,注意以下代码都是伪代码,很多只有方法而无实现, 这里主要是讲思路 。
BasePadActivity.java
public class BasePadActivity extends Activity{
/**
-
通过重写startActivityForResult来将Fragment转换为Activity
*/@Override
-
void startActivityForResult(Intent intent) {
if (isfragment && !isNewProcessActivty()){ //多进程Activity不转换为Fragment //初始化activity,注意该Activity是我们自己实例化出来的 BasePadActivity activity =(BasePadActivity)newInstance(intent); activity.attachBaseContext(getBaseContext()) activity.onCreate(intent.getExtras()); //模拟Activity的onCreate activity.addMyFragment(); } } }
-
class MyFragment extends Fragment {
@Override public View onCreateView() { View contentView = getWindow().getDecorView(); //Fragment的View为Activity的decorView return contentView; } @Override public void onResume() { super.onResume(); BasePadActivity.this.onResume(); //模拟Activity } @Override public void onPause() { super.onPause(); BasePadActivity.this.onPause(); //模拟Activity onPause } ........ ........ }
}
代码巧妙实现了所有继承 BasePadActivity 的 Activity 转换为Fragment 的过程,当然这里只展示了转换一小部分,其他细节问题并没有在代码中列出来。当打开 Activity 的过程转换为打开 Fragment 的过程后,我们需要让 Fragment 模拟 Activity 的行为。
2,Fragment 模拟 Activity 的行为
模拟 Activity 的 finish 方法
BasePadActivity.java
public void finish() {
if (bActivityToFragment) {
removeTopFragment(); //移除Fragment栈中最顶层Fragment
}
}
模拟 Activity 的 onActivityResult,在当前 Fragment 被 finish 去触发
private void removeTopFragment() {
if(popBackStackImmediate()){ //成功把顶层Fragment,从Fragment栈中移出。
handleSetResult(requestCode,false);
}
}
private void handleSetResult(int requestCode){
//触发上层Fragment的onActivityResult
topFragment.onActivityResult(requestCode, resultCode, data);
}
模拟 Activity 的返回事件,当把 Activity 转换为 Fragment 时,其返回事件已经由该 Fragment 所属的 Activity 接收,因此需要处理其所属真正 Activity 的返回事件。通过 FragmentManager 可以管理该 Activity 中所有 Fragment。
BasePadActivity.java
public void onBackPressed() {
if(isActivityForFragment()){ //,为Fragment所属Activity
if(!handleFragmentBackEvent()){ //先处理Fragment本身的返回事件,比如想先关闭当前Fragment菜单。
finishTopFragment(); //最后finish该Fragment
}
}
}
模拟 Activity 的启动方式,通过获取 intent.getFlags()值,来判断 Activity 的启动模式,通过
public boolean hasFlagClearTop(int flags){
return (flags & Intent.FLAG_ACTIVITY_CLEAR_TOP )!=0 ;
}
public boolean hasFlagNewTask(int flags){
return (flags & Intent.FLAG_ACTIVITY_NEW_TASK )!=0 ;
}
来判断 Flag 中是否包含相应启动方式值,来对 Fragment 的打开做相应处理。这里会稍微麻烦一点,故不作代码说明了。
3,Fragment 实现自定义的 TitleBar
在不改变原来 Activity 代码的情况下,通过改变 Window 对象,自己实现对 Fragment 布局的控制。实现自定义 Titlebar。
BasePadActivity.java
public Window getWindow() {
if(customWindow==null){ //自定义的window,主要为了解决很多Activity继承IphoneTitleBar的问题
customWindow = new Window4FragmentTitle(mContext);
}
return customWindow;
}
Window4FragmentTitle.java
public class Window4FragmentTitle extends Window{
@Override
public void setContentView(View view, LayoutParams params) {
if (mContentParent == null) {
installDecor();
}
mContentParent.addView(view, params);
}
@Override
public void setFeatureInt(int featureId, int value) {
if(featureId != FEATURE_CUSTOM_TITLE){
return;
}
FrameLayout titleContainer = (FrameLayout) findViewById(android.R.id.title);
if (titleContainer != null) {
mLayoutInflater.inflate(value, titleContainer);
}
}
}
通过 Window4FragmentTitle 调用 setFeatureInt(int)方法,会把自定义的 Title 布局嵌入到我们创建的布局 mDecor 中,然后把 mDecor 放入 Fragment,实现自定义 Titlebar 在 Fragment 中的显示。
4,使用 LocalActivityManager,实现对每个 Tab 中 Fragments 的独立管理。
使用 LocalActivityManager 实现标签布局,使每个 Tab 中都有一个 Acitvity 对象,而每个 Activity 中都会有一个 FragmentManager 对该 Tab 的 Frament 栈进行管理,这样每个 Tab 的 Fragments 相互独立,互不影响。
主Activity
addFrame(Tab1Content.class, mTabs[1]); //Tab1
addFrame(Tab2Content.class, mTabs[2]); //Tab2
.....
public void addFrame(){
mTabHost.setup(getLocalActivityManager());
TabSpec tabSpec = mTabHost.newTabSpec("").setIndicator(tab).setContent(new Intent(this, clz));
mTabHost.addTab(tabSpec);
}
通过 setContent(new Intent(this, clz) ,把每个标签栏内容以子 Activity 的方式添加进来。
5,多任务分屏显示。
前面说过,对于多进程的 Activity,为了保持其模块化以及分担主进程内存压力的特点,经过大家讨论,不把他们转换为 Fragment,那么就需要解决多个 Activity 一起展示的问题。经过研究,得出的有效实现方式是:让在每个标签栏内打开的 Activity 透明化,并且让其大小和位置刚好居于设计图3区,同时能让处于该 Activity 下方的左侧区域的主Activity 接收点击事件。
1,Activity 透明化实现,在配置文件中声明 Activity 的 theme 为透明
2,定义 Activity 的大小和位置。
在 Activity 的 onCreate 方法中实现
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.gravity = Gravity.RIGHT | Gravity.TOP; //位置居于右侧
layoutParams.width = mActivity.getRightPanelWidth(); //宽度为右侧区域宽度
3,让左侧主 Activity 接收事件
通过设置 window 的 WindowManager.LayoutParams 的 flag 为 FLAG_NOT_TOUCH_MODAL 可以让显示在透明 Activity 左侧下方的主 Activity 接收事件。
新打开的 Activity 位于右区,左区为主 Activity 显示区域。左区和右区能同时接收用户点击事件,看起来就好像同一个 Activity 一样。
但是由于在当前 Tab 打开的位于右区的 Activity,是跟随当前Tab的,在切换 Tab 后,应该消失,比如 Tab1中打开的 Actvity,切换到Tab2时应该隐藏掉,重新再切换回 Tab1时让其重新显示,保留现场。该功能要如何实现呢?经过对 Android 特性的理解以及思考,发现可以是用多任务分屏显示方式实现不同 Tab 多进程 Activitys 的显示和隐藏。让不同Tab打开的 Activitys 分属于不同 Task,每个 task 拥有一个 Activity 栈来管理其中 Activity,切换 tab 要做的就是不同 Task 的切换。这样逻辑非常清晰,也符合快速 Pad 化的原则。那么具体该怎么实现呢?
为每个 Tab 打开的第一个 Activity 提供一个不同的 TaskAffinity
首先我们来了解,什么是 TaskAffinity?
在某些情况下,Android 需要知道一个 Activity 属于哪个 Task,这是通过任务共用性(TaskAffinity)完成的,TaskAffinity 为运行一个或多个Activity 的 task 提供一个独特的名称,当使用 Intent.FLAG_ACTIVITY_NEW_TASK 标志的 Activity,并为该 Activity 声明一个独特的 TaskAffinity 时,该 Activity 不再运行在启动它的 Task 里,而是会重新启动一个新的 Task,新的 task 管理一个新的 Activity 栈,而打开的这个 Activity 则位于栈底。
了解了 TaskAffinity,我们在配置文件中为打开的多进程 Activity 设置相关 tab 的 TaskAffinity 值
下面展示对 web 进程 Activity 的处理
........
在不同 Tab 打开的 BrowserActivity,都为它们设置了不同的 TaskAffinity,在代码中当发现打开的页面是 Web 页面时,则在哪个Tab打开,页面重定向到设置了相应 TaskAffinity 的 Activity上。
public void startActivityForResult(Intent intent) {
if(isBrowserActivity(intent)){ //纯打开QQBrowserActivity
int curTab = getTabIndex(); //获取当前Tab索引
Class> c = getBrowserMap().get(curTab); //取出相应打开的Activity
redirectAndOpenInNewTask() //重定向
}
......
......
}
这样就为在不同 Tab 打开的 Activity 创建了不同的 Task。然后在切换Tab时通过发送广播动态的显示和隐藏 Task,
public void onTabSelected(int curTabIndex) {
Intent i = new Intent("action");
i.putExtra("cur_Tab_Id",curTabIndex); //切换到当前Tab的索引
sendBroadcast(i);
}
在 Task 的根 Activity 中接收广播,处理 Task 显示和隐藏逻辑
public void onReceive(Context context, Intent intent) {
int tab = intent.getIntExtra(CUR_TAB_ID,-1);
if(tabIndex>=0 && tabIndex == tab){
moveTaskToFront(mActivity.getTaskId()); //移动当前Task移入后台
}else{
moveTaskToBack(); //把该Activity所属Task移动到前台
}
}
到这里基本上解决了多进程 Activity 与主 Activity 同屏显示所带来的问题。
总结
通过上述方案,以及一些问题的巧妙解决,终于实现了 MyApp Pad 化的快速开发,而且 MyApp for Pad 第一个版本的发布上线,到现在6到7个版本的迭代,一直都是稳定运行的。该方案的优点是,只要维护好架构,其他开发者在对单个页面改造时,不需要管它是真正 Activity 还是 Fragment,只要知道这些页面表现的都是 Activity 的行为,就和在手机 APP 上开发是一样的。
扩展
通过 MyApp Pad 化开发方案的实现,我们想我们这个方案是否可以写成一个通用的手机 App Pad 化组件,为公司的其他Android产品Pad化提供技术支持。就算你的 APP 布局并不是 Tab 方式,我们开放出一个 Base Activity,其他 App 的 Activity 通过继承 Base,就能自动转换为Fragment,并且能为多进程 Activity 提供处理方案。但这肯定需要考虑更多的情况,和解决更多问题。路漫漫其修远兮,吾将上下而求索。期待完善的 Android App PAD 组件能与大家见面。
如果你觉得内容意犹未尽,如果你想了解更多相关信息,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~
腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!