IOS程序员必须知道的Android要点

移动开发 Android
在移动应用飞速发展的今天,APP只针对IOS平台进行开发已经不够了,如今Android在移动设备占有近80%的市场,如此大量的潜在用户怎么能被忽略掉呢。

在移动应用飞速发展的今天,APP只针对IOS平台进行开发已经不够了,如今Android在移动设备占有近80%的市场,如此大量的潜在用户怎么能被忽略掉呢。

在这篇文章中,本人会介绍在IOS开发中,怎么学习一些Android开发的理念,Android和IOS功能上本身有一定的相似之处,但是具体实现的方式各异,所以这篇文章会使用一个项目例子进行对比,说明怎么在这两个平台上分别去实现这个任务。

除 了了解IOS的开发知识,本文还需要对Java有一定的了解,并能够安装和使用ADT(Android Development Tools)。此外,如果你是一个Android新手,那么请试试去看看Android的官方教程—— building your first app,非常有用。

UI设计简要说明

本文不 会深入研究关于IOS和Android两个平台之间的用户体验或者设计模式之间的差异,不过如果能够理解Android上的一些优秀的UI范例也很有帮 助:ActionBar、Overflow menu、back button share action等等。假如你很想尝试Android开发,那么强烈推荐你去Google Play Store上购置一台Nexus5,然后把它作为你日常使用的设备使用一周,然后尝试仔细了解这个操作系统的各种功能和扩展特性,如果开发者连操作系统的 各种使用规则都不了解,那么做出来的产品一定有问题。

编程语言的应用框架

Objective-C和Java之间有很多不同之处,如果把Objective-C的编程风格带到Java里面的话,很多代码也许会和底层的应用框架冲突。简单地说,就是需要注意一些区别:

去掉Objective-C里面的类前缀,因为Java里有显式的命名空间和包结构,所以就没必要用类前缀了。

实例变量的前缀用“m”,不用“_”,在写代码的过程中要多利用JavaDoc文档。这样能使代码更清晰,更适合团队合作。

注意检查NULL值,Objective-C对空值检查做的很好,不过Java没有。

不直接使用属性,如果需要setter和getter,需要创建一个getVariableName()方法,然后显式调用它。如果直接使用“this.object”不会调用自定义的getter方法,你必须使用this.getObject这样的方法。

同样的,方法命名时带有get和set前缀来标示它是getter和setter方法,Java的方法很喜欢写成actions或者queries等,比如Java会使用getCell(),而不用cellForRowAtIndexPath。

项目结构

Android 应用程序主要分为两部分。***部分是Java源代码,以Java包结构排布,也可以根据自己的喜好进行结构排布。最基本的结构就是分为这几个顶层目 录:activities、fragments、views、adapters和data(models和managers)。

第 二部分是res文件夹,就是“resource”的简称,res目录存放的是图片、xml布局文件,还有其它xml值文件,是非代码资源的一部分。在 IOS上,图片只需要匹配两个尺寸,而在Android上有很多种屏幕尺寸需要考虑,Android上用文件夹来管理管理图片、字符串,还有其它的屏幕配 置数值等。res文件夹里也含有类似IOS中xib文件的xml文件,还有存储字符串资源、整数值,以及样式的xml文件。

***,在项目结构上还有一点相似的地方,就是AndroidManifest.xml文件。这个文件相当于IOS的Project-Info.plist文 件,它存储了activities、application还有Intent的信息,要了解更多关于Intent的资料,可以继续阅读这篇文章。

Activities

Activities 是Android APP最基本的可视单元,就像UIViewControllers是IOS最基本的显示组件一样。Android系统使用一个Activity栈来管理 Activity,而IOS使用UINavigationController进行管理。当APP启动的时候,Android系统会把Main Activity压栈,值得注意的是这是还可以再运行别的APP Activity,然后把它放到Activity栈中。返回键默认会从Activity栈进行pop操作,所以如果用户按下返回键,就可以切换运行已运行 的App了。

Activities还可以用Intent组件初始化别的Activity,初始化时 可携带数据。启动一个新的Activity类似于IOS上创建一个UIViewController。最基本的启动一个新的Activity的方式就是创 建一个带有data的Intent组件。Android上实现自定义Intent初始化器的***方法就是写一个静态getter方法。在Activity 结束的时候也可以返回数据,在Activity结束的时候可以往Intent里面放置额外的数据。

IOS 和Android的一个大的区别是,任何一个在AndroidManifest文件中注册的Activity都可以作为程序的入口,为Activity设 置一个intent filter属性比如“media intent”,就可以处理系统的媒体文件了。***的例子就是编辑照片Activity。它可以打开一张照片,然后进行修改,***在Activity结束 时返回修改后的照片。

附加提醒:要想在Activity和Fragment之间传递对象,必须要实 现Parcelable接口,就像在IOS里需要遵循协议一样。还有,Parcelable对象可以存在于Activity或者Fragment的 savedInstanceState里,这样在它们被销毁后可以更容易重建它们的状态。

下面就来看看怎么在一个Activity中启动另一个Activity,然后在第二个Activity结束时进行返回。

启动其它Activity并返回结果

  1. // A request code is a unique value for returning activities 
  2. private static final int REQUEST_CODE_NEXT_ACTIVITY = 1234
  3.   
  4. protected void startNextActivity() { 
  5.     // Intents need a context, so give this current activity as the context 
  6.     Intent nextActivityIntent = new Intent(this, NextActivity.class); 
  7.        startActivityForResult(nextActivityResult, REQUEST_CODE_NEXT_ACTIVITY); 
  8.   
  9. @Override 
  10. protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
  11.     switch (requestCode) { 
  12.     case REQUEST_CODE_NEXT_ACTIVITY: 
  13.         if (resultCode == RESULT_OK) { 
  14.             // This means our Activity returned successfully. For now, Toast this text.   
  15.             // This just creates a simple pop-up message on the screen. 
  16.                 Toast.makeText(this"Result OK!", Toast.LENGTH_SHORT).show(); 
  17.             } 
  18.             return
  19.         }     
  20.         super.onActivityResult(requestCode, resultCode, data); 

Fragment 的概念在Android上比较独特,从Android3.0开始引入。Fragment是一个迷你版的控制器,可以显示在Activity上。它有自己的 状态和逻辑,同时在一个屏幕上支持多个Fragment同时显示。Activity充当Fragment的控制器,Fragment没有自己的上下文环 境,只能依赖Activity存在。

使用Fragment***的例子就是在平板上的应用。可以在屏幕左边放一个fragment列表,然后在屏幕的右边放fragment的详细信息。Fragment可以把屏幕分成可重复利用的小块,分别控制管理。不过要注意Fragment的生命周期,会有些细微的差别。

 

 

Fragment 是实现Android结构化的一种新的方式,就像IOS中的不用UITableview而用UICollectionView实现列表数据结构化。因为只 使用Activity而不用Fragment的话,会简单一些。不过,之后你会遇到麻烦。如果不使用Fragment代替全盘使用Activity的话, 在后面需要利用intent和进行多屏幕支持的时候就会遇到困难。

下面看一个UITableViewController的例子和一个ListFragment的地铁时刻表示例。

表格实现

  1. @interface MBTASubwayTripTableTableViewController () 
  2.   
  3. @property (assign, nonatomic) MBTATrip *trip; 
  4.   
  5. @end 
  6.   
  7. @implementation MBTASubwayTripTableTableViewController 
  8.   
  9. -(instancetype)initWithTrip:(MBTATrip *)trip 
  10.     self = [super initWithStyle:UITableViewStylePlain]; 
  11.     if (self) { 
  12.         _trip = trip; 
  13.         [self setTitle:trip.destination]; 
  14.     } 
  15.     return self; 
  16.   
  17. -(void)viewDidLoad 
  18.     [super viewDidLoad]; 
  19.       
  20.     [self.tableView registerClass:[MBTAPredictionCell class] forCellReuseIdentifier:[MBTAPredictionCell reuseId]]; 
  21.     [self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass([MBTATripHeaderView class]) bundle:nil] forHeaderFooterViewReuseIdentifier:[MBTATripHeaderView reuseId]]; 
  22.   
  23. #pragma mark - UITableViewDataSource 
  24.   
  25. -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 
  26.     return 1
  27.   
  28. -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 
  29.     return [self.trip.predictions count]; 
  30.   
  31. #pragma mark - UITableViewDelegate 
  32.   
  33. -(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section 
  34.     return [MBTATripHeaderView heightWithTrip:self.trip]; 
  35.   
  36. -(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section 
  37.     MBTATripHeaderView *headerView = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:[MBTATripHeaderView reuseId]]; 
  38.     [headerView setFromTrip:self.trip]; 
  39.     return headerView; 
  40.   
  41. -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
  42.     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[MBTAPredictionCell reuseId] forIndexPath:indexPath]; 
  43.       
  44.     MBTAPrediction *prediction = [self.trip.predictions objectAtIndex:indexPath.row]; 
  45.     [(MBTAPredictionCell *)cell setFromPrediction:prediction]; 
  46.       
  47.     return cell; 
  48.   
  49. -(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath 
  50.     return NO; 
  51.   
  52. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 
  53.     [tableView deselectRowAtIndexPath:indexPath animated:YES]; 
  54.   
  55. @end 
  56.  
  57.  
  58.  

#p#

List Fragment实现

 

 

 

  1. public class TripDetailFragment extends ListFragment { 
  2.   
  3.     /** 
  4.      * The configuration flags for the Trip Detail Fragment. 
  5.      */ 
  6.     public static final class TripDetailFragmentState { 
  7.         public static final String KEY_FRAGMENT_TRIP_DETAIL = "KEY_FRAGMENT_TRIP_DETAIL"
  8.     } 
  9.   
  10.     protected Trip mTrip; 
  11.   
  12.     /** 
  13.      * Use this factory method to create a new instance of 
  14.      * this fragment using the provided parameters. 
  15.      * 
  16.      * @param trip the trip to show details 
  17.      * @return A new instance of fragment TripDetailFragment. 
  18.      */ 
  19.     public static TripDetailFragment newInstance(Trip trip) { 
  20.         TripDetailFragment fragment = new TripDetailFragment(); 
  21.         Bundle args = new Bundle(); 
  22.         args.putParcelable(TripDetailFragmentState.KEY_FRAGMENT_TRIP_DETAIL, trip); 
  23.         fragment.setArguments(args); 
  24.         return fragment; 
  25.     } 
  26.   
  27.     public TripDetailFragment() { } 
  28.   
  29.     @Override 
  30.     public View onCreateView(LayoutInflater inflater, ViewGroup container, 
  31.                              Bundle savedInstanceState) { 
  32.         Prediction[] predictions= mTrip.predictions.toArray(new Prediction[mTrip.predictions.size()]); 
  33.         PredictionArrayAdapter predictionArrayAdapter = new PredictionArrayAdapter(getActivity(), predictions); 
  34.         setListAdapter(predictionArrayAdapter); 
  35.         return super.onCreateView(inflater,container, savedInstanceState); 
  36.     } 
  37.   
  38.     @Override 
  39.     public void onViewCreated(View view, Bundle savedInstanceState) { 
  40.         super.onViewCreated(view, savedInstanceState); 
  41.         TripDetailsView headerView = new TripDetailsView(getActivity()); 
  42.         headerView.updateFromTripObject(mTrip); 
  43.         getListView().addHeaderView(headerView); 
  44.     } 

下面,我们来分析Android上特有的一些组件。

Android通用组件

ListView和Adapter

ListView 和IOS的UITableView最像,也是使用最频繁的组件之一。类似于UITableView的 UITableViewController,ListView也有一个ListActivity,还有ListFragment。这些组件会更好地处理 一些布局问题,也为操作数据适配器提供了便利,这个接下来会说到。下面这个例子就是使用ListFragment来展示数据,类似TableView的 datasource。

关于datasource,Android上没有datasource和 delegate,只有Adapter。Adapter有很多种形式,主要功能其实就是为了把datasource和delegate合在一起。 Adapter拿到数据然后填充到Listview中,在ListView中初始化响应的组件并显示出来,下面是arrayAdapter的使用:

  1. public class PredictionArrayAdapter extends ArrayAdapter { 
  2.   
  3.     int LAYOUT_RESOURCE_ID = R.layout.view_three_item_list_view; 
  4.   
  5.     public PredictionArrayAdapter(Context context) { 
  6.         super(context, R.layout.view_three_item_list_view); 
  7.     } 
  8.   
  9.     public PredictionArrayAdapter(Context context, Prediction[] objects) { 
  10.         super(context, R.layout.view_three_item_list_view, objects); 
  11.     } 
  12.   
  13.     @Override 
  14.     public View getView(int position, View convertView, ViewGroup parent) 
  15.     { 
  16.         Prediction prediction = this.getItem(position); 
  17.         View inflatedView = convertView; 
  18.         if(convertView==null
  19.         { 
  20.             LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 
  21.             inflatedView = inflater.inflate(LAYOUT_RESOURCE_ID, parent, false); 
  22.         } 
  23.   
  24.         TextView stopNameTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_left_text_view); 
  25.         TextView middleTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_middle_text_view); 
  26.         TextView stopSecondsTextView = (TextView)inflatedView.findViewById(R.id.view_three_item_list_view_right_text_view); 
  27.   
  28.         stopNameTextView.setText(prediction.stopName); 
  29.         middleTextView.setText(""); 
  30.         stopSecondsTextView.setText(prediction.stopSeconds.toString()); 
  31.   
  32.         return inflatedView; 
  33.     } 

可 以看到,adapter里面有一个很重要的方法叫getView,和IOS的cellForRowAtIndexPath方法一样。还有一个相似之处就是 循环利用的策略,和IOS6上的实现很相似。在Android和IOS上循环利用View都很重要,事实上它对列表的实现有很大帮助。这个adapter 很简单,使用了一个内建的类ArrayAdapter来存放数据,也解释了怎么把数据填入ListView中。

AsyncTask

对于IOS上的Grand Central Dispatch,Android上也有AsyncTask。它是异步操作工具的又一选择,用一种很友好的方式实现异步任务。不过AsyncTask有点超出了本文的范围,所以本人还是推荐你看看这里。

#p#

Activity的生命周期

IOS开发者在写Android的过程中还要注意的就是Android的生命周期。可以先从Activity的生命周期文档开始:

本 质上Activity的生命周期很像UIViewController的生命周期,主要区别在于Android上可以任意销毁Activity,所以保证 Activity的数据和状态很重要,如果在onCreate()中保存了的话,可以在saved state中恢复Activity的状态。***的方法就是使用saveInstanceState来存储bundled数据,例如下面的 TripListActivity是示例工程的一部分,用来保存当前显示的数据:

  1. public static Intent getTripListActivityIntent(Context context, TripList.LineType lineType) { 
  2.     Intent intent = new Intent(context, TripListActivity.class); 
  3.     intent.putExtra(TripListActivityState.KEY_ACTIVITY_TRIP_LIST_LINE_TYPE, lineType.getLineName()); 
  4.     return intent; 
  5.   
  6. public static final class TripListActivityState { 
  7.     public static final String KEY_ACTIVITY_TRIP_LIST_LINE_TYPE = "KEY_ACTIVITY_TRIP_LIST_LINE_TYPE"
  8.       
  9. TripList.LineType mLineType;     
  10.       
  11. @Override 
  12. protected void onCreate(Bundle savedInstanceState) { 
  13.    super.onCreate(savedInstanceState); 
  14.    mLineType = TripList.LineType.getLineType(getIntent().getStringExtra(TripListActivityState.KEY_ACTIVITY_TRIP_LIST_LINE_TYPE)); 
  15. }     

还 有一个要注意的地方就是屏幕旋转:如果屏幕发生旋转,会改变Activity的生命周期。也就是说,Activity会先被销毁,然后再重建。如果已经保 存了数据和状态,Activity可以重建原来的状态,实现无缝重建。很多APP开发者在遇到APP旋转时会出现问题,因为Activity没有处理旋转 的改变。注意不要用锁定屏幕的方向来解决这个问题,因为这样会存在一个隐含的生命周期的bug,在某些情况下还是可能发生的。

Fragment生命周期

Fragment的生命周期和Activity的很像,但是有一些区别:

还 有一个问题就是Fragment和Activity通信的问题。需要注意的是onAttach()方法在onActivityCreated()方法之前 被调用,这就意味着在fragment创建完成后Activity还不能保证已经存在。如果需要为父Activity设置接口或者代理,则需要在 onActivityCreated()方法调用之后。

Fragment也有可能会在系统需要的时候被创建和销毁。如果要保存它的状态,那么也要像Activity一样进行处理。下面这个是示例项目中的一个小例子,trip列表Fragment会记录相应的数据,和上面的地铁时间示例一样:

  1. /** 
  2.  * The configuration flags for the Trip List Fragment. 
  3.  */ 
  4. public static final class TripListFragmentState { 
  5.     public static final String KEY_FRAGMENT_TRIP_LIST_LINE_TYPE = "KEY_FRAGMENT_TRIP_LIST_LINE_TYPE"
  6.     public static final String KEY_FRAGMENT_TRIP_LIST_DATA = "KEY_FRAGMENT_TRIP_LIST_DATA"
  7.   
  8. /** 
  9.  * Use this factory method to create a new instance of 
  10.  * this fragment using the provided parameters. 
  11.  * 
  12.  * @param lineType the subway line to show trips for. 
  13.  * @return A new instance of fragment TripListFragment. 
  14.  */ 
  15. public static TripListFragment newInstance(TripList.LineType lineType) { 
  16.     TripListFragment fragment = new TripListFragment(); 
  17.     Bundle args = new Bundle(); 
  18.     args.putString(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_LINE_TYPE, lineType.getLineName()); 
  19.     fragment.setArguments(args); 
  20.     return fragment; 
  21.   
  22. protected TripList mTripList; 
  23. protected void setTripList(TripList tripList) { 
  24.     Bundle arguments = this.getArguments(); 
  25.     arguments.putParcelable(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_DATA, tripList); 
  26.     mTripList = tripList; 
  27.     if (mTripArrayAdapter != null) { 
  28.         mTripArrayAdapter.clear(); 
  29.         mTripArrayAdapter.addAll(mTripList.trips); 
  30.     } 
  31.   
  32. @Override 
  33. public void onCreate(Bundle savedInstanceState) { 
  34.     super.onCreate(savedInstanceState); 
  35.     if (getArguments() != null) { 
  36.         mLineType = TripList.LineType.getLineType(getArguments().getString(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_LINE_TYPE)); 
  37.         mTripList = getArguments().getParcelable(TripListFragmentState.KEY_FRAGMENT_TRIP_LIST_DATA); 
  38.     } 
  39. }     

还 要注意的是,Fragment经常会在onCreate方法中利用bundled参数重建自己的状态。而自定义的Trip列表模型类相关的setter方 法也会把对象添加到bundled参数中。这样就可以保证在Fragment被销毁或者重建时,比如屏幕旋转后,可以利用***的数据去重建状态。

#p#

关于布局

和Android上其它部分的开发工作一样,指定布局文件也有自己的优缺点。Android上的布局文件都存放在res/layouts文件夹中,以易读的xml形式存储。

地铁列表布局

  1. xmlns:tools="http://schemas.android.com/tools" 
  2.     android:layout_width="match_parent" 
  3.     android:layout_height="match_parent" 
  4.     tools:context="com.example.androidforios.app.activities.MainActivity$PlaceholderFragment"
  5.   
  6.     
  7.         android:id="@+id/fragment_subway_list_listview" 
  8.         android:layout_width="match_parent" 
  9.         android:layout_height="match_parent" 
  10.         android:paddingBottom="@dimen/Button.Default.Height"/> 
  11.   
  12.     
  13.         android:id="@+id/fragment_subway_list_Button" 
  14.         android:layout_width="match_parent" 
  15.         android:layout_height="@dimen/Button.Default.Height" 
  16.         android:minHeight="@dimen/Button.Default.Height" 
  17.         android:background="@drawable/button_red_selector" 
  18.         android:text="@string/hello_world" 
  19.         android:textColor="@color/Button.Text" 
  20.         android:layout_alignParentBottom="true" 
  21.         android:gravity="center"/> 

下面这个是IOS上用UITableView和UIButton来制作的类似效果:

可以发现,Android的布局文件更容易阅读和理解,而且提供了多种布局方式,我们只介绍了其中的一小部分。

通常来说,我们接触的最基本的UI结构就是ViewGroup的子类,RelativeLayout、LinearLayout、FrameLayout是最常用的。这些ViewGroup的子类可以容纳别的View,并包含了一些排布控件的属性。

一个很好的例子就是上面用到的RelativeLayout,在里面可以使用android:layout_alignParentBottom="true"来把按钮定位到布局底部。

***,如果要在Fragment或者Activity中使用这些控件的话,可以在onCreateView()方法中使用布局的资源ID:

  1. @Override 
  2. public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 
  3.     return inflater.inflate(R.layout.fragment_subway_listview, container, false); 

布局小贴士

请使用dp(density-independent pixels),不直接使用dx(pixels);

不要在可视化编辑器中移动布局组件——通常来说可视化编辑器在你调好高和宽后,会为组件添加一些多余的像素,所以***就是直接操作xml文件;

如果在布局的height和width看到有用fill_parent这个属性的话,你会发现在API 8的时候这个属性就已经被限制了,改用match_parent替换。

如果要了解更多关于这方面的内容可以看看这篇文章——responsive android applications。

数据

Android上的数据存储也和IOS上差不多:

SharedPreferences、NSUserDefaults;

内存存储对象;

internal、external文件读写document directory文件读写;

SQLite数据库存储Core Data形式数据库存储。

其中最基本的区别就是Core Data。在Android上可以直接访问sqlite数据库并可以返回cursor对象得到结果。更详细的介绍请看这篇文章—— using SQLite on Android。

Android课后作业

之前已经讨论的东西只是描述了Android的大概 ,要想好好利用Android上的更多的特性,本人建议你看看下面的这些概念:

ActionBar,Overflow Menu,还有Menu Button;

跨应用间数据共享;

响应系统actions;

好好学习Java的特性:泛型、抽象方法和抽象类等等;

看看Google的低版本兼容库;

关于Android上的模拟器:可以安装x86 HAXM plugin来使模拟器更流畅。

***的工作

以上所有涉及的知识点都在MBTA中实现了(托管在Github上)。这个项目仅仅是为了解释两个不同平台之间的一些基本的概念,比如应用架构、数据处理、界面开发等。

我们可以学到更多的解决问题的技巧和方式。因为两平台的实现细节各不相同,也许了解Android的工作原理可以对IOS的下一个版本的开发工作有所帮助。系统之间有很多相似的地方,谁知道下个版本的IOS会出现什么呢?

本文链接:http://www.apkbus.com/android-7863-1.html

责任编辑:chenqingxiang 来源: apkbus
相关推荐

2014-05-15 16:20:26

iOS程序员Android要点

2022-01-21 08:21:02

Web 安全前端程序员

2013-12-16 09:36:49

程序员编程语言

2011-08-18 16:34:28

程序员必须知道

2023-11-01 08:01:48

数据结构软件工程

2014-06-20 16:16:32

程序员算法

2020-04-28 10:03:12

前端开发Mac

2015-03-06 10:10:18

程序员基础实用算法讲解

2020-04-02 15:37:58

数据结构存储

2020-03-04 11:10:14

数据结构程序员编译器

2010-07-16 09:00:00

.NET

2013-07-09 15:26:29

程序员算法

2024-09-03 13:24:12

2015-03-20 13:15:40

Java程序员JVM命令令行标志

2023-01-10 08:12:52

Java程序员负载均衡

2020-03-22 15:54:14

全栈开发框架

2014-09-02 10:29:24

程序员必备英语词汇

2023-02-06 16:46:59

JavaScript程序员技巧

2018-08-20 09:58:01

数据结构程序员面试数据

2013-12-23 12:24:59

IT技术周刊
点赞
收藏

51CTO技术栈公众号